Endpoints
Every ZoltVPN Reseller API endpoint with examples: account, plans and quotes, clients and usage, purchases, codes, wallet and push notifications.
On this page
- GET /me
- GET /plans
- GET /quote
- GET /clients
- GET /clients/{id}
- GET /clients/{id}/usage?days=30
- POST /clients — scope purchase
- POST /clients/{id}/extend — scope purchase
- POST /clients/{id}/devices — scope purchase
- POST /clients/{id}/static-ip — scope purchase
- GET /clients/{id}/data-addons and POST /clients/{id}/data-addons — scope purchase to buy
- POST /clients/{id}/suspend | /resume | /disconnect — scope manage
- GET /codes
- POST /codes — scope purchase
- POST /codes/{id}/void — scope purchase
- GET /codes/{id}/qr
- GET /wallet
| Method | Path | Scope | What |
|---|---|---|---|
| GET | /me | read | Your account, wallet, limits and this key |
| GET | /plans | read | The plans you may sell, with your price and the retail price |
| GET | /quote | read | What a purchase would cost now |
| GET | /clients | read | Your clients, with plan, expiry, devices and usage |
| GET | /clients/{id} | read | One client in detail |
| GET | /clients/{id}/usage | read | Daily traffic and traffic per location |
| GET | /clients/{id}/data-addons | read | Extra-data packs you can sell this client |
| POST | /clients | purchase | Create a client, optionally with a plan |
| POST | /clients/{id}/extend | purchase | Renew, or change the plan |
| POST | /clients/{id}/devices | purchase | Extra devices until the client's expiry |
| POST | /clients/{id}/static-ip | purchase | A static IP until the client's expiry |
| POST | /clients/{id}/data-addons | purchase | Buy an extra-data pack |
| POST | /clients/{id}/suspend | manage | Disable sign-in and drop live connections |
| POST | /clients/{id}/resume | manage | Re-enable a suspended client |
| POST | /clients/{id}/disconnect | manage | Drop live connections; the client can reconnect |
| GET | /codes | read | Your subscription codes (ZP…) |
| POST | /codes | purchase | Buy subscription codes |
| POST | /codes/{id}/void | purchase | Void an unused code (refunded to your wallet) |
| GET | /codes/{id}/qr | read | The code's QR as a PNG |
| GET | /wallet | read | Balance and transactions |
| POST | /notifications | notify | Send a push notification to up to 100 of your clients (Push notifications) |
| GET | /notifications | read | The notifications you sent |
A machine-readable description (OpenAPI 3) is at https://app.zoltvpn.com/api/reseller-api/openapi/reseller-v1.json.
GET /me
{
"id": "…", "email": "[email protected]", "name": "Agent Name",
"walletBalanceCents": 125000, "currency": "USD", "lowBalanceThresholdCents": 20000,
"clientLimit": null, "liveClients": 84, "payingClients": 80,
"limitClientNotifications": false,
"key": { "id": "…", "name": "My CRM", "prefix": "zrk_3f9a1c2b7d4e8f60", "scopes": ["read","purchase"], "expiresAt": null },
"rateLimits": { "perMinute": 60, "perDay": 10000, "blockHours": 24, "usedToday": 152, "dayResetsAt": "2026-11-06T00:00:00Z" }
}
| Field | Meaning |
|---|---|
walletBalanceCents | What you can spend now. Every purchase is taken from it. |
lowBalanceThresholdCents | Your own low-balance alert level (portal setting), or null. |
clientLimit | How many of your own clients may have a live plan at once; null = no limit. |
liveClients / payingClients | Your own clients with a live plan / with a paid (non-trial) live plan. |
limitClientNotifications | true: we send your clients no renewal or billing messages, so following up with them is up to you. |
rateLimits.usedToday | Calls counted today (UTC). |
GET /plans
{ "items": [ {
"id": "…", "name": "ZoltVPN Gold", "maxDevices": 2, "durationDays": 30,
"prices": { "monthly": 600, "6month": 3240, "yearly": 6000 },
"retailPrices": { "monthly": 999, "6month": 5394, "yearly": 9990 },
"extraDeviceMonthlyCents": 150, "retailExtraDeviceMonthlyCents": 299,
"staticIpMonthlyCents": 300
} ] }
| Field | Meaning |
|---|---|
prices | What you are charged per term: your price list, set by us. |
retailPrices | What end users pay us directly; your clients see these prices. |
extraDeviceMonthlyCents | What you pay per extra device per month; null = you can't sell extra devices on this plan. |
staticIpMonthlyCents | What you pay per static IP per month; null = you can't sell static IPs on this plan. |
maxDevices | Devices included in the plan. |
durationDays | Length of one monthly term in days. |
Extra devices and static IPs are charged per month until the client's subscription ends, and a part month
counts as a full month. Use GET /quote for the exact amount.
GET /quote
Query: action = create | extend | code | device | static_ip | addon, plus what that action needs:
create:planId,billingInterval;extend:clientId,planId,billingInterval;code:planId,billingInterval,quantity(1–100);device:clientId,quantity(1–20);static_ip:clientId;addon:clientId,addonId.
{ "sellable": true, "reason": null, "unitCents": 600, "quantity": 1, "totalCents": 600,
"balanceCents": 125000, "balanceAfterCents": 124400, "insufficient": false,
"term": { "startsAt": "2026-11-05T18:00:00Z", "expiresAt": "2026-12-05T18:00:00Z" }, "planChange": false }
sellable: false comes with a reason, one of: plan_not_resellable, device_not_sellable,
static_ip_not_sellable, no_live_subscription, not_your_client, client_limit_reached,
static_ip_already_held, static_ip_covers_term, no_static_ip_available. planChange: true means an
extend would switch plans; it starts now, not at the current expiry.
GET /clients
Query:
search: email or name contains;status=all|live|expiring(live, ends within 7 days) |expired|suspended|noplan;scope=all|direct;sort=createdAt|email|expiresAt|lastSeenAt|usage30d;dir=asc|desc;page,pageSize.
{ "total": 84, "page": 1, "pageSize": 50, "items": [ {
"id": "…", "email": "[email protected]", "name": "Client Name",
"status": "live", "isVerified": true, "isDirect": true, "soldBy": null,
"plan": { "id": "…", "name": "ZoltVPN Gold" }, "expiresAt": "2026-12-01T10:00:00Z",
"isOnline": true, "lastSeenAt": "2026-11-05T17:58:00Z",
"devices": { "active": 2, "max": 3 },
"dataUsedTermBytes": 15032385536, "usage30dBytes": 21474836480,
"createdAt": "2026-08-01T09:00:00Z"
} ] }
| Field | Meaning |
|---|---|
status | live (plan running), expired, suspended (sign-in disabled), noplan (never had one). |
isDirect / soldBy | Your own client / a sub-agent's client, with that agent's email in soldBy. |
expiresAt | When the current plan ends; null with no plan. |
devices.active / devices.max | Devices signed in now / allowed (plan + extras). |
dataUsedTermBytes | Traffic in the current term. |
usage30dBytes | Traffic in the last 30 days. |
GET /clients/{id}
Everything in a list row (devices is still { active, max }), plus isActive (false while suspended) and:
{
"subscription": { "planId": "…", "planName": "ZoltVPN Gold", "startedAt": "…", "expiresAt": "…", "isExpired": false, "autoRenew": false },
"deviceList": [ { "id": "…", "name": "Ali's iPhone", "platform": "ios", "model": "iPhone15,2", "appVersion": "0.7.3", "status": "active", "lastSeenAt": "…" } ],
"extraDevices": [ { "id": "…", "code": "ZD4K7Q2M9P", "status": "bound", "source": "reseller", "expiresAt": "…" } ],
"staticIp": { "ip": "187.7.201.4", "source": "reseller", "expiresAt": "…" },
"activeSessions": [ { "countryCode": "CA", "startedAt": "…", "seconds": 3600, "bytes": 104857600 } ]
}
deviceList[].statusisactiveorrevoked.extraDevices[].codeis the code the client enters on the extra device. Itsstatusisunbound(not used yet) orbound(in use on a device).staticIpisnullwhen the client has none.activeSessions[].countryCodeis the server's location.
GET /clients/{id}/usage?days=30
days runs from 1 to 400.
{ "daily": [ { "date": "2026-11-04", "downBytes": 734003200, "upBytes": 52428800 } ],
"byLocation": [ { "countryCode": "DE", "bytes": 1073741824 } ] }
POST /clients — scope purchase
{ "email": "[email protected]", "password": "at-least-8-chars", "name": "New Client",
"planId": "…", "billingInterval": "monthly" }
planIdis optional. Without it the client is created with no plan, at no charge.- With it, the term is charged to your wallet at your price.
- The client signs in with that email and password, and is asked to verify the email.
Answer:
{ "client": { …list row… }, "chargedCents": 600, "balanceCents": 124400 }
POST /clients/{id}/extend — scope purchase
{ "planId": "…", "billingInterval": "6month", "confirmPlanChange": false }
- With the same plan, the new term starts when the current one ends, so no days are lost.
- With a different plan, the call answers
409 plan_change_needs_confirmationuntil you sendconfirmPlanChange: true. The new plan starts now.
Answer: { "expiresAt": "…", "chargedCents": 3240, "balanceCents": 121160 }.
POST /clients/{id}/devices — scope purchase
Body: { "count": 1 }, with count from 1 to 20.
Answer:
{ "devicesMax": 3, "until": "…", "chargedCents": 300, "balanceCents": 120860, "codes": ["ZD4K7Q2M9P"] }
Give each code to the client for the extra device. Extras end with the client's subscription.
POST /clients/{id}/static-ip — scope purchase
No body.
Answer: { "ip": "187.7.201.4", "until": "…", "months": 2, "extended": false, "chargedCents": 600, "balanceCents": 120260 }.
- The IP is the client's own until their subscription ends.
- After a renewal, call again: the same IP is extended (
extended: true), charged only for the new months.
GET /clients/{id}/data-addons and POST /clients/{id}/data-addons — scope purchase to buy
GET returns:
{ "addons": [ { "id": "…", "name": "Extra 50 GB", "quotaBytes": 53687091200, "priceCents": 300, "retailPriceCents": 499, "countryCode": null } ] }
priceCentsis what you pay.countryCodeis set for a pack that adds data for one location.
To buy, POST the body { "addonId": "…" }. Answer: { "grantId": "…", "quotaBytes": 53687091200, "chargedCents": 300, "balanceCents": 119960 }.
POST /clients/{id}/suspend | /resume | /disconnect — scope manage
No body.
suspendsigns the client out everywhere, drops their connections and blocks sign-in untilresume.disconnectonly drops the current connections.
Answers:
suspend:{ "suspended": true, "sessionsDisconnected": 1 };resume:{ "suspended": false, "sessionsDisconnected": 0 };disconnect:{ "sessionsDisconnected": 1 }.
Each one is recorded in the client's activity in the portal, with the key's prefix.
GET /codes
Query: status = active | redeemed | void; search; page, pageSize.
{ "total": 12, "page": 1, "pageSize": 50, "items": [ {
"id": "…", "code": "ZP7K2M9Q4X1A", "planId": "…", "planName": "ZoltVPN Gold", "billingInterval": "monthly",
"status": "active", "note": "for Reza", "pricePaidCents": 600,
"redeemedAt": null, "expiresAt": "2027-02-03T00:00:00Z", "batchId": "…", "createdAt": "…"
} ] }
expiresAt is the last day an unused code can be redeemed.
POST /codes — scope purchase
Body: { "planId": "…", "billingInterval": "monthly", "quantity": 5, "note": "for shop #2" }, with quantity from 1
to 100 and note up to 200 characters.
Answer: { "batchId": "…", "balanceCents": 116960, "codes": [ …code rows… ] }.
The client redeems a code in the app, or scans its QR.
POST /codes/{id}/void — scope purchase
Voids an unused code; its price goes back to your wallet.
- Answer:
{ "id": "…", "status": "void", "refunded": true, "balanceCents": 117560 }. - Voiding an already void code answers the same, with
refunded: false. - A used code answers
409 code_not_active. - A code that is not yours answers
404 code_not_found.
GET /codes/{id}/qr
image/png. The QR opens ZoltVPN on the redeem screen. Don't cache or share it publicly: it is a working code.
GET /wallet
Query: page, pageSize.
{ "balanceCents": 116960, "currency": "USD",
"transactions": { "total": 40, "page": 1, "pageSize": 50, "items": [ {
"id": 9123, "type": "subscription_grant", "amountCents": -600, "balanceAfterCents": 116960,
"reference": "…", "note": "Extend [email protected]", "createdAt": "…" } ] } }
amountCentsis negative for purchases and positive for top-ups, refunds and commission.typeis one of:- purchases:
subscription_grant,code_purchase,device_purchase,static_ip_purchase,addon_purchase; - top-ups:
topup_stripe,topup_paypal,topup_crypto,topup_usdt,topup_admin; - your share of a client's own payment:
client_commission,client_commission_reversal,addon_commission; addon_refund,adjustment. New types can appear; treat an unknown one by the sign ofamountCents.
- purchases:
- Top-ups are made in the portal, not through the API.