Reseller API

Endpoints

Every ZoltVPN Reseller API endpoint with examples: account, plans and quotes, clients and usage, purchases, codes, wallet and push notifications.

On this page
  1. GET /me
  2. GET /plans
  3. GET /quote
  4. GET /clients
  5. GET /clients/{id}
  6. GET /clients/{id}/usage?days=30
  7. POST /clients — scope purchase
  8. POST /clients/{id}/extend — scope purchase
  9. POST /clients/{id}/devices — scope purchase
  10. POST /clients/{id}/static-ip — scope purchase
  11. GET /clients/{id}/data-addons and POST /clients/{id}/data-addons — scope purchase to buy
  12. POST /clients/{id}/suspend | /resume | /disconnect — scope manage
  13. GET /codes
  14. POST /codes — scope purchase
  15. POST /codes/{id}/void — scope purchase
  16. GET /codes/{id}/qr
  17. GET /wallet
MethodPathScopeWhat
GET/mereadYour account, wallet, limits and this key
GET/plansreadThe plans you may sell, with your price and the retail price
GET/quotereadWhat a purchase would cost now
GET/clientsreadYour clients, with plan, expiry, devices and usage
GET/clients/{id}readOne client in detail
GET/clients/{id}/usagereadDaily traffic and traffic per location
GET/clients/{id}/data-addonsreadExtra-data packs you can sell this client
POST/clientspurchaseCreate a client, optionally with a plan
POST/clients/{id}/extendpurchaseRenew, or change the plan
POST/clients/{id}/devicespurchaseExtra devices until the client's expiry
POST/clients/{id}/static-ippurchaseA static IP until the client's expiry
POST/clients/{id}/data-addonspurchaseBuy an extra-data pack
POST/clients/{id}/suspendmanageDisable sign-in and drop live connections
POST/clients/{id}/resumemanageRe-enable a suspended client
POST/clients/{id}/disconnectmanageDrop live connections; the client can reconnect
GET/codesreadYour subscription codes (ZP…)
POST/codespurchaseBuy subscription codes
POST/codes/{id}/voidpurchaseVoid an unused code (refunded to your wallet)
GET/codes/{id}/qrreadThe code's QR as a PNG
GET/walletreadBalance and transactions
POST/notificationsnotifySend a push notification to up to 100 of your clients (Push notifications)
GET/notificationsreadThe 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" }
}
FieldMeaning
walletBalanceCentsWhat you can spend now. Every purchase is taken from it.
lowBalanceThresholdCentsYour own low-balance alert level (portal setting), or null.
clientLimitHow many of your own clients may have a live plan at once; null = no limit.
liveClients / payingClientsYour own clients with a live plan / with a paid (non-trial) live plan.
limitClientNotificationstrue: we send your clients no renewal or billing messages, so following up with them is up to you.
rateLimits.usedTodayCalls 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
} ] }
FieldMeaning
pricesWhat you are charged per term: your price list, set by us.
retailPricesWhat end users pay us directly; your clients see these prices.
extraDeviceMonthlyCentsWhat you pay per extra device per month; null = you can't sell extra devices on this plan.
staticIpMonthlyCentsWhat you pay per static IP per month; null = you can't sell static IPs on this plan.
maxDevicesDevices included in the plan.
durationDaysLength 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"
} ] }
FieldMeaning
statuslive (plan running), expired, suspended (sign-in disabled), noplan (never had one).
isDirect / soldByYour own client / a sub-agent's client, with that agent's email in soldBy.
expiresAtWhen the current plan ends; null with no plan.
devices.active / devices.maxDevices signed in now / allowed (plan + extras).
dataUsedTermBytesTraffic in the current term.
usage30dBytesTraffic 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[].status is active or revoked.
  • extraDevices[].code is the code the client enters on the extra device. Its status is unbound (not used yet) or bound (in use on a device).
  • staticIp is null when the client has none.
  • activeSessions[].countryCode is 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" }
  • planId is 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_confirmation until you send confirmPlanChange: 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 } ] }
  • priceCents is what you pay.
  • countryCode is 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.

  • suspend signs the client out everywhere, drops their connections and blocks sign-in until resume.
  • disconnect only 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": "…" } ] } }
  • amountCents is negative for purchases and positive for top-ups, refunds and commission.
  • type is 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 of amountCents.
  • Top-ups are made in the portal, not through the API.