Skip to content

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 enteredWho runs the checkYou do
Our hosted page (approval_url)The pageNothing
Your checkout (@tuppence/js)sheet.confirm(), in a frame over your pageShow error.message if it fails
A saved card, nobody thereNobody can: the payer is brought backSend them the approval_url

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.

Nothing to do. The page runs the check if the bank asks, and you receive payment.succeeded.

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

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.

EventWhen
payment.requires_actionThe bank wants to check the payer. Sent each time it asks
payment.succeededThey passed and the payment went through
payment.failedThey 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.

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:

PayerYour serverTuppencePOST /v1/payments/charge_savedauthentication_requiredpayment.requires_actionapproval_urlsaved card + bank's checkpayment.succeeded
  • status is requires_payment_method, with last_error.code authentication_required.
  • You receive payment.requires_action, not payment.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’s client_secret and tuppence.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.

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.

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.

Test cards, entered in the card fields or on our hosted page, open a test challenge with buttons to pass or fail it:

Card numberServer-side nameWhat happens
4000 0025 0000 3155pm_card_authenticationRequiredThe bank checks every payment, including charges to the saved card
Nonepm_card_authenticationRequiredOnSetupThe bank checks only when the card is saved
4000 0582 6000 0005pm_card_gbNo 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.

Docs assistant

Open as a page
Loading the assistant…