DocsEmbedded checkout
  1. Docs
  2. Guides
  3. Integrations

Embedded checkout

Keep the payer on your page: the VelPay checkout runs in an iframe inline in your layout or in a popup, and tells your page when the payer has paid.

La documentazione per sviluppatori è disponibile in inglese.

1. Allow your site to embed

Browsers only show the checkout inside pages you have allowed. In the dashboard, open Settings → Stores, pick the store and add your site under Allowed embed origins, one per line:

  • An exact origin: https://shop.example.com (with a port if you use one: https://shop.example.com:8443).
  • A wildcard for one subdomain level: https://*.example.com matches https://eu.example.com and https://us.example.com, but not https://example.com itself or https://a.b.example.com.
  • https only, up to 10 per store, no paths. https://*.com is not allowed.

The store must be approved, and the order must belong to that store: create it with an API key limited to the store. Changes take up to a minute to apply.

2. Create the order on your server

Exactly as for a redirect (see the quickstart). Pass the order's id to your page. Never create orders from the browser: that would expose your secret key.

3. Show the checkout

HTML
<script src="https://velpay.pro/embed.js"></script>

<div id="pay"><p id="spinner">Loading payment…</p></div>

<script>
  // The order id comes from POST /api/v1/orders, made on your server.
  const pay = velpay.embed("ord_7Hq2Lx9vTn3Kp0aWm1Qz8c", { locale: "de" });

  pay
    .on("loaded", () => {
      // The checkout replaced the spinner; nothing to do unless you show your own.
    })
    .on("paid", () => showMessage("Thanks! Your payment is confirmed."))
    .on("expired", () => showMessage("This payment expired. Reload to try again."))
    .on("closed", (e) => {
      if (e.reason === "dismissed") showMessage("Payment not finished.");
    })
    .mount("#pay");

  // Later, to remove it yourself:
  // pay.unmount();               // fires "closed" with reason "unmounted"
</script>

velpay.embed(orderId, options) returns a handle and throws a TypeError if the id is malformed. The only option is locale, the checkout language (for example "de"); without it the checkout uses the order's locale if you set one, else the payer's language. Every handle method returns the handle, so calls chain.

MethodNotes
on(event, fn)Adds a handler. Add them before mount() or open(): a handler added after its event fired is not called for it.
mount(target)Shows the checkout inside target (CSS selector or Element), replacing its content. Throws if nothing matches.
open()Shows the checkout in a modal over your page. It closes with ×, Esc or a click outside.
unmount()Removes the checkout and fires closed with reason unmounted. After that you can mount or open again.
EventPayloadWhen
loaded{ order, state }The checkout has loaded and is talking to your page.
paid{ order, state: "paid" }The order is paid.
expired{ order, state: "expired" }The order expired before it was paid.
closed{ order, reason, url? }The checkout went away. reason: dismissed (×, Esc, click outside the modal), canceled (the payer chose to cancel; url is the order's cancel_url, where the window goes next), voided (you voided the order), unmounted (your code).

The iframe grows and shrinks with its content, so don't give it a fixed height.

HTML
<button id="pay-button">Pay now</button>
<script src="https://velpay.pro/embed.js"></script>
<script>
  document.querySelector("#pay-button").addEventListener("click", async () => {
    const res = await fetch("/api/create-velpay-order", { method: "POST" }); // your server
    const { id } = await res.json();
    velpay.embed(id).on("paid", () => location.assign("/thanks")).open();
  });
</script>

Redirects

If the order has a success_url, the whole window (not the iframe) goes there a few seconds after paid. If the payer cancels and the order has a cancel_url, the window goes there right after closed. Leave both out when you want to handle everything in your handlers.

Card payments continue at the provider in a new tab; the embedded checkout updates by itself when the provider confirms.

Fulfil on the webhook

The paid event is for your interface: show a thank-you, move to the next step. It runs in the payer's browser and can be faked. Ship goods only on the signed order.paid webhook, or after checking GET /api/v1/orders/:id on your server.

Without embed.js

embed.js is small and has no dependencies; we recommend it. If you build your own frame, load https://velpay.pro/c/<orderId>?embed=1&origin=<your origin> (add &lang=de to pick the language). The checkout and your page then connect in two steps:

  1. The checkout posts { velpay: "hello", v: 2, order } to your origin, never to *.
  2. You answer with a MessageChannel: post { velpay: "connect", v: 2, order } to the checkout with port2 attached. From then on every event arrives on port1, which only the checkout can write to.

If the checkout reloads (a language switch does that), it says hello again: answer with a new channel. Events on the port are { event, … }:

eventFieldsMeaning
loadedstateLoaded and connected.
heightpxContent height in CSS pixels; set the iframe height.
paidstatePaid.
expiredstateExpired.
closedreason, url?Remove the frame. With canceled, url is the order's cancel_url: navigate there.
navigateurl, outcomeNavigate the top window to url.

Answer a hello only when it comes from the VelPay origin and from your iframe:

JavaScript
const VELPAY = "https://velpay.pro";
const iframe = document.createElement("iframe");
iframe.src = `${VELPAY}/c/${orderId}?embed=1&origin=${encodeURIComponent(location.origin)}`;
document.querySelector("#pay").append(iframe);

window.addEventListener("message", (ev) => {
  // 1. The checkout says hello. Answer with a fresh channel (again after a reload).
  if (ev.origin !== VELPAY || ev.source !== iframe.contentWindow) return;
  if (ev.data?.velpay !== "hello" || ev.data.order !== orderId) return;
  const channel = new MessageChannel();
  channel.port1.onmessage = ({ data }) => {
    // 2. Every event arrives here, from the checkout only.
    switch (data.event) {
      case "height":   iframe.style.height = data.px + "px"; break;
      case "navigate": location.assign(data.url); break;
      case "closed":   iframe.remove(); if (data.url) location.assign(data.url); break;
      // "loaded", "paid", "expired": update your page
    }
  };
  iframe.contentWindow.postMessage({ velpay: "connect", v: 2, order: orderId }, VELPAY, [channel.port2]);
});

Troubleshooting

  • The frame stays empty or shows a browser error: your page's origin isn't in the store's allowed embed origins, or the order was created with a key that isn't limited to that store. Check the exact scheme, host and port.
  • No events fire: the page is served from an origin that isn't allowed (for example http://localhost). Use a tunnel with https and add its origin while you develop.
  • Test mode: the simulate buttons work inside the frame too, so you can test every event.