Skip to main content

Embedded Fields — Tokenization

The flow that turns a buyer's card details + the rest of the checkout fields into a single opaque token your server uses for every payment operation. This page covers the model end-to-end: what gets tokenized, how reusability works, and how the token feeds your downstream API calls.

Two flows: tokenize-only vs charge-and-save

The embed supports two flows. Which one applies depends on your active checkout configuration, and the shape of the elements.submit() result tells you which path ran.

  • Tokenize-only. The embed mints a reusable vp_pmt_* token at submit time and charges nothing. Your server runs every payment operation later via POST /v1/payment_intents. This is the model the diagrams and reuse scenarios below describe in detail.
  • Charge-and-save. The embed charges the buyer's card at submit time. With a buyer attached to the session it ALSO vaults a reusable vp_pmt_* in the same step; a guest / no-buyer session charges once and saves nothing (no token, { charged: true }). On this flow your server must NOT also call POST /v1/payment_intents for the same session — doing so charges the buyer a second time. Confirm settlement via the webhook before fulfilling.

Discriminate on the result: if (result.error) … else if (result.token) … else if (result.charged) …. On a charge-at-submit session the result also carries chargeStatus, which adds two outcomes that are neither a success nor a failure — "requires_action" (redirect the buyer to result.redirectUrl; nothing charged) and "pending" (settling asynchronously; await the webhook and do not re-charge). Branch on those before token / charged, or they match nothing and the sale is silently lost. See Charge at submit for the full contract and the SubmitResult shape below.

If you only need the happy-path sequence, jump to Quickstart. If you're trying to understand WHY the flow works the way it does, read this.


The model in one diagram

The diagram above is the tokenize-only flow: the embed mints the token and your server moves the money later with POST /v1/payment_intents.

Under the charge-and-save flow the embed charges on submit, so the POST /v1/payment_intents leg does not apply — calling it for the same session double-charges the buyer:

The page never sees the PAN. Your server never sees the PAN. Your code only ever holds the opaque vp_pmt_* handle.


Why the model looks like this

ConcernHow the model addresses it
PCI scopeCard data lives only inside the binder-served iframe — never touches your DOM, never touches your server. You stay SAQ A.
Vendor independenceYour server only ever speaks to VORA's API using vp_* tokens. Swap the underlying card processor without changing merchant code.
Mixed data collectionOne element collects the card; other elements collect non-PCI data (email, name, address). elements.submit() aggregates them into one token registration.
ReusabilityThe same vp_pmt_* shape covers single-use and reusable scenarios. The setup_for_future_use field on the token row governs what operations are allowed.

The token shape

Every token VORA mints uses the vp_pmt_* prefix. There's no separate prefix for reusable vs single-use — 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" | "expired" | "revoked"Lifecycle state. Tokens cannot be re-activated; vault a fresh one if you hit expired or revoked.
setup_for_future_usenull | "on_session" | "off_session"Reusability scope (see below)
card.brand"visa" | "mastercard" | …Network
card.last4stringLast 4 digits
card.exp_month / card.exp_yearnumberExpiration

VORA's tokenization model follows the standard reusable-payment-method convention common across payment processors — a vaulted method (vp_pmt_*) plus a setup_for_future_use reuse scope — so if you're migrating from or running alongside another processor, the field names and semantics map cleanly.


What gets tokenized vs what gets attached

Which fields render separately depends on integrationMode

The iframe-vs-DOM split below is for integrationMode: "elements". In the default embed mode the card field, cardholder name, billing address, method picker, saved methods, and wallet buttons all render inside the card iframe — only email and save-for-future-use are separate fields you place. (How the modes work →.)

Tokenization isn't "the whole checkout becomes the token." Only the card credential becomes the token. Everything else is metadata attached to it at registration time.

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, when your server later calls payment_intents.create, the engine pulls the card credential AND the attached metadata to drive the charge. You don't have to forward the email / address / cardholder name yourself — they travel with the token.

Charge-and-save: do not create a second payment intent

On the charge-and-save flow the buyer's card is charged at submit time, inside the embed. Do NOT call POST /v1/payment_intents for that session — doing so charges the buyer a second time. Confirm the charge server-side via the webhook before fulfilling. Whether a session tokenizes-only or charges-and-saves is decided by your active checkout configuration; the result shape (result.charged vs a tokenize-only result.token you charge yourself) tells you which path ran.


Mixed iframe + DOM collection — the submit step

elements.submit() is the moment everything comes together. It reads from every mounted element regardless of whether each one renders as an iframe (PCI) or as SDK-controlled DOM (non-PCI), then drives the tokenize + register flow.

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 submit returns a single SubmitResult object regardless of how many elements were involved. Fields are present iff their corresponding element was mounted in the collection:

interface SubmitResult {
token?: string; // vp_pmt_* — present when a vault row was registered
charged?: true; // present on the charge-and-save guest/no-buyer arm
last4?: string;
brand?: string;
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;
}

