Checking access…

Skip to main content
Version: v2

Split-Tender Payment Flow

πŸ”’ InternalInternal information β€” not visible in the public (merchant) site.

Overview​

A split-tender payment distributes a single transaction across exactly 2 payment allocations (currently CARD + CARD only). The system treats the two allocations as an atomic unit β€” if one fails, the successful allocation is automatically rolled back.

Key characteristics:

  • Exactly 2 entries in paymentAllocations
  • Only CARD + CARD combinations supported
  • Only SALE transactions (not pre-auth)
  • Automatic rollback (cancel/reverse) if any allocation fails
  • ROLLED_BACK status and remediation object on reversed allocations

Sequence Diagram​


API Request​

Sample Request β€” Split-Tender (2 Cards)
curl -X POST "https://api-stg.uhg.com/api/financial/commerce/nonprodcheckout/v2/payments" \
-H "Authorization: Bearer <token>" \
-H "X-Merchant-Id: b955db5e-aef2-47de-bbb9-c80b9cc16e8f" \
-H "X-Upstream-Env: dev" \
-H "Content-Type: application/json" \
-d '{
"merchantTransactionId": "split-20260404-001",
"amount": 20000,
"currencyCode": "USD",
"authorizeCard": false,
"partialAuthorization": false,
"customer": {
"hsid": "b313c1d1-b5b6-4ec7-8b5a-3a9cf7755060"
},
"paymentAllocations": [
{
"amount": 12000,
"paymentMethodId": "b0b3c48d-4cf6-404a-a554-e14640a51c5b"
},
{
"amount": 8000,
"paymentMethodId": "c1d4e5f6-7890-1234-5678-90abcdef1234"
}
],
"metadata": {
"orderId": "split-order-789"
}
}'

Validation Rules (Split-Tender Specific)​

RuleHTTP StatusError
paymentAllocations must have exactly 2 items400INVALID_REQUEST
alloc[0].amount + alloc[1].amount must equal top-level amount400INVALID_REQUEST
Both allocations must use CARD payment methods400INVALID_REQUEST
authorizeCard must be false (pre-auth not supported for split-tender)400INVALID_REQUEST
Each allocation amount must be β‰₯ 1400INVALID_REQUEST

API Response β€” Both Succeed (GET 200)​

{
"url": "https://api-stg.uhg.com/.../v2/payments/660e8400-e29b-41d4-a716-446655440000",
"data": {
"id": "660e8400-e29b-41d4-a716-446655440000",
"merchantTransactionId": "split-20260404-001",
"amount": 20000,
"authorizedAmount": 20000,
"capturedAmount": 20000,
"currencyCode": "USD",
"status": "COMPLETED",
"paymentAllocations": [
{
"id": "aaaa1111-2222-3333-4444-555566667777",
"amount": 12000,
"authorizedAmount": 12000,
"capturedAmount": 12000,
"status": "COMPLETED",
"paymentMethod": {
"id": "b0b3c48d-4cf6-404a-a554-e14640a51c5b",
"type": "CARD",
"card": { "lastFour": "4242", "brand": "visa", "expMonth": 12, "expYear": 2028 }
},
"vendor": { "name": "STRIPE", "transactionId": "pi_alloc1_abc" }
},
{
"id": "bbbb1111-2222-3333-4444-555566667777",
"amount": 8000,
"authorizedAmount": 8000,
"capturedAmount": 8000,
"status": "COMPLETED",
"paymentMethod": {
"id": "c1d4e5f6-7890-1234-5678-90abcdef1234",
"type": "CARD",
"card": { "lastFour": "1234", "brand": "mastercard", "expMonth": 6, "expYear": 2027 }
},
"vendor": { "name": "STRIPE", "transactionId": "pi_alloc2_def" }
}
]
}
}

API Response β€” One Fails + Rollback (422)​

When allocation[1] fails, allocation[0] is automatically rolled back:

