Embedded Fields — Element reference
The catalog of individual UI elements you can create with vora.elements.create() — in either render mode. Each element handles one slice of the buyer's input and exposes a uniform mount / event / collect API. This page is the reference for what's available, the options each element accepts, and the value shapes they emit on collect; for which mode renders which element as its own field, see Render modes: embed vs elements.
This reference documents the default "embed" surface, where 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 — and only email and save-for-future-use are placed separately. The same elements are available in "elements" mode, where you place cardholder, address, and the others as their own fields; each element's section notes what changes between the two modes.
Choosing your integration mode
These elements render one of two ways, set per session via integrationMode (default "embed"): the embed monolith (one card iframe for the whole surface — only email and save-for-future-use are separate) or composable elements (you place each element individually). This reference documents the elements themselves — for the render-mode model, the per-binder capability matrix, and the silent elements→embed fallback, see Render modes: embed vs elements.
The card is your choice in "elements" mode: the combined card element (one box — number + expiry + CVC together) or the three discrete card-number / card-expiry / card-cvc fields, placed independently. Both produce the same vp_pmt_* from one collection.submit(). In "embed" mode the card is always the combined box. See Custom checkout (Elements mode) for the walkthrough.
Status overview
Every kind you can pass to vora.elements.create(kind, options) — the complete element index, with a one-line description and a link to its section.
create() kind | Status | What it is |
|---|---|---|
card | ✅ Live | The combined card iframe (number + expiry + CVC in one box) — your payment surface. See card. |
card-number | ✅ Live | Discrete PAN field — mount independently in "elements" mode. Adds showIcon. See Discrete card fields. |
card-expiry | ✅ Live | Discrete expiry (MM / YY) field. See Discrete card fields. |
card-cvc | ✅ Live | Discrete security-code (CVC) field. See Discrete card fields. |
email | ✅ Live | VORA-rendered email input with validation. See email. |
address | ✅ Live | VORA-rendered billing-address form ("elements" mode). See Custom checkout (Elements mode). |
cardholder | ✅ Live | VORA-rendered "name on card" text field ("elements" mode). See Custom checkout (Elements mode). |
save-for-future-use | ✅ Live | Compliance-locked opt-in checkbox. See save-for-future-use. |
saved-methods | 🚧 Not usable yet | Discrete picker that lists a returning buyer's saved cards in elements mode. The element ships in the SDK, but the surface it reads a buyer's cards from is not enabled, so it returns no cards — don't build a returning-buyer flow on it yet. Returning-buyer saved cards do render inside the embed card mount when you set a buyer on the session (see the display controls and create-session → buyer identification). |
express-checkout | ✅ Live | Standalone Apple Pay / Google Pay wallet button row. Use this only when you create the session with integrationMode: "elements" (discrete rendering) — in the default "embed" mode the wallet buttons render inside the card mount instead, with no separate element to add. 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. |
The wallet button is the express-checkout element — search or jump to Standalone wallets for the full Apple Pay / Google Pay integration. (There is no wallet-button / googlePay / applePay element name — express-checkout renders both wallets from one element.) In the default "embed" mode you don't add any element at all — the wallet buttons appear inside the card mount automatically; see Apple Pay & Google Pay.
Cardholder name and billing address are collected inside the
cardmount in the default"embed"integration mode — you don't mount separate elements for them. If you create the session withintegrationMode: "elements"(and your account's gateway supports discrete rendering),cardholderandaddressmount as standalone fields instead; see Custom checkout (Elements mode).
Apple Pay & Google Pay — built into the card mount
In the default "embed" integration mode, 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. (In integrationMode: "elements" you instead mount the standalone express-checkout element for the wallet buttons.)
Placement inside the mount is decided by the gateway 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 the charge result, carrying awallet: "apple_pay" | "google_pay"discriminator. On the charge-and-save flow the submit has already moved the money, so do not also callPOST /v1/payment_intentsfor that payment; see Charge and save.Branch on the result, and cover all four outcomes — a token is not the discriminator:
Result Meaning errorFailed before any charge. chargeStatus: "requires_action"3DS. Nothing charged; send the buyer to redirectUrl.charged: trueMoney moved. A reusable token may also be present — it is not a handle to bill this payment again. chargeStatus: "pending"In flight, no token yet. An ordinary async settlement, not a failure — await the charge.*webhook.token, nochargedNothing charged; vault-only accounts charge server-side. A missingpendingarm double-chargespendingis the routine outcome for an async capture, not an edge case. If it falls through to your failure path, the buyer sees an error, retries, and is charged twice when the first charge settles.
3DS on wallet payments. Wallets usually satisfy issuer authentication through the wallet network's device attestation, so a separate 3DS challenge is uncommon. When the issuer does require one (some EU markets, high-value transactions), it is not silent — the submit resolves chargeStatus: "requires_action" with a redirectUrl and nothing is charged until the buyer authenticates. You must handle that branch: see Charge at submit → 3-D Secure, and Wallets and 3-D Secure for the standalone wallet element.
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.
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 }; // these three keys ONLY
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
}
Placeholder text — three keys, and only three
placeholder on the combined card element takes an object, not a string. Exactly three keys are accepted:
const card = collection.create("card", {
placeholder: { number: "Card number", expiry: "MM / YY", cvc: "CVC" },
});
There is no error and no console message on older SDK builds — the field simply keeps its default label, so a mis-keyed placeholder looks like the field ignored you.
The wrong guess is specific and predictable: merchants reach for the field names used by the card processor whose library runs inside the iframe, because those are the names in that vendor's own documentation. { number, expiryDate, securityCode } is the common one — number lands, the other two vanish, and the form renders with two unlabelled boxes. That reads as "the card fields are broken," and it has cost an integrator a full day of debugging iframes when the actual fault was two misspelled keys.
Ours are number, expiry, cvc.
Placeholder text is applied by binders that render their own field labels; a binder whose card iframe doesn't expose per-field placeholders ignores it, the same way it ignores any rendering knob for UI it doesn't draw. On the discrete fields (card-number / card-expiry / card-cvc) placeholder is a plain string on each element instead — see Custom checkout.
Don't confuse this with
style.color.placeholder, which sets the placeholder's colour. The two are unrelated and both can appear in the same options object.
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. Saved methods only appear when a returning buyer is identified on the session and that buyer vaulted a card in a prior session — see create-session → buyer identification; a guest or first-time buyer has none, so display: "storedOnly" would render an empty list for 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.error?: { code: string; message: string } // scrubbed validation error; undefined when the field is valid
});
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.
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.
Discrete card fields
card-number, card-expiry, and card-cvc are the split-field alternative to the combined card element. Instead of one iframe that renders PAN + expiry + CVC together, you mount three independently-placed secure fields — each into its own container — and style and position them anywhere on your page. The PAN, expiry, and CVC still live inside secure field iframes, so your DOM never sees card data.
The three fields share one secure-fields controller and tokenize together via a single collection.submit(), which resolves the same vp_pmt_* token the combined card element produces. You never tokenize a field on its own.
"elements" mode onlyDiscrete card fields require a session created with integrationMode: "elements" and a binder that renders discrete secure fields. If the active binder doesn't support them, .mount() throws frame_unsupported_element — fall back to the combined card element or route the buyer to a supported binder. In the default "embed" mode the card is always the one combined box.
The rules
- Mount all three.
card-number,card-expiry, andcard-cvcmust all be created and mounted beforesubmit(). Mounting only some can't produce a complete card, sosubmit()returnsframe_field_validation_failed. - Don't mix with the combined
card. A collection holds one card-entry surface — either the combinedcardelement or the three discrete fields, never both. Mixing them returnsframe_field_validation_failedfromsubmit()(they would tokenize twice). - One
submit()for all three. Because they share a controller,collection.submit()vaults all three fields in a single call — you call it once, not per field.
Options
Each discrete field takes the same base options; card-number adds showIcon:
interface DiscreteCardFieldOptions {
style?: VoraFieldStyle; // the same token set every element accepts
placeholder?: string; // placeholder shown when the field is empty
disabled?: boolean; // default false — no input accepted when true
}
// card-number only:
interface CardNumberElementOptions extends DiscreteCardFieldOptions {
showIcon?: boolean; // show the card-brand icon inside the field; default false
}
// card-expiry and card-cvc take DiscreteCardFieldOptions as-is.
| Option | Fields | Default | What it does |
|---|---|---|---|
style | all three | inherits the collection theme | The unified VoraFieldStyle token set (font + color). See Customize. |
placeholder | all three | binder default | Placeholder text shown when the field is empty. |
disabled | all three | false | Disables input on the field. Honored on the direct-card binder only — the secure-fields binder has no native disabled state and drops it. |
showIcon | card-number only | false | Show the card-brand icon (Visa / Mastercard / …) inside the number field. |
Mount and submit
const collection = vora.elements.create({
theme: { font: { size: "16px" }, color: { text: "#1a1a1a" } },
});
// Create + mount all three, each into its own container:
collection.create("card-number", { placeholder: "Card number", showIcon: true }).mount("#card-number");
collection.create("card-expiry", { placeholder: "MM / YY" }).mount("#card-expiry");
collection.create("card-cvc", { placeholder: "CVC" }).mount("#card-cvc");
// One submit() tokenizes all three fields together.
const result = await collection.submit();
if (result.error !== undefined) {
// Validation or tokenize failure — surface result.error.message
} else if (result.token !== undefined) {
// vp_pmt_* minted from all three fields. result.last4 / result.brand are display-only.
// Charge it server-side via POST /v1/payment_intents (or, on a charge-and-save
// session, the charge already ran — see Tokenization).
}
The SubmitResult shape is identical to the combined card element's — see SubmitResult shape. Optional sibling-element values (email, cardholder, billingAddress, setupForFutureUse) are present when those elements were also mounted in the collection.
The discrete fields expose the same on() event API as every element (ready / change / focus / blur), but event granularity depends on which binder backs your account today:
- Direct-card binder — events are genuinely field-scoped. Each field fires its own
change/focus/blurcarrying that field's owncomplete, so you can drive per-field UX (e.g. highlight only the CVC field on a CVC error). - Secure-fields binder —
changeandreadyare whole-form (the shared controller fires one event for all three fields, with a form-widecomplete), andfocus/blurdo not fire yet.
Don't build portable per-field UX on these events without gating on the binder. For button-enablement and validation that works everywhere, treat change as a form-level signal (complete = the whole card is valid) rather than assuming per-field delivery. Genuine per-field events on every binder are a planned follow-up. Independent placement works on both binders — only the event granularity differs.
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 } in the submit result. This element renders and validates an email input and hands you the value back — it does not attach the email to the payment or the buyer on its own.
To associate an email with the buyer (so their transactions link together and duplicate-charge lookups work), set buyerEmail when you create the session — see Buyer identification. That is the field that flows to the buyer record.
Use this element when you want VORA to render + validate the email field for you: read result.email from the submit result and pass it as buyerEmail on your server's session-create call (or set buyerEmail directly if you already have the address).
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. 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_...",
apiBaseUrl: "https://checkout.vonpay.com", // the default; must match the host your server called
});
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:
Rendering elements discretely
To mount these as separately-placed elements, create the session with integrationMode: "elements" — see Choosing your integration mode above for the model, Custom checkout (Elements mode) for the end-to-end walkthrough, and Render modes: embed vs elements for the per-element capability matrix.
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