Advanced — binder profiles
Embedded Fields runs on the monolith-embed binder profile by default, and the Elements reference documents that path completely. Read this page only if your account's capability map reports a different profile — your integration contact will tell you, or you'll see it in the capability map returned by vora.sessions.retrieve().
Embedded Fields exposes one developer API across more than one underlying binder. Different binders render the payment surface differently, and that difference is captured by a binder profile. Your account is provisioned on exactly one profile; the SDK reports it in the capability map so your code never has to name or detect a vendor.
How the SDK reports it — element render modes
vora.sessions.retrieve(sessionId) returns a capability map. Each element renders in one of three modes for the active binder:
- NATIVE — the binder ships a UI primitive for this element and the SDK wraps it (one iframe; the binder owns internal layout; your
styleoption translates to the binder's theme API). - PROXY — the SDK renders the DOM itself, no iframe; your CSS has full control. Identical across every binder.
- MONOLITH — the binder's main embed iframe already contains this element's surface. There's no separate mountable slot — the element returns
nullat create time.
The three binder profiles
| Profile | Card-field shape | What lives in the card iframe |
|---|---|---|
| Monolith-embed (default) | One iframe for the entire checkout | PAN + expiry + CVC + cardholder + wallets + payment-method picker, all together |
| Direct-card | One combined card iframe | PAN + expiry + CVC packed in one row |
| Split-field (coming soon) | Multiple iframes — one per PCI input | PAN iframe + CVC iframe; expiry and cardholder render as your DOM inputs |
Capability matrix
| Element | Monolith-embed (default) | Direct-card | Split-field (coming soon) |
|---|---|---|---|
card | MONOLITH (one iframe contains the entire checkout) | NATIVE (combined PAN/MM-YY/CVC iframe) | NATIVE (PAN + CVC iframes; merchant-DOM expiry + cardholder) |
cardholder | MONOLITH (inside the main card iframe) | PROXY (SDK-rendered input) | PROXY |
email | PROXY | PROXY | PROXY |
address (billing) | MONOLITH (inside the main iframe) | NATIVE (binder-shipped address iframe) | PROXY |
payment-method-picker | MONOLITH | PROXY (SDK-rendered button row) | PROXY |
save-for-future-use | PROXY (consent surface is always SDK-rendered for compliance) | PROXY | PROXY |
saved-methods | MONOLITH | NATIVE / PROXY (coming soon) | PROXY |
Wallet buttons (Apple Pay / Google Pay) render inside the
cardmount on every profile when the buyer's device and your domain are eligible — see Elements reference → Apple Pay & Google Pay. There's no separate row element to position.
What each profile means in practice
- Monolith-embed (default) — you mount
cardand that's effectively your whole payment surface. Its iframe contains the card fields, wallets, picker, and cardholder.emailandsave-for-future-useare still separate PROXY elements you can place and style freely; the other elements either returnnullor render inside the monolith. This is the path the Elements reference documents. - Direct-card — you can mount
email,cardholder,address,card, andsave-for-future-useas five independently-placed elements, with full outer-styling control on the PROXY ones. Wallet buttons render alongside the card inputs inside thecardmount. - Split-field (coming soon) — the most-separable surface: PCI iframes for card number + CVC only, plus PROXY elements for everything else, all under your CSS.
Composing multiple elements
On the direct-card profile, mount each element where you want it and submit the collection as a unit:
const elements = vora.elements.create({
theme: { font: { size: "16px" }, color: { text: "#1a1a1a" } },
});
const card = elements.create("card", {});
const address = elements.create("address", {
mode: "billing",
fields: { line2: { hidden: false } },
});
const email = elements.create("email", {});
card.mount("#card-element");
address.mount("#address-element");
email.mount("#email-element");
// Submit collects from every element + drives binder tokenization,
// returning a unified result — every field optional. Branch on all three outcomes:
const result = await elements.submit();
if (result.error) {
// Failed before any charge — surface result.error.message
} else if (result.token) {
// A buyer was on the session → reusable vp_pmt_* (already charged on
// charge-and-save; charge server-side on tokenize-only). result.email,
// result.cardholder, result.billingAddress, result.setupForFutureUse are
// each present iff the matching element was mounted.
} else if (result.charged) {
// GUEST / no buyer → one-time charge, NO token. Confirm via the webhook;
// never re-charge a charged result.
}
The elements collection shares theme defaults across child elements. On the monolith-embed profile the same code is valid — but address and cardholder render inside the card iframe rather than at their own mount points, so mounting them separately has no visible effect.
SubmitResult shape
SubmitResult is one flat interface — every field is optional. There are no separate SubmitTokenResult / SubmitChargedResult types to import; you branch at runtime on which fields are populated. Check error first, then token (a buyer was on the session → reusable vp_pmt_*), then charged (guest / no-buyer one-time charge, no token):
interface SubmitResult {
// Card-route fields — present when a card element was mounted and tokenize succeeded:
token?: string; // vp_pmt_* — present on a saved-card submission
last4?: string;
brand?: string; // "visa" | "mastercard" | …
charged?: true; // GUEST / no-buyer one-time charge — NO token vaulted
transactionId?: string; // vp_tx_* — reconcilable ref for a guest charge
amount?: number; // minor units (guest charge)
currency?: string; // ISO-4217 (guest charge)
wallet?: WalletType; // set when minted via Apple Pay / Google Pay
// Aggregated form data — present iff the matching element was mounted:
email?: string;
cardholder?: string; // the cardholder name (a plain string, not an object)
billingAddress?: BillingAddressValue;
setupForFutureUse?: boolean; // true when the buyer opted to save (vaulted off_session)
// Error path:
error?: VoraMirrorError; // submit failed before any charge / tokenize
}
The submit result is one object regardless of how many elements you mount. Because every field is optional, never read token unconditionally — branch error → token → charged. Optional form fields are present iff the corresponding element was mounted in the collection.
cardholder — Cardholder name input
On the direct-card and split-field profiles, cardholder is a separate SDK-rendered input (no iframe — cardholder name isn't PCI-restricted, and a VORA-controlled input gives better styling + native browser autofill). On the monolith-embed profile the cardholder name is collected inside the card mount and this element isn't mounted separately.
Options
interface CardholderElementOptions {
style?: VoraFieldStyle;
placeholder?: string; // default "Name on card"
required?: boolean; // default true
}
Collect
Returns { cardholder: string } from the collection's collect step. Forwarded to the binder at tokenize as the cardholder billing-details name.
address — Billing address form
On the direct-card and split-field profiles, address is a separately-mountable billing-address form. On the monolith-embed profile the billing address is collected inside the card mount.
Field set: line1, line2, city, state, postalCode, country. On the direct-card profile it renders as a binder-shipped iframe; on split-field as native HTML inputs with autocomplete="billing street-address" browser-autofill compatibility.
Options
interface AddressElementOptions {
style?: VoraFieldStyle;
mode: "billing"; // required; billing only today ("shipping" reserved for a future release)
fields?: {
line1?: { required?: boolean }; // default required
line2?: { hidden?: boolean }; // default visible
city?: { required?: boolean }; // default required
state?: { required?: boolean }; // default required
postalCode?: { required?: boolean }; // default required
country?: { default?: string }; // ISO-3166-1 alpha-2; default "US"
};
allowedCountries?: string[]; // restrict country dropdown (ISO 3166-1 alpha-2)
}
Collect
interface BillingAddressValue {
line1: string;
line2?: string;
city: string;
state: string;
postalCode: string;
country: string; // ISO-3166-1 alpha-2
}
// emitted as { billingAddress: BillingAddressValue }
Styling the direct-card profile
The direct-card card iframe renders only the input values — no internal labels — so a floating-label pattern works cleanly:
<div class="my-card-wrap">
<label class="my-card-label">Card Number</label>
<div id="card-mount"></div>
</div>
<style>
.my-card-wrap {
position: relative;
background: #fff;
border: 1px solid #ccc;
border-radius: 6px;
padding: 20px 12px 6px 12px; /* room for label */
}
.my-card-label {
position: absolute;
top: 6px;
left: 12px;
font-size: 11px;
color: #666;
pointer-events: none;
}
</style>
<script>
elements.create("card", {
style: {
color: { text: "#000000", placeholder: "#9CA3AF" },
background: "#FFFFFF",
border: { color: "transparent", radius: "0px", width: "0px" }, /* let outer wrapper draw it */
font: { family: "Poppins, system-ui, sans-serif", size: "15px" },
},
}).mount("#card-mount");
</script>
The wrapper draws the box; your absolutely-positioned label floats at the top-left; the iframe content sits in the lower padded area. On the monolith-embed profile the iframe already includes its own labels — skip the floating-label wrapper and let the iframe drive the visual.
The inner-iframe vs outer-iframe boundary itself — what you can and can't style, and why — is the same on every profile. See Customize → inner vs outer iframe.
What's next
- Elements reference — the default monolith-embed path, per-element options, theming
- Quickstart — end-to-end integration walkthrough
- Error handling —
VoraMirrorErrorcodes and recovery