PanelDOCSv1.0.0

Partner webhooks

Signed messages the licence server sends to your systems about your licences, how to check the HMAC-SHA256 signature, and the retries.

Instead of asking the partner API again and again whether something changed, your systems can be told. The licence server sends a signed POST to your own address when something happens to one of your licences.

Set up a webhook

Send HostFog Ltd the HTTPS address that should receive the messages. You get a signing secret back, once: store it like your API key. Your address must answer from the public internet over HTTPS. If the secret is lost or leaked, ask for a new one; the old one stops working at once. Licences you already have are not sent when the address is set: only what changes after that.

Events

Event Sent when
licence.issued a licence was issued, by the partner API or on the partner page
licence.changed a licence's products or plans changed through the partner API
licence.moved a licence moved to another server; from_ip is the old address
licence.removed a licence was removed
licence.suspended a licence stopped working: revoked, or your bulk licences ended
server.unseen a licensed server has not checked in for 7 days

What the partner API does is sent at once. Changes made anywhere else, and servers going quiet, are found every few minutes, so they can take up to about 5 minutes to arrive. Each change is sent once.

What a message looks like

Each message is one POST with a JSON body and these headers:

http
Content-Type: application/json
X-HF-Event: licence.issued
X-HF-Delivery: 9051
X-HF-Timestamp: 1793023200
X-HF-Signature: sha256=5c1f…

X-HF-Event names the event and X-HF-Delivery numbers this delivery. A retry of the same message carries the same delivery number, so you can skip a message you already handled. The body looks like this:

json
{
  "id": "4f1c2a9e0b7d3c55",
  "event": "licence.moved",
  "time": "2026-11-02T10:15:00Z",
  "partner": {"id": 7, "company": "Example Hosting"},
  "licence": {"key": "HFL-…", "email": "customer@example.com", "ip": "203.0.113.20", "hostname": "srv1.example.com",
              "status": "active", "licence_status": "active", "last_check_at": "2026-11-02T05:47:00Z",
              "version": "1.0.0", "accounts": 42,
              "products": [{"product": "panel", "name": "HF Panel", "plan": "pro", "plan_name": "Pro"}]},
  "from_ip": "203.0.113.10"
}

licence.status is the same as in the fleet. A licence.removed message carries only the licence's key, email, ip and hostname.

Check the signature

Never act on a message before checking it. X-HF-Signature is sha256= followed by the hexadecimal HMAC-SHA256 of <X-HF-Timestamp>.<body>, made with your signing secret.

python
import hmac, hashlib, time

def genuine(secret: bytes, ts: str, body: bytes, header: str) -> bool:
    if abs(time.time() - int(ts)) > 300:      # refuse old or replayed messages
        return False
    want = "sha256=" + hmac.new(secret, ts.encode() + b"." + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(want, header)
php
function genuine(string $secret, string $ts, string $body, string $header): bool {
    if (abs(time() - (int) $ts) > 300) {
        return false;
    }
    $want = 'sha256=' . hash_hmac('sha256', $ts . '.' . $body, $secret);
    return hash_equals($want, $header);
}

Important: compute the signature over the body exactly as it arrived, before any JSON parsing, and compare it in constant time (compare_digest, hash_equals).

Answer and retries

  • Answer with any 2xx status, quickly. Do slow work after answering.
  • A 5xx, 408 or 429 answer, or no answer at all (10 seconds at most), is tried again 5 times: after about 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours. After that it counts as failed.
  • Any other 4xx answer counts as failed at once. Redirects are not followed.
  • Each retry has a new X-HF-Timestamp and signature, and the same X-HF-Delivery.

Because a message can arrive more than once, and messages can arrive out of order, treat each one as a hint: when it matters, read the licence's current state with the partner API.