API Documentation

Accept USDT and USDC without holding your customers' funds anywhere in between.

Deployed on BNB Smart Chain Testnet and Ethereum Sepolia. Not yet on mainnet.

Base URL: https://api.vorixpay.com

Quick Start

Five steps from sign-up to a verified payment. Step 2 is the one people miss.

1. Get your API key

Sign up at vorixpay.com/register, then generate a test key (sk_test_…) from your dashboard.

2. Register your payout wallet

In Dashboard → Payout wallet, connect the wallet you want to be paid into and sign, once per network. It is recorded on-chain and every payment is collected into it. This cannot be done with an API key, because it needs a signature from the wallet itself. Until it is done, creating an invoice on that network returns 400.

3. Create an invoice

cURL
curl -X POST https://api.vorixpay.com/api/v1/invoices \
  -H "Authorization: Bearer YOUR_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Order #123",
    "amount": 50,
    "currency": "USDT",
    "chain": "bsc-testnet"
  }'

# Save "id" and "txRef" from the response against your order —
# they are how you look the payment up afterwards.

4. Send the customer to checkout

JavaScript
// Use the slug from the response to build the checkout URL
const checkoutUrl = "https://vorixpay.com/pay/" + invoice.slug;
window.location.href = checkoutUrl;

// The customer sends exactly that token, on that network, to the
// invoice's deposit address. Vorixpay detects the payment on-chain.
// redirectUrl (if set) is opened unchanged once the invoice completes —
// put your own order id in it if you need one on return.

5. Verify payment

cURL
# After the invoice.completed webhook, or when the customer returns
curl https://api.vorixpay.com/api/v1/invoices/verify/VXP_abc123 \
  -H "Authorization: Bearer YOUR_SECRET_KEY"

# status: "COMPLETED" = payment confirmed. Safe to fulfill.

Authentication

All API requests require your secret key in the Authorization header.

HTTP Header
Authorization: Bearer YOUR_SECRET_KEY

API Keys

Generate your API key from Dashboard → API Keys.

Your secret key is shown only once when generated — store it securely. Never expose it in frontend code or public repositories.

sk_test_ keys work only on test networks and sk_live_ keys only on mainnet. Creating an invoice or address on the other kind of network returns 403; naming one to GET /api/v1/invoices/tokens returns 400. Every network is a testnet today, so use a test key.

Test and live

Everything in Vorixpay is either test (test networks, worthless tokens) or live (real networks, real money), decided by the network it is on. A test key only sees and creates test invoices, payments and addresses; a live key only live ones. Invoice and webhook payloads carry livemode: true | false so your code can check.

Webhook endpoints are test or live too — set by the key (or the dashboard mode) that created them — and receive only that mode's events, so a test payment never reaches your production server. The dashboard has a Test / Live switch at the top of the sidebar.

What an API key cannot do

API keys are for taking payments: invoices, addresses, payments, balances, customers and webhook endpoints. Anything that manages the account needs you signed in to the dashboard, and an API key gets 403 with error: "dashboard_only":

creating, listing or revoking API keys · editing your profile · two-factor settings · reading or regenerating the webhook secret · registering, changing, cancelling or applying a payout wallet.

So a key that leaks from a server or a log cannot create more keys, read your webhook secret, switch off 2FA or move where you are paid. It can still do real damage with what is left:

create and cancel invoices · accept an underpaid invoice as paid with settle-short, which sends a genuine invoice.completed to your server · add webhook endpoints, or change or switch off yours (PATCH /api/v1/webhooks/:id takes url and isActive), so your server stops hearing about payments · read your customer records, including emails and phone numbers.

If a key leaks, revoke it in Dashboard → API Keys straight away, then check your webhook endpoints and any invoice settled short since.

Invoices

Invoices are one-time payment requests. Each invoice gets a unique checkout page where the customer can pay.

POST/api/v1/invoices

Create a new invoice. Name the token it is paid in; the customer pays in exactly that token. Each token has a minimum — see GET /api/v1/invoices/tokens — and a merchant-paid invoice below it is refused, because the fee would take the whole payment.

