Skip to content
Search documentation titles, headings, and guides
API v1Updated 2026-09-20

Reseller API

Complete REST reference for the reseller program: signup, login, quota top-up, minting, and customer management.

The reseller API is a REST surface under /api/reseller. Every endpoint except signup and login requires a reseller JWT sent as Authorization: Bearer <token>. The token is issued at login and is valid for 24 hours (HS256).

Authentication endpoints#

POST/api/reseller/auth/signup
Auth: public

Register a new reseller account. On success the account is created in pending status and awaits admin review.

Parameters

emailstringbodyrequired

Login email. Must not be blacklisted.

passwordstringbodyrequired

Account password. Minimum 8 characters.

namestringbodyrequired

Display name of the reseller.

wa_numberstringbodyrequired

WhatsApp number for contact, digits only with country code (for example `6281234567890`).

Request Example

curl -X POST "https://api.zrouter.dev/api/reseller/auth/signup" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "password": "<password>",
    "name": "Your Name",
    "wa_number": "6281234567890"
  }'

Responses

201Reseller account created in `pending` status.
{
  "reseller": {
    "id": "<reseller-id>",
    "email": "[email protected]",
    "name": "Your Name",
    "status": "pending"
  }
}
409`email_exists` — an account already exists for this email.
422`validation_error` — missing or malformed required fields.
403`email_blacklisted` — the email is not permitted to register.
POST/api/reseller/auth/login
Auth: public

Authenticate and obtain a reseller JWT. The token is HS256 and valid for 24 hours.

Parameters

emailstringbodyrequired

Registered reseller email.

passwordstringbodyrequired

Account password.

Request Example

curl -X POST "https://api.zrouter.dev/api/reseller/auth/login" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "password": "<password>"
  }'

Responses

200Authenticated. Returns the JWT and the reseller profile.
{
  "token": "<token>",
  "reseller": {
    "id": "<reseller-id>",
    "email": "[email protected]",
    "name": "Your Name",
    "status": "active"
  }
}
401`invalid_credentials` — wrong email or password.
429`rate_limited` — too many login attempts (5 per minute).

Account endpoints#

GET/api/reseller/me
Auth: reseller-jwt

Return the authenticated reseller's profile, including current status.

Responses

200Reseller profile.
{
  "id": "<reseller-id>",
  "email": "[email protected]",
  "name": "Your Name",
  "status": "active"
}
401`invalid_credentials` — missing or invalid JWT.
GET/api/reseller/balance
Auth: reseller-jwt

Return the current quota balance as an integer micro-USD value.

Responses

200Current quota.
{
  "balance_usd_micro": "99500000"
}
401`invalid_credentials` — missing or invalid JWT.

Top-up endpoints#

POST/api/reseller/topup
Auth: reseller-jwt

Create a top-up order that adds USD quota. Available methods: qris (IDR, fixed rate), crypto (USDC/USDT on Solana, 1:1), sumopod (QRIS, zero fee), or crypto_bep20 (USDT/USDC on BNB Smart Chain, 1:1). The order starts in pending and is credited only after confirmation.

Parameters

methodstringbodyrequired

Payment method for this order.

Allowed values:qriscryptosumopodcrypto_bep20
amount_idrintegerbodyoptional

Principal amount in IDR. Required when `method` is `qris` or `sumopod`.

amount_usd_microintegerbodyoptional

Amount in micro-USD. Required when `method` is `crypto` or `crypto_bep20`.

token_kindstringbodyoptional

Deposit token for `crypto_bep20` (BEP20 on BSC chain 56). Ignored by every other method.

Allowed values:usdtusdc

Request Example

curl -X POST "https://api.zrouter.dev/api/reseller/topup" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"method": "qris", "amount_idr": 50000}'

Responses

