Skip to main content

VORA Mirror — Quickstart

Get a working VORA Mirror integration in ~10 minutes. Card-only happy path; advanced elements covered on the Elements reference page.

VORA Mirror is the embedded path — buyers stay on your domain; sensitive card data never touches your servers; tokenization happens inside provider-issued iframes mounted via the @vonpay/vora-js browser SDK. Compare with Redirect to checkout (hosted checkout — buyer goes to checkout.vonpay.com) which is faster to integrate but gives less branding control.

Architecture in one diagram

[merchant browser]                     [merchant server]              [VORA]
│ │ │
│ 1. POST /api/create-session ─────────▶│ │
│ │ 2. POST /v1/sessions ──▶│
│ │ Authorization: │
│ │ Bearer vp_sk_* │
│ │◀── { id: vp_cs_test_…} │
│◀───────────────────────────────────── │ │
│ { session_id } │ │
│ │ │
│ 3. new VORA({ vp_pk_* }) │ │
│ 4. vora.sessions.retrieve(id) ──────────────────────────────────▶│
│ GET /v1/public/sessions/:id (Bearer vp_pk_*) │
│◀── { binder, publishableKey, capabilities, … } ─────────────────│
│ │ │
│ 5. card.mount('#card') │ │
│ (binder iframe loads inside #card) │ │
│ │ │
│ 6. submit → tokenize() ───────────────────────────────────────▶│
│◀── { token: "vp_pmt_…" } │ │
│ │ │
│ 7. POST /api/charge { vp_pmt_… } ────▶│ │
│ │ 8. POST /v1/payment_intents
│ │ { payment_method: │
│ │ "vp_pmt_…" } ──────▶│
│ │◀── { status: succeeded }│

The buyer's browser never sees your secret key. The publishable key (vp_pk_*) is safe to embed in JavaScript bundles — it can only authenticate the public endpoints (/v1/public/sessions/:id, /v1/public/binder-load, /v1/public/tokens).


Step 0 — Get your test keys

If you haven't already, see Quickstart §0. For embedded checkout you need both:

  • vp_sk_test_* — secret key (your server)
  • vp_pk_test_* — publishable key (your browser bundle)

Both are available in the developer dashboard at app.vonpay.com/dashboard/developers.


Step 1 — Install the browser SDK

npm

npm install @vonpay/vora-js

CDN (no build step)

<script src="https://js.vonpay.com/v1/vora.js"></script>

The /v1/vora.js channel auto-updates within the v1 major; integrity is enforced by Subresource Integrity. To pin a specific version:

<script
src="https://js.vonpay.com/v1.0.8/vora.js"
integrity="sha384-…"
crossorigin="anonymous"
></script>

The exact integrity value for each pinned version is published at docs.vonpay.com/mirror/sri (link will activate once SRI publishing ships).


Step 2 — Create a session on your server

A VORA session is the unit of one buyer's checkout attempt. Your server creates it with the secret key (the secret key never reaches the browser):

// server/api/create-session.ts (Node example)
import express from "express";
const app = express();
app.use(express.json());

const SECRET = process.env.VONPAY_SECRET_KEY; // vp_sk_test_*
const API = process.env.VONPAY_API_BASE ?? "https://checkout.vonpay.com";

app.post("/api/create-session", async (req, res) => {
const response = await fetch(`${API}/v1/sessions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${SECRET}`,
},
body: JSON.stringify({
amount: 4999, // $49.99 in cents
currency: "USD",
successUrl: "https://mystore.com/thank-you",
cancelUrl: "https://mystore.com/cart",
}),
});
const session = await response.json();
// session.id is vp_cs_test_* — safe to send to the browser.
res.json({ session_id: session.id });
});

Returned session ID format: vp_cs_test_* for sandbox keys, vp_cs_live_* for live keys.


Step 3 — Initialize VORA in the browser

import { VORA } from "@vonpay/vora-js";

const vora = new VORA({
publishableKey: "vp_pk_test_…", // your publishable key
});

Constructor validations:

  • publishableKey is required and must start with vp_pk_test_ or vp_pk_live_. Passing a secret key throws a TypeError immediately — there's no way to leak a secret key through this surface.
  • The page must be served over HTTPS (or localhost for development). Insecure contexts throw frame_insecure_context.

Step 4 — Retrieve the session

