Checking access…

Skip to main content
Version: v2

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 merchantTransactionId can 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.

Business Context

For business rules, scenarios, and lifecycle details, see Split Tender Payments — Business Guide.


Integration Details


API Request

API Specification

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

Response Structure

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

Understanding Failure Responses

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 remediation object 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 error object within each failed allocation. error.detail contains the CCG-mapped human-readable message. error.errorDetails contains raw diagnostic data returned directly by the payment processor (e.g., decline reason); notably error.errorDetails.message is the vendor's original error text and may differ from error.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 paymentId or merchantTransactionId.
    • Get real-time status for all allocations in a single response.
    • See the complete transaction timeline and error details.
  • Not Supported:
    • Polling individual allocation (child) payment IDs.
    • Partial status requests for specific allocations.
    • Component-level webhook subscriptions.
warning

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 PENDING while any allocation is still processing.
  • Weakest link: The most problematic allocation status determines the parent status.

Status Mapping Table

Card 1Card 2Parent StatusDescription
⏳ PENDING⏳ PENDING⏳ PENDINGBoth cards processing
✅ COMPLETED✅ COMPLETED✅ COMPLETEDTransaction fully completed
⏳ PENDING❌ FAILED⏳ PENDINGOne card failed, rollback in progress
🔄 ROLLED_BACK❌ FAILED❌ FAILEDRollback completed
❌ FAILED❌ FAILED❌ FAILEDBoth 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

Webhook Payload Structure
EventWhen Triggered
PAYMENT_SUCCEEDEDBoth cards completed successfully
PAYMENT_FAILEDOne or both cards failed
caution

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

Error Code Reference

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 type3DS enabledStripe param sentNotes
CARD (user/guest session)✅ Yesconfirmation_method=manual3DS challenge surfaced to user
CARD (agent session)❌ Blockedconfirmation_method=manualChallenge cannot be completed; payment fails
BANK_ACCOUNT❌ Noerror_on_requires_action=trueACH has no 3DS concept
Merchant-initiated❌ Noerror_on_requires_action=trueOff-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:

  1. The allocation transitions to AUTH_REQUIRED
  2. An event is published to the session service
  3. The UI receives a client_secret to render the Stripe challenge
  4. User completes the challenge → allocation status becomes CONFIRMATION_REQUIRED
  5. Widget (or merchant) calls PATCH .../confirm → allocation transitions to CONFIRMATION_INITIALIZED
  6. Stripe processes the PaymentIntent → allocation proceeds to AUTHORIZED / COMPLETED
Status after challenge

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.

Capture gating

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.

Common failure: authentication_not_handled

If 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 3DS

authorizeCard 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): Setting authorizeCard=true is validation-blocked — the request will be rejected.
  • POST v2/sessions (widget): Setting authorizeCard=true is an eligibility condition — the session will not surface the split-tender UI. Split tender requires a sale payment flow (authorizeCard=false or 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 FAILED and 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 merchantTransactionId for 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 new merchantTransactionId

FAQ

Q: How do I correlate webhook events with my payment records?

  • Use the parent paymentId or merchantTransactionId provided 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?

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=true but the card required a 3DS challenge. This is a platform-level error — ensure you are running wallet-payment-service version 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 paymentAllocations with 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