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

The core tension is conversion versus liability: a frictionless flow keeps conversion but only shifts liability if authenticated; a challenge shifts liability but drops conversion; exemptions keep conversion but keep liability with you. The map contrasts the paths.

3DS2 flow trade-offs Frictionless has no conversion hit and shifts liability if authenticated, a challenge shifts liability but drops conversion, and exemptions keep conversion but keep liability. Frictionless conversion: none lost liability: if authenticated issuer trusts txn Challenge conversion: -5 to -15% liability: strongest shift required or no exemption Exemption conversion: none lost liability: kept by you TRA / low-value / MIT
Ask for the strongest exemption a payment qualifies for; fall back to a challenge only when required — it trades conversion for liability.
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

Put concrete numbers on the conversion column before you decide anything. A step-up challenge does not fail closed; it fails to abandonment. On a well-optimised European checkout you lose roughly 5% of authenticated sessions to a one-time-code challenge and closer to 12-15% when the issuer forces a redirect into a banking app on mobile, where app-switching, cold starts, and expired sessions all compound. If you process 200,000 EUR-denominated transactions a month at an average ticket of 4,500 minor units, moving even 20 percentage points of that volume from challenge_required to a granted TRA exemption is the difference between challenging 40,000 payments and challenging 4,000 — at a 10% abandonment delta that recovers on the order of 3,600 completed sales a month that a naive “always challenge” integration silently discards.

The liability column is where engineers most often reason incorrectly. A frictionless flow shifts liability only when the authentication actually resolves to authenticated; a frictionless exemption (TRA, low-value) looks identical to the customer but leaves the fraud-chargeback liability sitting with you. The two are indistinguishable at the checkout UI and completely different on the dispute ledger, which is why step 4 persists the exact outcome rather than a boolean. Treating “no challenge appeared” as “protected” is how a merchant discovers, one representment at a time, that its exemption-heavy routing quietly re-took liability on tens of thousands of transactions.

TRA ceilings scale with your fraud rate

Transaction Risk Analysis is not a single threshold — it is a tiered ceiling keyed to your acquirer’s rolling fraud rate in basis points. Under the RTS, an acquirer whose fraud rate stays below 13 bps may claim TRA up to 100 EUR, below 6 bps up to 250 EUR, and below 1 bps up to 500 EUR. The acquirerFraudRateBps <= 13 guard in planSca encodes the lowest tier; a mature integration reads the live rate from the processor and picks the ceiling dynamically, because a fraud spike that pushes you from the 6 bps band to the 13 bps band silently invalidates every 100-250 EUR exemption you were relying on and the issuers start forcing challenges you did not budget for. Monitor the rate as a leading indicator, not a monthly report.

Step-by-Step Implementation

The four steps classify the payment for an exemption, request it (or a challenge), handle the frictionless-versus-challenge branch, then record the exact outcome. The decision tree shows the exemption ladder that keeps the challenge rate as low as the rules allow.

SCA exemption ladder Off-session goes to MIT, small euro amounts to low-value, low-fraud acquirers to TRA, and everything else falls back to a full challenge. Classify cheapest first MIT off-session Low-value < €30 TRA low fraud rate Challenge fallback
Walk the exemption ladder from cheapest to strongest — a challenge is the last resort, not the default.

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.

The order of the checks in planSca is deliberate and worth defending against well-meaning refactors. MIT is tested first because an off-session charge is categorically outside SCA scope — it can never legally carry a challenge, so no exemption reasoning applies. Low-value is tested before TRA because the low-value exemption costs the issuer nothing to track and does not consume your fraud-rate budget, whereas leaning on TRA for a 1,500-minor-unit charge burns exemption headroom you would rather reserve for the 20,000-minor-unit transactions where a challenge actually hurts. Reversing these branches produces the same authentication outcome per payment but a worse aggregate: you spend your scarce low-fraud-rate capacity on payments that a free exemption would have covered.

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).

There are two places an exemption can be claimed, and they behave differently. An acquirer exemption travels in the authorization message and asks the issuer to approve without SCA; an issuer exemption is the issuer deciding, from its own risk model, that the transaction is trusted (this is what powers most frictionless flows). You control the former by signalling the exemption on the intent; you influence the latter only by sending rich risk data — accurate billing address, prior transaction history on the customer_id, device fingerprint — through the 3DS2 message so the issuer’s ACS has enough signal to trust the payment on its own. A payment intent created with sparse data forces the issuer to fall back to a challenge even when it would otherwise have waved a returning customer through frictionlessly, so treat the 3DS2 data fields as conversion levers, not compliance boilerplate.

Idempotency and the retry envelope

Every branch here runs inside a checkout that customers retry, refresh, and double-submit. Pass an idempotency_key derived from subscription_id plus the billing period (or the cart hash for one-off sales) on paymentIntents.create, so a soft-decline retry, a network timeout, and an impatient second click all collapse onto the same intent rather than authorizing the customer twice. The forced-challenge retry in step 3 must reuse the original intent — creating a fresh one throws away the issuer’s prior context and can present the challenge a second time, which reads to the customer as a broken checkout and drives exactly the abandonment you are trying to avoid.

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

