Skip to main content

Embedded Fields — Tokenization

elements.submit() turns the buyer's card details plus the rest of the checkout fields into one opaque vp_pmt_* token. Which of two flows runs depends on your account's binder and the session you created — the result tells you which:

  • Tokenize-only. submit() mints a vp_pmt_* and charges nothing; your server runs every payment operation later via POST /v1/payment_intents.
  • Charge-and-save. submit() charges the card at submit. With a buyer attached to the session it also vaults a reusable vp_pmt_* in the same step; a guest session charges once and saves nothing ({ charged: true }, no token). Your server must not call POST /v1/payment_intents for that session — that charges the buyer a second time.

Branch error → chargeStatus → token → charged. On a charge-at-submit session chargeStatus adds two outcomes that are neither success nor failure — "requires_action" (redirect the buyer to result.redirectUrl; nothing charged) and "pending" (settling asynchronously; await the webhook, do not re-charge) — requires_action carries no token, and a token on a pending result is a vaulted card, not a settled charge. Whichever arm you land in, the client result is a UX signal: confirm settlement via the webhook before fulfilling. The full interface: SubmitResult shape.

The model in one diagram​

Tokenize-only — the embed mints the token and your server moves the money later:

Charge-and-save — the embed charges on submit, so the POST /v1/payment_intents leg does not apply:

The token shape​

Every token uses the vp_pmt_* prefix; reusability is a property of the token row, not the identifier.

FieldTypeMeaning
idvp_pmt_*Opaque token handle you pass to POST /v1/payment_intents
status"active" | "revoked"Lifecycle state. There is no expired status — card expiry lives on card.exp_month / card.exp_year and the token itself stays active until it is revoked. Tokens cannot be re-activated; vault a fresh one if you hit revoked.
setup_for_future_usenull | "on_session" | "off_session"Reusability scope (see below)
card.brand"visa" | "mastercard" | … | nullNetwork, read from the payment provider. null if it could not be read; the card is still saved.
card.last4string | nullLast 4 digits, read from the payment provider. null if they could not be read.
card.exp_month / card.exp_yearnumber | nullExpiration, read from the payment provider. null if it could not be read.

What gets tokenized vs what gets attached​

Only the card credential becomes the token; everything else is metadata attached at registration. Which fields render as their own inputs depends on integrationMode — in the default embed mode cardholder name, billing address, method picker, saved methods and wallets all render inside the card iframe (Render modes).

