Split Tender — 3DS / Authentication Failures
Symptom
A split-tender payment using a CARD allocation fails immediately with:
HTTP 422 PAYMENT_ERROR
"authentication_not_handled"
Stripe's full error message:
This payment required an authentication action to complete, but
error_on_requires_actionwas set.
The payment leg never reaches AUTH_REQUIRED status and the user is never presented with a 3DS challenge.
Cause
The V2 split-tender flow derives authRequired from the payment method type of each allocation:
| Payment method type | authRequired sent to Stripe |
|---|---|
CARD | true → Stripe uses confirmation_method=manual, allows 3DS challenge |
BANK_ACCOUNT | false → Stripe uses error_on_requires_action=true, no challenge |
If authRequired is incorrectly set to false for a CARD allocation, Stripe will immediately fail any card that mandates a 3DS challenge instead of returning a client_secret for the challenge flow.
authorizeCard and split tenderauthorizeCard controls capture timing (SALE vs PRE_AUTH) and cannot be used to drive 3DS behaviour. Its relationship to split tender differs by integration path:
POST v2/payments(API): SettingauthorizeCard=trueis validation-blocked — the request will be rejected with"authorizeCard is not supported for split-tender payments".POST v2/sessions(widget): SettingauthorizeCard=trueis an eligibility condition — the session will not surface the split-tender UI at all. Split tender requires a sale payment flow (authorizeCard=falseor unset).
In both paths, authorizeCard is effectively always false for split-tender flows and plays no role in the 3DS challenge decision.
Resolution
This was a platform bug fixed in CC-23416. After the fix:
CARDallocations always setauthRequired=true, enabling the Stripe 3DS challenge flow.BANK_ACCOUNTallocations always setauthRequired=false(ACH has no 3DS concept).- Merchant-initiated (off-session) payments keep
authRequired=false— no user is present to complete a challenge.
If you are on a version predating this fix, upgrade wallet-payment-service to include CC-23416.
3DS Payment Lifecycle
When a CARD allocation requires 3DS, the payment leg enters AUTH_REQUIRED status and an event is published downstream. The confirm call is required to advance the allocation after the challenge completes — it does not auto-advance.
PENDING → PROCESSING → AUTH_REQUIRED
↓
(session service surfaces client_secret → user completes 3DS challenge)
↓
CONFIRMATION_REQUIRED (challenge complete; confirm call pending)
↓
(merchant calls PATCH .../confirm — or widget auto-confirms)
↓
CONFIRMATION_INITIALIZED (confirm received; Stripe processing)
↓
AUTHORIZED / COMPLETED
After the user finishes the 3DS challenge, the allocation status becomes CONFIRMATION_REQUIRED. This is expected — it means the challenge succeeded but the confirm call has not yet been made. If you poll and see CONFIRMATION_REQUIRED after the challenge, call the Confirm Allocation endpoint.
The session service receives the AUTH_REQUIRED status event and surfaces the client_secret to the UI for the challenge. See Payment Lifecycle for full state transitions.
Sibling Failure During 3DS
A CARD allocation can be waiting in AUTH_REQUIRED while the sibling allocation is still being processed in parallel. If the sibling fails (FAILED, PAYMENT_METHOD_FAILED, CANCELED, CANCEL_INITIALIZED, or CANCEL_FAILED) before the 3DS challenge is resolved:
- The payment service automatically cancels the
AUTH_REQUIREDallocation — it transitions toCANCEL_INITIALIZEDand a void is sent to Stripe (the Payment Intent is inrequires_actionstate and holds no captured funds). - The transaction resolves to
FAILED/ROLLED_BACKand a terminal webhook event is published. - Any confirm call received during or after this window is rejected with
422.
Prior to CC-23416, the AUTH_REQUIRED allocation was not automatically cancelled when a sibling failed — it remained stuck indefinitely, no terminal event was published, and HCP continued to show AUTH_REQUIRED on both legs. This was fixed by adding AUTH_REQUIRED to CancelPaymentCommand.CANCELLABLE_STATUSES.
What to do: If you receive 422 on a confirm call for an allocation, check the parent transaction status first. If it is FAILED or ROLLED_BACK, the transaction has already been resolved — do not retry the confirm; initiate a new payment.
3DS Retry Dead-End Screen
Symptom
A 3DS payment attempt fails (card declined or challenge abandoned). On retry, the checkout widget shows a dead-end error screen instead of allowing the customer to try again or enter a different card.
Polling GET /v2/checkout-sessions/{id} returns HTTP 422 during the retry window even though retryAllowed=true was sent in the PAYMENT_FAILED event.
Cause
When a 3DS payment fails with retryAllowed=true, the auth-service was unconditionally storing the failure error on the checkout session. The next GET /v2/checkout-sessions/{id} call then threw a 422 because checkSessionError() was rejecting any session that had a stored error. The UI's polling code treated this as a terminal PROCESSING_FAILED state and rendered the dead-end screen.
Resolution
Fixed in CC-23416. When the auth-service receives a PAYMENT_FAILED event with retryAllowed=true, it now clears any stale error on the checkout session (sets it to null) instead of setting a new one. The session status remains PENDING and the next GET /v2/checkout-sessions/{id} returns 200, keeping the checkout widget open for the next attempt.
When retryAllowed=false (retries exhausted), the error is still stored as before so the user sees the final decline reason.
If a customer makes multiple 3DS attempts that all fail, a stale error from the first attempt could linger on the session even when retryAllowed=true for subsequent attempts. The fix explicitly clears any stale error on every retryable failure — not just the current one.
Flow Applicability
| Flow | 3DS enabled? | Notes |
|---|---|---|
| V2 user payment (CARD) | ✅ Yes | authRequired=true derived from method type |
| V2 guest payment (CARD) | ✅ Yes | User is present in browser, can complete challenge |
| V2 CARD + CARD (both cards) | ✅ Yes (each) | Both allocations can independently enter AUTH_REQUIRED; each needs a separate confirm call. Capture is gated until all 3DS challenges are resolved. |
| V2 bank account | ❌ No | ACH has no 3DS concept |
| V2 merchant-initiated | ❌ No | Off-session, no user present |
| V2 agent session | ❌ Blocked | Agents cannot complete 3DS challenges. If a CARD allocation triggers 3DS in an agent session, the confirm endpoint rejects the call and the entire payment fails. |
| V1 payments | Caller-controlled | authRequired is an explicit field on PaymentRequest; caller must set it |