Skip to content
Documentation menu

Developer documentation

Vitix Pay lets your own product accept top-ups and one-time payments through Vitix's billing stack. You keep your users, credits and ledger; Vitix handles checkout, payment processing, invoices and tells you when money arrives.

Hosted checkout

One POST creates a checkout; the customer pays on a hosted page. Cards, Apple Pay, Google Pay and local methods are handled for you.

Signed webhooks

Every payment, refund and expiry is delivered as a signed event with automatic retries, redelivery and a full delivery log.

Real test mode

Test keys never touch a payment provider: a hosted sandbox page completes or declines payments and fires the same webhooks.

Safe by default

Idempotency keys, request ids, rate limits, allow-listed return domains and typed errors, modelled on the APIs you already know.

How it works

A payment is a three-step conversation between your server, the customer's browser and Vitix:

  • Your server creates a checkout with the amount, who is paying and where to send them afterwards. You get back a hosted url.
  • The customer pays on that page and stays on Vitix's confirmation screen. Vitix never sends the customer to your site and never shows your name to the payment provider.
  • Vitix creates a payment and POSTs a signed payment.succeeded event to your webhook endpoint. That event is the source of truth for crediting the customer.

Money is settled to Vitix. Every payment carries your customer.id, reference and metadata unchanged, so reconciling with your own ledger is a lookup, not a guess.

Quickstart

Six steps take you from nothing to a live top-up flow. The samples use the Node SDK; the same calls work with any HTTP client.

1

Get your API keys

Vitix issues two secret keys per account: vx_test_… for development and vx_live_… for production. Store them server-side only, for example in VITIX_API_KEY. Keys are shown once; you can rotate them at any time by asking Vitix for a new one.

bash
npm install @vitix/pay# .env.localVITIX_API_KEY=vx_test_••••••••••••••••••••••••••••••••VITIX_WEBHOOK_SECRET=whsec_••••••••••••••••••••••••••••••••••••••••••=
2

Create a checkout on your server

Amounts are integers in the currency's minor unit: cents for usd, đồng for vnd. VND amounts are converted to USD at 26,000 ₫ per dollar and the USD figure is returned as amount_usd. Send an Idempotency-Key so a retried request never creates two checkouts.

import { VitixPay } from "@vitix/pay";const vitix = new VitixPay(process.env.VITIX_API_KEY!);// On your server, when the customer picks an amountconst checkout = await vitix.checkouts.create(  {    amount: 2000,                 // $20.00, in cents    currency: "usd",              // or "vnd" (amount in đồng)    customer: { id: "user_42", email: "[email protected]" },    reference: "topup_8f1c",      // your own id, echoed on the payment    success_url: "https://app.example.com/billing/success?cs={CHECKOUT_ID}",    cancel_url: "https://app.example.com/billing",  },  { idempotencyKey: "topup_8f1c" },);// Send the customer to the hosted checkout pageredirect(checkout.url);

The response is a checkout object with the hosted page in url:

201 Created
{  "id": "cs_live_0aWdSNgZbvGU0BjvCISLh07o",  "object": "checkout",  "livemode": true,  "status": "open",  "payment_status": "unpaid",  "amount": 600000,  "currency": "vnd",  "amount_usd": 2308,  "description": "Acme credits",  "customer": { "id": "user_42", "email": "[email protected]", "name": "Ana Nguyen" },  "reference": "topup_8f1c",  "metadata": { "plan": "pro" },  "success_url": "https://app.example.com/billing/success?cs={CHECKOUT_ID}",  "cancel_url": "https://app.example.com/billing",  "url": "https://vitix.net/pay?cs=cs_live_0aWdSNgZbvGU0BjvCISLh07o",  "payment": null,  "expires_at": "2026-10-14T07:32:45.000Z",  "completed_at": null,  "created_at": "2026-10-14T06:32:45.156Z"}
3

Redirect the customer

Send the customer's browser to checkout.url with a normal HTTP redirect. The page is hosted by Vitix (or by the Vitix sandbox in test mode), so you never handle card data. A checkout stays open for one hour by default (expires_in accepts 5 minutes to 24 hours).

4

Show the result on your side

The customer is not redirected to success_url: after paying they see Vitix's own confirmation page. If you opened the checkout in a popup, your page receives the success message and can move to your own success screen; the example below retrieves the checkout by id to show the right message there. success_url is kept on the checkout object for your records only. Use that screen for UX only; the webhook in the next step is what you credit from.

ts
// app/billing/success/page.tsx (Next.js, server component)import { VitixPay } from "@vitix/pay";export default async function Success({ searchParams }: { searchParams: Promise<{ cs?: string }> }) {  const { cs } = await searchParams;  const vitix = new VitixPay(process.env.VITIX_API_KEY!);  const checkout = await vitix.checkouts.retrieve(cs!);  if (checkout.payment_status === "paid") {    return <p>Thanks! ${(checkout.amount_usd / 100).toFixed(2)} was added to your account.</p>;  }  // The webhook may still be on its way: show a pending state and poll, never credit here.  return <p>Confirming your payment…</p>;}
Checkout after payment
{  "id": "cs_live_0aWdSNgZbvGU0BjvCISLh07o",  "object": "checkout",  "livemode": true,  "status": "complete",  "payment_status": "paid",  "amount": 600000,  "currency": "vnd",  "amount_usd": 2308,  "description": "Acme credits",  "customer": { "id": "user_42", "email": "[email protected]", "name": "Ana Nguyen" },  "reference": "topup_8f1c",  "metadata": { "plan": "pro" },  "success_url": "https://app.example.com/billing/success?cs={CHECKOUT_ID}",  "cancel_url": "https://app.example.com/billing",  "url": "https://vitix.net/pay?cs=cs_live_0aWdSNgZbvGU0BjvCISLh07o",  "payment": "py_live_4YMjlAzqiKxSYAIsAWEToqvL",  "expires_at": "2026-10-14T07:32:45.000Z",  "completed_at": "2026-10-14T06:35:02.410Z",  "created_at": "2026-10-14T06:32:45.156Z"}
5

Fulfil from the webhook

Register an endpoint (API or ask Vitix) and verify every delivery with the secret. Credit the customer when you receive payment.succeeded, keyed by payment.id so a redelivered event is a no-op. Return any 2xx within 10 seconds; anything else is retried for up to 12 hours.

ts
// app/api/vitix/webhook/route.tsimport { Webhooks, WebhookSignatureError } from "@vitix/pay";export async function POST(req: Request) {  const body = await req.text();  let event;  try {    event = await Webhooks.constructEvent(body, req.headers, process.env.VITIX_WEBHOOK_SECRET!);  } catch (err) {    if (err instanceof WebhookSignatureError) return new Response("Invalid signature", { status: 400 });    throw err;  }  if (event.type === "payment.succeeded") {    const payment = event.data.object;    // Idempotent: use event.id (or payment.id) as the unique key of your ledger row.    await creditCustomer({ userId: payment.customer!.id, usdCents: payment.amount_usd, key: payment.id });  }  return new Response("ok");}
6

Go live

Swap vx_test_ for vx_live_, register a live-mode webhook endpoint (https only) and you are done. Live checkouts have a minimum of $10.00 and a per-account maximum; every payment also gets a downloadable invoice via GET /v1/payments/:id/invoice.

Questions or a missing feature? Email [email protected]