VorticPanel

API and billing

Webhooks

Webhooks are signed POST requests the panel sends to your URL when something happens, so billing and other systems don’t have to poll.

Who Where Gets
Staff /admin/api (needs the “API & webhooks” permission) Every event
Resellers Reseller → Webhooks Events about their own customers, never transfers
Customers Account → Webhooks Events about their own servers

Each webhook has its events, a signing secret shown once, Send test, pause and delete, and a delivery log showing each attempt’s status, timing, and full request and response, with Redeliver.

  • Up to 10 webhooks per account.
  • The URL must be https://, and private or loopback addresses are refused.

Events

Event When Staff Resellers Customers
server.created An order is accepted ✓ ✓
server.provisioned Built and booted ✓ ✓ ✓
server.suspended Suspended, from anywhere ✓ ✓ ✓
server.unsuspended Unsuspended ✓ ✓ ✓
server.terminated Terminated ✓ ✓ ✓
server.reinstalled Reinstalled ✓ ✓ ✓
server.resized Changed package ✓ ✓ ✓
server.addresses_changed An address added or removed ✓ ✓ ✓
server.speed_changed Speed limits overridden ✓ ✓
server.owner_changed Moved to another customer ✓
floating_ip.created A floating IP reserved ✓ ✓
floating_ip.released A floating IP released ✓ ✓
alert.fired A server alert fires ✓ ✓ ✓
alert.recovered It recovers ✓ ✓ ✓
backup.failed A backup fails ✓ ✓ ✓
customer.created A new account ✓ ✓
gpu_instance.created A GPU instance was created and is being set up ✓ ✓
gpu_instance.ready A new GPU instance finished setting up and runs ✓ ✓ ✓
gpu_instance.started A GPU instance started running (started, restarted or unsuspended) ✓ ✓ ✓
gpu_instance.stopped A GPU instance stopped, when asked or on its own ✓ ✓ ✓
gpu_instance.failed Setting up, starting or changing a GPU instance failed ✓ ✓ ✓
gpu_instance.image_changed A GPU instance was made again from another image ✓ ✓ ✓
gpu_instance.suspended A GPU instance was suspended, with the reason ✓ ✓ ✓
gpu_instance.unsuspended A GPU instance was unsuspended ✓ ✓ ✓
gpu_instance.deleted A GPU instance and its /workspace were deleted ✓ ✓ ✓
quota.near_limit A reseller passes 90% of a quota ✓ ✓
transfer.started A move between nodes starts ✓
transfer.finished It finishes ✓
transfer.failed It fails ✓

The request

POST /your/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: NordvikPanel-Webhooks/1.0
X-Panel-Event: server.suspended
X-Panel-Delivery: dlv_…
X-Panel-Timestamp: 1790879428
X-Panel-Signature: v1=5d41402abc4b2a76b9719d911017c592…

{
  "id": "evt_…",
  "type": "server.suspended",
  "created_at": "2026-10-01T14:30:28.000Z",
  "data": {
    "server": {
      "id": "srv_abc123",
      "hostname": "web-01.example.com",
      "status": "suspended",
      "package": "VPS-2",
      "location": "ams1",
      "ipv4": "203.0.113.10"
    },
    "customer": { "id": "usr_…", "email": "priya@example.com", "externalId": "1042" },
    "reason": "Invoice #1042 overdue"
  }
}

data by event

Events data
server.* server and customer (with your billing externalId), plus: reason (suspended), image (created, reinstalled), releasedAddresses (terminated), from and to (resized), added and removed (addresses_changed), speed and overridden (speed_changed), previousOwner (owner_changed)
alert.* server, customer, and alert with id, kind, threshold, description
backup.failed server, customer, and backup with id, kind, error
transfer.* transfer with id, mode, from, to; server with id, hostname; plus downtimeMs (finished) or error (failed)
customer.created customer with id, email, name, externalId
floating_ip.* floatingIp with id, address, location
gpu_instance.* gpuInstance with id, name, status, offerId, billingProductId, location, address, createdAt; customer as for servers. stopped and started add reason; a crash adds detail, exitCode and oomKilled; failed adds action and error; suspended adds reason
quota.near_limit resource, used, limit

Send test delivers { "test": true, "message": "Sent from the panel to check this endpoint. Ignore it." } as data.

Verifying the signature

X-Panel-Signature is v1= followed by the hex HMAC-SHA256 of timestamp.body, made with your webhook’s secret (whsec_…):

  1. Read the raw request body, before parsing it.
  2. Compute HMAC-SHA256(secret, X-Panel-Timestamp + "." + body).
  3. Compare it, in constant time, with each v1= value in the header.
  4. Reject requests whose timestamp is more than 5 minutes old.
  5. Use the event id to ignore duplicates.

After you rotate the secret, the old one keeps working for 24 hours: the header carries two signatures, new first, v1=<new>,v1=<old>. Accept either, so you can update your side without missing events.

$body = file_get_contents('php://input');
$ts   = $_SERVER['HTTP_X_PANEL_TIMESTAMP'];
$want = hash_hmac('sha256', $ts . '.' . $body, $secret);
$ok   = false;
foreach (explode(',', $_SERVER['HTTP_X_PANEL_SIGNATURE']) as $sig) {
  $ok = $ok || hash_equals($want, substr(trim($sig), 3));
}
if (!$ok || abs(time() - (int) $ts) > 300) { http_response_code(401); exit; }

Delivery and retries

  • Anything but a 2xx answer is a failure, and so is a receiver that goes quiet for 10 seconds or hasn’t finished its whole answer within 15 seconds. Redirects aren’t followed.
  • Failures are retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 8 hours, then given up: six attempts in all.
  • Waiting retries are saved with everything else, so a controller restart doesn’t drop them.
  • Send test and Redeliver are one-off: they aren’t retried. Each person can use them 10 times a minute.
  • A customer’s or reseller’s webhooks send at most 2 deliveries at once; the rest wait their turn. A slow receiver only holds up its own deliveries, never the platform’s or anyone else’s.
  • The delivery log keeps the last 200 deliveries for each customer or reseller, and the last 2,000 for the platform’s webhooks.
  • At most 500 deliveries wait to be sent (or retried) for each customer or reseller, and 5,000 for the platform. Past that, the oldest waiting one is dropped for each new one, and the owner gets a notification (at most once a day).
  • A webhook that has failed 50 times in a row, for more than a day, is paused: nothing more is queued for it, and its owner gets a notification and email. Fix the receiver, then turn the webhook back on from its page.
  • Webhooks only go to public addresses. When a customer’s or reseller’s address leads somewhere else, their log says That address isn’t reachable from here. Staff see the address it resolved to.

Answer quickly and do slow work afterwards, so deliveries don’t time out.

Every word has to appear. ↑ ↓ to move, Enter to open.