Skip to main content
A webhook sends events of your account to a URL of yours, so your code does not have to poll. It belongs to the account, not to the token that created it: revoking the token does not stop the webhook.

Create a webhook

POST /v1/webhooks with scope vms.manage. Subscribing to order_refunded also needs billing.read.
The answer 201 holds the webhook and its signingSecret, 64 hex characters. It is shown once, in this answer, and never again. Store it now. The URL is stored encrypted and is not shown again either, only its host, so it may carry a credential of yours. An account has at most 10 notification channels, the dashboard’s Telegram and Discord channels included. One more is 400 NOTIFICATION_CHANNEL_LIMIT_REACHED.
  • GET /v1/webhooks (scope vms.read) lists the account’s webhooks, including those set up in the dashboard under Settings → Notifications, with their status, consecutiveFailures, lastSuccessAt, lastFailureAt and lastError.
  • DELETE /v1/webhooks/{id} (scope vms.manage) deletes one. It gets no further events, and deliveries already queued are dropped.

Events

Every delivery is a POST with a JSON body:
id names the event: it is the same on every retry of it. occurredAt is when the change happened, not when it was sent. vmId is the server’s id, as in GET /v1/servers/{id}. reason of vm_suspended:
  • expired: the term ended unpaid. deletionAt says when the server is deleted with its data unless its term is renewed first.
  • abuse: the server was found abusing.
  • admin: suspended by our team.
  • system: a paid server could not be started again; our team takes over.
For every reason except expired, deletionAt is null. reason of vm_terminated: expired (the term ended unpaid), deleted (by you, in the dashboard or with DELETE /v1/servers/{id}) or admin (by our team).

Headers

Verify the signature

Check every request before you trust it:
  1. Compute the HMAC-SHA256 of the timestamp header, a dot, and the raw request body, exactly as received. Use the signing secret string as the key. Compare v1=<hex digest> with X-Exy-Signature in constant time.
  2. Reject the request when the timestamp is more than 300 seconds from your clock: that is a replay of an old request.
Parse the JSON only after the check. Parsing and serialising it again changes the bytes and breaks the signature.
Your endpoint has to be reachable over https on port 443. The sample servers listen on plain HTTP, so put them behind your TLS proxy.

Answer fast, then work

Store or queue the event first, answer with any 2xx within 10 seconds, then do the work. A 2xx counts as delivered and the event is not sent again, so answer only once the event is safe. A slow handler times out and the event is sent again.

Retries

When 5 deliveries in a row have failed for good, after all their retries, the webhook is switched off: its status becomes failed and the account owner gets an email. A successful delivery resets the count. Resume a failed webhook in the dashboard, or delete it and create it again. A webhook paused in the dashboard is paused; neither state gets events. Because of retries:
  • A delivery can arrive twice. Deduplicate on the event id (also in X-Exy-Delivery).
  • A retried delivery can arrive after a newer event. Order events by occurredAt, not by arrival.