VORA Mirror — 3D Secure
3D Secure (3DS / SCA) authentication is the issuer-side challenge that proves the buyer is the cardholder. VORA Mirror handles 3DS uniformly across every supported card processor — the merchant sees one challenge contract regardless of which underlying provider their account is configured for.
This page covers how 3DS surfaces in your code, the modal-harmonization toggle, the cancel + timeout paths, and what test cards to use.
When 3DS fires
3DS triggers on the binder side, not in your code. You don't decide whether to challenge — the issuer does. From your point of view:
- You call
vora.elements.create('card').tokenize()(orcollection.submit()for a collection) - If the issuer requires 3DS, VORA Mirror intercepts the binder's challenge and renders it
- On success, you get the
vp_pmt_*token as if 3DS had never happened - On failure / cancel / timeout, you get a
FrameErrorwith a 3DS-specific code
The challenge UI runs before VORA Mirror returns the token. There is no separate "wait for 3DS" callback in the merchant code.
The VORA 3DS modal
Different card processors render their native 3DS sheet in different ways — some use an iframe overlay, some use their own modal style, some redirect to a hosted page. To give buyers a consistent checkout experience across every supported processor, VORA Mirror wraps each provider's challenge UI in a VORA-styled modal:
- Backdrop — semi-transparent dark layer over your checkout
- Spinner + status text — "Authenticating with your bank…"
- Cancel button — buyer can abandon the challenge cleanly
- Accent color — inherits from the card element's
style.color.primary
The modal renders by default. The provider's native sheet shows on top of it (overlay mode or in-modal-iframe, depending on what the active processor's adapter declares via its threeDsMode).
Opting out — disable3dsModal
If your checkout's UX requires the binder's native sheet to render directly (no VORA chrome — for full custom branding, third-party analytics hooked to the binder's challenge events, etc.), pass disable3dsModal: true on the card element:
const card = collection.create("card", {
disable3dsModal: true,
});
The merchant flag wins over any per-adapter default. When disable3dsModal: true, the binder's native sheet shows directly with no VORA chrome.
Cancel + timeout paths
Cancel
The VORA modal includes a Cancel button. If the buyer clicks Cancel during the challenge:
catch (err) {
if (err.code === "frame_3ds_challenge_failed") {
// err.message will mention "User cancelled" if cancel was the cause
// Surface a "you cancelled — try again or use a different card" message
}
}
Timeout
Default timeout is 10 minutes (challengeTimeout: 600_000). The buyer can override per element:
const card = collection.create("card", {
challengeTimeout: 300_000, // 5 minutes
});
If the timeout elapses without buyer interaction:
catch (err) {
if (err.code === "frame_3ds_challenge_timeout") {
// Surface "the bank's authentication timed out — please try again"
}
}
The timer starts when the challenge UI mounts. Networks-side delays before the challenge appears don't count against the timeout.
Two server-driven flows
For most integrations, the synchronous tokenize-then-charge path is enough — call tokenize() / submit(), send the vp_pmt_* to your server, your server creates a payment_intent with confirm: true, and the result tells you whether the charge succeeded. 3DS happens transparently inside tokenize().
Two patterns exist for cases where your server needs more control:
Tokenize → server creates intent → forward client_confirm to browser → SDK confirms
This is the canonical end-to-end pattern as of 2026-05-10. Three round-trips:
1. Browser tokenizes the card
const result = await collection.submit();
// result.token === "vp_pmt_test_..."
2. Browser sends the token to your server; server creates the intent
// Browser
const intent = await fetch("/api/charge", {
method: "POST",
body: JSON.stringify({ payment_method: result.token, amount: 4999 }),
}).then((r) => r.json());
// server/api/charge.ts (Node example)
app.post("/api/charge", async (req, res) => {
const response = await fetch(`${API}/v1/payment_intents`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${SECRET_KEY}`, // vp_sk_test_*
},
body: JSON.stringify({
amount: req.body.amount,
currency: "USD",
payment_method: req.body.payment_method,
confirm: true,
}),
});
const intent = await response.json();
// For NON-3DS cards: intent.status === "succeeded", you're done.
// For 3DS cards: intent.status === "requires_action" + intent.client_confirm populated.
if (intent.status === "requires_action" && intent.client_confirm) {
// Forward the client_confirm block to the browser. Server must NOT
// log this value or persist it beyond the request — `client_secret`
// is bearer-equivalent for this single intent.
return res.json({
id: intent.id,
status: intent.status,
action: intent.client_confirm, // forwarded as-is to the SDK
});
}
// Non-3DS path
res.json({ id: intent.id, status: intent.status });
});
3. Browser passes action back into the SDK; the SDK renders the 3DS modal
// Browser — back in the submit handler
if (intent.status === "requires_action") {
const confirmed = await collection.submit({
paymentIntent: { id: intent.id, action: intent.action },
});
// confirmed.status === "succeeded" if 3DS passed
}
The SDK treats intent.action as opaque — each binder adapter unwraps it internally and calls the appropriate confirmation method on the underlying processor. Your code never sees inside action.
The harmonized 3DS modal renders during step 3. Buyer authenticates with their bank; result returns to the merchant page. Both disable3dsModal: false (default — VORA chrome) and disable3dsModal: true (binder native sheet) work the same way — just different UI.
confirmPaymentIntent (React hook variant)
For React integrations, the same flow runs through the useVORA() hook:
import { useVORA } from "@vonpay/vora-react";
const { confirmPaymentIntent } = useVORA();
// 1. Tokenize
const { token } = await collection.submit();
// 2. Server creates a manual-confirm intent (same as above)
const intent = await fetch("/api/charge", {
method: "POST",
body: JSON.stringify({ payment_method: token, amount: 4999 }),
}).then((r) => r.json());
// 3. SDK handles the 3DS challenge
if (intent.status === "requires_action") {
const result = await confirmPaymentIntent(intent);
// result.status: "succeeded" | "requires_action" | "failed"
}
handleAction — next-action flow
Your server returns a nextAction object directly:
const result = await handleAction(intent.nextAction);
Same end-state as confirmPaymentIntent. Use this when your server is already wrapped in a third-party SDK that produces next_action-shaped payloads instead of VORA's client_confirm block.
All three flows render inside VORA Mirror's harmonized 3DS modal (or the binder's native sheet when disable3dsModal: true).
Test cards
| Card | Behavior |
|---|---|
4242 4242 4242 4242 | Tokenizes; no 3DS challenge |
4000 0025 0000 3155 | 3DS2 / SCA always-authenticate — recommended for VORA 3DS modal testing |
4000 0027 6000 3184 | Older 3DS1 always-authenticate; behavior may vary across processor configurations |
4000 0000 0000 9995 | Tokenizes; charge declines at payment_intents.create (no 3DS) |
Any future expiry, any 3-digit CVC. The frame-react sample's test recipe walks the modal-on / modal-off / cancel / timeout paths step by step.
For binder-specific test cards, see:
- See your active processor's test-card documentation (linked from your dashboard's integration details page)
- Reference → Test cards for the cross-binder matrix
Error codes
3DS surfaces three FrameErrorCode values; full handling reference on the errors page:
| Code | When |
|---|---|
frame_3ds_challenge_failed | Buyer failed the challenge (wrong code, cancelled, issuer rejected) |
frame_3ds_challenge_timeout | Buyer didn't complete within challengeTimeout |
frame_tokenization_failed | Binder rejected the card outright (decline before challenge fired) |
What's next
- Quickstart — end-to-end card-only happy path (no 3DS)
- React —
useVORA()patterns includingconfirmPaymentIntent+handleAction - Errors — full
FrameErrorreference samples/frame-react— interactive 3DS modal toggle in the sample app