Skip to main content

Render modes: embed vs elements

Embedded Fields renders in one of two modes, chosen per session via integrationMode when you create the session — the same account can create one session in embed and the next in elements.

  • embed (the default) — a monolith: one combined card iframe renders the whole payment surface (card number + expiry + CVC, cardholder name, billing address, the payment-method picker, a returning buyer's saved cards, and any eligible Apple Pay / Google Pay buttons). You place only email and save-for-future-use yourself.
  • elements — composable: you mount the card, email, cardholder, address and the other pieces as discrete elements you place yourself, where your account supports it. The card can stay the combined box or split into the three discrete card-number / card-expiry / card-cvc secure fields, each mounted into its own container (they share one controller and tokenize together via one submit()); on a binder without discrete-field support a discrete field's .mount() throws frame_unsupported_element.
elements falls back to embed silently — it never errors

Where discrete rendering is unavailable, an integrationMode: "elements" session renders as embed: the card iframe collects the cardholder name and billing address itself, and a cardholder / address element you mounted separately still renders the SDK's own input — the buyer sees the field twice. Read the integrationMode echoed on the created session, and the capability map from vora.sessions.retrieve(), then code both layouts. See Custom checkout with Elements for the create-session call.

The capability map​

vora.sessions.retrieve(sessionId) returns a capability map. Each element takes one of four lowercase values for the active session:

  • "native" — the gateway ships a UI primitive for this element and the SDK wraps it (one iframe; the gateway owns internal layout; your style option translates to the gateway's theme API).
  • "proxy" — the SDK renders the DOM itself, no iframe; your CSS has full control.
  • "monolith" — the main embed iframe already contains this element's surface, so there is no separate mountable slot. elements.create() for the payment-method picker in this state throws frame_unsupported_element — it does not return null, and the throw happens in create(), not mount(), so a try/catch around .mount() will not catch it. Do not list that element when the map says "monolith".
  • "not_available" — this binder does not support the element at all; for the payment-method picker any method marked this way is filtered out of methods before render. Same throw in create() as above. Handle this value explicitly — it is the case a three-way branch falls through.

Your code never has to name or detect the underlying vendor — the capability map is the contract.

Capability matrix​

Elementembed (default)elements
card"monolith" (one iframe contains the entire checkout)"native" (one combined number/expiry/CVC card iframe)
card-number / card-expiry / card-cvc(not applicable — card is one combined box)"native" (three discrete secure fields, mounted independently)
cardholder"proxy" — not gated (see below)"proxy" (SDK-rendered input)
email"proxy""proxy"
address (billing)"monolith" (inside the main iframe)"monolith" is what the map reports; the SDK renders its own input — style it as SDK-rendered DOM, and do not branch on "proxy"
payment-method-picker"monolith""monolith" — create() throws frame_unsupported_element; the processor's own surface handles method selection, so do not list it
save-for-future-use"proxy" (the consent surface is always SDK-rendered)"proxy"
saved-methods"proxy" — renders its unavailable state: the saved-card read surface is not enabled, for any account. A returning buyer's saved cards appear inside the embed card mount instead"proxy" — same unavailable state; see the element index

Apple Pay / Google Pay buttons render inside the card mount in every mode when the buyer's device and your domain are eligible — Element reference → Apple Pay & Google Pay; there is no separate row element to position. Per-element options and collect shapes are in the Element reference.

Composing multiple elements​

With integrationMode: "elements", 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" });
const email = elements.create("email", {});

card.mount("#card-element");
address.mount("#address-element");
email.mount("#email-element");

// One submit() collects from every mounted element, drives tokenization,
// and returns a single flat SubmitResult — branch error -> chargeStatus ->
// token -> charged. See Elements for the full branch order.
const result = await elements.submit();

The collection shares theme defaults across child elements. With the default integrationMode: "embed" the same code is valid — but the card iframe already collects the billing address, so a separately mounted address produces a second form. Mount it only in elements mode.

cardholder is the exception: it is not gated by the capability map — the SDK always renders its own text input, in both modes, on every binder. Mounting it in embed mode therefore produces a visible field, and if the binder's own iframe also collects a cardholder name you get two. Mount it only where you want the SDK's input. email behaves the same way.

The submit result is one flat SubmitResult regardless of how many elements you mount — every field optional, branched in the order error → chargeStatus → token → charged. The interface and the full contract: Element reference → SubmitResult shape; the styling model (what style themes vs what you style on your own DOM): Customize the look & feel.

What's next​