Hosted checkout & popup
Every checkout comes with a page on vitix.net that handles cards, wallets and 3-D Secure. Send the customer there with a redirect, or open it in a popup window and let it tell your page when the payment is done.
The checkout url
checkout.url always points to https://vitix.net/pay?cs=<checkout id>. In live mode the page shows the card form; in test mode it shows the sandbox checkout where you can complete or decline the payment. The page is brand-neutral: it displays the amount, your description and reference, and nothing about your internal customer ids or metadata.
When the customer pays, the page shows Vitix's confirmation and keeps the customer there; it never redirects to your site and shows no link back to it. With the popup flow your page receives the success message and decides where to go next. The payment.succeeded webhook remains the source of truth for crediting the account.
Option A — redirect
The simplest integration and the most robust one in every browser: send the customer to checkout.url with a 303 and let the hosted page do the rest.
// Server: create the checkout, then send the browser to checkout.urlconst checkout = await vitix.checkouts.create({ amount: 2000, customer: { id: user.id, email: user.email }, success_url: "https://app.example.com/billing/done?cs={CHECKOUT_ID}", cancel_url: "https://app.example.com/billing",});redirect(checkout.url); // → https://vitix.net/pay?cs=cs_live_…Option B — popup window
Open checkout.url with window.open to keep the customer on your page. The hosted page posts a message to the window that opened it at each step and closes itself after a successful payment. Always check e.origin === "https://vitix.net" before trusting a message, and fall back to a redirect when the popup is blocked.
<!-- Client: open the Vitix checkout in a popup and keep your page underneath --><button id="topup">Add credits</button><script> document.getElementById("topup").addEventListener("click", async () => { // 1. Ask your server for a checkout (it calls vitix.checkouts.create and returns { id, url }) const { url } = await fetch("/api/topup", { method: "POST" }).then((r) => r.json()); // 2. Open the hosted page as a popup; fall back to a redirect if popups are blocked const w = window.open(url, "vitix-checkout", "popup=yes,width=600,height=900"); if (!w) { window.location.href = url; return; } // 3. Listen for the result window.addEventListener("message", function onMessage(e) { if (e.origin !== "https://vitix.net") return; // only trust Vitix const msg = e.data; if (!msg || msg.source !== "vitix-pay") return; if (msg.event === "success") { window.removeEventListener("message", onMessage); // Show a success state now; credit the account from the payment.succeeded webhook. window.location.href = "/billing/done?cs=" + encodeURIComponent(msg.checkout_id); } else if (msg.event === "close") { // Customer abandoned the payment form — show your own "try again" UI. } }); });</script>The same thing as a React component:
"use client";import { useCallback, useEffect, useRef, useState } from "react";export function TopupButton() { const [done, setDone] = useState<string | null>(null); const win = useRef<Window | null>(null); useEffect(() => { const onMessage = (e: MessageEvent) => { if (e.origin !== "https://vitix.net" || e.data?.source !== "vitix-pay") return; if (e.data.event === "success") setDone(e.data.checkout_id); }; window.addEventListener("message", onMessage); return () => window.removeEventListener("message", onMessage); }, []); const open = useCallback(async () => { const { url } = await fetch("/api/topup", { method: "POST" }).then((r) => r.json()); win.current = window.open(url, "vitix-checkout", "popup=yes,width=600,height=900"); if (!win.current) window.location.href = url; // popup blocked → redirect }, []); return done ? <p>Thanks! Your credits are on the way.</p> : <button onClick={open}>Add credits</button>;}Messages posted by the hosted page
| event | When | Payload |
|---|---|---|
| loaded | The payment form is ready. | { source: 'vitix-pay', event: 'loaded', checkout_id } |
| success | The payment was confirmed. Credits follow via webhook within seconds. | { source: 'vitix-pay', event: 'success', checkout_id } |
| close | The customer closed or abandoned the payment form. | { source: 'vitix-pay', event: 'close', checkout_id } |
After success the popup closes itself. If the browser refuses to close it, the page shows a “Continue” button that leads to Vitix's confirmation page. Your page still receives the message, so handle success there.
Why not an iframe?
The card form's security policy only allows vitix.net as a frame ancestor, and browsers check every ancestor in the chain. A page that embeds vitix.net/pay in an <iframe> would therefore see the form refuse to load. Use the popup instead: it is a top-level window, so the policy is satisfied and the customer still stays on your page.
Testing
Create a checkout with a vx_test_ key, open its url as a popup, click Pay in the sandbox form and confirm that your listener receives success and your webhook receives payment.succeeded. Click Simulate a declined card to exercise your error state. See Testing.