XRP Partial Payments Explained: How to Avoid the Delivered Amount Trap
A validated XRPL Payment can return tesSUCCESS after delivering less than the transaction's headline amount. That is intended behavior when the sender enables tfPartialPayment. The trap is not the feature itself. It is an integration that credits Amount instead of the metadata field delivered_amount.
The one distinction that prevents the exploit
In API version 1, a Payment's Amount field names what the destination should receive for an ordinary exact payment. With the partial-payment flag enabled, the same field becomes a ceiling: the payment may deliver less and still succeed. API version 2 renames that field DeliverMax to make the maximum behavior explicit. A transaction uses Amount or DeliverMax according to API version, never both.
The receiver's source of truth comes later. After execution, transaction metadata reports delivered_amount, the asset and quantity actually received by the destination. Official XRPL guidance says to use this field for every Payment, not only transactions whose flags already look suspicious. That single rule survives API versions, pathfinding, issuer transfer fees, rounding, and partial-payment behavior.
| Field | Role | Safe interpretation |
|---|---|---|
Amount | API v1 destination instruction | Exact target normally; maximum when partial |
DeliverMax | API v2 name for the same serialized field | Maximum destination amount |
SendMax | Maximum source cost | Includes path exchange and issuer fees, not the XRP network fee |
DeliverMin | Optional partial-payment floor | Transaction fails unless at least this destination amount arrives |
delivered_amount | Processed metadata | What the destination actually received |
What the partial-payment flag changes
tfPartialPayment is Payment flag 0x00020000, decimal 131072. Without it, the payment must deliver the full destination amount without exceeding SendMax, or fail. With it, the engine may reduce the destination receipt. If DeliverMin is present, delivery must reach that floor. Without DeliverMin, any positive delivery can be enough for protocol success.
This is useful when the sender wants to spend no more than a fixed budget while liquidity or transfer fees make the exact destination amount uncertain. Imagine a token payment capped at 100 units to the receiver and 20 units of source currency from the sender. If available paths can deliver only 73 destination units within that source budget, an ordinary Payment fails. A partial Payment can succeed at 73, provided its optional minimum is no higher than 73.
tesSUCCESS means the transaction executed according to its instructions. It does not mean a partial Payment reached Amount or DeliverMax. Read delivered_amount after validation.
Why direct XRP behaves differently
A direct XRP-to-XRP Payment is deliberately simple. Its destination amount is a string containing drops of XRP. It must omit SendMax and Paths, and it always delivers the exact specified amount if successful. Setting tfPartialPayment on this payment type is malformed and returns temBAD_SEND_XRP_PARTIAL. A partial payment also cannot provide the XRP that creates a previously unfunded account; the documented result is telNO_DST_PARTIAL.
Do not turn that limitation into the broader claim that “XRP can never be partial.” A cross-currency path may spend XRP to deliver an issued token, or spend an issued token to deliver XRP. Those are not direct XRP-to-XRP transfers, so partial-payment behavior can apply. The deciding question is the payment structure, not whether the letters XRP appear on one side.
How the delivered amount trap works
The vulnerable pattern is straightforward. An attacker submits a Payment with a very large Amount or DeliverMax, enables tfPartialPayment, and supplies conditions that result in a tiny positive delivery. The ledger correctly validates the payment as successful. A naive exchange or merchant sees the large instruction field, mistakes it for a receipt, and creates an oversized balance or marks an invoice paid.
The exploit crosses an accounting boundary. No extra assets appear on the XRP Ledger. The loss occurs because an external system records more value than its XRPL address actually received, then permits goods, trading, or withdrawal against that false credit. Checking the flag helps explain what happened, but flag filtering alone is not the durable fix. Crediting delivered_amount is.
if (!record.validated) reject("not final")
if (meta.TransactionResult !== "tesSUCCESS") reject("not successful")
if (tx.TransactionType !== "Payment") reject("wrong type")
if (tx.Destination !== expectedAddress) reject("wrong destination")
received = meta.delivered_amount
if (!isSupportedPositiveAmount(received)) reject("invalid receipt")
creditOnce(tx.hash, expectedCustomer, received)
This pseudocode is intentionally incomplete around customer routing, asset allowlists, precision, and storage transactions. Its important property is ordering: establish a final validated result, match the intended Payment, read the actual receipt, then perform one idempotent credit.
A safe integration checklist
- Require finality. Accept only a record with
validated: true. Proposed transaction streams and submit responses are not final. - Require successful payment semantics. Check
TransactionType: Payment,meta.TransactionResult: tesSUCCESS, the exact destination, and the sender if the business rule requires one. - Read the receipt. Parse
meta.delivered_amount. For XRP it is a string in drops. For issued currencies it is an object containing currency, issuer, and decimal value. Never apply the one-million-drops conversion to token objects. - Match the asset, issuer, and route. A currency code alone does not identify an issued asset. Verify the issuer, destination tag or invoice identifier, and any other authenticated mapping before crediting a customer.
- Credit once. Enforce uniqueness on the transaction hash and the relevant credit event. Replayed WebSocket messages, backfills, and process restarts must not duplicate an internal balance.
- Reconcile liabilities. Compare on-ledger balances with internal customer obligations. Stop automated movement when reconciliation is unexplained rather than allowing an accounting mismatch to grow.
delivered_amount is central, but safe crediting also requires finality, routing, idempotency, and reconciliation.Edge cases worth handling explicitly
For nonpartial Payments, delivered_amount ordinarily equals the instruction amount, although token delivery can differ slightly because of rounding. That is another reason to use metadata consistently. For a legacy partial Payment validated before 20 January 2014, the field can be the string unavailable. Do not replace it with Amount. Reconstruct the receipt from AffectedNodes, accounting for trust-line orientation and the possibility that delivery is split across several trust lines, or send the case to manual review.
Field location varies by API response. The tx method uses result.meta.delivered_amount; account_tx places it in each transaction member's metadata; expanded ledger and subscription responses have their own wrappers. Normalize response shapes in one tested adapter. Do not confuse lower-case generated delivered_amount with the binary metadata field DeliveredAmount, which may be omitted.
Finally, monitor more than Payment transactions if your goal is complete account balance accounting. Checks, escrows, payment channels, offers, and other transaction types can change what an address receives. The partial-payment rule protects Payment deposit crediting; it is not a complete indexer by itself.
FAQ
What is an XRP Ledger partial payment?
It is a Payment with tfPartialPayment enabled. It can succeed after delivering less than Amount or DeliverMax, provided the delivery is positive, reaches DeliverMin when specified, and does not exceed SendMax.
Can a direct XRP-to-XRP payment be partial?
No. A direct XRP-to-XRP Payment must deliver its exact amount and omit SendMax and Paths. A cross-currency payment can still be partial when XRP is only one side of the conversion.
Should an exchange credit the Amount field of an incoming XRPL payment?
No. It should wait for a validated tesSUCCESS result and credit only the asset and quantity in metadata delivered_amount, after matching the destination and required customer routing.
What does delivered_amount unavailable mean?
It is a legacy case for partial payments validated before 20 January 2014. The actual receipt must be reconstructed from AffectedNodes with issuer-aware trust-line accounting, not guessed from Amount.
Is tesSUCCESS enough to credit an XRP Ledger deposit?
No. The record must also be validated, be the expected Payment to the intended destination, carry the required route, and contain a supported delivered_amount. Credit the hash only once.
Sources checked
- XRP Ledger, Partial Payments, semantics, limitations, legacy metadata, and exploit prevention
- XRP Ledger, Payment transaction, API v1 and v2 fields, payment types, flags, and quality controls
- XRP Ledger, Transaction Metadata,
DeliveredAmount,delivered_amount, and validated finality - XRP Ledger, Robustly Monitoring for Payments, success filtering and reconciliation guidance
- XRP Ledger, Monitor Incoming Payments with WebSocket, validated stream and metadata checks
- XRP Ledger, Look Up Transaction Results, authoritative status and balance-change interpretation
- XRP Ledger, tem Codes, invalid direct-XRP partial-payment combinations
- XORA, yield source disclosure, native XRP subsidy and estimated reward-value framing
- XORA, security and custody disclosure, product risk context
Put verified XRP to work
Safe receipt accounting comes first. Yield does not remove custody, liquidity, market, operational, or counterparty risk. XORA's headline is up to 22% APY value: 15% native XRP yield currently subsidised by the XORA treasury, plus estimated XORA reward value. The rate and reward value are variable, not guaranteed, and capital is at risk.
xora.finance is where to put your XRP to work and earn up to 22% APY value instead of leaving it idle on an exchange.