Skip to main content

Embedded Fields — Embedded charge-and-save

On the embedded 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.

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

This is different from the tokenize-only model, where submit() only mints a token and your server charges later via POST /v1/payment_intents. Under charge-and-save, calling /v1/payment_intents for the same session double-charges the buyer — see the warning below.


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. With a buyer attached to the session, submit resolves with a reusable vp_pmt_* token (the card was charged and saved). Without a buyer, submit resolves with { charged: true } and no token — the card was charged once and nothing was saved for reuse.


The three-way result

Every elements.submit() call resolves to one of three shapes — a discriminated union you branch on in order:

const elements = vora.elements.create();
const card = elements.create("card", {});
card.mount("#card-element");

const result = await elements.submit(); // collection-level submit — there is no card.submit()

if (result.error) {
// Pre-charge failure — nothing was charged. Show the error, let the buyer retry.
showError(result.error.message);
} else if (result.token) {
// Buyer on session: the card was charged AND a reusable vp_pmt_* was vaulted.
// Do NOT charge again — the embed already charged under charge-and-save.
// Store result.token for future charges; confirm settlement via webhook.
} else if (result.charged) {
// Guest charge-only: the card was charged once. No token, nothing vaulted.
// Do NOT charge again. Confirm settlement via webhook before fulfilling.
}

Branch on result.error first, then result.token, then result.charged. A guest charge resolves with result.charged === true and no result.token; a buyer charge resolves with result.token set (and the card is already charged). Only result.error means nothing was charged.

Do NOT call POST /v1/payment_intents for this session. On the charge-and-save flow the embed already charged the buyer at submit. Calling /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, not this one. The vaulted vp_pmt_* token is for future charges (a later, separate session), never for re-charging the session that just created it.


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.

Confirm the charge server-side via the webhook before you fulfill the order, grant access, or ship anything. Treat the resolved result as "show the success state"; treat the webhook as "money is actually settled."


A successful guest charge is not an error

On the charge-and-save flow, 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). Never re-charge after a charged result — the card has already been charged.

See Error handling for the full VoraMirrorError union.