3-D Secure
3-D Secure is the payer’s bank checking it’s really them: a prompt in their banking app, a text message code, or a check they don’t see. Under Strong Customer Authentication (SCA) in the UK and Europe, banks ask on many card payments. The bank decides when, not you or us.
| Where the card is entered | Who runs the check | You do |
|---|---|---|
Our hosted page (approval_url) | The page | Nothing |
Your checkout (@tuppence/js) | sheet.confirm(), in a frame over your page | Show error.message if it fails |
| A saved card, nobody there | Nobody can: the payer is brought back | Send them the approval_url |
What the payer sees
Section titled “What the payer sees”The check opens in a secure frame over the payment page, so the payer never leaves it. If they pass, the payment continues automatically. If they fail or close it, nothing is charged and they can try again or use another card. The check also runs when a card is saved, so later charges with nobody there need the payer back less often.
On our hosted page
Section titled “On our hosted page”Nothing to do. The page runs the check if the bank asks, and you receive payment.succeeded.
In your own checkout
Section titled “In your own checkout”@tuppence/js runs the check inside confirm():
const { error, payment } = await sheet.confirm();if (error) { showMessage(error.message); // declined, or the bank's check failed: written for the payer} else { showConfirmation(payment.status); // "succeeded", or "requires_capture" for a manual capture}A refused card or failed check returns error, never an exception, and the payment stays payable.
If the response is lost in transit, the SDK reads back what happened rather than presenting the card
again. To resume a half-finished check (resumePayment) or pay with a saved card on your page, see
the browser SDK.
What your server sees
Section titled “What your server sees”While the bank checks, the payment is requires_action with next_action set:
{ "id": "pay_01K…", "status": "requires_action", "next_action": { "type": "use_stripe_sdk", "stripe_client_secret": "pi_…_secret_…" }, "approval_url": "https://pay.tuppence.ai/pay/pay_01K…"}next_action is for the payer’s browser (its field names come from the card processor; you never
use them yourself). @tuppence/js and the hosted page complete it; a server
can’t. If your server confirmed the payment, send the payer to the approval_url.
| Event | When |
|---|---|
payment.requires_action | The bank wants to check the payer. Sent each time it asks |
payment.succeeded | They passed and the payment went through |
payment.failed | They failed the check or the card was declined. The payment returns to requires_payment_method with last_error, and is payable again |
Fulfil on payment.succeeded (webhooks). requires_action isn’t a failure:
most payers pass.
Saved cards, with nobody there
Section titled “Saved cards, with nobody there”A charge to a saved card (POST /v1/payments/charge_saved) is off-session: we tell the
bank the payer isn’t present and agreed to a mandate. Most banks don’t ask. When one insists, it’s
an outcome, not an error:
statusisrequires_payment_method, withlast_error.codeauthentication_required.- You receive
payment.requires_action, notpayment.failed. - Nothing is charged until the payer returns to the same payment: at its
approval_url(the page offers their saved card), or in your checkout with the payment’sclient_secretandtuppence.confirmPayment({ clientSecret, savedCard: true }).
Running the check when you save the card (a
setup session, or save_card on a payment)
makes this rarer, because the bank has already seen the payer agree.
Charges far out of character
Section titled “Charges far out of character”A charge far out of character for the API key making it is held for the payer in the same way, and
their bank is always asked to check: step_up: true, last_error.code authentication_required.
See a charge far out of character.
Agents
Section titled “Agents”An agent can create and confirm payments, but only the payer can pass a bank’s check. When a
payment needs one, the agent gives its user the approval_url. Over MCP, the payment
tools return it.
Testing it
Section titled “Testing it”Test cards, entered in the card fields or on our hosted page, open a test challenge with buttons to pass or fail it:
| Card number | Server-side name | What happens |
|---|---|---|
4000 0025 0000 3155 | pm_card_authenticationRequired | The bank checks every payment, including charges to the saved card |
| None | pm_card_authenticationRequiredOnSetup | The bank checks only when the card is saved |
4000 0582 6000 0005 | pm_card_gb | No check: succeeds |
Confirming server-side with pm_card_authenticationRequired returns the requires_action response
above. To finish, open the approval_url and pass the challenge. On the simulated gateway (local
development and CI), the hosted page shows a pretend bank check instead. See
testing.
- Take a payment: the whole flow.
- The browser SDK: resuming a check.