3D Secure
Handle the 3DS challenge redirect and resume the flow after the customer returns
Overview
3D Secure (3DS2 / EMV 3DS) is required for most European card payments under PSD2 Strong Customer Authentication rules. The gateway handles the orchestration with the acquirer — your integration only needs to handle the browser redirect and the status check after the customer comes back.
When does 3DS trigger?
- When the card's issuer requires it (the common case for European cards).
- When you force it explicitly by sending
"3DS": "yes"on the payment request. - When your account is configured as 3DS-only — in that case payments without
3DS=yesfail with error code15.
Some combinations are mandatory:
| Scenario | Behaviour |
|---|---|
| Maestro card | Always requires 3DS. Missing 3DS=yes returns code 22. |
| Account not allowed to use 3DS | 3DS=yes rejected with code 8. |
3DS=yes sent without ReturnUrl | Rejected with code 106. |
Flow
┌────────────┐ 1. POST /v1/payment/direct ┌─────────────┐
│ Merchant │ ─────────────────────────────▶ │ Gateway │
│ server │ │ │
│ │ ◀───── { 3DSecureUrl } ──────── │ │
└────────────┘ └─────────────┘
│
│ 2. Return 3DSecureUrl to the browser
▼
┌────────────┐ 3. Browser redirected ┌─────────────┐
│ Customer │ ─────────────────────────────▶ │ Gateway │
│ browser │ │ hand-off │
│ │ hands the customer on ──▶ │ page │
└────────────┘ └─────────────┘
│ │
│ ▼
│ ┌─────────────┐
│ 4. 3DS challenge │ Card issuer │
│ ◀───────────────────────────────────── │ ACS page │
▼ └─────────────┘
│
│ 5. Customer redirected back
▼
┌────────────┐ 6. POST /v1/status ┌─────────────┐
│ Merchant │ ─────────────────────────────▶ │ Gateway │
│ server │ ◀────── { Status: ... } ────── │ │
└────────────┘ └─────────────┘
│
│ 7. Settleflow also fires a webhook
▼ asynchronously to your configured endpoint3DSecureUrl is always a URL on the API host — never the card issuer's or the provider's own
address. The customer lands on a short hand-off page that passes them to the challenge. Follow the
URL as-is: do not parse it, store it, or allow-list a destination domain from it. It is
single-purpose and short-lived (about an hour), so mint a new payment rather than reusing an old
link.
Step-by-step
1. Request the payment with 3DS enabled
curl -X POST https://api.sandbox.settleflow.io/v1/payment/direct \
-H "epro-api-key: pk_test_..." \
-d '{
"Amount": "4999",
"Uid": "customer-42",
"Tid": "order-2026-001",
"Email": "jane@example.com",
"CardNumber": "4000000000003220",
"CardMonth": "12",
"CardYear": "2028",
"CardCVV": "123",
"3DS": "yes",
"ReturnUrl": "https://your-shop.com/payment/return"
}'2. Inspect the response
{
"Code": 0,
"Result": {
"OperationType": "payment",
"Status": "pending",
"Tid": "order-2026-001",
"Reference": "pr_abc123",
"3DSecure": "yes",
"3DSecureUrl": "https://api.settleflow.io/redirect/3ds/pa_9f2c1d..."
}
}When 3DSecure=yes, the 3DSecureUrl field is present and Status is pending.
3. Redirect the customer
Return an HTTP 302 from your server, or open 3DSecureUrl in the customer's browser. They land on a brief hand-off page and are then taken to the issuer's challenge page.
res.redirect(302, result["3DSecureUrl"]);4. Handle the return to ReturnUrl
After the challenge, the customer is sent back to the ReturnUrl you supplied. By default the gateway does this with an HTTP POST — an auto-submitted form carrying the outcome as body fields:
The body carries the standard response — the same fields you get from /v1/status and from a webhook:
| Field | Example | Meaning |
|---|---|---|
OperationType | payment | Always payment on this return. |
Status | captured | captured, authorized, pending, failed or cancelled. |
Tid | order-42 | Your own order reference, as sent in step 2. |
Reference | pr_abc123 | Our payment reference. |
Date | 2026-06-06 10:00:00 | Transaction date. |
Amount | 10.00 | Amount in major units. |
UserId | customer-7 | The Uid you sent in step 2. |
Message | Payment was successful | Human-readable outcome. |
3DSecure | yes | Whether the payment was authenticated. |
Error | Expired card | Acquirer reason — present only when Status is failed. |
So your ReturnUrl endpoint must accept POST, and any query string you put on the URL yourself is preserved untouched.
The return method is configured per application. If your integration expects a GET redirect
instead — the same fields arriving as query parameters — ask us to switch your application to
GET. Do not implement both and guess.
Whatever the method, treat this return as a hint, not as the source of truth: the customer may never come back. Steps 5 and 6 are what actually settle the order.
5. Confirm the final status
Query POST /v1/status to resolve the payment:
curl -X POST https://api.sandbox.settleflow.io/v1/status \
-H "epro-api-key: pk_test_..." \
-d '{ "Reference": "pr_abc123" }'Possible outcomes:
Status | What to show the customer |
|---|---|
captured | Success — order confirmed. |
authorized | Success — capture pending. |
pending | Still processing. Retry the status call after 1–2 s, or display a processing screen. |
failed | Challenge failed or declined — surface a recoverable error. |
cancelled | Customer abandoned the challenge. |
See the polling pattern on the status page for a defensive retry loop.
6. Trust the webhook for authoritative state
Even if the customer never returns to your ReturnUrl (closed tab, mobile browser lost focus…), the gateway will still deliver a webhook to your configured endpoint. Use that as the source of truth for order fulfillment — see Webhooks.
Testing
Use the dedicated 3DS test card in sandbox:
| Card number | Behaviour |
|---|---|
4000000000003220 | Forces a 3DS challenge and returns a 3DSecureUrl in sandbox. |
See Sandbox & test cards for the full list.