The tests use 3DS test cards to force each branch, unit-test the classifier’s exemption logic, verify the soft-decline retry, and assert liability-shift only on a full authentication. The panel lists them.

3DS2 routing tests Test cards force challenge and frictionless, the classifier returns MIT/low-value/challenge correctly, a soft decline retries with a challenge, and liability shifts only when authenticated. Test cards both branches challenge / frictionless Classifier planSca cases correct exemption Soft decline issuer overrides retry w/ challenge Liability authenticated shift = true
The soft-decline retry test guards conversion — an issuer can reject your exemption and you must not lose the sale.

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.

The boundary cases are where the classifier actually earns its keep, so test them explicitly rather than trusting the happy path. Assert that a charge of exactly 3000 minor units is not low-value (the threshold is strictly under €30, and an off-by-one that treats 3000 as exempt requests an exemption the issuer will reject); that a non-EUR currency never takes the low-value branch even below 3000, because the €30 figure is a euro-denominated limit and other currencies carry their own converted thresholds; and that an acquirerFraudRateBps of exactly 13 lands on the correct side of your tier boundary. Add a property-style test that feeds a random amount and fraud rate and asserts the plan never claims a TRA ceiling above what the fraud band allows — this catches the failure mode where a config change to the fraud-rate feed silently widens your exemptions.

Beyond deterministic branch tests, treat the challenge rate as a monitored SLO with an alert, not a number someone eyeballs. Emit a metric tagged by plan (mit, low_value, tra, challenge_required) and by outcome (frictionless, challenged, soft_declined, hard_declined) on every intent, and watch the soft-decline rate specifically: a jump in soft_declined means issuers in some BIN range have started rejecting an exemption you keep requesting, and every one of those is a customer who saw a spinner, then a challenge, then decided against the purchase. Segmenting the frictionless-approval rate by issuer BIN surfaces the handful of banks that reject exemptions wholesale, which is signal to route their cards straight to a challenge and skip the doomed exemption round-trip entirely.

Gotchas & Production Pitfalls

The pitfalls are about false assumptions: that exemptions shift liability, that the issuer honors them, that MIT works without a mandate, that low-value has no cap, and that attempted equals authenticated. The map groups them.

3DS2 routing pitfalls Exemptions keep liability, the issuer can override, MIT needs a mandate, low-value has cumulative caps, and attempted is not authenticated. Exemption keeps liability → deliberate choice Override issuer forces SCA → retry challenge MIT mandate not established → set up first Low-value cap cumulative → don't over-rely attempted ≠ authenticated → persist exact
Five false assumptions — the issuer override and the attempted-versus-authenticated distinction are the two that cost sales or chargebacks.
  • 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.

The exemption-then-challenge round-trip costs conversion too

There is a subtler pitfall than any single wrong assumption: the recovery path you built to protect conversion can itself erode it. When you request a TRA exemption and the issuer soft-declines, the customer waits through the first authorization attempt, then gets pulled into a challenge on the retry — a two-round-trip experience that is measurably slower and abandons harder than if you had challenged up front. For BIN ranges or amount bands where your soft-decline rate is high, requesting the exemption is negative expected value: you are paying a conversion penalty on the retry for an exemption the issuer was never going to grant. This is why the monitoring in the previous section feeds back into routing. Once you can see that a given issuer rejects exemptions on, say, 60% of transactions above 15,000 minor units, the correct move is to send those straight to challenge_required and accept the known 10% challenge cost rather than gamble on a 40% frictionless chance that carries an abandonment tail on every miss.

Scheme and regional scope drift

The rules are not static and they are not universal. The €30 low-value figure, the TRA fraud-rate bands, and even the list of currencies in scope shift as the EBA revises the RTS and as the UK diverges from the EU post-Brexit — a payment routed through a UK acquirer plays by the FCA’s timeline, not the ECB’s. Cards issued outside the EEA are generally out of SCA scope entirely, so a hard-coded planSca that challenges a US-issued card on a EUR transaction is throwing away conversion for a rule that does not apply. Key the exemption logic on issuer region and acquirer region rather than currency alone, and keep the thresholds in configuration you can update without a deploy, because the day a regulator moves a limit is the day your challenge rate and your chargeback rate both move with it.

Frequently Asked Questions

Do exemptions always reduce friction? Only when the issuer accepts them. An exemption request that the issuer declines results in a challenge anyway, plus a round trip, so the exemption strategy should be measured on final authorisation rate rather than on challenge rate alone.

What happens to liability when a challenge is completed? For most card schemes, completing an authentication shifts liability for fraud-related disputes to the issuer. That is the commercial reason to accept some friction on higher-value or higher-risk transactions.

How should off-session renewal authentication be handled? Not by retrying. Email the customer a link to complete the authentication, because nobody is present to respond to a challenge during a scheduled renewal.

Does authentication apply outside Europe? The regulatory requirement is regional, but the technology and the liability shift are not. Some issuers elsewhere challenge transactions too, so the flow has to handle it globally even where it is not mandated.