Request Body

{
  "title": "Pro Plan",              // Required
  "amount": 50,                     // Required — in token units (e.g. 50 USDT), at least 0.01
  "currency": "USDT",               // Required — a token the chain accepts, e.g. "USDT" or "USDC"
  "chain": "bsc-testnet",           // Optional — defaults to bsc-testnet. See GET /api/v1/invoices/chains
  "description": "Monthly access",  // Optional
  "feeBearer": "merchant",          // "merchant" (default) or "customer"
  "expiryMinutes": 30,              // Default: 30. Set 0 for no expiry.
  "toleranceBps": 0,                // Optional, 0–500: how far short a payment may fall and still complete
  "redirectUrl": "https://yourshop.example/success",  // Opened unchanged after payment
  "customerEmail": "[email protected]",
  "metadata": { "orderId": "123", "plan": "pro" }
}
// callbackUrl is also accepted, but only stored and echoed back for your
// reference — nothing is ever sent to it. Register a webhook endpoint instead.

Response

{
  "id": "uuid",
  "livemode": false,
  "explorerUrl": "https://testnet.bscscan.com",
  "slug": "xfmzFA9g4qXr",
  "url": "/pay/xfmzFA9g4qXr",
  "txRef": "VXP_abc123def456",
  "title": "Pro Plan",
  "description": "Monthly access",
  "amount": "50.000000000000000000",
  "amountReceived": "0.000000000000000000",
  "currency": "USDT",
  "fee": "1.000000000000000000",      // 1% is 0.50, below USDT's 1.00 minimum fee
  "netAmount": "49.000000000000000000",
  "feeBearer": "merchant",
  "status": "PENDING",
  "depositAddress": "0x7a3F...4d9b",
  "chain": "bsc-testnet",
  "chainId": 97,
  "toleranceBps": 0,
  "paidLate": false,
  "settledShort": false,
  "redirectUrl": "https://yourshop.example/success",
  "customerEmail": "[email protected]",
  "metadata": { "orderId": "123", "plan": "pro" },
  "expiresAt": "2025-01-01T12:30:00.000Z",
  "createdAt": "2025-01-01T12:00:00.000Z"
}

Fee model

The fee is the network's percentage or the token's minimum fee, whichever is larger. The percentage is set per network — on testnet today, 1% on BNB Smart Chain Testnet and 3% on Ethereum Sepolia — and is fixed into an invoice when it is created. The minimum fee is set per token and applied when the payment is collected. Read both from GET /api/v1/invoices/chains rather than hardcoding them.

When feeBearer is "merchant": the customer pays the invoice amount and you receive it minus the fee. The amount must be at least the token's minAmount, or the invoice is refused — below it the fee would take everything.

When feeBearer is "customer": the invoice is raised by the fee, so amount in the response is what the customer pays and you receive the amount you asked for.

Money fields in invoice responses are decimal strings with 18 places ("50.000000000000000000"). Parse them as decimals, not floats.

GET/api/v1/invoices/chains

Networks you can invoice on, each network's fee rate, and each token it accepts with the smallest invoice that token can settle.

Response

[
  {
    "key": "bsc-testnet",
    "name": "BNB Smart Chain Testnet",
    "chainId": 97,
    "family": "evm",
    "nativeSymbol": "tBNB",
    "isTestnet": true,
    "feeBps": 100,
    "tokens": [
      { "symbol": "USDC", "minAmount": "3.01" },
      { "symbol": "USDT", "minAmount": "1.01" }
    ],
    "available": true,
    "isDefault": true
  }
]
GET/api/v1/invoices/tokens

Tokens accepted on one network, with contract address, decimals and minAmount. Query: ?chain=bsc-testnet (omit for the default network).

GET/api/v1/invoices

List your invoices with pagination and optional status filter.

Request Body

Query params: ?page=1&limit=20&status=COMPLETED   // limit: default 20, max 100

Response

