Split Payments Extension¶
- Capability Name:
dev.ucp.shopping.split_payments
Note on examples: Instrument
typestrings used in this spec (card,gift_card,store_credit,loyalty) are illustrative. Onlycardhas a normative instrument schema in the base spec (card_payment_instrument.json); the others are handler-defined. Credential shapes shown in examples are similarly illustrative — real handlers MUST publish their credential schemas. Each usage example below assumes a businessallowed_combinationsconfig that admits the instrument set shown; the "Example Configuration" block later in this doc is one such config, not the only one.
Overview¶
The Split Payments extension lets buyers pay with more than one payment
instrument in a single checkout. Businesses declare the instrument
combinations they support in allowed_combinations.
Each instrument is submitted in one of two modes:
- Specified-amount (
amountpresent): the platform requests a specific contribution. - Open-amount (
amountomitted): the business determines the instrument's contribution at processing time (e.g., by querying a gift card's available balance).
On the response, the business reports the actual amount authorized or
charged for each successfully-processed instrument in the same amount
field. Response amount is informational only; see "Response: Actual
Charges" below.
All amount values are expressed in checkout.currency minor units
(ISO 4217). Handlers using foreign-currency-denominated instruments
(e.g., a CAD gift card in a USD checkout) or non-currency instruments
like loyalty points MUST convert each instrument's contribution to the
checkout currency.
Instruments are submitted in allocation priority order — the first instrument gets first claim on the checkout total, the second gets next claim, and so on. The business MAY process payment authorizations in any order for operational reasons (e.g., gift cards before open-loop cards to minimize reversal costs); array order governs amount allocation, not processing sequence.
Schema¶
Payment Instrument (Split Payments)¶
When this capability is active, each payment instrument in
checkout.payment.instruments gains an optional amount field.
Payment instrument extended with an optional per-instrument amount for split payments.
| Name | Type | Required | Description |
|---|---|---|---|
| id | string | Yes | A unique identifier for this instrument instance, assigned by the platform. |
| handler_id | string | Yes | The unique identifier for the handler instance that produced this instrument. This corresponds to the 'id' field in the Payment Handler definition. |
| type | string | Yes | The broad category of the instrument (e.g., 'card', 'tokenized_card'). Specific schemas will constrain this to a constant value. |
| billing_address | object | No | The billing address associated with this payment method. |
| credential | object | No | The base definition for any payment credential. Handlers define specific credential types. |
| display | object | No | Display information for this payment instrument. Each payment instrument schema defines its specific display properties, as outlined by the payment handler. |
| amount | integer | No | Contribution amount for this instrument in checkout.currency minor units (ISO 4217). On request: the platform's requested contribution (omit for open-amount). On response: the actual amount authorized or charged (omitted when not finally processed). |
Configuration¶
Businesses declare split payments configuration in their profile.
Business Profile¶
| Name | Type | Required | Description |
|---|---|---|---|
| allowed_combinations | Array[array] | Yes | Array of valid instrument combinations. Each combination is an array of instrument groups. A payment is valid if it matches any combination. |
allowed_combinations¶
An array of valid instrument combinations. Each combination is an array of instrument groups -- constraints that together define one valid way to split a payment.
A set of instruments is valid if it matches any combination in the array.
Instrument Group¶
Each group within a combination defines a "slot" that accepts certain instrument types:
| Name | Type | Required | Description |
|---|---|---|---|
| types | Array[string] | Yes | Instrument types accepted by this group (OR logic). Any listed type qualifies. |
| min | integer | No | Minimum number of instruments required from this group. Defaults to 0 (optional). |
| max | integer | No | Maximum number of instruments allowed from this group. Defaults to 1. MUST be greater than or equal to min. |
Matching algorithm: a submission matches a combination if there exists an assignment of each submitted instrument to exactly one group such that
- the instrument's
typeis in the assigned group'stypes, and - every group's instrument count falls within its
minandmax(inclusive).
The business MAY use any algorithm to find such an assignment; if any valid assignment exists, the combination matches.
Example Configuration¶
A business that supports (a) a card with up to 2 redeemables, (b) up to 5 gift cards alone, and (c) two credit cards:
{
"capabilities": [{
"dev.ucp.shopping.split_payments": [
{
"version": "2026-01-23",
"config": {
"allowed_combinations": [
[
{ "types": ["card"], "min": 1, "max": 1 },
{ "types": ["gift_card", "store_credit"], "max": 2 }
],
[
{ "types": ["gift_card"], "min": 1, "max": 5 }
],
[
{ "types": ["card"], "min": 2, "max": 2 }
]
]
}
}
]
}]
}
Reading each combination:
- Card + redeemables: Exactly 1 card (required), plus up to 2 instruments that are either gift cards or store credit (optional). Valid payments: card alone, card + gift card, card + store credit, card + 2 gift cards, etc.
- Gift cards only: 1 to 5 gift cards with no other instrument types.
- Two cards: Exactly 2 credit/debit cards.
Using Split Payments¶
Instrument Processing Model¶
- The platform submits instruments in the
payment.instrumentsarray in allocation priority order. - The business MUST derive a contribution for each instrument, in array order:
- Specified-amount: the business MUST authorize for the stated
amount. - Open-amount: the business determines the contribution — typically the instrument's full available balance, up to the remaining checkout total after all prior contributions. A zero available balance is a valid $0 contribution, not a failure.
Error Handling¶
A split payment either completes fully or has no financial effect. If
the business cannot process an instrument with a specified amount, or
cannot achieve the final total, the business MUST return
payment_failed in messages[] and MUST void or reverse any
authorizations it made. Reversal MAY retry transiently (e.g., to work
around acquirer rate limits or a failing void call), but the buyer
MUST NOT remain charged for an incomplete split.
At the protocol layer, each request is processed independently — split-payments state does not persist between requests. The business MUST process each request as a fresh, full submission, without reference to prior requests or responses.
[!NOTE] Each split payments submission is processed as a complete, self-contained request: a modified instrument set is a new submission, requiring a fresh idempotency key. Split-payments state — including authorizations — does not persist between submissions; the unwind-on-failure requirement above enforces this.
Per-instrument reporting: when a split is incomplete or has failed,
the business MUST emit a payment_failed error for each failed
instrument, with path pointing at the instrument. Businesses MAY also
emit info messages for succeeded instruments to convey positive
context (e.g., "Gift card authorized for $10.00") that the platform can
surface to the buyer:
{
"messages": [
{
"type": "info",
"path": "$.payment.instruments[0]",
"content": "Gift card authorized for $10.00."
},
{
"type": "error",
"code": "payment_failed",
"path": "$.payment.instruments[1]",
"severity": "recoverable",
"content": "Card declined — insufficient funds."
}
]
}
Severity is recoverable because the platform's API supports collecting
a replacement instrument and re-submitting. This contrasts with
requires_buyer_input, which is reserved for cases where the API cannot
programmatically collect what the merchant needs.
Error conditions:
- If any instrument cannot be processed (invalid credentials, fraud flag, hard decline, expired, insufficient funds), the business MUST return an error. For open-amount instruments, a zero available balance is not a failure.
- If the checkout total cannot be reached after applying all submitted instruments, the business MUST return an error.
- If the sum of all specified
amountvalues exceeds the checkout total, the business MUST return an error. - If the submitted instruments did not match any valid
allowed_combinations, the business MUST return an error.
Response: Actual Charges¶
On the checkout response, the business MUST set amount on every
instrument that was authorized or charged, reflecting the actual
contribution. For open-amount instruments (submitted without amount),
the business MUST derive and report the actual contribution. The
business MUST omit amount on all other instruments (e.g., a
provisional authorization that was voided when a later instrument
failed, or an instrument the business never attempted).
Response amount is informational. It conveys what the business
processed for buyer-facing UX and audit (e.g., "$10.00 charged to your
gift card"). The platform MUST NOT treat prior response amount values
as preserved state on subsequent requests, and the business MUST NOT
rely on the platform echoing them. Each request is fresh intent — the
platform sets amount to specify a contribution or omits it for
open-amount, independent of any prior response.
Examples¶
Gift Card + Credit Card¶
"Pay with my gift card first, credit card for the rest."
Inbound (buyer's selection):
{
"payment": {
"instruments": [
{
"id": "pi_gc_1",
"handler_id": "example_handler_1",
"type": "gift_card",
"credential": { "type": "gift_card", "token": "gc_abc123" }
},
{
"id": "pi_card_1",
"handler_id": "example_handler_1",
"type": "card",
"credential": { "type": "card", "token": "tok_visa_xxxx" }
}
]
}
}
Neither instrument includes amount — the business determines both.
Outbound (completed checkout, $50 order):
{
"payment": {
"instruments": [
{
"id": "pi_gc_1",
"handler_id": "example_handler_1",
"type": "gift_card",
"credential": { "type": "gift_card", "token": "gc_abc123" },
"amount": 1000
},
{
"id": "pi_card_1",
"handler_id": "example_handler_1",
"type": "card",
"credential": { "type": "card", "token": "tok_visa_xxxx" },
"amount": 4000
}
]
}
}
The business queried the gift card's balance ($10), charged it in full, and charged the credit card for the remaining $40.
Loyalty Points + Credit Card¶
"Use 500 of my 2000 loyalty points ($5 equivalent), credit card for the rest."
Inbound (buyer's selection):
{
"payment": {
"instruments": [
{
"id": "pi_lp_1",
"handler_id": "example_handler_1",
"type": "loyalty",
"credential": { "type": "loyalty", "token": "lp_abc123" },
"amount": 500
},
{
"id": "pi_card_1",
"handler_id": "example_handler_1",
"type": "card",
"credential": { "type": "card", "token": "tok_visa_xxxx" }
}
]
}
}
The platform specifies amount: 500 on the loyalty instrument (the
customer chose to redeem exactly 500 points). The credit card covers
the rest.
Outbound (completed checkout, $50 order):
{
"payment": {
"instruments": [
{
"id": "pi_lp_1",
"handler_id": "example_handler_1",
"type": "loyalty",
"credential": { "type": "loyalty", "token": "lp_abc123" },
"amount": 500
},
{
"id": "pi_card_1",
"handler_id": "example_handler_1",
"type": "card",
"credential": { "type": "card", "token": "tok_visa_xxxx" },
"amount": 4500
}
]
}
}
The business charged the loyalty points for $5 as requested, and the credit card covers the remaining $45.
Gift Card + Gift Card + Credit Card (mixed amounts)¶
"Use both gift cards, credit card for the rest."
Inbound:
{
"payment": {
"instruments": [
{
"id": "pi_gc_1",
"handler_id": "handler_gc",
"type": "gift_card",
"credential": { "type": "gift_card", "token": "gc_abc123" }
},
{
"id": "pi_gc_2",
"handler_id": "handler_gc",
"type": "gift_card",
"credential": { "type": "gift_card", "token": "gc_def456" }
},
{
"id": "pi_card_1",
"handler_id": "handler_card",
"type": "card",
"credential": { "type": "card", "token": "tok_visa_xxxx" }
}
]
}
}
Outbound (completed checkout, $100 order):
{
"payment": {
"instruments": [
{
"id": "pi_gc_1",
"handler_id": "handler_gc",
"type": "gift_card",
"credential": { "type": "gift_card", "token": "gc_abc123" },
"amount": 2500
},
{
"id": "pi_gc_2",
"handler_id": "handler_gc",
"type": "gift_card",
"credential": { "type": "gift_card", "token": "gc_def456" },
"amount": 0
},
{
"id": "pi_card_1",
"handler_id": "handler_card",
"type": "card",
"credential": { "type": "card", "token": "tok_visa_xxxx" },
"amount": 7500
}
]
}
}
The first gift card had a $25 balance (charged in full). The second gift card had a $0 balance — this is not an error, it simply contributes nothing. The credit card covers the remaining $75.
Partial Failure with Recovery¶
"Pay with my gift card first, credit card for the rest." — but the credit card declines. The business voids the gift card authorization and signals the platform that the checkout can be retried with a replacement card.
Outbound (incomplete checkout, $50 order — gift card auth was voided per the atomic invariant):
{
"status": "incomplete",
"payment": {
"instruments": [
{
"id": "pi_gc_1",
"handler_id": "example_handler_1",
"type": "gift_card",
"credential": { "type": "gift_card", "token": "gc_abc123" }
},
{
"id": "pi_card_1",
"handler_id": "example_handler_1",
"type": "card",
"credential": { "type": "card", "token": "tok_visa_xxxx" }
}
]
},
"messages": [
{
"type": "info",
"path": "$.payment.instruments[0]",
"content": "Gift card has $10.00 available balance."
},
{
"type": "error",
"code": "payment_failed",
"path": "$.payment.instruments[1]",
"severity": "recoverable",
"content": "Card declined — insufficient funds."
}
]
}
Both instruments omit amount because neither contributed funds —
the gift card was provisionally authorized and then voided when the
card declined, and the card declined outright. The info message
conveys the discovered gift card balance so the platform can render
accurate buyer UX; the payment_failed error identifies the failing
instrument with severity: recoverable.
Inbound (platform re-submits with a replacement card, $50 order):
{
"payment": {
"instruments": [
{
"id": "pi_gc_1",
"handler_id": "example_handler_1",
"type": "gift_card",
"credential": { "type": "gift_card", "token": "gc_abc123" }
},
{
"id": "pi_card_2",
"handler_id": "example_handler_1",
"type": "card",
"credential": { "type": "card", "token": "tok_visa_yyyy" }
}
]
}
}
The platform submits a new request — the gift card is re-submitted as open-amount and the failing card is replaced. The business processes this as new intent.