Embedded Fields — Elements reference
Embedded Fields ships individual UI elements that you mount into your checkout page. Each element handles one slice of the buyer's input and exposes a uniform mount / event / collect API.
On the default binder the card element is a single iframe that renders the entire payment surface — card fields plus, when the buyer is eligible, Apple Pay / Google Pay. Alongside it you can mount email and save-for-future-use as separate fields. This page covers what's available, the options each element accepts, and the value shapes they emit on collect.
Status overview
| Element type | Status | Notes |
|---|---|---|
card | ✅ Live | The card iframe — your payment surface. Generally available. |
email | ✅ Live | VORA-rendered email input with validation. |
save-for-future-use | ✅ Live | Compliance-locked opt-in checkbox. |
express-checkout | ✅ Live | Standalone Apple Pay / Google Pay buttons (Elements mode). See Standalone wallets. |
payment-method-picker | ✅ Live | Method-selector button row. Surfaces the buyer's choice on submit() — it does not tokenize. See payment-method-picker. |
Cardholder name and billing address are collected inside the
cardmount on the default binder — you don't mount separate elements for them. If your account's capability map reports a non-default binder profile, where these mount as standalone fields, see Advanced — binder profiles.
Apple Pay & Google Pay — built into the card mount
When the buyer's device supports a wallet (Apple Pay on Safari / iOS, Google Pay on Chrome with a saved card) and your domain is verified with each wallet network, the wallet button appears automatically as part of the card mount. There's no separate element to add — elements.create('card', …) is the only thing your integration needs.
Placement inside the mount is decided by the binder runtime so the buyer sees the same wallet UI they see on other checkouts — sometimes a row above the card inputs, sometimes a tab, sometimes a sheet that takes over the mount when tapped.
Eligibility gates (all must be true for the button to appear):
- The buyer's device + browser supports the wallet. Apple Pay needs Safari (macOS / iOS) or a Mac with Touch ID + paired iPhone. Google Pay needs Chrome, Edge, or any Chromium-based browser signed in to a Google account.
- The buyer has a payment method on file with the wallet.
- Your serving domain is registered with the wallet network for your active merchant account. New merchants are registered automatically on first session-create; existing merchants who haven't yet completed registration fall back to the card field with no degradation in the checkout flow.
Failure-mode contract: if any gate fails, the wallet button doesn't appear and the buyer completes checkout via the card field. The button never appears pointing at the wrong destination. The two relevant error codes (frame_wallet_unavailable for device-side ineligibility, frame_wallet_domain_unverified for the domain-registration gate) are emitted on the change event so your analytics layer can record coverage; neither blocks the buyer or the card field.
Buyer flow when the button appears:
- Buyer taps the wallet button. The wallet sheet (Apple Pay sheet, Google Pay sheet) opens on the device.
- Buyer authenticates (Face ID, Touch ID, Chrome dialog).
elements.submit()resolves with avp_pmt_*token plus awallet: "apple_pay" | "google_pay"discriminator on the result. The downstreamPOST /v1/payment_intentsis identical to a manually-typed card — samepayment_methodfield, same response shape.
3DS for wallet-derived tokens. Wallets short-circuit issuer-side authentication via the wallet network's device attestation; a wallet token rarely requires a separate 3DS challenge. When the issuer does require one (some EU markets, high-value transactions), the same VORA 3DS modal drives the challenge — no wallet-specific code path on your side.
Apple Pay domain setup. Each domain that serves Apple Pay needs a one-time verification ceremony with Apple — host a small file at https://{your-domain}/.well-known/apple-developer-merchantid-domain-association and we trigger the verification call. Full walkthrough at Apple Pay setup, including framework-specific hosting recipes (Next.js, Vite, Vercel, Cloudflare, Nginx, Apache, Express) and what to do when verification fails. Google Pay doesn't need this step — Google verifies the page's TLS handshake on the buyer's first wallet sheet.
Testing. Wallet buttons require HTTPS plus a domain registered with each wallet network. Local development on
http://localhostshows the card field but not the wallet buttons (Apple Pay refuses non-HTTPS; Google Pay'sTESTenvironment is browser-version-sensitive onlocalhost). To test the wallet buttons end-to-end, run theframe-reactsample on your own HTTPS dev domain (any tunnel that gives you a publichttps://URL works, e.g. an ngrok/Cloudflare-tunnel subdomain), then register that domain for wallets yourself: add it under Settings → Wallets in the dashboard and complete the Apple Pay domain verification (Google Pay needs no extra step). No account-rep touch is required.
Creating elements
The card element is your payment surface. Create an elements collection, add the card element, and optionally mount email and save-for-future-use alongside it:
const elements = vora.elements.create({
theme: { font: { size: "16px" }, color: { text: "#1a1a1a" } },
});
const card = elements.create("card", {});
card.mount("#card-element");
// Optional — separate SDK-rendered fields you place and style yourself:
const email = elements.create("email", {});
email.mount("#email-element");
const save = elements.create("save-for-future-use", {});
save.mount("#save-element");
// Submit collects from every mounted element + drives the charge.
const result = await elements.submit();
if (result.error !== undefined) {
// Validation or charge failure — surface result.error.message
} else if (result.token !== undefined) {
// A reusable "vp_pmt_*" was vaulted (buyer on the session).
// The card was also charged at submit — do NOT create a server-side
// payment_intent for this session. Pass the token to your server only
// if you need it for a later off-session charge.
// result.email and result.setupForFutureUse are present iff their
// elements were mounted. See Tokenization for the reusability model.
} else if (result.charged) {
// Guest charge-only (no buyer on the session): the card was charged at
// submit and nothing was vaulted — there is no token. Do NOT create a
// server-side payment_intent for this session (it would double-charge).
// Confirm settlement via the webhook before fulfilling.
}
// The client result is a UX signal. Always confirm settlement via the
// webhook before fulfilling the order.
The elements collection shares theme defaults across child elements.
SubmitResult shape
interface SubmitResult {
token?: string; // vp_pmt_* — present only when a reusable method was vaulted
charged?: true; // present on a guest charge-only submit (no token vaulted)
last4?: string; // display-only
brand?: string; // "visa" | "mastercard" | … (display-only)
email?: string;
cardholder?: string; // cardholder name (a plain string, not an object)
billingAddress?: BillingAddressValue;
setupForFutureUse?: boolean; // true when the buyer opted to save (vaulted off_session)
error?: VoraMirrorError; // present iff submit failed
}
SubmitResult is one flat interface — every field is optional (there are no separate SubmitTokenResult / SubmitChargedResult types to import). Submit charges the card and populates exactly one of three outcomes — discriminate at runtime in this order:
error— validation or charge failure. Surfaceresult.error.message. Nothing was charged.token— a buyer is on the session, so the card was charged and a reusablevp_pmt_*was vaulted in one step.charged— a guest (no buyer on the session): the card was charged once and nothing was vaulted, so there is no token.last4/brandare display-only.
if (result.error) {
// failed
} else if (result.token) {
// charged + vaulted (buyer on session)
} else if (result.charged) {
// guest charge-only (no token)
}
Because submit already charged the card, do not also call POST /v1/payment_intents for the same session — that double-charges. The client result is a UX signal; confirm settlement via the webhook before fulfilling.
The submit result is one object regardless of how many elements you mount. Optional fields are present iff the corresponding element was mounted in the collection.
Every token uses the
vp_pmt_*prefix.SubmitResult.setupForFutureUseis a boolean —truewhen the buyer checked the save-for-future-use box (the token was vaulted reusable). The underlying reusability scope —on_sessionfor in-session reuse like upsells,off_sessionfor recurring / MIT, or single-use when unset — is the server-sidesetup_for_future_usewire value on the vault row. See Tokenization for the full reusability model.
Legacy single-field shorthand (deprecated)
@deprecated.
vora.fields.create("card")+card.tokenize()is the original single-field path. It is deprecated and on this page's default monolith-embed binder it now refuses withframe_method_not_supported_for_session. Use the canonical collection path instead:const elements = vora.elements.create();
const card = elements.create("card", {});
card.mount("#card-element");
const result = await elements.submit();See Creating elements for the full
SubmitResultdiscrimination.
// Deprecated — kept for reference. Refuses on a single-embed binder with
// code frame_method_not_supported_for_session. Use elements.create() above.
const card = vora.fields.create("card", { style: {…} });
card.mount("#card-element");
vora.fields.create (singular, card-only) is the original API — it returns a VoraField. It is deprecated; new integrations must use vora.elements.create() + elements.submit(). On a single-embed binder the legacy card.tokenize() call refuses with frame_method_not_supported_for_session (an integration-time wrong-method condition, not a server bug).
card — PAN / expiry / CVC iframe
Your payment surface. The iframe is served directly from the active binder's CDN; your DOM never sees the PAN.
Options
interface CardElementOptions {
style?: VoraFieldStyle;
classes?: { focus?: string; invalid?: string; complete?: string };
placeholder?: { number?: string; expiry?: string; cvc?: string };
challengeTimeout?: number; // ms — default 300_000 (5 minutes); clamped to 600_000 (10-min) max
locale?: string; // BCP-47, e.g. "en-US"; phase 2A is en-only
disable3dsModal?: boolean; // opt out of the harmonized VORA 3DS modal; default false (modal on)
// Card-iframe rendering knobs. Each is opt-in (default off);
// the binder ignores a knob for UI it doesn't render.
disableAutofillAssist?: boolean; // suppress the binder's autofill / login-link pill
hidePostalCode?: boolean; // hide the inline postal-code input the card iframe renders
hideBrandIcon?: boolean; // hide the card-brand icon inside the card-number field
// Payment-method display controls. Monolith-embed binders only;
// direct-card binders ignore them.
display?: "all" | "addOnly" | "storedOnly" | "supportsTokenization"; // default "all"
autoSelectOption?: "first" | "firstStored" | "firstNonStored" | "none"; // default "first"
compactPaymentOptions?: boolean; // default true — tighten spacing between method rows
}
Card-iframe rendering knobs
These three options suppress optional UI the binder renders inside its card iframe. Each is OPT-IN — set to true to suppress, omit (or set to false) to leave the binder's default behavior intact. A knob for UI the binder doesn't render is ignored silently.
| Option | What it does | Typical use case |
|---|---|---|
disableAutofillAssist | Hides the saved-card / login-link autofill pill the card iframe renders inline. | Merchants whose checkout already has its own saved-card UX, or who want a minimal-chrome card field with no autofill prompt. |
hidePostalCode | Removes the postal-code input the card iframe renders alongside PAN / expiry / CVC. | Merchants collecting postal code elsewhere in their own form — avoids asking for it twice. |
hideBrandIcon | Hides the card-brand icon (Visa / Mastercard / …) the binder renders inside the card-number field. | Merchants whose visual style requires a flat-text card field with no inline brand icon. |
Payment-method display controls
On a monolith-embed binder, the card mount renders the full payment-method list — the new-card form, any saved methods, and the Apple Pay / Google Pay buttons (see Apple Pay & Google Pay). These options shape that list. Direct-card binders render only the card field and ignore them.
| Option | Values | Default | What it does |
|---|---|---|---|
display | "all" · "addOnly" · "storedOnly" · "supportsTokenization" | "all" | Which method surfaces render. "all" shows the full list including the wallet buttons; "addOnly" shows only the new-card form; "storedOnly" shows only saved methods; "supportsTokenization" shows only methods that support tokenization. UI-surface only — it does not change what is charged. |
autoSelectOption | "first" · "firstStored" · "firstNonStored" · "none" | "first" | Auto-selects a method on load so the buyer isn't forced to pick first. Selection is by position — "first" selects whatever the binder lists first (often a wallet when wallets precede the card). Defaults to "first"; pass "none" to leave nothing selected. |
compactPaymentOptions | true · false | true | Tightens the vertical spacing between method rows for a denser layout. Defaults to true; pass false for the spacious layout. |
Events
card.on("ready", () => { /* iframe loaded and accepting input */ });
card.on("change", (event) => {
// event.complete: boolean — all of number/expiry/cvc are valid
// event.empty: boolean
// event.error?: { type: "validation_error", message }
});
card.on("focus", () => {});
card.on("blur", () => {});
Collect — use the collection's submit()
The canonical collect path is elements.submit() on the collection — see Creating elements for the unified SubmitResult shape and the error → token → charged discrimination:
const elements = vora.elements.create();
const card = elements.create("card", {});
card.mount("#card-element");
const result = await elements.submit(); // collection-level submit charges + (when a buyer is on the session) vaults
There is no card.submit() and a card created via elements.create() has no .tokenize() method — submit() is a collection method (elements.submit()), and it is what charges the card and resolves with the flat, three-outcome SubmitResult.
@deprecated —
card.tokenize()(singularvora.fieldsAPI). The legacy single-fieldcard.tokenize()is deprecated. On this page's default monolith-embed binder it now refuses withframe_method_not_supported_for_session. The recovery is:card.tokenize()is deprecated. Usevora.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.
When a vp_pmt_* is vaulted it's bound to the buyer on the session that minted it. Reusability is governed by setup_for_future_use ("on_session" for upsells, "off_session" for recurring / MIT, null for single-use); see Tokenization. On charge-and-save the current charge already happened at submit(), so pass the token to your server only for a later off-session charge via POST /v1/payment_intents — never to re-charge the session that minted it.
email — Email input
VORA-rendered <input type="email"> with HTML5-aligned validation. Renders as a standalone field on every binder profile.
Options
interface EmailElementOptions {
style?: VoraFieldStyle;
placeholder?: string; // default "you@example.com"
required?: boolean; // default true
}
Collect
Returns { email: string }. Used both as billing_details.email for the binder and as the buyer email on the session for receipt delivery.
save-for-future-use — Opt-in checkbox
VORA-rendered checkbox + label that controls the reusability of the token your submit returns. Renders as a standalone field on every binder profile — the consent surface is always SDK-rendered for compliance. The element is a plain consent checkbox — it has no scope / policy option. Every token is a vp_pmt_*; what changes is the setup_for_future_use field on the vault row. When the box is checked, the submit vaults the token as reusable (setup_for_future_use: off_session) so your server can later charge it off-session. When unchecked, the token is registered single-use (setup_for_future_use: null) — usable only by the originating intent. To set a scope explicitly without the checkbox, a server-side caller passes setup_for_future_use to POST /v1/tokens, or the SDK's card.tokenize({ setupForFutureUse }) shorthand forwards it. See Tokenization — reusability for the full model.
Compliance lock
The checkbox starts unchecked. There is no defaultChecked option. EU GDPR + several US state privacy laws require active opt-in for storing payment-method tokens for future MIT use. Pre-checked default would void the consent record downstream. This is a hard design lock — do not work around it.
Options
interface SaveForFutureUseElementOptions {
style?: VoraFieldStyle;
label?: string; // default "Save this card for future purchases"
}
Collect
Returns { setupForFutureUse: boolean } — true when the buyer checked the box. Flows through to /v1/public/tokens registration; a checked box vaults the returned vp_pmt_* as off_session (eligible for future MIT charges by your server), unchecked as single-use. See Tokenization — reusability for the full model.
payment-method-picker — Method-selector button row
A VORA-rendered button row that lets the buyer pick which payment method to use. Unlike the other elements, the picker does not tokenize or charge — it surfaces the buyer's choice on submit() and leaves your checkout UX to route the next step (mount the card element, hand off to express-checkout, and so on).
Available on direct-card (proxy) binders. On a monolith-embed binder the active binder renders its own method selector, so
elements.create('payment-method-picker', …)throwsframe_unsupported_elementat create time — drop the element and let the binder's UI handle method selection.
Options
create() takes a required options object — elements.create('payment-method-picker', { … }).
interface PaymentMethodPickerElementOptions {
style?: VoraFieldStyle;
methods?: PaymentMethodType[]; // omitted/empty = every method the binder supports
defaultMethod?: PaymentMethodType; // pre-select on render (still mutable); default: no pre-selection
buttonLayout?: "horizontal" | "vertical"; // default "horizontal"
}
type PaymentMethodType =
| "card"
| "apple_pay"
| "google_pay"
| "link"
| "cashapp"
| "klarna"
| "affirm";
| Option | Values | Default | What it does |
|---|---|---|---|
methods | PaymentMethodType[] | every supported method | Methods to surface, in render order (left-to-right for horizontal, top-to-bottom for vertical). The list is capability-filtered against the active session's binder — see below. |
defaultMethod | a PaymentMethodType | none | Pre-selects a method on render so the buyer isn't forced to pick first. The selection is still mutable. A defaultMethod that isn't a recognized PaymentMethodType throws frame_field_validation_failed at create time. |
buttonLayout | "horizontal" · "vertical" | "horizontal" | Row of buttons (wraps on narrow viewports) vs. one button per row. |
Capability filtering
The methods you pass are intersected with the session's binder capability map: any method the binder declares not_available for is hidden even if you listed it, and methods you didn't list are hidden too. When you explicitly supply methods and one or more are dropped, the SDK emits a one-time console.warn naming the filtered methods so the filtering isn't silent. If the intersection is empty, mount() throws frame_unsupported_element — review your methods array or route the buyer to a binder that supports at least one of them.
Collect — the nested picker result
The picker contributes a nested picker group to the collection's SubmitResult, present only when the element was mounted. The picker neither charges nor vaults, so it never sets token / charged:
interface PickerSubmitResult {
selectedMethod: PaymentMethodType; // the method the buyer chose
}
const elements = vora.elements.create();
const picker = elements.create("payment-method-picker", {
methods: ["card", "apple_pay", "google_pay"],
defaultMethod: "card",
buttonLayout: "horizontal",
});
picker.mount("#picker-element");
const result = await elements.submit();
if (result.error === undefined) {
switch (result.picker?.selectedMethod) {
case "card":
// mount the `card` element and run the card path
break;
case "apple_pay":
case "google_pay":
// hand off to `express-checkout`
break;
// "link" | "cashapp" | "klarna" | "affirm" — route per your checkout UX
}
}
Read result.picker?.selectedMethod to branch — it is the only field the picker populates today, and reading it with optional chaining keeps your code stable as the picker's nested shape grows in future releases.
Where Embedded Fields lives in your page
Two pieces plug into your HTML/server:
<head>
<!-- 1. The SDK. One line. -->
<script src="https://js.vonpay.com/v1/vora.js"></script>
</head>
<body>
<!-- 2. The card mount — your payment surface. -->
<div id="card-mount"></div>
<div id="email-mount"></div> <!-- optional -->
<!-- ...your existing checkout HTML (shipping, summary, CTA, etc.)... -->
<script>
// 3. Wire it up.
const vora = new Vora({ publishableKey: "vp_pk_test_..." });
await vora.sessions.retrieve(sessionId); // sessionId from your server
const elements = vora.elements.create();
elements.create("card", { style: {/* … */} }).mount("#card-mount");
elements.create("email", {}).mount("#email-mount"); // optional
</script>
</body>
Plus two server endpoints in your backend:
POST /v1/sessions— call with your secret key (vp_sk_*), returnsession_idto the browser.POST /v1/payment_intents— call to mint the intent your browser passes toelements.submit({ paymentIntent }).
The browser never sees your secret key. The session ID is short-lived and scoped to one buyer.
Styling & theming
Every element accepts a style option — the VoraFieldStyle token set (font, color, spacing, border, background, per-layer surfaces) — plus a classes option for focus / invalid / complete pseudo-states. The complete styling reference — the full token set, format-validation rules, the inner-vs-outer-iframe model, the three reference themes, and an interactive playground — lives on one page:
Other binder profiles
This page documents the monolith-embed binder — the default, and the profile the large majority of merchants integrate against. A small number of accounts are provisioned on a different binder profile, where elements like cardholder and address mount as separate fields and the card surface is split differently.
If vora.sessions.retrieve() reports a profile other than monolith-embed, see Advanced — binder profiles for the capability matrix and the multi-element composition model.
What's next
- React wrapper —
<VoraProvider>,<CardField>,useVora()for the React-shaped version of every element - Error handling —
VoraMirrorErrorshape, codes, retry semantics - Quickstart — end-to-end integration walkthrough