{
  "data": [
    {
      "id": "uuid",
      "txRef": "VXP_abc123...",
      "title": "Pro Plan",
      "amount": "50.000000000000000000",
      "amountReceived": "50.000000000000000000",
      "currency": "USDT",
      "status": "COMPLETED",
      "createdAt": "2025-01-01T12:00:00Z"
    }
  ],
  "total": 42,
  "page": 1,
  "limit": 20,
  "totalPages": 3
}
GET/api/v1/invoices/:id

Get a single invoice with full details.

POST/api/v1/invoices/:id/cancel

Cancel a PENDING invoice. Cannot cancel invoices that already received payments.

POST/api/v1/invoices/:id/settle-short

Accept an UNDERPAID invoice as settled for what it received. It becomes COMPLETED with settledShort: true, invoice.completed is sent, and the money is collected to your wallet. This is the way out of UNDERPAID — an invoice with no expiry otherwise stays open, and its money waits, indefinitely.

Request Body

{ "reason": "Customer paid the exchange fee" }   // Optional

Verify Payment

After a customer pays, verify the payment server-side using the transaction reference. This is the most important step — always verify before fulfilling an order.

GET/api/v1/invoices/verify/:txRef

Verify an invoice payment by its transaction reference. Check that status is COMPLETED before fulfilling.

Response

{
  "id": "uuid",
  "txRef": "VXP_abc123...",
  "status": "COMPLETED",
  "amount": "50.000000000000000000",
  "amountReceived": "50.000000000000000000",
  "currency": "USDT"
}

Important

Never trust client-side data. Always call this endpoint from your server to confirm the payment actually went through before delivering goods or services.

txRef format

New invoices carry a VXP_ txRef. Invoices created before the rename from Pay3 keep their PAY3_ one, and both resolve here. Treat a txRef as an opaque string: store it and look it up, but do not validate or parse the prefix.

Permanent Addresses

A permanent address belongs to one of your customers rather than to one invoice. You name it with your own reference for them — a user id, an account number — and every payment into it is attributed back to that reference. It never expires, so a customer can save it and top up whenever they like. Issuing one is computation only: nothing is deployed on-chain until there is money to collect, so an address that is never paid costs nothing.

Payments into these addresses have no invoice, so there is no invoice.completed. Credit the customer on payment.confirmed where data.kind is "user", using data.reference — see the example under Webhooks. Unlike an invoice address, which fixes its fee rate when it is created, a permanent address is charged the network's rate at the time it is collected, because it can keep receiving indefinitely.

POST/api/v1/deposit-addresses

Issue an address for one of your customers, or get back the one they already have. Idempotent on reference: the same reference on the same network returns the same address. Sending it again with a different label or metadata updates those. Returns 400 if you have not registered a payout wallet on that network — without one there would be nowhere for the money to go — and 403 on a network of the other mode.

Request Body

{
  "reference": "user_12345",          // Required — your id for this customer, 1–128 characters
  "chain": "bsc-testnet",             // Optional — defaults to your mode's default network
  "label": "Wallet top-up — Ada",     // Optional, up to 200 characters — for your own dashboard
  "customerEmail": "[email protected]", // Optional — files it under a customer record; never part of the address
  "metadata": { "plan": "pro" }       // Optional — echoed back on every payment.* webhook for this address
}

Response

{
  "id": "uuid",
  "chain": "bsc-testnet",
  "address": "0x9c1E...b27A",
  "kind": "user",
  "reference": "user_12345",
  "label": "Wallet top-up — Ada",
  "metadata": { "plan": "pro" },
  "isDeployed": false,               // No contract until the first collection — normal
  "createdAt": "2025-01-01T12:00:00.000Z"
}

When the same reference gets a new address

An address is derived from the network's deposit contracts. When a network moves to new contracts, asking for the same reference issues a new address. The old one is not retired: it still pays your registered wallet, so a customer who saved it loses nothing. Show customers the address the API returns now rather than one you cached long ago.

GET/api/v1/deposit-addresses

