Handle 3DS / SCA
Some cards require an extra authentication step before the charge can
complete — Strong Customer Authentication (SCA) under PSD2 in Europe,
3DS challenges in many other regions. Paylera surfaces this through a
single, structured response: payment.requires_action.
The contract
When a payment can’t capture without authentication, the response is
not an error. It’s a 200 with status: "requires_action" and a
next_action block describing what to do:
{ "id": "pay_…", "status": "requires_action", "amount": "49.00", "currency": "USD", "next_action": { "type": "redirect_to_url", "url": "https://hooks.paylera.io/3ds/p_a3b4…" }}Or for newer PSD2 flows where the challenge is rendered inline:
{ "next_action": { "type": "use_provider_sdk", "provider": "stripe", "payment_intent_client_secret": "pi_…_secret_…" }}Server-driven flow (redirect)
Use this when you have control of the browser and just want it to work:
- Receive the
requires_actionresponse fromPOST /v1/invoices/{id}/pay. - 302-redirect the user to
next_action.url. - Paylera hosts the 3DS page, runs the challenge, and redirects the
user back to a return URL you provided (default: the hosted-checkout
completion page; or set
return_urlon the pay call). - On return, the payment has settled to
succeededorfailed.
POST /v1/invoices/{id}/pay{ "payment_method_id": "pm_…", "return_url": "https://yourapp.example/billing/done?payment_id={PAYMENT_ID}"}{PAYMENT_ID} is replaced by Paylera before redirect.
Client-driven flow (provider SDK)
Use this when you need the challenge to render inline (modal, no full redirect):
- Receive
next_action.payment_intent_client_secret. - Pass it to the provider’s client-side SDK (Stripe.js, PayPal SDK, etc.) — they handle the challenge UI and call the provider directly.
- On success, the SDK returns; Paylera receives the provider’s
webhook and updates the payment to
succeeded. - Your backend listens for
payment.succeededand updates UI.
For Stripe specifically:
const { error } = await stripe.handleNextAction({ clientSecret: next_action.payment_intent_client_secret});Confirming completion
Two signals say the payment is done:
- The webhook event
payment.succeeded(authoritative). - A poll of
GET /v1/payments/{id}returningstatus: "succeeded".
Don’t show “payment complete” to the user just because they came back from the redirect — the redirect is a UX cue, not a confirmation. Wait for the webhook (or poll once after the redirect to be safe).
Failures during 3DS
If the customer fails the challenge (wrong code, abandoned, expired),
the payment transitions to failed with last_error.code: "authentication_failed". The invoice stays open; you can retry with a
different method or invite the customer to try again.
Off-session vs on-session
A subscription’s recurring charges are off-session — the customer isn’t sitting at the keyboard. PSD2 generally exempts these from SCA if the first payment used SCA (initial credential-on-file consent). Paylera flags this automatically: the SetupIntent and first charge collect SCA; subsequent charges are exempted where the rules allow.
If a recurring charge does require step-up (rare; bank discretion):
- The payment goes to
requires_action. - Paylera emits
payment.requires_actionandsubscription.past_due. - Email the customer with a link to a hosted action page:
GET /v1/payments/{id}/action-url.
Common pitfalls
- Treating
requires_actionas an error. It’s not — your code must branch on it explicitly. - Calling pay again before the action completes. The original
payment is still
processing; a second pay creates a duplicate attempt. Wait for the webhook or for the user to return. - Not setting
return_url. Your customer ends up on a generic Paylera “thanks” page; better UX to send them home.
Webhook events
| Event | When |
|---|---|
payment.requires_action | The payment needs a customer action. |
payment.action_completed | The customer completed the action; capture is in progress. |
payment.succeeded | Capture succeeded. |
payment.failed | Capture failed (including SCA rejected). |