Split Tender Payments API
Split Tender allows a customer to divide a single payment across two saved cards, both charged in parallel as one coordinated transaction. This guide covers how to integrate with the Split Tender Payments API — from making the request, to monitoring status, to handling errors.
Single-card payments? If the transaction uses only one card, see Single Allocation Payments — no rollback logic applies.
- Behaviour: All-or-nothing — both cards succeed or neither is charged
- Processing: Both cards charged in parallel; automatic rollback on partial failure
- Idempotent: Same
merchantTransactionIdcan be retried up to 5 times
Note: In split tender payments, each card is processed as a separate allocation under a single payment transaction. The API request and response will always reflect this, with each allocation representing an individual card’s processing status, errors, and remediation details.
For business rules, scenarios, and lifecycle details, see Split Tender Payments — Business Guide.
- Enable Split Tender: Ensure your merchant account is configured. See Merchant Configuration
- API Endpoint:
- Use
POST v2/paymentsfor API - Use
POST v2/sessionsfor widget flows. See Widget Integration Guide
- Use
- Scope: All operations are at the
merchantscope. - Specs: Payment API Reference | Payment Events & Webhooks
- API:
v2| Widget:v3 - Testing: Use Test Cards to simulate the flow. See Test Cards
- ⚠️ Error Handling: See Payment Error Codes for split tender–specific errors and remediate using resolution steps
API Request
For complete field definitions, constraints, and validations, refer to the Create Payment API Spec.
Stored Payment Method (v2/payments)
Submit a split tender payment using saved cards from the customer's wallet. The key difference from a standard payment is the paymentAllocations array — exactly 2 entries, each specifying a paymentMethodId and amount that together equal the total.
Sample Request — Stored Payment Method
curl -X POST "https://api-stg.uhg.com/api/financial/commerce/nonprodcheckout/v2/payments" \
-H "Authorization: <token>" \
-H "X-Merchant-Id: b955db5e-aef2-47de-bbb9-c80b9cc16e8f" \
-H "X-Upstream-Env: dev" \
-H "Accept: */*" \
-H "Content-Type: application/json" \
-d '{
"merchantTransactionId": "c407ecc7-25f0-4fd9-844b-e9baca53707a",
"amount": 100,
"authorizeCard": false,
"customer": {
"hsid": "b313c1d1-b5b6-4ec7-8b5a-3a9cf7755060",
"metadata": {}
},
"metadata": null,
"paymentAllocations": [
{
"paymentMethodId": "7b30960f-8a19-4ae0-8d73-46849365f81d",
"amount": 60
},
{
"paymentMethodId": "a1c2d3e4-f5a6-7b8c-9d0e-1f2a3b4c5d6e",
"amount": 40
}
]
}'
Session-Based (v2/sessions)
For widget-based (v3) flows, the customer selects cards and distributes amounts through the PAYMENT_WITH_WALLET UI. The session handles card selection and split allocation interactively.
Refer to the Widget Integration Guide for session setup.
API Response
The complex object is trimmed for brevity. Refer to the API Spec for the full response structure.
Successful Response
Sample Response — COMPLETED
{
"url": "http://api-stg.uhg.com:443/v2/payments/3222ac8a-c8fa-4426-bd99-e96a909569b0",
"data": {
"id": "3222ac8a-c8fa-4426-bd99-e96a909569b0",
"merchantTransactionId": "30d279fa-9d7d-48c9-8e61-c3783eecf18c",
"amount": 200,
"description": "V2Split Tender Payment",
"currencyCode": "USD",
"status": "COMPLETED",
"vendor": {
// ... see API Spec for full vendor object ...
},
"statementDescriptorSuffix": "splitTheTe",
"paymentDateUtc": "2026-03-21T00:24:01.741688Z",
"metadata": {
"orderId": "123",
"orderType": "prescription"
},
"merchant": {
// ... see API Spec for full merchant object ...
},
"paymentDetails": {
"healthcare": {
"iias": {
"qualifiedAmount": 82,
"qualifiedAmountDetails": {
"prescriptionAmount": 42
}
},
"visionAmount": 8
}
},
"paymentAllocations": [
{
"id": "8cd647c5-f9ff-42a0-b7d9-084c7f5d7d47",
"amount": 100,
"status": "COMPLETED",
"paymentMethod": {
// ... see API Spec for full paymentMethod object ...
},
"paymentDetails": {
"healthcare": {
"iias": {
"qualifiedAmount": 41,
"qualifiedAmountDetails": {
"prescriptionAmount": 21
}
},
"visionAmount": 4
}
}
},
{
"id": "777373ac-dccf-499f-8c77-1d5e198508ad",
"amount": 100,
"status": "COMPLETED",
"paymentMethod": {
// ... see API Spec for full paymentMethod object ...
},
"paymentDetails": {
"healthcare": {
"iias": {
"qualifiedAmount": 41,
"qualifiedAmountDetails": {
"prescriptionAmount": 21
}
},
"visionAmount": 4
}
}
}
],
"customer": {
"id": "fa6dcef4-6400-467b-aa28-0f02a7afd724"
},
"agent": {
"firstName": "AgentFirstName",
"lastName": "AgentLastName",
"isAccessVerified": null,
"userId": "s438"
}
}
}
Failure Response
When a split tender payment fails, the API response provides detailed information for each allocation (if allocations were attempted):
- For rollbacks (when one card succeeds and the other fails), check the
remediationobject in the rolled-back allocation. This object explains why the allocation was canceled and confirms that split payments are atomic (all-or-nothing). - For failures (when an allocation fails), refer to the
errorobject within each failed allocation.error.detailcontains the CCG-mapped human-readable message.error.errorDetailscontains raw diagnostic data returned directly by the payment processor (e.g., decline reason); notablyerror.errorDetails.messageis the vendor's original error text and may differ fromerror.detail.
Business Rule Violations:
If the request violates a business rule (e.g., unsupported payment method, invalid configuration), the response will not include any paymentAllocations array. Instead, the error is described at the top level of the response.
Business Rule Violation (No Allocations)
Sample Response — Business Rule Violation
{
"title": "UNPROCESSABLE_ENTITY",
"detail": "Split payments cannot be processed using a bank account. Please select a different payment method",
"status": 422,
"data": {
"id": "de989762-5a8f-4a82-95e1-04c2a5356a15",
"merchantTransactionId": "13c7de91-b649-436e-8cdc-edff5e233238",
"amount": 200,
"currencyCode": "USD",
"status": "FAILED",
"vendor": {
// ... see API Spec for full vendor object ...
},
"paymentDateUtc": "2026-03-21T00:24:59.102492Z",
"merchant": {
// ... see API Spec for full merchant object ...
},
"customer": {
"id": "a903448b-7090-4f37-8400-26eac6b0a13b"
}
}
}
Both Allocations failed during processing
Sample Response — Both Allocations (CARD) Failed
{
"title": "PAYMENT_ERROR",
"detail": "One of the split tender card payments failed during processing",
"status": 422,
"data": {
"id": "9cbecc7b-6401-4f1b-bc1d-c2769d466e7f",
"merchantTransactionId": "95b8da69-cf36-49cb-8dbe-1f0948cd38ed",
"amount": 200,
"description": "RollBack",
"currencyCode": "USD",
"status": "FAILED",
"vendor": {
"name": "STRIPE"
},
"paymentDateUtc": "2026-03-21T00:25:45.206679Z",
"merchant": {
"id": "b955db5e-aef2-47de-bbb9-c80b9cc16e8f",
"groupId": "3cf34cba-ffec-48fb-b7ca-09977db1077a",
"name": "ccg-optum-rx"
},
"paymentAllocations": [
{
"id": "99407904-f239-4a50-9968-c75fa4d99623",
"amount": 100,
"status": "FAILED",
"paymentMethod": {
// ... see API Spec for full paymentMethod object ...
},
"error": {
"title": "PAYMENT_ERROR",
"detail": "Your card was declined.",
"errorDetails": {
"code": "card_declined",
"message": "Your card was declined.",
"declineCode": "generic_decline",
"networkAdviceCode": null,
"networkDeclineCode": "01"
}
}
},
{
"id": "99407904-f239-4a50-9968-c75fa4d99623",
"amount": 100,
"status": "FAILED",
"paymentMethod": {
// ... see API Spec for full paymentMethod object ...
},
"error": {
"title": "PAYMENT_ERROR",
"detail": "Your card was declined.",
"errorDetails": {
"code": "card_declined",
"message": "Your card was declined.",
"declineCode": "generic_decline",
"networkAdviceCode": null,
"networkDeclineCode": "01"
}
}
}
],
"customer": {
"id": "5e395f2d-d000-4e79-9f33-ca68280bd2b6"
}
}
}
Rolled Back Response - One of the Allocation Failed during Processing
Sample Response — ROLLED_BACK
{
"title": "PAYMENT_ERROR",
"detail": "One of the split tender card payments failed during processing",
"status": 422,
"data": {
"id": "9cbecc7b-6401-4f1b-bc1d-c2769d466e7f",
"merchantTransactionId": "95b8da69-cf36-49cb-8dbe-1f0948cd38ed",
"amount": 200,
"description": "RollBack",
"currencyCode": "USD",
"status": "FAILED",
"vendor": { "name": "STRIPE" },
"paymentDateUtc": "2026-03-21T00:25:45.206679Z",
"merchant": {
"id": "b955db5e-aef2-47de-bbb9-c80b9cc16e8f",
"groupId": "3cf34cba-ffec-48fb-b7ca-09977db1077a",
"name": "ccg-optum-rx"
},
"paymentAllocations": [
{
"id": "0da5b8ee-a75f-4468-8eaa-85c387251a7c",
"amount": 100,
"status": "ROLLED_BACK",
"paymentMethod": {
// ... see API Spec for full paymentMethod object ...
},
"remediation": {
"type": "CANCELLATION",
"message": "This paymentAllocation was canceled because another paymentAllocation failed. Split payments are atomic—all paymentAllocations must succeed"
}
},
{
"id": "99407904-f239-4a50-9968-c75fa4d99623",
"amount": 100,
"status": "FAILED",
"paymentMethod": {
// ... see API Spec for full paymentMethod object ...
},
"error": {
"title": "PAYMENT_ERROR",
"detail": "Your card was declined.",
"errorDetails": {
"code": "card_declined",
"message": "Your card was declined.",
"declineCode": "generic_decline",
"networkAdviceCode": null,
"networkDeclineCode": "01"
}
}
}
],
"customer": { "id": "5e395f2d-d000-4e79-9f33-ca68280bd2b6" }
}
}
Status Handling
How to Track Payment Status
Split tender payments are tracked at the parent transaction level. Each allocation (card) has its own status, but all polling and webhook notifications are provided at the parent level.
Polling Guidelines
- ✅ Supported:
- Poll the parent transaction using
paymentIdormerchantTransactionId. - Get real-time status for all allocations in a single response.
- See the complete transaction timeline and error details.
- Poll the parent transaction using
- ❌ Not Supported:
- Polling individual allocation (child) payment IDs.
- Partial status requests for specific allocations.
- Component-level webhook subscriptions.
Always use the parent transaction identifier for polling. Using child payment IDs will result in API errors.
How Parent Status is Determined
The parent payment status is derived from the statuses of all allocations:
- All allocations succeed: Parent status is
COMPLETED. - Any allocation fails: Parent status is
FAILED(successful allocations are rolled back automatically). - Processing: Parent status is
PENDINGwhile any allocation is still processing. - Weakest link: The most problematic allocation status determines the parent status.
Status Mapping Table
| Card 1 | Card 2 | Parent Status | Description |
|---|---|---|---|
| ⏳ PENDING | ⏳ PENDING | ⏳ PENDING | Both cards processing |
| ✅ COMPLETED | ✅ COMPLETED | ✅ COMPLETED | Transaction fully completed |
| ⏳ PENDING | ❌ FAILED | ⏳ PENDING | One card failed, rollback in progress |
| 🔄 ROLLED_BACK | ❌ FAILED | ❌ FAILED | Rollback completed |
| ❌ FAILED | ❌ FAILED | ❌ FAILED | Both cards failed |
Webhooks
CCG emits one webhook per parent transaction (not per allocation/card). Webhooks provide real-time status updates for split tender payments, allowing you to automate downstream actions and keep your system in sync.
Note: To configure webhooks, Refer Configuration guide
Webhook Event Summary
| Event | When Triggered |
|---|---|
PAYMENT_SUCCEEDED | Both cards completed successfully |
PAYMENT_FAILED | One or both cards failed |
PAYMENT_FAILED is not final if retries remain. It becomes final only after the 5th failure or when the merchant stops retrying. Always check the retry counter and parent status before taking irreversible actions.
Error Handling
For the complete list of error codes, status codes, and error details, see Payment Error Codes.
Split Tender–Specific Errors
Beyond the standard payment errors, split tender has these use-case-specific failure modes:
- Partial failure → automatic rollback: When one card succeeds and the other fails, CCG cancels the successful card. The response includes
remediation.type: "CANCELLATION"on the rolled-back allocation. - 3DS in agent session: If either card triggers 3DS in an API or UI with agent flow, the entire payment fails — agents can't complete 3DS challenges.
3DS / Strong Customer Authentication (SCA)
For CARD allocations, the platform automatically enables 3DS support by sending confirmation_method=manual to Stripe. This allows cards that require Strong Customer Authentication (SCA) to proceed through the challenge flow rather than fail immediately.
| Allocation type | 3DS enabled | Stripe param sent | Notes |
|---|---|---|---|
CARD (user/guest session) | ✅ Yes | confirmation_method=manual | 3DS challenge surfaced to user |
CARD (agent session) | ❌ Blocked | confirmation_method=manual | Challenge cannot be completed; payment fails |
BANK_ACCOUNT | ❌ No | error_on_requires_action=true | ACH has no 3DS concept |
| Merchant-initiated | ❌ No | error_on_requires_action=true | Off-session, no user present |
CARD + CARD (both allocations are cards): Both allocations can independently enter AUTH_REQUIRED. Each requires a separate PATCH .../confirm call. The parent transaction status shows AUTH_REQUIRED until all legs are resolved, and the capture pipeline is paused for the whole transaction.
When a CARD allocation encounters a 3DS challenge:
- The allocation transitions to
AUTH_REQUIRED - An event is published to the session service
- The UI receives a
client_secretto render the Stripe challenge - User completes the challenge → allocation status becomes
CONFIRMATION_REQUIRED - Widget (or merchant) calls
PATCH .../confirm→ allocation transitions toCONFIRMATION_INITIALIZED - Stripe processes the PaymentIntent → allocation proceeds to
AUTHORIZED/COMPLETED
After the user completes the 3DS challenge, the allocation status becomes CONFIRMATION_REQUIRED. This means the challenge succeeded but the confirm call has not yet been made. Call the Confirm Allocation endpoint to advance the allocation. In widget flows this is handled automatically.
While any allocation is in AUTH_REQUIRED, the orchestrator pauses the entire capture pipeline for the transaction. No allocation will be captured until all pending 3DS challenges are resolved. This prevents partial captures across a split-tender transaction.
authentication_not_handledIf you see 422 PAYMENT_ERROR with authentication_not_handled, the Stripe adapter received authRequired=false for a CARD allocation. This indicates the service version predates the CC-23416 fix. Upgrade wallet-payment-service to include CC-23416. See Split Tender 3DS Troubleshooting for full root cause.
authorizeCard vs 3DSauthorizeCard controls capture timing (SALE vs PRE_AUTH) and is not related to 3DS. Its relationship to split tender differs by integration path:
POST v2/payments(API): SettingauthorizeCard=trueis validation-blocked — the request will be rejected.POST v2/sessions(widget): SettingauthorizeCard=trueis an eligibility condition — the session will not surface the split-tender UI. Split tender requires a sale payment flow (authorizeCard=falseor unset).
Retry Handling
Split tender payments allow up to 5 attempts per merchantTransactionId (1 initial + 4 retries). The retry logic ensures atomicity and flexibility:
When to Retry
- Only retry if the parent payment status is
FAILEDand attempts remain.
How Many Retries Are Allowed
- You can retry up to 5 times per
merchantTransactionId(1 initial + 4 retries). - The retry counter increments automatically for each attempt with the same
merchantTransactionId.
Idempotency & Tracking
- All retries must use the same
merchantTransactionIdfor idempotency and tracking.
What Can Be Changed on Retry
- Each retry is all-or-nothing — you cannot retry just the failed card; both allocations are retried together.
- On each retry, you may change the cards and redistribute the amounts between them.
- After failure, you may switch to a single-card payment if desired.
After Max Retries
- After the 5th failure, the transaction is failed and no further retries are allowed for that
merchantTransactionId. New transaction should be initaited with newmerchantTransactionId
FAQ
Q: How do I correlate webhook events with my payment records?
- Use the parent
paymentIdormerchantTransactionIdprovided in both the API response and webhook payload to match events to your records.
Q: What should I do if a payment is stuck in PENDING?
- Continue polling the parent transaction. If the status does not resolve after a reasonable time, check for webhook delivery issues or contact support.
Q: Can I retry only the failed card in a split tender payment?
- No. All retries must include both allocations; split tender is all-or-nothing.
Q: How do I handle duplicate webhook events?
- Implement idempotency in your webhook handler by tracking processed event IDs or transaction IDs.
Q: What if I do not receive a webhook?
- Check your endpoint logs, firewall, and ensure your endpoint returns a 2xx status code. See Webhook Integration & Configuration for troubleshooting.
Q: When is PAYMENT_FAILED considered final?
- Only after the 5th failed attempt or when you stop retrying. Until then, retries are allowed.
Q: A CARD allocation fails with authentication_not_handled. What does that mean?
- Stripe received
error_on_requires_action=truebut the card required a 3DS challenge. This is a platform-level error — ensure you are runningwallet-payment-serviceversion CC-23416 or later. See Split Tender 3DS Troubleshooting.
Q: Does guest checkout support 3DS for split tender?
- Yes. Guest checkout is UI-initiated — the user is present and can complete a 3DS challenge. Both user and guest flows fully support 3DS for CARD allocations.
Integration Checklist
- Merchant account is configured for split tender payments
- API endpoints and authentication are set up
- Payment request includes exactly two
paymentAllocationswith correct amounts - Error handling and remediation logic implemented
- Webhook endpoint is configured and tested
- Webhook handler is idempotent and logs all payloads
- Retry logic follows documented rules (max 5 attempts, all-or-nothing)
- All error codes and business rule violations are handled
- Integration tested with test cards and simulated failures