{
"title": "PAYMENT_ERROR",
"status": 422,
"detail": "Payment processing failed for all records. Check individual records for error details",
"data": {
"id": "660e8400-e29b-41d4-a716-446655440000",
"merchantTransactionId": "split-20260404-001",
"amount": 20000,
"status": "FAILED",
"paymentAllocations": [
{
"id": "aaaa1111-2222-3333-4444-555566667777",
"amount": 12000,
"status": "ROLLED_BACK",
"remediation": {
"action": "CANCEL",
"reason": "CCG_AUTOMATED_CANCEL",
"vendorTransactionId": "pi_cancel_abc"
},
"paymentMethod": {
"type": "CARD",
"card": { "lastFour": "4242", "brand": "visa" }
}
},
{
"id": "bbbb1111-2222-3333-4444-555566667777",
"amount": 8000,
"status": "FAILED",
"error": {
"code": "card_declined",
"message": "Your card was declined."
},
"paymentMethod": {
"type": "CARD",
"card": { "lastFour": "1234", "brand": "mastercard" }
}
}
]
}
}

Rollback Logic​

Operational Risk

If rollback (cancel of allocation[0]) fails, the allocation enters CANCEL_FAILED state. This requires manual intervention β€” see Incident Playbooks (coming soon).


Webhook β€” Split-Tender PAYMENT_FAILED (with rollback)​

{
"name": "PAYMENT_FAILED",
"source": "merchant-portal",
"payload": {
"id": "660e8400-e29b-41d4-a716-446655440000",
"merchantTransactionId": "split-20260404-001",
"amount": 20000,
"status": "FAILED",
"paymentAllocations": [
{
"id": "aaaa1111-2222-3333-4444-555566667777",
"amount": 12000,
"status": "ROLLED_BACK"
},
{
"id": "bbbb1111-2222-3333-4444-555566667777",
"amount": 8000,
"status": "FAILED"
}
]
}
}

Comparison: Single vs Split-Tender​

AspectSingle AllocationSplit-Tender
Cards12
paymentAllocations1 entryExactly 2 entries
RollbackN/AAutomatic (cancel successful if sibling fails)
ROLLED_BACK statusNeverYes, on the cancelled allocation
remediation objectNever presentPresent on rolled-back allocation
Retry scopeRetry the single cardAll-or-nothing β€” both retried together
Pre-auth supportYesNo (authorizeCard must be false)


3DS Authentication in Split-Tender​

When a CARD allocation requires 3DS authentication, the leg enters AUTH_REQUIRED status and the consumer (session service / HCP) is expected to surface the Stripe client_secret for the challenge. The sibling allocation may still be in-flight at this point.

Sibling failure while AUTH_REQUIRED​

If the sibling allocation fails (PAYMENT_METHOD_FAILED, FAILED, CANCELED, or a cancel variant) while another leg is waiting in AUTH_REQUIRED, the orchestrator dispatches an automatic cancel to the AUTH_REQUIRED leg via CancelPaymentCommand.

StepWhat happens
Sibling β†’ PAYMENT_METHOD_FAILEDTransactionAttemptOrchestrator.handlePaymentUpdated() fires
NextPaymentCommandResolverResolves AUTH_REQUIRED leg β†’ CANCEL_PAYMENT_COMMAND; sets authRequiredSiblingPresent=true in context
CancelPaymentCommand.execute()AUTH_REQUIRED ∈ CANCELLABLE_STATUSES β†’ proceeds to CancelPaymentCommandHandler
CancelPaymentCommandHandlerSets status β†’ CANCEL_INITIALIZED; publishes cancel event to Stripe via Event Grid
Stripe adapterVoids the Payment Intent (requires_action state, no captured funds)
TransactionAttemptOrchestratorDerives parent attempt/transaction status β†’ FAILED/ROLLED_BACK
TransactionServicePublishes terminal webhook event to downstream consumers (HCP/Auth Service)
History β€” CC-23416

Prior to CC-23416, CancelPaymentCommand.CANCELLABLE_STATUSES only contained {AUTHORIZED, ACCEPTED}. The orchestrator would dispatch CANCEL_PAYMENT_COMMAND to the AUTH_REQUIRED leg, but the command silently returned a no-op error result β€” the allocation was never cancelled, no terminal event was published, and HCP kept showing AUTH_REQUIRED on both legs indefinitely. The fix adds AUTH_REQUIRED to the allowed set.

Context flag: authRequiredSiblingPresent​

NextPaymentCommandResolver.buildCancelCommandContext() sets authRequiredSiblingPresent=true in the pipeline context whenever any allocation is AUTH_REQUIRED during a cancel scenario. This flag is available to downstream command consumers that may need to distinguish a cancel-of-unconfirmed-challenge from a cancel-of-authorized-payment (e.g. for messaging or audit purposes).