Routing 3DS2/SCA Challenge Flows Without Killing Conversion

You hit this the moment European cards start declining at checkout because PSD2 Strong Customer Authentication is mandatory and your integration is challenging every payment — including ones that qualify for an exemption. The art of 3DS2 routing is asking for the strongest exemption a payment qualifies for, so the issuer can approve frictionlessly, and only falling back to a full challenge when required. This decision sits under Fraud Prevention & Dispute Management, because a completed 3DS2 challenge is also the fraud-liability shift that moves chargeback risk to the issuer.

The tension is direct: a challenge (one-time code, banking-app approval) adds friction and drops conversion several points, but it shifts liability and is sometimes legally required. A frictionless flow keeps conversion high but only happens when the issuer trusts the transaction or an exemption applies. You want the challenge rate as low as the rules and your risk appetite allow.

Trade-offs

Flow Conversion impact Liability shift When it applies
Frictionless (no challenge) None — invisible to customer Yes, if 3DS2 authenticated Issuer trusts the transaction; risk data sufficient
Challenge (step-up) -5% to -15% per challenge Yes — strongest protection Issuer requires it, or no exemption applies
TRA exemption None when granted No — you keep liability Acquirer fraud rate below ECB thresholds, amount under limit
Low-value exemption (< €30) None when granted No — you keep liability Amount < €30 and under cumulative counters
MIT (merchant-initiated) None — off-session N/A — outside SCA scope Stored credential, recurring/retry charges

Step-by-Step Implementation

1. Classify the payment for an exemption first

Before requesting 3DS2, decide whether the payment qualifies for an exemption. The cheapest authentication is the one you never trigger. Off-session retries are merchant-initiated and out of SCA scope entirely.

type ScaPlan = 'mit' | 'low_value' | 'tra' | 'challenge_required';

function planSca(input: {
  offSession: boolean;          // recurring retry / stored credential
  amountMinor: number;          // cents
  currency: string;
  acquirerFraudRateBps: number; // basis points, from your processor
}): ScaPlan {
  if (input.offSession) return 'mit';                              // ✅ outside SCA scope
  if (input.currency === 'eur' && input.amountMinor < 3000) return 'low_value'; // < €30
  // TRA: allowed up to a ceiling that depends on your acquirer's fraud rate.
  if (input.acquirerFraudRateBps <= 13 && input.amountMinor <= 25000) return 'tra'; // ⚠️ keeps liability
  return 'challenge_required';
}

2. Request the exemption or 3DS2 on the payment intent

Translate the plan into Stripe parameters. Requesting an exemption tells the issuer it may skip the challenge; the issuer can still override and force one (a “soft decline” you must retry with authentication).

async function createIntent(plan: ScaPlan, amountMinor: number, currency: string, customerId: string, pmId: string) {
  const base = { amount: amountMinor, currency, customer: customerId, payment_method: pmId, confirm: true };

  if (plan === 'mit') {
    return stripe.paymentIntents.create({ ...base, off_session: true }); // merchant-initiated, no challenge
  }
  if (plan === 'low_value' || plan === 'tra') {
    return stripe.paymentIntents.create({
      ...base,
      payment_method_options: { card: { request_three_d_secure: 'automatic' } },
      // Signal the exemption so the issuer may approve frictionlessly.
      // (exemption metadata is passed via the card network by the acquirer)
    });
  }
  // Force authentication when no exemption applies — strongest liability protection.
  return stripe.paymentIntents.create({
    ...base,
    payment_method_options: { card: { request_three_d_secure: 'challenge' } }, // ⚠️ adds friction
  });
}

3. Handle the frictionless vs challenge branch on the client

Stripe.js resolves authentication transparently. Your job is to handle the requires_action status (challenge) without breaking the checkout flow, and to surface a soft-decline retry when the issuer forces a step-up.

const { error, paymentIntent } = await stripe.confirmCardPayment(clientSecret);

if (error?.code === 'authentication_required') {
  // Issuer rejected the exemption and demands SCA. Retry forcing a challenge.
  await retryWithChallenge(paymentIntentId);                 // ⚠️ soft-decline recovery
} else if (paymentIntent?.status === 'succeeded') {
  finishCheckout(paymentIntent);                             // ✅ frictionless or challenge passed
} else if (error) {
  showDecline(error.message);                                // ✗ hard decline
}

4. Record the authentication outcome and liability status

Persist the exact 3DS outcome so the dispute workflow knows whether liability actually shifted. Only a fully authenticated result shifts fraud-chargeback liability to the issuer.

UPDATE charge_risk_assessment
SET three_ds_outcome = $2,                     -- authenticated | attempted | not_required
    liability_shifted = ($2 = 'authenticated') -- only a full pass shifts liability
WHERE charge_id = $1;

Verification & Testing

Use Stripe’s 3DS test cards: 4000002500003155 requires a challenge on every payment and 4000002760003184 authenticates frictionlessly, so you can assert both branches deterministically. Write a unit test over planSca asserting an off-session retry returns mit, a €20 charge returns low_value, and a €2,000 charge with a high acquirer fraud rate returns challenge_required. Add an integration test that confirms a requires_action intent surfaces the challenge and that completing it sets status = 'succeeded'. Assert that after a frictionless authenticated flow, liability_shifted is true, and after a TRA exemption it is false. Reconcile your challenge rate weekly against conversion — a rising challenge rate on exemption-eligible traffic means your classifier is too conservative.

Gotchas & Production Pitfalls

  • Exemptions keep liability with you. TRA and low-value exemptions skip the challenge but do not shift chargeback liability. Trading conversion for liability is a deliberate choice — do not assume an exemption protects you like a challenge does.
  • The issuer can override your exemption. Requesting an exemption is a hint, not a guarantee. Always handle the authentication_required soft decline by retrying with a forced challenge, or you lose the sale.
  • MIT only works with a properly set-up mandate. Off-session charges need the initial transaction to have established a stored-credential mandate. Skipping that makes later retries fail SCA. This ties into how you handle stored credentials in Grace Period & Retry Logic.
  • Low-value counters are cumulative. The < €30 exemption is also capped by a running count and total per card; the issuer tracks it and will force SCA once exceeded. Do not rely on it for every small charge.
  • attempted is not authenticated. If the issuer’s ACS is down, you may get an attempted result. Scheme rules often still give protection, but persist the exact value and treat only authenticated as a guaranteed full liability shift.