Skip to content

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:

  1. Receive the requires_action response from POST /v1/invoices/{id}/pay.
  2. 302-redirect the user to next_action.url.
  3. 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_url on the pay call).
  4. On return, the payment has settled to succeeded or failed.
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):

  1. Receive next_action.payment_intent_client_secret.
  2. Pass it to the provider’s client-side SDK (Stripe.js, PayPal SDK, etc.) — they handle the challenge UI and call the provider directly.
  3. On success, the SDK returns; Paylera receives the provider’s webhook and updates the payment to succeeded.
  4. Your backend listens for payment.succeeded and 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} returning status: "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_action and subscription.past_due.
  • Email the customer with a link to a hosted action page: GET /v1/payments/{id}/action-url.

Common pitfalls

  • Treating requires_action as 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

EventWhen
payment.requires_actionThe payment needs a customer action.
payment.action_completedThe customer completed the action; capture is in progress.
payment.succeededCapture succeeded.
payment.failedCapture failed (including SCA rejected).