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) …. See 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.expMonth / card.expYearnumberExpiration

VORA's model is aligned with Stripe's PaymentMethod + setup_future_usage convention. If you're moving from Stripe to VORA (or running both), the field names + semantics match precisely.


What gets tokenized vs what gets attached

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 nameSDK-rendered DOM inputNo (attached as billing-details metadata)Yes
EmailSDK-rendered DOM inputNoYes (used for receipt delivery + AVS)
Billing addressSDK-rendered DOM inputNoYes (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)
├── cardholder element (DOM input)
├── email element (DOM input)
├── address element (DOM inputs or NATIVE iframe per binder)
└── save-for-future-use element (DOM checkbox — sets setup_for_future_use)


elements.submit()

├─ 1. Card iframe runs binder.tokenize() → binder-native token returns to SDK
│ (the iframe → SDK message channel; cross-origin postMessage, no PAN leaks
│ to your page)

├─ 2. SDK reads DOM-side element values (email, cardholder, address, sff-checkbox)

├─ 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.


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.
  • 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.

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) with a self-healing hint telling you to vault a fresh token with the right setup_for_future_use.

The value is set ONCE, at vault time. To upgrade scope (e.g. null"off_session"), the buyer needs to re-vault their card with the new consent — VORA doesn't expose a "promote consent" call because PSD2/SCA stored-credential rules require the consent record to match what the buyer agreed to at the moment of vault.


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 → tokenize with setupForFutureUse: "on_session"


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: "subscription",
sequence: N }
→ succeeds

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


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
Higher auth approvalIssuers typically favor network-token-backed auths. Typical lift: 1-4 percentage points.
Survives card reissuesWhen the buyer's bank reissues their card (new PAN, same account), the network silently updates the credential behind the same vp_pmt_*. Your subscription / saved-card flow doesn't break.

Provisioning happens automatically wherever the underlying binder supports it. The network_token field on the token response indicates whether one was provisioned for this specific token.

You don't have to do anything to opt into network-token coverage. The engine handles it.


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 a single-embed binder. 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".Re-vault with setup_for_future_use: "off_session" after obtaining explicit buyer consent (typically a "Save my card for future purchases" checkbox). PSD2/SCA stored-credential rules require this consent at vault time.
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_mismatch422Token was vaulted under a different binder than the merchant's current configuration. Tokens are NOT portable across binders.Vault a fresh token under the current binder.

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