SubmitResult is one flat interface — every field is optional (no separate union-arm types to import). Always branch at runtime in this order:

const result = await elements.submit();

if (result.error) {
// Submit failed. Surface result.error to the buyer; nothing was charged or vaulted.
} else if (result.token) {
// A vault row was registered → result.token is a vp_pmt_*.
// 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/no-buyer: the card was charged once and nothing
// was vaulted, so there is no token. Do NOT re-prompt or re-charge.
// 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 rather than vault-row values.


Charge-and-save

On the charge-and-save flow, the collection's submit() does the payment in one step: the embed charges the buyer's card on submit. When the session has a buyer attached, that same submit also vaults a reusable vp_pmt_* token so you can charge the card again later. There is no separate server-side charge step — the charge already happened inside the embed by the time the promise resolves. This is the other of the two flows; the tokenize-only model above is the one where submit() mints a token and your server charges later.

Use elements.submit() (the collection method) here — the canonical path is vora.elements.create() + collection.create('card') + collection.submit(). The deprecated single-field card.tokenize() refuses on this binder with frame_method_not_supported_for_session.

What submit does

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, submit() resolves with { charged: true } and no token — the card was charged once and nothing was saved for reuse. Branch on result.error first, then result.token, then result.charged (the same three-way discrimination as tokenize-only).

Do NOT create a second payment intent

On the charge-and-save flow the embed already charged the buyer at submit. Calling POST /v1/payment_intents for the same session charges the buyer a second time. The server-side /v1/payment_intents leg belongs to the tokenize-only flow. The vaulted vp_pmt_* is for a future, separate charge — never for re-charging the session that minted it.

A successful guest charge is not an error

A successful guest charge resolves as { charged: true } — it is not frame_tokenization_failed. A missing result.token on a guest session is the expected charge-only success, not a failure. Branch on result.charged before treating an absent token as a problem, reserve frame_tokenization_failed handling for a genuine pre-charge failure (it arrives under result.error), and never re-charge after a charged result. See Error handling.

Confirm settlement before fulfilling

The client-side charged / token result is a UX signal only — it tells your front-end the embed accepted the card so you can advance the buyer to a confirmation screen. It is not proof of settlement, and a timeout or network error on submit can land after the charge already succeeded — so never mark the order failed on a client error, and never resubmit blindly (you may double-charge). Confirm the charge server-side via the webhook before you fulfill the order, grant access, or ship anything — see Reconciliation for the duplicate-safe pattern (dedupe on the event envelope id, keep fulfillment idempotent per session).


Reusability — setup_for_future_use

Buyer requirement for vaulting

A reusable token is only minted when a buyer is attached to the session. On the charge-and-save flow:

  • With a buyer on the session, elements.submit() resolves with a vp_pmt_* token (the card is charged AND a reusable credential is vaulted in one step), and the setup_for_future_use model below governs that token's reuse scope. On a charge-at-submit session the card is vaulted when it is charged, so a submit that returns before the charge completes — chargeStatus: "requires_action" (every 3-D Secure card) or "pending" — carries no token yet. The card is vaulted at settlement; read it back from the payment intent.
  • Without a buyer (guest session), the embed charges the card once and vaults nothing — the result is { charged: true } with no token. There is no card-on-file to reuse.

So before designing any card-on-file / saved-card / recurring UX, make sure a buyer is on the session; otherwise no reusable token is produced regardless of the save-for-future-use checkbox or merchant policy.

This fails silently — the payment still succeeds

If the buyer ticks the save-card box on a guest session, 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 it as frame_consent_requires_buyer, which never throws and never blocks the charge.

You find out at the first rebill, and it cannot be repaired — consent is recorded when the card is first vaulted. Either create the session with a buyer before rendering the control, or don't render it on guest checkouts. See Recurring & saved cards — step 3.

Reuse scope

A vp_pmt_* token's reuse scope is governed by the setup_for_future_use field set at vault time. Three values:

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.Single charge by the originating payment_intents.create. Subsequent CIT or MIT charges referencing this token return payment_method_consent_missing.
"on_session"Buyer is present and consented to "save my card for this purchase session." Set explicitly — card.tokenize({ setupForFutureUse: "on_session" }) in the browser, or the merchant's server calls POST /v1/tokens with setup_for_future_use: "on_session". (The save-for-future-use checkbox itself only toggles off_session vs single-use; it does not emit on_session.)Single + follow-on cardholder-initiated charges where the buyer is interactively present (e.g. one-click upsells, order-bumps within the same checkout window). MIT/recurring is still rejected.
"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 explicitly via card.tokenize({ setupForFutureUse: "off_session" }) / 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 at every payment_intents.create call. If your server tries an operation the token wasn't consented for, you get payment_method_consent_missing (HTTP 422). The fix is always a fresh token vaulted with the right setup_for_future_use — never an update to the existing one.

The value is set ONCE, at vault time, and cannot be changed afterwards. To move a card to a wider scope (e.g. null"off_session"), the buyer must present it again and the new consent must be sent on the call that vaults it. There is no "promote consent" call — no PATCH, no PUT, no update path — because PSD2/SCA stored-credential rules require the consent record to match what the buyer agreed to at the moment of vault. See Fixing a missing consent for the exact endpoint per flow.


Reuse scenarios

What's allowed depends on the setup_for_future_use value and the network rules for merchant-initiated transactions.

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.
Any subsequent charge against this token → payment_method_consent_missing.

Most merchants integrating Embedded Fields for the first time use this flow.

One-click upsell / order bump

Buyer enters card → server vaults with setup_for_future_use: "on_session"
│ (POST /v1/tokens — the browser save-for-future-use checkbox
│ only emits off_session|single-use, and the deprecated
▼ card.tokenize() is refused on embed-mode binders)
Returns vp_pmt_* with setup_for_future_use: "on_session"


First charge → POST /v1/payment_intents { payment_method: { id: vp_pmt_* } } → succeeded


Upsell page renders, buyer clicks "Yes, add to my order"


Second charge → POST /v1/payment_intents with the SAME vp_pmt_*
+ cit: true (cardholder-initiated; buyer clicked)
→ succeeds (still within session-scope consent)

The on-session scope means the buyer needs to be interactively present (cardholder-initiated transactions only). Off-session/scheduler charges against an "on_session" token return 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

The off_session scope is what every "saved card on file" / "recurring" / "scheduled charge" flow needs. Without it, merchant-initiated charges are rejected.

The mit block takes exactly three fields — initiator, reason, and original_transaction_id (the first payment in the chain). reason is one of recurring, installment, or unscheduled; anything else, or any extra field, is refused. Declare the same word when the card is stored — see Recurring & saved cards.


Network tokens

A network token (issued by the card network — Visa Token Service, Mastercard MDES, Amex Token Service) can be provisioned at vault time when the underlying binder supports it. It's invisible to merchant code — same vp_pmt_* prefix, same handle — but it can affect two things:

EffectWhat changes
Fewer declines from stale card detailsA network token stays current when the underlying card number changes or expires, so charges that would otherwise be declined on out-of-date credentials can still go through. The effect is largest on cards you charge repeatedly.
Can survive a card reissueWhen the buyer's bank reissues their card — new number, same account — the credential behind your vp_pmt_* can be updated by the network, so a saved-card or subscription charge keeps working without asking the buyer for the card again.
Treat this as a tailwind, not a guarantee you can build on

Both effects depend on the account's connection actually being backed by network tokens, and on the card network and issuer participating for that particular card. None of that is visible to you per-card — there is no field on the token that tells you whether a given saved card is covered.

So don't design a subscription on the assumption that stored cards never go stale. Keep handling renewal declines, and keep a path for asking the buyer to re-enter a card.

Network tokens are not a Von Payments API surface. You do not mint, store, or reference one, and no request or response field exposes them — including on the token response. Whether an account is backed by them is a property of the connection, configured outside the API. The only place it surfaces is supported_operations.network_tokens on GET /v1/capabilities, which is read-only.

There is nothing to switch on. There is something to build for: an account backed by network tokens enforces the rules around stored cards more strictly, so the same request that works elsewhere can be refused. Most importantly, every payment must identify the buyer. See Recurring & saved cards — constraints before you build a subscription or card-on-file flow.


How the token feeds the downstream API

                           ┌─── POST /v1/payment_intents
│ (creates the auth or auth+capture)

├─── POST /v1/payment_intents/:id/capture
vp_pmt_* ─────▶│ (captures an auth-only intent)

├─── POST /v1/payment_intents/:id/void
│ (voids an uncaptured auth)

└─── POST /v1/refunds
(refunds by intent ID — token reference
lives on the intent, not passed directly)

The token (or rather, the intent the token produced) is the handle for everything. Your server doesn't need to know which underlying processor ran the charge; the intent ID is opaque and uniform across binders.


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 an embed-mode session (the default integration mode, where the whole payment surface renders in one card iframe). This is an integration-time wrong-method condition, not a server bug — do not page on it.card.tokenize() is deprecated. Use vora.elements.create() + collection.create('card') + collection.submit() to charge and save. Your server configuration is fine.
payment_method_consent_missing422Your server tried an MIT charge (or a CIT outside the originating intent) against a token whose setup_for_future_use is null or "on_session".The existing token cannot be upgraded — consent is write-once. Have the buyer present the card again, sending setup_for_future_use: "off_session" 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. Obtain explicit consent first — typically a "Save my card for future purchases" checkbox — because PSD2/SCA stored-credential rules require the consent record to match what the buyer agreed to.
payment_method_inactive422Token's status is "revoked" (or age-expired).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, the iframe/DOM boundary, per-binder capability matrix
  • 3D Secure — when the issuer requires SCA, how the confirm path drives it
  • Errors — full VoraMirrorError taxonomy