Your permanent addresses, newest first, in the caller's mode. Filter with ?chain=bsc-testnet, or ?reference=user_12345 to find one customer's address. Not paged: it returns at most the 200 most recent, so look a specific customer up by reference.

Response

{
  "data": [
    { "id": "uuid", "chain": "bsc-testnet", "address": "0x9c1E...b27A", "kind": "user",
      "reference": "user_12345", "label": "Wallet top-up — Ada", "metadata": { "plan": "pro" },
      "isDeployed": false, "createdAt": "2025-01-01T12:00:00.000Z" }
  ]
}
GET/api/v1/deposit-addresses/:id

One address, with what it has received attached as ledger. Balances are in token units and split the same way as your account balance — see Payouts.

Response

{
  "id": "uuid",
  "address": "0x9c1E...b27A",
  "reference": "user_12345",
  ...
  "ledger": {
    "balances": [
      { "token": "USDT", "decimals": 18, "amount": "25.0",
        "settled": "20.0", "pendingCollection": "5.0", "unconfirmed": "0.0", ... }
    ],
    "paymentCount": 2,
    "firstPaymentAt": "2025-01-02T09:00:00.000Z",
    "lastPaymentAt": "2025-01-05T16:30:00.000Z"
  }
}
GET/api/v1/deposit-addresses/:id/payments

Every transfer into one address, newest first, including reversed ones (status ORPHANED). Query: ?limit — default 100, max 200. collectedBy is null while the money is still in the address.

Response

{
  "data": [
    {
      "txHash": "0x4359...",
      "logIndex": 12,
      "from": "0xabc...",
      "asset": "USDT",
      "amount": "5.0",
      "amountBaseUnits": "5000000000000000000",
      "decimals": 18,
      "status": "CONFIRMED",
      "confirmations": 20,
      "blockNumber": "48210345",
      "collectedBy": null,
      "receivedAt": "2025-01-05T16:30:00.000Z"
    }
  ]
}
GET/api/v1/deposit-addresses/balances

Your permanent addresses with their balances attached, 100 per page, newest first. Query: ?page=2, and ?chain=bsc-testnet to limit to one network. hasMore says whether there is another page. First and last payment times are only filled in on GET /api/v1/deposit-addresses/:id.

Response

{
  "data": [
    { "id": "uuid", "address": "0x9c1E...b27A", "reference": "user_12345", "kind": "user",
      "label": "Wallet top-up — Ada", "chain": "bsc-testnet", "chainName": "BNB Smart Chain Testnet",
      "balances": [ ... ], "paymentCount": 2, "firstPaymentAt": null, "lastPaymentAt": null }
  ],
  "page": 1,
  "hasMore": false
}

Customers

A customer is created automatically when an invoice or permanent address is created with a customerEmail. You can also create and manage them via the API.

POST/api/v1/customers

Create a customer, or update the existing one with the same email or wallet address.

Request Body

{
  "email": "[email protected]",   // email or walletAddress is required
  "walletAddress": "0xabc...",   // email or walletAddress is required
  "name": "John Doe",            // Optional
  "phone": "+1234567890"         // Optional
}

Response

{
  "id": "uuid",
  "walletAddress": "0xabc...",
  "name": "John Doe",
  "email": "[email protected]",
  "createdAt": "2025-01-01T00:00:00Z"
}
GET/api/v1/customers

List your customers.

Request Body

Query params: ?page=1&limit=20
GET/api/v1/customers/:id

Get a customer with their subscriptions and payment history.

PATCH/api/v1/customers/:id

Update customer information.

Request Body

{
  "name": "John Smith",
  "email": "[email protected]"
}

Subscription Plans

Coming soon

Plans can be created and read, but nothing charges against them — the recurring runner is not running. Build on invoices for now.

Plans define the pricing and billing cycle for recurring payments. Create a plan, then share its checkout link with customers.

POST/api/v1/subscription-plans

Create a subscription plan. Coming soon — not supported yet.

Request Body

{
  "name": "Pro Plan",              // Required
  "amount": 29.99,                 // Required
  "currency": "USDT",             // Required
  "intervalDays": 30,              // Required: billing cycle in days
  "description": "Monthly access", // Optional
  "trialDays": 7                   // Optional: free trial
}

