Skip to main content
Unlisted page
This page is unlisted. Search engines will not index it, and only users having a direct link can access it.

Advanced — binder profiles

Most integrations don't need this page

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 style option 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 null at create time.

The three binder profiles

ProfileCard-field shapeWhat lives in the card iframe
Monolith-embed (default)One iframe for the entire checkoutPAN + expiry + CVC + cardholder + wallets + payment-method picker, all together
Direct-cardOne combined card iframePAN + expiry + CVC packed in one row
Split-field (coming soon)Multiple iframes — one per PCI inputPAN iframe + CVC iframe; expiry and cardholder render as your DOM inputs

Capability matrix

ElementMonolith-embed (default)Direct-cardSplit-field (coming soon)
cardMONOLITH (one iframe contains the entire checkout)NATIVE (combined PAN/MM-YY/CVC iframe)NATIVE (PAN + CVC iframes; merchant-DOM expiry + cardholder)
cardholderMONOLITH (inside the main card iframe)PROXY (SDK-rendered input)PROXY
emailPROXYPROXYPROXY
address (billing)MONOLITH (inside the main iframe)NATIVE (binder-shipped address iframe)PROXY
payment-method-pickerMONOLITHPROXY (SDK-rendered button row)PROXY
save-for-future-usePROXY (consent surface is always SDK-rendered for compliance)PROXYPROXY
saved-methodsMONOLITHNATIVE / PROXY (coming soon)PROXY

Wallet buttons (Apple Pay / Google Pay) render inside the card mount 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 card and that's effectively your whole payment surface. Its iframe contains the card fields, wallets, picker, and cardholder. email and save-for-future-use are still separate PROXY elements you can place and style freely; the other elements either return null or render inside the monolith. This is the path the Elements reference documents.
  • Direct-card — you can mount email, cardholder, address, card, and save-for-future-use as five independently-placed elements, with full outer-styling control on the PROXY ones. Wallet buttons render alongside the card inputs inside the card mount.
  • 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 errortokencharged. 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