Docs / Outgoing webhooks

Outgoing webhooks

An outgoing webhook is a message the panel sends to an address of yours when something happens to a hosting account, so another system can react: your own billing, a chat channel, a spreadsheet, a provisioning script.

Webhooks are available to administrators and resellers. An administrator hears about every account. A reseller hears only about the accounts that belong to that reseller. Customers and API tokens cannot create them: webhooks are set up in the panel itself, in a person's own session, so that a leaked token can never become a permanent feed of the panel's events.

Add one

Open your account security page (an administrator: Security; a reseller: Login Security), and find Webhooks below the API tokens.

  1. Type the address, for example https://example.com/hooks/panel.
  2. Tick the events you want.
  3. Press Add webhook.

The panel shows a signing secret (it starts with whsec_) once. Copy it now. It is stored encrypted and cannot be shown again; if you lose it, delete the webhook and add a new one.

Each person can have at most 10 webhooks.

The address

  • It must start with https://. Plain http:// is refused, because the call carries account details.
  • It must not contain a user name or password. Check the signature instead.
  • It can be at most 500 characters.
  • It must be on the public internet. Addresses on this server, on a private network, on the loopback interface, or in the cloud provider's metadata range are refused, both when you save the address and again at the moment the panel connects (so a host name that later points at a private address does not work either).
  • Redirects are not followed. Give the final address.

The events

Event Sent when
account.created A hosting account is created
account.suspended An account is suspended
account.unsuspended A suspended account is brought back
account.deleted An account is deleted
account.activity Other activity on an account

A Test button on each webhook sends an event called ping, so you can see that your address answers. Only one test can wait at a time.

What is sent

Each call is an HTTP POST with a JSON body:

{
  "event": "account.created",
  "at": "2026-10-23T08:15:00Z",
  "account": { "id": 42, "username": "example", "domain": "example.com" },
  "detail": "short text about what happened"
}

at is a UTC time. The ping event has the shape {"event":"ping","at":"…","webhook_id":7}.

These headers come with every call:

Header Meaning
X-Nimbopanel-Event The event name, for example account.created
X-Nimbopanel-Delivery A number that identifies this delivery. A retry of the same delivery keeps the same number, so use it to ignore duplicates
X-Nimbopanel-Timestamp When the call was signed, in Unix seconds
X-Nimbopanel-Signature sha256= followed by the signature (below)
Content-Type application/json
User-Agent Nimbopanel-Webhook/1

Check the signature

Anyone can send a request to your address. The signature is how you know a call came from your panel.

The signature is an HMAC-SHA256, in hex, of the timestamp, a dot, and the raw body exactly as received, made with your signing secret:

X-Nimbopanel-Signature == "sha256=" + hex( HMAC_SHA256( secret, X-Nimbopanel-Timestamp + "." + body ) )

Check it in this order:

  1. Compute the value above from the raw request body (before any JSON parsing or re-formatting).
  2. Compare it with the header using a constant-time comparison.
  3. Reject the call if the timestamp is more than 5 minutes away from your clock. This stops someone replaying an old call.
  4. Use X-Nimbopanel-Delivery to ignore a delivery you have already handled.

Example in PHP:

$body = file_get_contents('php://input');
$ts   = $_SERVER['HTTP_X_NIMBOPANEL_TIMESTAMP'] ?? '';
$sig  = $_SERVER['HTTP_X_NIMBOPANEL_SIGNATURE'] ?? '';
$want = 'sha256=' . hash_hmac('sha256', $ts . '.' . $body, $secret);
if (!hash_equals($want, $sig) || abs(time() - (int) $ts) > 300) {
    http_response_code(401);
    exit;
}

Example in Node.js:

const crypto = require('crypto');
function valid(rawBody, headers, secret) {
  const ts = headers['x-nimbopanel-timestamp'] || '';
  const want = 'sha256=' + crypto.createHmac('sha256', secret).update(ts + '.' + rawBody).digest('hex');
  const got = headers['x-nimbopanel-signature'] || '';
  return got.length === want.length
    && crypto.timingSafeEqual(Buffer.from(got), Buffer.from(want))
    && Math.abs(Date.now() / 1000 - Number(ts)) <= 300;
}

Answering, retries and pausing

  • Answer with any 2xx status within 10 seconds. Anything else counts as a failure.
  • A failed call is tried again after 1 minute, then 5 minutes, 30 minutes and 2 hours. That is five tries in all, spread over about two and a half hours, and then the delivery is marked failed.
  • Calls to one address are sent one at a time, oldest first. A delivery that is waiting to be retried does not hold back newer ones, so do not rely on events arriving in strict order. A slow address only holds up itself.
  • After 50 failed deliveries in a row, the webhook pauses: new events are no longer queued for it. A successful Test clears the count and events flow again.
  • At most 200 undelivered calls wait for one webhook. Beyond that, further events for it are not queued until some have been delivered or have failed.
  • The panel keeps the newest 50 finished deliveries for each webhook, with the answer it got, so you can see what failed. The most recent ones are listed under the webhook.

If the panel shows a reason instead of a status, it is one of: the address is not on the public internet, no answer within 10 seconds, the host name does not exist, the HTTPS certificate is not valid, or the panel could not connect.

Good habits

  • Answer quickly with 200 and do the real work afterwards. A receiver that takes longer than 10 seconds looks like a failure, and gets the same event again.
  • Handle the same delivery twice without harm. Retries and network errors make duplicates possible.
  • Keep the secret out of your code repository. If it leaks, delete the webhook and add a new one.

Related: Reseller guide · Troubleshooting