Response

{
  "id": "uuid",
  "name": "Pro Plan",
  "amount": "29.99",
  "currency": "USDT",
  "intervalDays": 30,
  "trialDays": 7,
  "isActive": true,
  "createdAt": "2025-01-01T00:00:00Z"
}

Subscribe link

Share this link with your customers to subscribe:

https://vorixpay.com/subscribe/{planId}

Not active yet: until subscriptions launch, the public routes this page relies on answer 404, so the link does not work.

GET/api/v1/subscription-plans

List your subscription plans with subscriber counts.

PATCH/api/v1/subscription-plans/:id

Update a plan's name, amount, or active status.

Request Body

{
  "name": "Pro Plan v2",
  "amount": 39.99,
  "isActive": false
}
DELETE/api/v1/subscription-plans/:id

Deactivate a plan. Existing subscribers continue but no new sign-ups.

Subscriptions

Coming soon

Recurring charges are not available yet. These endpoints exist but are not part of the supported surface — one-off payments were finished first. Do not build against them.

GET/api/v1/subscriptions

List all subscriptions with customer info and charge stats.

Request Body

Query params: ?page=1&limit=20

Response

{
  "data": [
    {
      "id": "uuid",
      "planName": "Pro Plan",
      "subscriberAddress": "0xabc...",
      "amount": "29.99",
      "tokenSymbol": "USDT",
      "status": "ACTIVE",
      "nextChargeAt": "2025-02-01T00:00:00Z",
      "chargeCount": 3,
      "failedChargeCount": 0,
      "customerName": "John Doe",
      "customerEmail": "[email protected]",
      "createdAt": "2025-01-01T00:00:00Z"
    }
  ],
  "total": 12,
  "page": 1,
  "totalPages": 1
}
GET/api/v1/subscriptions/:id

Get full subscription details including billing period and charge history.

GET/api/v1/subscriptions/:id/charges

Get charge history for a subscription — every billing attempt with status and amount.

Response

[
  {
    "id": "uuid",
    "amount": "29.99",
    "fee": "0.45",
    "netAmount": "29.54",
    "txHash": "0xabc...",
    "status": "COMPLETED",
    "chargedAt": "2025-01-01T00:00:00Z"
  }
]
POST/api/v1/subscriptions/:id/cancel

Cancel a subscription. No further charges will be attempted.

Response

{
  "message": "Subscription cancelled",
  "id": "uuid"
}

Webhooks

Webhooks notify your server when events happen. They are delivered only to endpoints you register here — an invoice's callbackUrl is stored for your reference and never called. Every delivery is signed with HMAC-SHA256 using your webhook secret — one secret per account, shared by all your endpoints — so you can verify it came from Vorixpay.

POST/api/v1/webhooks

Create a webhook endpoint. events must name events from the Webhook Events table below; an unknown name — a typo, or an event for a feature that is not live — is refused with 400 rather than saved and never sent. From the dashboard the response includes your account's webhook secret; with an API key it does not — the secret is only ever shown in the dashboard.

Request Body

{
  "url": "https://yourshop.example/webhooks/vorixpay",
  "events": [
    "invoice.completed",
    "invoice.underpaid",
    "invoice.expired",
    "payment.confirmed",
    "payment.orphaned",
    "wallet.change_requested"
  ]
}

Response

{
  "id": "uuid",
  "url": "https://yourshop.example/webhooks/vorixpay",
  "events": ["invoice.completed", ...],
  "isActive": true,
  "mode": "test",                     // Receives only test-network events
  "createdAt": "2025-01-01T12:00:00.000Z"
}
GET/api/v1/webhooks

List your webhook endpoints in the caller's mode. Not paged.

PATCH/api/v1/webhooks/:id

Update a webhook endpoint's url, events or isActive. Event names are checked the same way as on create.

DELETE/api/v1/webhooks/:id

Delete a webhook endpoint.

