Skip to main content

Element events

Every element exposes the same event API — wire your checkout UI to it for loading states, button enablement, validation messages, and focus styling.

The API — on / off​

const card = elements.create("card", {}); // no style → the SDK default look applies

const handler = (e) => { /* … */ };
card.on("change", handler); // subscribe
card.off("change", handler); // unsubscribe (pass the same handler reference)

card.mount("#card-element");

on(event, handler) and off(event, handler) are on every element type (card, email, cardholder, address, save-for-future-use, express-checkout, …).

The four events​

EventFires whenTypical use
readyThe element has finished rendering and is interactive.Hide your loading spinner / skeleton; mark the form ready.
changeThe element's value changes (and on unmount, with complete: false).Enable the Pay button when complete; surface error for inline validation.
focusThe field gains focus.Apply a focus ring or floating-label state on your wrapper.
blurThe field loses focus (or, on the express-checkout element, the buyer dismisses the wallet sheet).Validate on blur; reset focus styling.

All four fire with the same payload:

interface ElementChangeEvent {
complete: boolean; // value passes its own validation and is non-empty (or empty + not required)
error?: { code: string; message: string }; // scrubbed validation error, undefined when valid
}

Gate the submit button + show validation​

const card = elements.create("card", {}); // no style → the SDK default look applies

card.on("change", (e) => {
payButton.disabled = !e.complete; // enable only when valid
showFieldError(e.error ? e.error.message : null); // inline validation
});

card.on("ready", () => hideSpinner());
card.on("focus", () => cardWrapper.classList.add("focused"));
card.on("blur", () => cardWrapper.classList.remove("focused"));

card.mount("#card-element");

error.message is already scrubbed for display; error.code is the stable identifier to branch on.

Wallet buttons (express-checkout) use the same events​

The express-checkout element (standalone wallets) maps the native wallet sheet's lifecycle onto these same four events — so you wire it the same way. When the buyer authorizes in the wallet sheet, it fires change with complete: true; call submit() on that collection to read the charge result:

const elements = vora.elements.create();
const express = elements.create("express-checkout", {});

express.on("change", async (e) => {
if (e.error) return; // buttons unavailable (e.g. frame_wallet_domain_unverified)
if (!e.complete) return; // buyer hasn't authorized yet

const result = await elements.submit(); // submit the collection this element belongs to

if (result.chargeStatus === "requires_action") {
// From vora.js 1.22.0 the express-checkout element navigates to
// result.redirectUrl FOR YOU. Nothing is charged yet either way.
// ⚠ This handler is async — if you await work here, pass
// { handleRedirect: false } on create() and navigate yourself once it
// finishes, or the navigation cancels it. See Standalone wallets.
return;
}
if (result.charged || result.chargeStatus === "pending") {
return awaitWebhookThenFulfil(); // money already moving — do NOT charge again
}
await chargeOnYourServer(result.token, result.wallet); // vault-only accounts
});

express.mount("#express-checkout");
The wallet tap charges the buyer

On the default wallet path the tap moves the money, and the result may also carry a reusable vp_pmt_*. Branching on result.token therefore double-charges. Branch on charged / chargeStatus — see the full result table.

(If the buyer dismisses the sheet, you get blur; if it can't render, you get change with an error.)

Discrete card fields — field vs form events​

The discrete card fields (card-number / card-expiry / card-cvc) expose the same four events, but event granularity depends on the binder behind your account:

Binderchange / readyfocus / blur
Direct-card binderField-scoped — each field fires its own, carrying that field's complete.Fire per field.
Secure-fieldsWhole-form — one event for all three fields, with a form-wide complete.Do not fire.

For button enablement and validation that works on every binder, treat change as a form-level signal — complete means the whole card is valid. Per-field UX (highlighting only the CVC field) needs to be gated on which binder backs your account; independent field placement works on both.

Localization (locale)​

The SDK accepts a BCP-47 locale — on the client and per element:

const vora = new window.Vora({
publishableKey: "vp_pk_test_…",
locale: "fr-FR", // client-wide default
});

const card = elements.create("card", { locale: "de-DE" }); // per-element override

The value is forwarded to the processor's combined card iframe in embed mode, which localizes its own text where it supports the locale; in elements mode nothing is localized. The SDK's own labels, validation messages and error strings are English.