Checking access…

Skip to main content
Version: v2

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_action was 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 typeauthRequired sent to Stripe
CARDtrue → Stripe uses confirmation_method=manual, allows 3DS challenge
BANK_ACCOUNTfalse → 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 tender

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

  • CARD allocations always set authRequired=true, enabling the Stripe 3DS challenge flow.
  • BANK_ACCOUNT allocations always set authRequired=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
CONFIRMATION_REQUIRED after challenge complete

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:

  1. The payment service automatically cancels the AUTH_REQUIRED allocation — it transitions to CANCEL_INITIALIZED and a void is sent to Stripe (the Payment Intent is in requires_action state and holds no captured funds).
  2. The transaction resolves to FAILED/ROLLED_BACK and a terminal webhook event is published.
  3. Any confirm call received during or after this window is rejected with 422.
Auto-cancel of AUTH_REQUIRED legs

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.

Stale error from previous attempt

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

Flow3DS enabled?Notes
V2 user payment (CARD)✅ YesauthRequired=true derived from method type
V2 guest payment (CARD)✅ YesUser 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❌ NoACH has no 3DS concept
V2 merchant-initiated❌ NoOff-session, no user present
V2 agent session❌ BlockedAgents 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 paymentsCaller-controlledauthRequired is an explicit field on PaymentRequest; caller must set it