GET/api/v1/webhooks/deliveries

Webhook delivery logs, newest first, with every attempt's response. Query: ?page=1&limit=20 (limit max 100).

POST/api/v1/webhooks/deliveries/:id/retry

Send a failed delivery again now.

GET/api/v1/webhooks/secret

Your webhook signing secret (whsec_…). Dashboard only — refused to API keys.

POST/api/v1/webhooks/secret/regenerate

Replace your webhook secret. Takes effect immediately for every endpoint: deliveries after this are signed with the new secret only, so update your server first. Dashboard only.

What a delivery looks like

Every event arrives in the same envelope. The fields that describe what happened are in data, and differ by event type.

JSON
{
  "id": "evt_3f9c...",                   // Stable across retries: your idempotency key
  "type": "payment.confirmed",
  "livemode": false,                     // null when the event names no network
  "sequence": 1042,                      // Only ever increases for your account
  "created": "2025-01-05T16:30:00.000Z", // When the event was recorded, not when this attempt was sent
  "data": {
    "paymentId": "uuid",
    "address": "0x9c1E...b27A",
    "reference": "user_12345",           // Your reference, for a permanent address
    "kind": "user",                      // "user" for a permanent address, "invoice" for an invoice's
    "label": "Wallet top-up — Ada",
    "metadata": { "plan": "pro" },
    "chain": "bsc-testnet",
    "asset": "USDT",
    "amount": "5000000000000000000",     // Base units: divide by 10^decimals
    "decimals": 18,
    "txHash": "0x4359...",
    "logIndex": 12,
    "blockNumber": "48210345",
    "confirmations": 15,
    "from": "0xabc...",
    "isSupportedToken": true             // false for a token Vorixpay does not accept — never credit it
  }
}

sweep.completed reports amount and fee in base units but carries no decimals. Match its tokenAddress against GET /api/v1/invoices/tokens?chain=… for the event's chain — the same symbol has different decimals on different networks.

Ordering and retries

Respond with any 2xx within 10 seconds. Anything else — including a 4xx — is retried: up to 12 attempts over about 16 hours (5s, 15s, 45s, 2m, 5m, 10m, 30m, 1h, 2h, 4h, 8h, with some jitter). An endpoint that fails 10 times in a row is paused for 15 minutes. Each request carries Vorixpay-Signature, Vorixpay-Event-Id, Vorixpay-Event-Type and Vorixpay-Delivery-Attempt headers.

Because failures are retried, events can reach you out of order. The common case: an invoice.underpaid delivery fails, the customer sends the rest, your endpoint accepts invoice.completed, and then the retried underpaid event arrives. Acting on arrival order would mark a paid invoice unpaid.

Every event carries a sequence — an integer that only ever increases for your account, assigned when the event is recorded rather than when it is delivered. Keep the highest you have processed for a given invoice and discard anything lower. Events are also delivered at least once, so the same id can arrive twice; the same check handles both. Nothing changes without its event: a payment, invoice or collection is saved in the same database transaction as the event that describes it, so if the event cannot be recorded neither is, and both are tried again. Webhooks are a notification, not a ledger: confirm anything that matters with GET /api/v1/invoices/verify/:txRef.

Verifying Webhook Signatures

Node.js
const crypto = require('crypto');

