Embedded Fields — Error handling
Embedded Fields' error surface is VoraMirrorError — a typed exception with a stable code, a human-readable message, and optional sub-hints. Server-side errors from your own calls to /v1/* are a different surface: Reference → Error codes. FrameError is a deprecated alias of the same class.
Detection
The browser SDK is a <script> from js.vonpay.com, so the helpers live on the Vora global:
const { VoraMirrorError, isVoraMirrorError } = Vora;
try {
await vora.sessions.retrieve(sessionId); // required before creating a card element
const elements = vora.elements.create();
const card = elements.create("card", {});
card.mount("#card-element");
// submit() RETURNS tokenize / validation / 3DS errors on result.error — it does
// NOT throw for those. Branch on the result for the payment-path errors:
const result = await elements.submit();
if (result.error) {
// result.error is a VoraMirrorError; result.error.code is one of the union below
console.error(result.error.code, result.error.message);
}
} catch (err) {
// Thrown errors: session-retrieve failures, frame_session_not_ready (create()
// before retrieve), insecure-context / CSP mount failures, and programming
// bugs (TypeError on construction, etc.).
if (isVoraMirrorError(err)) {
console.error(err.code, err.message);
} else {
throw err;
}
}
Always use isVoraMirrorError() — instanceof VoraMirrorError can fail across module boundaries (bundle-splitting / multiple package copies).
Code union
type VoraMirrorErrorCode =
| "frame_insecure_context"
| "frame_session_not_found"
| "frame_session_expired"
| "frame_session_not_ready"
| "frame_provider_request_rejected"
| "frame_binder_load_failed"
| "frame_unsupported_element"
| "frame_field_validation_failed"
| "frame_tokenization_failed"
| "frame_charge_in_progress"
| "frame_3ds_challenge_failed"
| "frame_3ds_challenge_timeout"
| "frame_3ds_challenge_cancelled"
| "frame_3ds_required"
| "frame_payment_declined"
| "frame_wallet_unavailable"
| "frame_wallet_domain_unverified"
| "frame_wallet_rendered_by_card_element"
| "frame_style_invalid"
| "frame_consent_requires_buyer"
| "frame_buyer_required_for_saved_card"
| "frame_sandbox_account_required"
| "frame_wallet_total_mismatch"
| "frame_wallet_confirm_failed"
| "frame_method_not_supported_for_session"
| "frame_rate_limited";
The frame_* strings are the stable contract; VoraMirrorErrorCode is the TypeScript name and FrameErrorCode its deprecated alias.
Codes by recovery family
Configuration / setup errors (fix the integration)
| Code | When it fires | Recovery |
|---|---|---|
frame_insecure_context | new Vora(...) outside HTTPS / localhost | Serve the page over HTTPS in production. localhost is allowed for development. |
frame_session_not_found | Session ID is unknown to VORA | Verify the merchant's vp_sk_* is for the same merchant whose session you're loading. Check test/live mode parity (publishable key prefix vs session ID prefix must match). |
frame_unsupported_element | elements.create(type, ...) called for an element the active session's binder doesn't support — or payment-method-picker's methods all filter out against per-binder capabilities, or an express-checkout element given collect, shippingOptions or onShippingAddressChange on a payment connection that cannot collect details in the wallet sheet | The active binder declared not_available (or monolith, for elements whose surface the binder renders natively — e.g. a processor that renders its own payment-method picker as part of an all-in-one embed). Swap the element type, route the merchant to a binder that supports it, or — for picker-on-monolith — remove the element from your elements.create() list and rely on the binder's native UI. The error message identifies which binder and which element. |
frame_method_not_supported_for_session | The deprecated card.tokenize() single-field path was called against a single-embed binder session | "card.tokenize() is deprecated. Use vora.elements.create() + collection.create('card') + collection.submit() to charge and save. Your server configuration is fine." This is an integration-time wrong-method condition (single-embed binder) — not a server bug, not high-severity, and should not page. Switch to the canonical vora.elements.create() + collection.create('card') + collection.submit() path. |
frame_style_invalid | Style option contains a key outside the allowlist | The thrown error's property field names the offending key. Drop it or alias to a supported VoraFieldStyle key. |
frame_consent_requires_buyer | The buyer ticked the save-card box on a session with no buyer attached, and the card was silently not saved. This never throws — the buyer was charged. | Create the session with a buyerId (or buyer email) before offering the save-card control, or don't render the control on guest sessions. ⚠ Consent is not repairable afterwards — it is recorded when the card is first vaulted and cannot be added later, so the buyer must present the card again. See Recurring & saved cards. |
frame_sandbox_account_required | A test key was used where test payments cannot run: on a live account, or on a sandbox whose processor test account is not set up. Nothing was charged. | Retrying never helps. Use your sandbox account's test keys; if it is your sandbox, open it in Developer Tools to finish setting up test payments. Server-side name: sandbox_account_required. |
frame_buyer_required_for_saved_card | Same missing buyer, but on a session whose gateway vaults the card as part of taking the payment — so it refused outright. submit() rejects and nothing was charged. | Do not fulfil. Do not retry this session: the buyer is fixed at session creation, so the same call fails the same way forever. Create a new session with buyerId and have the buyer enter the card again. Server-side name: buyer_required_for_saved_card. |
Which one you get depends on the gateway, so read the code before you decide what to do — the first means the buyer paid, the second means nothing was charged.
Transient errors (retry / refresh)
| Code | When it fires | Recovery |
|---|---|---|
frame_session_expired | The session's TTL elapsed before tokenize | Create a fresh session on your server, then vora.sessions.retrieve(newId). Sessions default to 30 minutes. |
frame_session_not_ready | An action method (tokenize, confirmPaymentIntent, handleAction) was called before ready === true, or submit() was called while collection.fetchUpdates() was still reading a new total (nothing was charged) | Wait for ready === true before exposing your submit button. In React, gate the button render on ready. After a total change, wait for fetchUpdates() to resolve, show the buyer the new total, then submit. |
frame_binder_load_failed | The active processor's CDN script failed to load | Inspect the hint field — network_error / csp_blocked / script_blocked / timeout. Most commonly fixed by adding the processor's domain to your CSP script-src; see Reference → Security. |
frame_3ds_challenge_timeout | Buyer didn't complete 3DS challenge within challengeTimeout (default 5 min / 300_000 ms) | Surface a "the bank's authentication timed out" message; let the buyer retry. |
frame_rate_limited | The publishable-key endpoints (sessions.retrieve, binder-load, the embed-token poll) are per-IP rate-limited (HTTP 429) and the caller exceeded the ceiling — usually rapid reloads or a tight retry loop | Back off and retry after a short delay; honor the Retry-After header if present. Do not retry immediately in a loop — that deepens the throttle. Not a CSP/binder/session problem. |
Buyer-action errors (surface a message)
| Code | When it fires | Recovery |
|---|---|---|
frame_field_validation_failed | Card number / expiry / CVC failed VORA's pre-tokenize validation | Already surfaced inline by the iframe. Disable the submit button until the change event reports complete: true. |
frame_tokenization_failed | The adapter rejected the card before any charge — a genuine pre-charge rejection — or the card form was disconnected because the SDK was initialised twice (see below). Read error.message before telling the buyer anything. | Show a generic "your card couldn't be authorized" message and suggest a different payment method. The full reason is logged server-side; do not leak adapter messages to buyers. |
frame_3ds_challenge_failed | The challenge ran and was rejected, or failed technically. Not buyer cancellation — that has its own code below. | Show "authentication failed — please try again or use a different card" and re-enable the submit button. |
frame_3ds_challenge_cancelled | The buyer closed the bank's confirmation step. The card was not charged. | An abandoned checkout, not a failure: re-enable the submit button and let them retry; do not show an error or alert on it. |
frame_provider_request_rejected | The provider refused the request itself — not a card decline. Common culprits: currency, amount, descriptor/metadata length. | Do not retry unchanged, and do not ask the buyer for another card — the same request fails identically and the card was never the problem. Fix the offending field; if the culprit isn't obvious, quote the request id to support. |
frame_3ds_required | Charge at submit and standalone wallets only: a challenge was required but no usable challenge URL came back, so the charge failed closed — nothing was charged. (On the tokenize-only flow there is no SDK error for this; the server's status: "requires_action" is the signal — see 3D Secure.) | Re-running submit() will not help — a different card fails identically; quote the session id to support. See Charge at submit → 3-D Secure. |
frame_payment_declined | The charge was attempted and the issuer declined it (flows where the embed charges at submit) | Show a generic "your payment was declined — please try a different card" message. Then, on vora.js 1.38.0 and later, read error.retryable: true means the card form has already been reset on this session, so re-enable the submit button; false means the checkout session is finished, so create a new session for the buyer to pay again (a resubmit on this one is refused before anything is sent). On 1.37.0, re-enable the submit button. The decline reason is logged server-side. |
On the charge-and-save flow, a successful guest / no-buyer submit charges the card once and returns no token by design — { charged: true }, not frame_tokenization_failed. Branch on result.charged before treating an absent token as a failure, and never re-charge after a charged result.
frame_tokenization_failed also means "you initialised the SDK twice"
One condition reports frame_tokenization_failed with a different message and a different fix:
The card form was disconnected before the card could be saved. This happens when a second card-fields instance is created on the same page — initialize the SDK once per page, then retry.
The buyer's card is fine. A second card-fields instance on the page tore down the first one's fields — the vendor holds one card-field set per page. Initialise once per page and reuse that instance. The code is the same, so only the message distinguishes them: read error.message before choosing what to show the buyer.
Duplicate-charge guards (do not retry)
| Code | When it fires | Recovery |
|---|---|---|
frame_charge_in_progress | A charge for this session is already in flight, and this request was refused to stop a duplicate. The first attempt has not failed — it has not returned yet. | Do not retry, and do not create a new session. On some processor configurations a second attempt is a second charge that nothing can merge afterwards. Wait for the original request to return; if it never does, read the outcome from the session. The charge.* webhook is authoritative. |
Wallet errors (Apple Pay / Google Pay)
None of these blocks the card flow. They arrive on the change event with complete: false, so analytics can record wallet-coverage gaps.
| Code | When it fires | Recovery |
|---|---|---|
frame_wallet_domain_unverified | An express-checkout element mounted and either no wallet is domain-verified for this checkout, or no requested wallet is available on the buyer's device or browser — error.message says which. | Domain case: register your domain with each wallet network (Apple Pay setup). Device case: no action — show your card form as the fallback. The card field keeps working either way. |
frame_wallet_unavailable | The wallet buttons were built but none painted on this device or browser. Which of these two codes a device without a wallet produces depends on the processor, so handle both as "no wallet here". | No action — show your card form as the fallback. |
frame_wallet_total_mismatch | The checkout refused a payment whose total no longer matches the session's: the total changed after the buyer saw it, or while the wallet sheet was open. Nothing was charged. Also fires on a card payment once you use fetchUpdates(). | Show the buyer the current total and let them pay again. Server-side name: expected_amount_mismatch. |
frame_wallet_confirm_failed | Your onConfirm threw, timed out (15 seconds) or answered something other than { ok: true } / { ok: false }. Nothing was charged. | Fix the hook, and keep it fast. See Buyer details and shipping in the wallet sheet. |
frame_wallet_rendered_by_card_element | You mounted a standalone wallet element, but the active binder renders Apple Pay / Google Pay inside the card mount. | Remove the standalone element; wallets appear inside the card mount when eligible. See Elements reference → Apple Pay & Google Pay. |
frame_binder_load_failed — sub-hints
When this fires, inspect error.hint:
catch (err) {
if (isVoraMirrorError(err) && err.code === "frame_binder_load_failed") {
switch (err.hint) {
case "network_error":
case "timeout":
// Transient; suggest retry
break;
case "csp_blocked":
case "script_blocked":
// Configuration; merchant needs to update CSP / disable adblockers
break;
}
}
}
CSP requirements
The card iframe loads the active processor's tokenization library from a processor-owned CDN, so your CSP needs VORA's CDN plus your active processor's domain. Always:
script-src 'self' js.vonpay.com;
The processor hostname depends on how your account is provisioned; it is provided at go-live setup — ask support if you need it earlier. The processor's card iframe needs frame-src for its host as well, and Google Pay loads its button script from https://pay.google.com, so a strict script-src must allow it. A CSP that rejects it produces frame_binder_load_failed with hint: "csp_blocked" (when detectable) or hint: "script_blocked" (when the script tag was blocked outright).
Server-side errors
VoraMirrorError is client-side. 4xx / 5xx responses from /v1/* (your server's calls) use the self-healing error envelope and are catalogued on Reference → Error codes. The browser-callable /v1/public/* routes accept publishable keys only — a secret key is refused with 403 auth_key_type_forbidden, and a missing or bad key returns the *_publishable variants (auth_missing_bearer_publishable, …) that steer to a publishable key.
What's next
- Quickstart — end-to-end walkthrough
- Elements reference — per-element behavior
- Reference → Error codes — server-side code catalog