Webhooks let ZoltVPN tell your system the moment something happens, instead of your system asking again and again. When one of your subscription codes is redeemed, a client uses an extra-device code, a plan renews or is about to expire, a client pays, or your wallet runs low, we send an HTTPS POST with a small JSON body to an address on your server. Every event is signed with your endpoint's own secret, so you can prove it came from us, and failed deliveries are retried for about two days. Webhooks are part of the ZoltVPN Reseller API; ZoltVPN is one of the few VPN providers offering a complete reseller API.
Why webhooks beat polling
Without webhooks, a CRM finds out about changes by polling: calling GET /clients or GET /codes every few minutes and comparing the results. That works, but it has costs.
- Delay. You learn about a redeemed code only at your next poll.
- Wasted calls. Most polls return nothing new, yet every call counts against your rate limits: by default 60 calls a minute and 10,000 a day, with the API blocked for 24 hours if you exceed the daily limit.
- Complexity. Working out what changed means storing and comparing snapshots.
Webhooks reverse the direction. You hear about each change once, within moments, and keep your API calls for actions. A light daily sync through the API is still a good safety net; our guide to automating resale with the reseller API shows a three-call sync.
The events
Client events go to the client's own reseller, so sub-agents receive their own clients' events.
| Event | When it is sent |
|---|---|
code.redeemed | One of your subscription codes (ZP…) was redeemed |
device_code.redeemed | An extra-device code (ZD…) of one of your clients was used on a device |
client.created | A new client of yours, created by you or by redeeming your code |
subscription.renewed | A client's plan started or was extended |
subscription.expiring | A client's plan ends in 7 days, and again at 1 day |
subscription.expired | A client's plan ended and nothing replaced it |
client.payment | A client of yours paid us directly, with your share of the payment |
client.data_limit | A client used 80% or 100% of a data allowance |
wallet.low_balance | Your wallet went below your low-balance alert level |
api.blocked | Your API access was blocked for going over the daily limit |
webhook.test | You pressed Send test in the portal |
Setting up an endpoint
In the reseller portal, open API → Webhooks → Add endpoint and enter:
- the HTTPS address on your server that should receive events;
- the events you want;
- an optional description.
The signing secret, which starts with whsec_, is shown once, like an API key. Store it in your server's secret store straight away. You can have up to five endpoints, and API access must be switched on for your account. The address must be public HTTPS; private and internal addresses are refused. Use Send test to fire a webhook.test event and confirm everything works end to end.
What arrives
Each delivery is a POST with a JSON body like this:
{
"id": "6d1f0c2e-1b7a-4c33-9f0e-2a8b5d7c9e10",
"type": "code.redeemed",
"createdAt": "2026-11-05T18:00:00Z",
"data": { "code": "ZP7K2M9Q4X1A", "planName": "ZoltVPN Gold",
"clientEmail": "[email protected]", "expiresAt": "2026-12-05T18:00:00Z" }
}
The example is shortened; the full field list for each event is in the webhooks guide. Alongside the body come headers that identify the event (Zolt-Event-Id, Zolt-Event-Type), the delivery attempt (Zolt-Delivery-Id) and the signature (Zolt-Signature).
Verifying the signature
Never act on an event you haven't verified. Anyone can send a POST to a public address; only ZoltVPN and you know your signing secret. The Zolt-Signature header has the form t=…,v1=…, where t is the time in Unix seconds and v1 is the signature in hex. For every event:
- Read
tandv1from the header. - Compute an HMAC-SHA256 with your signing secret over the text
t, a full stop, and the raw request body, then hex-encode it. - Compare it with
v1in constant time, using your language's safe comparison function. - Refuse the event if
tis more than five minutes away from your server's clock, so an old message can't be replayed.
The most common mistake is verifying a re-serialised body. Many web frameworks parse JSON automatically; you need the exact bytes we sent. The webhooks guide has ready-made examples in Node.js, PHP and Python.
Answering, retries and duplicates
- Answer any 2xx within 10 seconds. Put the event on a queue and do the slow work afterwards.
- Anything else counts as a failure, including a time-out or a redirect, because redirects are not followed.
- We retry after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours: eight attempts over about two days. After that the delivery is marked failed, and you can send it again from the portal.
- After 30 failed attempts in a row, we turn the endpoint off and notify you. Turn it back on in the portal once it works.
- Events can arrive more than once, or out of order. Store each event's
idand ignore repeats; usecreatedAtto order them.
Practical recipes
Close the sale when a code is redeemed
On code.redeemed, find the code in your CRM, mark the order complete and link it to the client. If you sold codes through a shop, this is the moment to send your own receipt or welcome message.
Track extra devices
On device_code.redeemed, record which device platform and model used the code. Your support team can then see at a glance which devices each client has added.
Turn expiry into renewals
subscription.expiring arrives seven days before a plan ends and again one day before. Use it to start your renewal flow: an email, a message, or a task for your sales team. If your account has limited notifications switched on, your CRM can also send a push notification to the client's devices through POST /notifications, up to 100 clients per request, with daily limits per account and per client. The push notifications guide explains the rules.
Keep the wallet topped up
On wallet.low_balance, alert whoever handles finance. Top-ups are made in the portal, so a quick alert avoids failed purchases later.
Reconcile direct payments
client.payment includes your share of a client's own payment, which makes month-end reconciliation much simpler.
Watch your integration's health
api.blocked means your integration exceeded the daily limit. Route it to your on-call developer: it usually points to a retry loop.
FAQ
Do webhooks replace the API?
No. Webhooks tell you what happened; the API is how you act on it and how you run a periodic sync. Most integrations use both.
What if my server is down when an event is sent?
We make eight attempts in total over about two days. If every attempt fails, you can resend the delivery from the portal once your server is back.
Can the same event arrive twice?
Yes. Store each event's id and skip any you have already processed.
Do sub-agents receive their clients' events?
Yes. Client events go to the client's own reseller, so each sub-agent receives events for the clients they sold to.
Start receiving events today: add an endpoint in the portal, read the full Reseller API guide, or download ZoltVPN to try the apps your clients use, free for 7 days.