201Top-up order created in `pending` and `payment_status` `pending`. For QRIS the response carries the QR content and image; for SumoPod it carries the raw QRIS string (`qr_content`) and a hosted payment page (`payment_url`); for crypto it carries deposit addresses. `crypto_bep20` orders additionally carry the immutable network facts: `network` (`bsc`), `chain_id` (`56`), `token_kind`, `contract_address`, `expected_raw` (the exact 18-decimal token amount to send), and — once observed — `received_raw`/`received_micro`; the deposit EOA is `deposit_address`. `quota_usd_micro` is the credited amount (fee never credited); `paid_at`/`credited_at` appear once set.
{
  "id": "0d9c6f5e-4c1b-4f6e-9d2a-7b3f8c1e5a20",
  "order_number": "RTO-0d9c6f5e-4c1b-4f6e-9d2a-7b3f8c1e5a20",
  "payment_method": "qris",
  "provider": "tako",
  "amount_idr": 50000,
  "fee_idr": 2500,
  "total_idr": 52500,
  "quota_usd_micro": 5000000,
  "status": "pending",
  "payment_status": "pending",
  "provider_payment_id": "...",
  "expires_at": "2026-09-16T00:15:00Z",
  "created_at": "2026-09-16T00:00:00Z",
  "qr_content": "0002...",
  "qr_image_url": "https://...",
  "payment_url": "https://..."
}
422`amount_out_of_range` or `validation_error` — amount outside the allowed range, or missing/contradictory fields.
503`provider_unavailable` — the payment provider is temporarily unavailable.
Create a crypto top-up order
curl -X POST "https://api.zrouter.dev/api/reseller/topup" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"method": "crypto", "amount_usd_micro": 20000000}'
Create a BEP20 (BSC) top-up order
curl -X POST "https://api.zrouter.dev/api/reseller/topup" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"method": "crypto_bep20", "amount_usd_micro": 20000000, "token_kind": "usdt"}'
GET/api/reseller/topup/{orderID}
Auth: reseller-jwt

Get the status of a top-up order and reconcile it. If the payment has finalized, the order is marked paid and your quota is credited in the same call. The status field is the order lifecycle (pending → crediting → credited, or expired / failed); payment_status is the derived payment view (paid, pending, expired, failed) — a paid payment on a not-yet-credited order still credits. Prefer this GET endpoint for polling.

Parameters

orderIDstringpathrequired

The order `id` returned by `POST /api/reseller/topup`.

Request Example

curl "https://api.zrouter.dev/api/reseller/topup/<order-id>" \
  -H "Authorization: Bearer <token>"

Responses

200The updated order. Same shape as the create response; `paid_at` and `credited_at` appear once set.
{
  "id": "0d9c6f5e-4c1b-4f6e-9d2a-7b3f8c1e5a20",
  "order_number": "RTO-0d9c6f5e-4c1b-4f6e-9d2a-7b3f8c1e5a20",
  "payment_method": "qris",
  "provider": "tako",
  "amount_idr": 50000,
  "fee_idr": 2500,
  "total_idr": 52500,
  "quota_usd_micro": 5000000,
  "status": "credited",
  "payment_status": "paid",
  "provider_payment_id": "...",
  "expires_at": "2026-09-16T00:15:00Z",
  "created_at": "2026-09-16T00:00:00Z",
  "paid_at": "2026-09-16T00:07:41Z",
  "credited_at": "2026-09-16T00:07:41Z"
}
404`topup_not_found` — the order does not exist for this reseller.
GET/api/reseller/topups
Auth: reseller-jwt

List your top-up orders, newest first. Each entry has the same shape as the single-order response. total is the total number of your orders (not the page size), for pagination.

Parameters

limitintegerqueryoptional

Page size, default 20, max 100.

offsetintegerqueryoptional

Page offset, default 0.

Request Example

curl "https://api.zrouter.dev/api/reseller/topups?limit=20&offset=0" \
  -H "Authorization: Bearer <token>"

Responses

200A page of your top-up orders plus the total count.
{
  "topups": [
    {
      "id": "0d9c6f5e-...",
      "order_number": "RTO-0d9c6f5e-...",
      "payment_method": "qris",
      "provider": "tako",
      "amount_idr": 50000,
      "fee_idr": 2500,
      "total_idr": 52500,
      "quota_usd_micro": 5000000,
      "status": "credited",
      "payment_status": "paid",
      "expires_at": "2026-09-16T00:15:00Z",
      "created_at": "2026-09-16T00:00:00Z",
      "paid_at": "2026-09-16T00:07:41Z",
      "credited_at": "2026-09-16T00:07:41Z"
    }
  ],
  "total": 1
}
POST/api/reseller/topup/{orderID}/check
Auth: reseller-jwt