// The raw body, not the parsed one. Re-serialising with JSON.stringify does
// not reliably reproduce the bytes that were signed — key order, spacing and
// unicode escaping can all differ — and the signature then fails for reasons
// that look random.
app.post(
  '/webhooks/vorixpay',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const header = req.headers['vorixpay-signature'];
    if (typeof header !== 'string') return res.status(401).send('Missing signature');
    const rawBody = req.body.toString('utf8');

    // t=<unix>,v1=<hmac>
    //
    // Accept any v1 value that matches rather than parsing the header into an
    // object, which would keep only the last one if more are ever sent.
    const parts = header.split(',').map((p) => p.split('='));
    const timestamp = parts.find(([k]) => k === 't')?.[1];
    const signatures = parts.filter(([k]) => k === 'v1').map(([, v]) => v);

    // Bound replay: reject anything older than five minutes.
    if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
      return res.status(401).send('Stale signature');
    }

    const expected = crypto
      .createHmac('sha256', YOUR_WEBHOOK_SECRET)
      .update(timestamp + '.' + rawBody)
      .digest('hex');

    // Constant time, so a wrong signature cannot be discovered a byte at a
    // time by measuring how long the comparison takes.
    const valid = signatures.some(
      (sig) =>
        sig.length === expected.length &&
        crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)),
    );

    if (!valid) return res.status(401).send('Invalid signature');

    const event = JSON.parse(rawBody);

    // The same event can arrive more than once — a delivery that timed out
    // after your server processed it is retried. `id` is stable across
    // retries, so record it and ignore a repeat.
    if (alreadyProcessed(event.id)) return res.status(200).send('OK');

    // Invoices: fulfil on invoice.completed, and only on that. It fires once
    // the invoice is paid in full (or you accepted it short). payment.* events
    // also fire for invoice payments — including partial ones and transfers in
    // the wrong token — so fulfilling on those would give goods away.
    if (event.type === 'invoice.completed') {
      fulfillOrder(event.data.txRef);
    }

    // Permanent customer addresses (kind "user") have no invoice, so credit
    // those on payment.confirmed. amount is in the token's base units:
    // divide by 10^decimals, which the event carries (18 for BSC testnet
    // USDT/USDC, 6 on Sepolia). Transfers of tokens Vorixpay does not accept
    // are reported too, with isSupportedToken: false — never credit those.
    if (
      event.type === 'payment.confirmed' &&
      event.data.kind === 'user' &&
      event.data.isSupportedToken
    ) {
      creditCustomer(event.data.reference, event.data.asset, event.data.amount, event.data.decimals);
    }

    res.status(200).send('OK');
  },
);

Payouts

There is no withdrawal API, because Vorixpay never holds your money. A customer pays a deposit address that can only pay out to the payout wallet you registered, less our fee, and the platform sweeps it there for you. Nothing to request, and nothing to wait for anyone to approve. Balances under the collection threshold (currently 5 USDT or USDC) are batched and collected within 24 hours.

GET/api/v1/deposit-addresses/balance

What you have been paid, one row per token per network, split by where the money currently is. Query: ?chain=bsc-testnet to limit to one network.

Response

{
  "balances": [
    {
      "token": "USDT",
      "name": "Tether USD",
      "tokenAddress": "0x...",
      "decimals": 18,
      "iconUrl": null,
      "amount": "1245.5",
      "settled": "1100.0",
      "pendingCollection": "145.5",
      "unconfirmed": "0.0"
    }
  ]
}

settled has reached your own wallet, after our fee. pendingCollection is yours but is still sitting in a deposit address awaiting a sweep, before our fee. unconfirmed is on-chain but not yet deep enough to rely on — a reorg can still take it back.

GET/api/v1/wallets

Your registered payout wallet on each chain.

GET/api/v1/sweeps

Collections into your wallet, and which payments each one covered, newest first. Query: ?page=1&limit=50 (limit max 200). Returned under sweeps, not data.

GET/api/v1/payments

Every transfer into any of your addresses, newest first. The unit is the on-chain transfer rather than the invoice: an invoice can be paid by two, a permanent address receives them with no invoice at all, and an underpayment is real money that an invoice-shaped view would file under unpaid. Filter by status, chain, token, kind and date; search by transaction hash, address or your own reference.

Request Body

Query params: ?page=1&limit=25&status=CONFIRMED&kind=user&search=0x4359   // limit: default 25, max 200
GET/api/v1/sweeps/pending

Confirmed money not yet in your wallet, and why each balance has not moved — below the amount worth a transaction, held while its invoice is open, or waiting on a payout wallet you have not registered. Answered by the collection planner itself, so it always matches what the sweeper is about to do.

Status Reference

Invoice Statuses

PENDING

Created, waiting for payment.

CONFIRMING