Field sourceLives where during entryBecomes part of the token credential?Stored on the vault row alongside
PAN / expiry / CVCInside binder iframe (PCI scope)Yes — this IS the token credential(it's the credential, not metadata)
Cardholder nameInside the card iframe in embed; SDK-rendered DOM input in elementsNo (attached as billing-details metadata)Yes
EmailSDK-rendered DOM input (both modes)NoNo — returned to you in result.email; it is not attached to the token or buyer automatically. Set the buyer email via buyerEmail on session-create (Buyer identification).
Billing addressInside the card iframe in embed; SDK-rendered DOM input in elementsNoYes (used for AVS)
save-for-future-use checkboxSDK-rendered DOM inputNoYes (sets setup_for_future_use — see reusability section)
Shipping address, phone, order summary, anything else on your pageYour own DOMNoNo — not VORA's data

On the tokenize-only flow the attached metadata travels with the token: when your server later calls payment_intents.create, the engine pulls the credential and the metadata — you do not forward email, address or cardholder name yourself.

The submit step​

elements.submit() reads every mounted element — iframe (PCI) and SDK-rendered DOM (non-PCI) alike — then drives tokenize + register:

elements collection
│
├── card element (iframe — PAN/expiry/CVC)
├── email element (DOM input — separate field in both modes)
├── cardholder element (inside the card iframe in embed mode;
│ separate DOM input in elements mode)
├── address element (inside the card iframe in embed mode;
│ separate field in elements mode)
└── save-for-future-use element (DOM checkbox — sets setup_for_future_use,
separate field in both modes)
│
▼
elements.submit()
│
├─ 1. Card iframe runs the gateway tokenize() → gateway-native token returns to SDK
│ (the iframe → SDK message channel; cross-origin postMessage, no PAN leaks
│ to your page)
│
├─ 2. SDK reads the values of whichever elements render as proxy DOM fields for the
│ active integrationMode (always email + sff-checkbox; also cardholder + address
│ in elements mode — in embed mode those are collected inside the card iframe)
│
├─ 3. SDK assembles the registration payload + posts to POST /v1/public/tokens
│ with the merchant's publishable key for auth
│
└─ 4. /v1/public/tokens registers the vault row → returns the vp_pmt_*, plus
last4 + brand + setup_for_future_use as set by the SDK

The result is one flat SubmitResult object regardless of how many elements were involved; fields are present iff their element was mounted. Branch in the order above:

const result = await elements.submit();

if (result.error) {
// Submit failed. Surface result.error; nothing was charged or vaulted.
} else if (result.chargeStatus === "requires_action") {
window.location.href = result.redirectUrl; // 3-D Secure; nothing charged yet
} else if (result.chargeStatus === "pending") {
// Charge in flight — await the charge.* webhook. Do NOT charge again.
// A token here (buyer on the session) is the vaulted card, not settlement.
} else if (result.token) {
// Tokenize-only: charge later via POST /v1/payment_intents.
// Charge-and-save with a buyer: the card was ALSO charged at submit —
// do NOT call payment_intents for this session; confirm via webhook.
} else if (result.charged) {
// Charge-and-save, guest: charged once, nothing vaulted, no token.
// Confirm settlement via the webhook before fulfilling.
}

On the { charged: true } arm there is no vaulted credential, so last4 / brand may be display-only placeholders.

Card expiry on the result​

expMonth and expYear come back on the vaulted-token arm — what you need to render "Visa ···· 4242 — expires 12/33" from your own markup. Three things about them:

  • They are camelCase. The API's token object spells expiry card.exp_month / card.exp_year (the wire format); the browser result spells it expMonth / expYear, like the rest of the interface.
  • expMonth is 1-12, not zero-based — the number printed on the card:
const expiry = `${String(result.expMonth).padStart(2, "0")}/${String(result.expYear).slice(-2)}`;
// → "12/33"
  • They are absent, never a placeholder, when the binder did not return them — a synthesised date would drive a wrong "your card is expiring" warning. Guard before you read them:
if (result.expMonth !== undefined) {
// safe to render an expiry
}

Charge-and-save​

On the charge-and-save flow submit() does the payment in one step: the embed charges the buyer's card on submit, and with a buyer on the session also vaults a reusable vp_pmt_*. There is no separate server-side charge step; use elements.submit(), the collection method.

Session stateWhat the embed does on submitWhat the result carries
Buyer on the sessionCharges the card and vaults a reusable payment method in one step{ token: "vp_pmt_..." } — already charged, and the token is reusable
Guest / no buyerCharges the card once, saves nothing{ charged: true } — no token

Vaulting requires a buyer: without one the result is { charged: true } and no token. A successful guest charge is not frame_tokenization_failed — that code arrives under result.error for a genuine pre-charge failure. The vaulted token is for a future, separate charge — never for re-charging the session that minted it.

Reusability — setup_for_future_use​

Buyer requirement for vaulting​

A reusable token is minted only when a buyer is attached to the session. With a buyer, charge-and-save resolves with a vp_pmt_* (charged and vaulted in one step) whose reuse scope the model below governs; on a charge-at-submit session the card is vaulted when it is charged, so a requires_action result carries no token and a pending one may not — read it back from the payment intent once the charge settles. Without a buyer the embed charges once and vaults nothing.

A save-card box on a guest session fails silently — the payment still succeeds

Nothing is vaulted and nothing fails: the charge goes through, your page shows success, and the card you believed you saved does not exist. The SDK reports frame_consent_requires_buyer without throwing; you find out at the first rebill, and consent cannot be repaired afterwards. On a gateway that vaults as part of the charge the opposite happens — submit() rejects with frame_buyer_required_for_saved_card and nothing is charged. Branch on the code. Either create the session with a buyer before rendering the control, or don't render it on guest checkouts (Recurring & saved cards).

Reuse scope​

setup_for_future_useUX that produces itWhat the token allows
null (default)Buyer didn't check save-for-future-use; merchant didn't set it explicitly at vault time.Charges made while the buyer is present. A merchant-initiated charge (mit.initiator: "merchant") returns payment_method_consent_missing. With no buyer on the session, the card can be charged only within 24 hours of being saved; after that it returns 404 payment_method_not_found.
"on_session"Buyer is present and the card may be charged again while they are, for example a one-click upsell. Set server-side: setupForFutureUse: "on_session" on POST /v1/sessions, or setup_for_future_use: "on_session" on POST /v1/tokens. The save-for-future-use checkbox toggles off_session vs single-use; it does not emit on_session.Charges made while the buyer is present, such as one-click upsells and order bumps. A merchant-initiated charge returns payment_method_consent_missing.
"off_session"Buyer consented to ongoing charges without being present at each one. Typical UX: a "Save my card for future purchases" checkbox or a subscription-signup confirmation step. Set by a checked save-for-future-use box, or server-side via POST /v1/tokens with setup_for_future_use: "off_session".All of the above, plus merchant-initiated transactions (recurring subscriptions, automated retries, scheduler-driven charges).

The gate is enforced on every merchant-initiated payment_intents.create: a charge the buyer is not present for needs an off_session token, and any other token returns payment_method_consent_missing (HTTP 422). The value is set once, at vault time, and cannot be changed — there is no update path. To widen a card's scope the buyer must present it again with the new consent sent on the call that vaults it (Fixing a missing consent).

Reuse scenarios​

One-shot charge​

Buyer enters card → submit (save-for-future-use unchecked → setup_for_future_use null)
│
▼
Returns vp_pmt_* with setup_for_future_use: null
│
▼
Your server → POST /v1/payment_intents → done.
A later merchant-initiated charge against this token → payment_method_consent_missing.

One-click upsell / order bump​

Your server creates the session with setupForFutureUse: "on_session"
│ and a buyer (POST /v1/sessions; or vault server-side with
│ POST /v1/tokens setup_for_future_use: "on_session")
▼
Buyer pays → first charge succeeds, card saved as vp_pmt_* (on_session)
│
▼
Upsell page renders, buyer clicks "Yes, add to my order"
│
▼
Second charge → POST /v1/payment_intents
payment_method: { id: vp_pmt_* } (the SAME token)
buyer_id: the session's buyer
no mit block (the buyer is present)
→ succeeds

There is no field that marks a charge as buyer-present: leaving out the mit block is what does it, and the request schema refuses unknown fields with 400 validation_unknown_field. on_session covers charges made while the buyer is present; a merchant-initiated charge against it returns payment_method_consent_missing.

Recurring / subscription​

Signup:
Buyer enters card → checks "Save my card for future purchases" → submit
(or: subscription signup flow with explicit off-session consent)
│
▼
Returns vp_pmt_* with setup_for_future_use: "off_session"
│
▼
Your server stores the vp_pmt_* + the subscription schedule
│
▼
On each recurring cycle:
Your scheduler fires → POST /v1/payment_intents with vp_pmt_*
+ mit: { initiator: "merchant",
reason: "recurring",
original_transaction_id: "vpi_live_..." }
→ succeeds

off_session is what every saved-card / recurring / scheduled flow needs. The mit block takes exactly three fields — initiator, reason (recurring, installment or unscheduled) and original_transaction_id (the first payment in the chain); anything else is refused. Declare the same word when the card is stored — Recurring & saved cards.

Network tokens​

Where the account's connection is backed by network tokens, the credential behind a vp_pmt_* can stay current through a card reissue or expiry — fewer declines on cards you charge repeatedly. Nothing in the API exposes this per card (supported_operations.network_tokens on GET /v1/capabilities is the only signal), so keep handling renewal declines and keep a path for asking the buyer to re-enter a card. Such an account enforces stored-card rules more strictly — every payment must identify the buyer — see Recurring & saved cards — constraints.

CodeHTTPCauseFix
frame_session_expiredn/a (client-side)Session was created more than the configured expiry window ago.Mint a new session on your server, retrieve it in the SDK.
frame_tokenization_failedn/a (client-side)The adapter rejected the tokenize call (invalid card, network issue, fraud-rule rejection).Surface the error to the buyer; they can retry with a different card. On the charge-and-save flow, a successful guest / no-buyer charge resolves as { charged: true } — NOT frame_tokenization_failed. Only treat frame_tokenization_failed as a genuine failure; never re-prompt or re-charge after a charged result.
frame_method_not_supported_for_sessionn/a (client-side)The deprecated single-field card.tokenize() was called on a session whose binder renders the whole payment surface in one card iframe.Use vora.elements.create() + collection.create('card') + collection.submit().
payment_method_consent_missing422Your server tried a merchant-initiated charge (mit.initiator: "merchant") against a token whose setup_for_future_use is null or "on_session".The existing token cannot be upgraded — consent is write-once. With the buyer's explicit consent, have them present the card again with setup_for_future_use: "off_session" sent on the call that vaults it (POST /v1/public/sessions/{id}/charge or POST /v1/public/tokens for embedded fields), then charge the new token.
payment_method_inactive422Token's status is "revoked".Tokens cannot be re-activated. Vault a fresh one.
payment_method_unvaulted422Token is registered but the upstream vault hasn't completed yet.Brief retry (2–5 seconds). If it persists, create a fresh token.
payment_method_binder_mismatch422The token belongs to a different payment provider than this merchant's current configuration. Tokens are NOT portable across providers — each provider holds its own vault (this happens after a re-onboarding moves you to a different processor).Vault a fresh token under the current provider via POST /v1/tokens.

What's next​

  • Quickstart — minimal end-to-end card-only integration
  • Elements reference — per-element options and the SubmitResult shape
  • 3D Secure — when the issuer requires SCA, how the confirm path drives it
  • Errors — full VoraMirrorError taxonomy