3-D Secure
3-D Secure (3DS) is the step where the cardholder authenticates with the issuing bank, usually with an SMS code or a banking app. The platform runs it for you; your integration redirects the customer and reacts to the result.
When 3DS happens
Control it per payment with payment_method_options.card.three_d_secure:
| Value | Behavior |
|---|---|
automatic (default) |
The platform decides. Payments within your non-secure limits that pass fraud screening may skip 3DS; everything else uses it. |
required |
Always authenticate. |
none |
Never authenticate. Only possible when your account is allowed non-secure payments and the amount is within your limits; otherwise the call fails with 400 non_secure_not_allowed. |
The payment reports what was requested and what applied in three_d_secure.requested and three_d_secure.effective.
Register the return URL first
After authentication the customer is sent back to your return_url. That URL must be on a website you registered or match a callback URL you registered under Settings → Websites & callbacks in the merchant panel (see the authentication guide). Otherwise the payment is refused with 400 return_url_not_allowed and param: return_url. Always send return_url when 3DS may be needed, on POST /v1/payment_intents or on POST /v1/payment_intents/{id}/confirm.
The flow
- Confirm the payment (create with
confirm: true, or call/confirm). - If authentication is needed the response has
status: requires_actionand anext_action. - Send the customer where
next_actionsays. - The customer authenticates at the bank. The bank reports back to the platform, which completes the payment.
- The platform redirects the customer to your
return_urlwithpayment_intent=pi_…appended to the query string. - On your return page, read the payment on your server and show the outcome. Do not trust the redirect itself.
Step 2 and 3: next_action
next_action.type is one of:
redirect_to_url: send the customer's browser tonext_action.redirect_to_url.url(an HTTP redirect, or a link).html_form: outputnext_action.html_form.htmlin the page you return to the browser; it posts the customer to the bank's page. Render it as-is.
{
"id": "pi_01J9ZQ4V8K2M3N5P7R9T1W3Y5B",
"object": "payment_intent",
"amount": 12500,
"currency": "try",
"status": "requires_action",
"capture_method": "automatic",
"three_d_secure": {"requested": "automatic", "effective": "required"},
"next_action": {
"type": "redirect_to_url",
"redirect_to_url": {
"url": "https://pay.example.com/3ds/start/att_01J9ZQ4V8K2M3N5P7R9T1W3Y5C",
"return_url": "https://shop.example.com/pay/return"
}
},
"return_url": "https://shop.example.com/pay/return",
"created": 1790000000,
"livemode": false
}
Step 6: your return page
curl https://pay.example.com/v1/payment_intents/pi_01J9ZQ4V8K2M3N5P7R9T1W3Y5B \
-H "Authorization: Bearer sk_sandbox_YOUR_KEY"
status |
Meaning |
|---|---|
succeeded |
Paid (automatic capture). Fulfil the order. |
requires_capture |
Authorized, waiting for your POST /v1/payment_intents/{id}/capture (capture_method: manual). |
processing |
The bank has not answered yet. Wait for the webhook; do not charge again. |
requires_payment_method |
Authentication or the payment failed. last_payment_error has the reason (for example authentication_failed); let the customer try another card. |
requires_action |
The customer is still at the bank. |
When the customer never comes back
Customers close tabs. Do not depend on the return redirect: subscribe to the payment_intent.succeeded, payment_intent.payment_failed and payment_intent.requires_action webhooks (see the webhooks guide) and reconcile on those. If you hold an order open, check it with GET /v1/payment_intents/{id} before releasing the stock.
Testing
In the sandbox, test cards cover the successful challenge, a failed authentication, a card that requires 3DS and an abandoned challenge. See the sandbox guide.