Money has been seen on-chain but is not yet confirmed. It may be only part of the amount.

UNDERPAID

A confirmed partial payment. Still open for the rest; accept it with settle-short if you want to.

COMPLETED

Paid in full and confirmed. Safe to fulfill. Check paidLate (paid after expiry) and settledShort (you accepted less).

EXPIRED

The deadline passed without the full amount. It may hold a partial payment, and becomes COMPLETED with paidLate: true if the rest arrives within 30 days of the invoice being created — counted from creation, not from expiry.

CANCELLED

Cancelled by you before any payment arrived.

Subscription Statuses

TRIALING

Free trial period. No charges yet.

ACTIVE

Billing normally.

PAST_DUE

Recent charges failed. Retrying next cycle.

CANCELLED

Cancelled. No future charges.

Webhook Events

EventDescription
payment.detectedA transfer was seen on-chain. Not yet safe to act on: a reorg can still take it back. amount is in base units; decimals is included. Sent again with reincluded: true if a reorg reversed the payment and the network then included it again.
payment.confirmedThat transfer is buried deep enough to be final. Credit a permanent-address (kind "user") customer on this. For invoices use invoice.completed instead — this also fires for partial payments. amount is in base units; decimals is included.
payment.orphanedA reorg reversed a payment you were told about. Undo whatever you credited.
invoice.completedAn invoice was paid in full, or you accepted it short (settledShort: true). paidLate: true if it arrived after expiry. The one to fulfil an order on.
invoice.underpaidSome money arrived but not enough, and the invoice is still open for the rest.
invoice.overpaidMore arrived than was asked for. Sent alongside invoice.completed, not instead of it.
invoice.expiredThe deadline passed without enough arriving.
invoice.cancelledYou cancelled the invoice before it was paid.
sweep.completedMoney left the deposit address and reached your payout wallet, less the fee. Amounts in base units.
wallet.registeredYour payout wallet was registered on a network.
wallet.change_requestedSomeone asked to change your payout wallet. It takes effect after 24 hours unless cancelled. If this was not you, cancel it now.
wallet.change_appliedA payout wallet change took effect.
wallet.change_cancelledA pending payout wallet change was cancelled.

Errors

All errors return JSON with a message field. For validation errors it is an array of strings, one per problem; unknown fields in a request body are rejected.

CodeMeaning
400Bad Request — invalid parameters, an unknown webhook event name, a token the network does not accept, an amount below the token’s minimum, or no payout wallet registered on that network. Also a network of the other mode named to GET /api/v1/invoices/tokens.
401Unauthorized — invalid or missing API key (error: "invalid_token").
403Forbidden — creating an invoice or address with a test key on a live network (or a live key on a test one), or an API key used for a dashboard-only action (error: "dashboard_only").
404Not Found — resource does not exist, belongs to the other mode, or the id is malformed.
429Too Many Requests — over 120 requests a minute from one IP address, counted across every key and session using it. Back off and retry.
500Server Error — something went wrong on our side.

Notes

Supported Tokens

USDT and USDC on BNB Smart Chain Testnet (18 decimals) and Ethereum Sepolia (6 decimals). An invoice is paid in exactly the token it names, on its network.

Amounts

Amounts you send are in token units (50 = 50 USDT). Responses return strings: invoice money fields with 18 decimal places, balances in token units, and webhook payment.* and sweep.* amounts in the token's base units.

Pagination

Paged lists take ?page (from 1) and ?limit. A limit above the maximum is lowered to it rather than refused, so check the limit or length you get back instead of assuming you got what you asked for.

EndpointDefaultMaxRows in
GET /invoices20100data
GET /customers20100data
GET /webhooks/deliveries20100data
GET /payments25200data
GET /sweeps50200sweeps
GET /deposit-addresses/:id/payments100200data (no page)
GET /deposit-addresses/balances100, fixed100data, with hasMore
GET /deposit-addressesnot paged200 newestdata
GET /webhooksnot pageda plain array

Paths are under /api/v1. Where there is a total, it comes back as total and totalPages beside the rows.