Legacy alias of GET /api/reseller/topup/{orderID} — same poll-and-reconcile behavior and the same response body. Kept for backward compatibility.

Parameters

orderIDstringpathrequired

The order id returned by `POST /api/reseller/topup`.

Request Example

curl -X POST "https://api.zrouter.dev/api/reseller/topup/<order-id>/check" \
  -H "Authorization: Bearer <token>"

Responses

200The updated order — same body as `GET /api/reseller/topup/{orderID}`.
404`topup_not_found` — the order does not exist for this reseller.

Minting endpoints#

POST/api/reseller/mint
Auth: reseller-jwt

Mint a Token Plan tier to a customer, debiting your quota atomically. Idempotent on operation_id. See Quota & Minting.

Parameters

operation_idstringbodyrequired

Client-generated idempotency key. Non-empty, at most 128 characters.

customer_emailstringbodyrequired

Email of the target customer (a zrouter user).

plan_tier_idstringbodyrequired

The Token Plan tier to grant. See the live [pricing page](/en/pricing).

Request Example

curl -X POST "https://api.zrouter.dev/api/reseller/mint" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "operation_id": "op_1234567890abcdef",
    "customer_email": "[email protected]",
    "plan_tier_id": "<tier-id>"
  }'

Responses

200Mint applied. `already_applied` is `true` on an idempotent replay.
{
  "customer_user_id": "<user-id>",
  "model_id": "<model-id>",
  "granted_tokens": 1000,
  "price_usd_micro": "1000000",
  "balance_after": "98500000",
  "already_applied": false
}
402`insufficient_quota` — your quota cannot cover this mint.
409`idempotency_conflict` — `operation_id` was reused with a different payload.
422`tier_not_available` or `validation_error` — the tier is not for sale, or fields are malformed.
429`rate_limited` — mint rate limit exceeded (30 per minute).
500`internal_error` — an unexpected internal failure.

Tier catalog#

GET /api/reseller/tiers lists the Token Plan tiers you can mint, one entry per tier, ordered by tier price ascending then model name ascending. The topup_idr_required field is the IDR top-up quota needed to cover the tier at the fixed rate of Rp10.000 per USD (tier_usd_cents × 100); the 5% QRIS processing fee is separate and never included. Only safe, public fields are returned.

GET/api/reseller/tiers
Auth: reseller-jwt

Return the list of Token Plan tiers available to mint, sorted by tier price ascending then model name ascending. Internal-only pricing fields are never exposed.

Responses

200The list of mintable tiers.
{
  "tiers": [
    {
      "plan_tier_id": "<tier-id>",
      "model_id": "<model-id>",
      "model_name": "<public-name>",
      "tier_usd_cents": 100,
      "price_usd_micro": 1000000,
      "granted_tokens": 1000,
      "topup_idr_required": 10000
    }
  ]
}
401`invalid_credentials` — missing or invalid JWT.

Customer endpoints#

GET/api/reseller/customers
Auth: reseller-jwt

List the customers associated with this reseller.

Responses

200List of customers.
{
  "customers": [
    { "user_id": "<user-id>", "email": "[email protected]" }
  ]
}
401`invalid_credentials` — missing or invalid JWT.
POST/api/reseller/customers
Auth: reseller-jwt

Resolve-or-create a customer by email. If the zrouter user does not exist, one is created and linked to this reseller.

Parameters

emailstringbodyrequired

The customer's email.

Request Example

curl -X POST "https://api.zrouter.dev/api/reseller/customers" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"email": "[email protected]"}'

Responses

200The resolved or newly created customer.
{
  "user_id": "<user-id>",
  "email": "[email protected]",
  "created": false
}
422`validation_error` — malformed email.
POST/api/reseller/customers/{userID}/keys
Auth: reseller-jwt

Create an API key for a customer. The secret is revealed once and never stored. The response is served with Cache-Control: no-store.

Parameters

userIDstringpathrequired

The customer's user id.

namestringbodyrequired

A label for this key.

Request Example

