Testing
Test mode is a complete parallel environment: the same API, the same objects and the same webhooks, backed by a hosted sandbox checkout instead of a payment provider. No card is charged and no money moves.
Test keys
Keys starting with vx_test_ operate in test mode. Everything they create (cs_test_…, py_test_…, evt_test_…) carries livemode: false and is invisible to live keys, and vice versa. Webhook endpoints are mode-scoped too: register your staging URL with the test key and your production URL with the live key.
| Test mode | Live mode | |
|---|---|---|
| Key prefix | vx_test_ | vx_live_ |
| Checkout url | Hosted sandbox page on api.vitix.net | Hosted payment page (Polar) |
| Minimum amount | Your account minimum (default $10) | $10.00 |
| success_url | https, or http://localhost | https on an allowed domain |
| Webhook endpoint url | https or http | https only |
| Refunds | Settle instantly | Settle asynchronously (pending → succeeded) |
| Invoices | Not available (invoice_unavailable) | PDF via /v1/payments/:id/invoice |
| fee_usd | Always 0 | Actual processing fee |
The sandbox checkout page
In test mode, checkout.url points to a Vitix-hosted page that looks like a real checkout but has two buttons: Pay, which completes the checkout, and Simulate a declined card, which shows a decline and leaves the checkout open so you can try again. Completing it emits the same checkout.completed and payment.succeeded events as production and then redirects to your success_url.
# 1. Create a test checkout (note the vx_test_ key and the localhost success_url)curl https://api.vitix.net/v1/checkouts \ -H "Authorization: Bearer vx_test_••••••••" \ -H "Content-Type: application/json" \ -d '{ "amount": 2000, "customer": { "id": "user_42" }, "success_url": "http://localhost:3000/billing/success?cs={CHECKOUT_ID}" }'# → "url": "https://vitix.net/pay?cs=cs_test_…" (Vitix-hosted sandbox checkout)# 2. Open that url in a browser and click "Pay" (or "Simulate a declined card")# 3. Your webhook receives checkout.completed + payment.succeeded; the browser stays on Vitix's confirmation page- The email you type on the sandbox page is stored on the payment, exactly as in production.
- Opening the page again after paying shows a "Payment complete" state; paying twice is impossible.
- An expired checkout (
expires_atpassed, orPOST /expire) renders an expired state and emitscheckout.expired.
Testing webhooks
- Register a test endpoint. For local development put your dev server behind a tunnel first:bash
# Expose a local dev server so Vitix can reach your webhooknpx cloudflared tunnel --url http://localhost:3000# or: ngrok http 3000# Register the public URL as a TEST endpoint (http is allowed only in test mode and only for localhost;# tunnels give you https anyway)curl https://api.vitix.net/v1/webhook_endpoints \ -H "Authorization: Bearer vx_test_••••••••" \ -H "Content-Type: application/json" \ -d '{ "url": "https://quiet-otter.trycloudflare.com/api/vitix/webhook", "description": "local dev" }' - Ping it. The
pingevent is signed like any other, so a passing ping proves your secret and verification code are correct:bashcurl -X POST https://api.vitix.net/v1/webhook_endpoints/we_…/ping \ -H "Authorization: Bearer vx_test_••••••••"# Your endpoint receives:# webhook-id: evt_test_…# webhook-timestamp: 1791959702# webhook-signature: v1,…# {"id":"evt_test_…","object":"event","livemode":false,"type":"ping","data":{"object":{"object":"ping","endpoint":"we_…","message":"Hello from Vitix Pay"}}} - Run the sandbox flow above and watch
payment.succeededarrive. Inspect every attempt with GET /v1/events/:id/deliveries; it shows the status code and body your server returned. - Make your handler return 500 once and observe the retry schedule (1 min, 5 min, …). Then fix it and resend instantly with POST /v1/events/:id/redeliver.
Testing refunds
# Test refunds await Vitix staff approval, then emit refund.succeeded and payment.refundedcurl https://api.vitix.net/v1/refunds \ -H "Authorization: Bearer vx_test_••••••••" \ -H "Content-Type: application/json" \ -d '{ "payment": "py_test_…", "amount_usd": 500 }'Testing error handling
Errors behave identically in both modes. Useful cases to cover:
| Request | Expected |
|---|---|
| amount below your minimum | 400 amount_too_small, param amount |
| success_url on a domain you did not register | 400 url_not_allowed, param success_url |
| a second POST with the same Idempotency-Key and a different body | 400 idempotency_key_reused |
| GET /v1/payments/py_test_nope | 404 resource_missing |
| any request with a revoked key | 401 api_key_invalid |
| 101 requests in 10 seconds | 429 rate_limit_exceeded |
Checklist before going live
- Swap the key for
vx_live_in production configuration only. - Create a live webhook endpoint (https) and store its separate secret.
- Make sure your production
success_urlandcancel_urldomains are on your account's allowed list (GET /v1/account). - Validate amounts against
account.limitsin your UI to avoid avoidable 400s. - Branch on
livemodein your webhook handler so a stray test event can never credit a production user.