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-fieldcard.tokenize()is deprecated and refuses on this embedded monolith binder withframe_method_not_supported_for_session— the canonical path isvora.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 state | What the embed does on submit | What the result carries |
|---|---|---|
| Buyer on the session | Charges the card and vaults a reusable payment method in one step | { token: "vp_pmt_..." } — already charged, and the token is reusable |
| Guest / no buyer | Charges 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_intentsfor this session. On the charge-and-save flow the embed already charged the buyer at submit. Calling/v1/payment_intentsfor the same session charges the buyer a second time. The server-side/v1/payment_intentsleg belongs to the tokenize-only flow, not this one. The vaultedvp_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.
Related
- Quickstart — end-to-end embedded integration
- Tokenization — the tokenize-only model, where your server charges later
- Elements reference — Card, Email, Save-for-future-use
- Using React —
useVora()and the imperative elements API - Error handling —
VoraMirrorErrorcodes and recovery - Webhooks — confirm settlement server-side