curl -X POST "https://api.zrouter.dev/api/reseller/customers/<user-id>/keys" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"name": "production"}'

Responses

201Key created. The `secret` is shown exactly once.
{
  "key_id": "<key-id>",
  "name": "production",
  "secret": "zr_<public-id>_<secret>"
}
404The target user is not a customer of this reseller.
DELETE/api/reseller/keys/{keyID}
Auth: reseller-jwt

Revoke an API key. Scoped: the key must belong to a customer of this reseller, otherwise the request fails with 404.

Parameters

keyIDstringpathrequired

The key id to revoke.

Request Example

curl -X DELETE "https://api.zrouter.dev/api/reseller/keys/<key-id>" \
  -H "Authorization: Bearer <token>"

Responses

200Key revoked.
404The key does not exist or is not owned by a customer of this reseller.

Balance endpoints#

The balance endpoints share one response shape: a payg object with a USD balance as a decimal string, and a token_plans array of integer token counts. Money is always a decimal string of micro-USD (for example "0.000001" is $0.000001) — never a float — and token counts are always integers. A revoked key still resolves to its owner's balances, so you can reconcile historical usage.

GET/api/reseller/keys/{keyID}/balances
Auth: reseller-jwt

Return the PAYG balance and Token Plan balances of the customer who owns the given API key. Works even if the key is revoked, for historical reconciliation. Scoped: the key must belong to a customer of this reseller, otherwise the request fails with 404.

Parameters

keyIDstringpathrequired

The API key id (UUID) to look up.

Request Example

curl "https://api.zrouter.dev/api/reseller/keys/<key-id>/balances" \
  -H "Authorization: Bearer <token>"

Responses

200The key owner's balances, including `payg` and `token_plans`.
{
  "key_id": "<key-id>",
  "customer_user_id": "<user-id>",
  "payg": {
    "currency": "USD",
    "balance": "0.000001"
  },
  "token_plans": [
    {
      "model": "<public-name>",
      "purchased_tokens": 1000,
      "used_tokens": 123,
      "remaining_tokens": 877,
      "status": "active"
    }
  ],
  "as_of": "2026-09-16T00:00:00Z"
}
401`invalid_credentials` — missing or invalid JWT.
404`not_found` — the key does not exist or is not owned by a customer of this reseller.
422`validation_error` — `keyID` is not a valid UUID.
GET/api/reseller/customers/{userID}/balances
Auth: reseller-jwt

Return the PAYG balance and Token Plan balances for one of your customers. Same shape as the key balance endpoint, minus the key_id field. The customer must be owned by this reseller, otherwise the request fails with 404.

Parameters

userIDstringpathrequired

The customer's user id (UUID).

Request Example

curl "https://api.zrouter.dev/api/reseller/customers/<user-id>/balances" \
  -H "Authorization: Bearer <token>"

Responses

200The customer's balances, including `payg` and `token_plans`.
{
  "customer_user_id": "<user-id>",
  "payg": {
    "currency": "USD",
    "balance": "0.000001"
  },
  "token_plans": [
    {
      "model": "<public-name>",
      "purchased_tokens": 1000,
      "used_tokens": 123,
      "remaining_tokens": 877,
      "status": "active"
    }
  ],
  "as_of": "2026-09-16T00:00:00Z"
}
401`invalid_credentials` — missing or invalid JWT.
404`not_found` — the customer is not owned by this reseller.
422`validation_error` — `userID` is not a valid UUID.

Usage endpoints#

GET/api/reseller/usage
Auth: reseller-jwt

Return settled usage for this reseller's customers, newest first, with cursor pagination. Each item includes the exact cost as display_cost_usd_micro.

Parameters

limitintegerqueryoptionaldefault: 50

Page size. Minimum 1, maximum 100.

cursorstringqueryoptional

Opaque cursor from a previous response's `next_cursor`.

Responses

200A page of usage events.
{
  "data": [
    {
      "user_id": "<user-id>",
      "model": "<public-name>",
      "total_tokens": 49,
      "display_cost_usd_micro": "0.000120",
      "timestamp": "2026-09-16T00:00:00Z"
    }
  ],
  "next_cursor": "<opaque-string>"
}
401`invalid_credentials` — missing or invalid JWT.