This fetches /v1/public/sessions/:id and lazy-loads the matching binder adapter for your account's active card processor:

// Browser — fetch the session_id from your server first
const res = await fetch("/api/create-session", { method: "POST" });
const { session_id } = await res.json();

const session = await vora.sessions.retrieve(session_id);
// session.id, session.amount, session.currency, session.binder, …

Binder selection happens server-side based on the merchant's gateway configuration — your code doesn't pick a provider.


Step 5 — Mount the card field

<div id="card-element"></div>
<button id="pay-button" disabled>Pay</button>
const card = vora.fields.create("card", {
style: {
font: { size: "16px", family: "system-ui, sans-serif" },
color: { primary: "#1a1a1a", placeholder: "#9ca3af" },
},
placeholder: { number: "4242 4242 4242 4242" },
});

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

// Unlock the pay button once the field is ready
card.on("ready", () => {
document.getElementById("pay-button").disabled = false;
});

card.on("change", (event) => {
// event.complete = true when number/expiry/cvc all valid
document.getElementById("pay-button").disabled = !event.complete;
});

The actual card iframe is served from the active processor's CDN and mounted inside #card-element. Card data never reaches your DOM or your server.


Step 6 — Tokenize on submit

document.getElementById("pay-button").addEventListener("click", async () => {
try {
const result = await card.tokenize();
// result.token === "vp_pmt_test_…"
await chargeServer(result.token);
} catch (err) {
if (err.code === "frame_field_validation_failed") {
alert("Please check the card details.");
} else if (err.code === "frame_3ds_challenge_failed") {
alert("3DS authentication failed. Try a different card.");
} else {
console.error(err);
}
}
});

vp_pmt_* is a VORA payment-method token. It's single-use by default and bound to the session that minted it. Pass it to your server:

async function chargeServer(token) {
await fetch("/api/charge", {
method: "POST",
body: JSON.stringify({ payment_method: token }),
});
}

Step 7 — Charge with the token

On your server, the token becomes the payment_method on a payment intent:

// server/api/charge.ts
app.post("/api/charge", async (req, res) => {
const { payment_method } = req.body;
const response = await fetch(`${API}/v1/payment_intents`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${SECRET}`,
},
body: JSON.stringify({
amount: 4999,
currency: "USD",
payment_method,
confirm: true,
}),
});
const intent = await response.json();

// Two outcomes:
// 1. intent.status === "succeeded" — funds captured, you're done.
// 2. intent.status === "requires_action" — the issuer wants 3DS.
// intent.client_confirm carries a binder-issued challenge token.
// Forward it to the browser; the SDK renders the 3DS modal.
if (intent.status === "requires_action" && intent.client_confirm) {
return res.json({
id: intent.id,
status: intent.status,
action: intent.client_confirm, // forward as-is to vora.js
});
}

res.json({ id: intent.id, status: intent.status });
});

For 3DS-required cards, the browser passes intent.action back into collection.submit({ paymentIntent }) to drive the modal. Full walkthrough on the 3D Secure page.

See /v1/payment_intents for the full server-side reference (capture, refund, void, MIT, etc.).


Test cards

CardOutcome
4242 4242 4242 4242Tokenizes; no 3DS challenge
4000 0025 0000 31553DS2 / SCA always-authenticate — recommended for testing the VORA 3DS modal
4000 0000 0000 9995Tokenizes; charge declines at payment_intents.create

Any future expiry, any 3-digit CVC. See Reference → Test cards for the full matrix including decline codes, 3DS-required cards, and per-processor test PANs.


Working sample — frame-react

The complete end-to-end integration lives at samples/frame-react in the SDK repo. It demos all five live elements (card + email + cardholder + address + save-for-future-use) plus the 3DS modal harmonization with a VORA-modal toggle:

  • An Express stub server (server/index.ts) for POST /api/create-session
  • A React app using the imperative vora.elements.create() API (the multi-element collection API isn't yet wrapped in vora-react components — see the React page for what's available there today)
  • A Vite dev setup with .env separating server-side VONPAY_SECRET_KEY from browser-side VITE_VONPAY_PUBLISHABLE_KEY

Clone, cp .env.example .env, fill in your keys, pnpm dev. The sample's README carries the canonical test recipe — what to verify when each element mounts, validation behavior, the 3DS modal-vs-native-sheet comparison, and the production-wiring checklist.


What's next