# Vitix Pay developer documentation | Vitix Pay

> Accept top-ups and one-time payments in your own SaaS through Vitix Pay: a Stripe-style REST API, Node SDK, signed webhooks and a full test mode.

Canonical: https://vitix.net/developers
Markdown: https://vitix.net/developers.md


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.
[API reference](https://vitix.net/developers/api)[Node SDK](https://vitix.net/developers/sdk)[Webhooks](https://vitix.net/developers/webhooks)

## 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: "ana@example.com" }, 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": "ana@example.com", "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](https://vitix.net/developers/embed#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": "ana@example.com", "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](https://vitix.net/developers/api#create-webhook-endpoint) 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`.

[Testing →](https://vitix.net/developers/testing)
Questions or a missing feature? Email [support@vitix.net](mailto:support@vitix.net)
