HF PanelDocsv1.0.0

Event hooks

The events HF Panel raises, their JSON payloads, signed webhooks with retries, and root script hooks in /etc/hostfog/hooks.

HF Panel raises an event each time something happens on the server: a hosting account is created, a domain is removed, a backup finishes, a staff member signs in. You can send these events to your own systems with webhooks, or run your own programs on the server with script hooks. Both get the same JSON payload.

Where: Server Admin › System › Plugins & hooks

Hooks never hold up the person who did the action: they run in the background, and a hook that fails is written to a log, never shown as an error.

Events

Event When it is raised Its own fields
account.created a hosting account is created (Accounts screen, the API, an order, an import) account, account_id, email, package, domain, reseller
account.suspended an account is suspended (by staff, the API or unpaid invoices) account, account_id
account.unsuspended a suspended account is switched back on account, account_id
account.deleted an account is deleted account, account_id
account.package_changed an account moves to another package account, account_id, package
accounts.bulk_changed a bulk change of accounts has finished (or was cancelled) job_id, kind, status, by, total, changed, unchanged, refused, failed, skipped
account.limit_reached an account has been at a CPU, memory, process or disk limit for 15 minutes, or a process was stopped for lack of memory (once a day per kind) account, account_id, kind (cpu, memory, procs, io or oom), minutes, detail
account.moved an account moved to another server (Move accounts or Server balancing), or an automatic move was rolled back account, account_id, from, to, move_id, automatic, rolled_back
account.password_changed an account's password changes account, account_id, by (self, staff or system)
domain.added a website, subdomain or parked domain is added account, account_id, domain, kind (main, subdomain, alias)
domain.removed a domain is removed account, account_id, domain
email.created a mailbox is created account, account_id, address, domain, quota_mb
email.deleted a mailbox is deleted account, account_id, address, domain
mail.limit_reached an account reaches its emails per hour or per day (once per hour or day) account, account_id, kind (hour or day), window, sent, limit
mail.spike_held an account suddenly sends far more than usual and its sending is put on hold account, account_id, kind (spike), window, sent, limit
database.created a MySQL, PostgreSQL or MongoDB database is created account, account_id, database, engine (mysql, postgresql, mongodb)
database.deleted a database is deleted account, account_id, database, engine
db.dump_finished a database dump is made (by hand, on a schedule or through the API) account, account_id, database, engine (mysql, postgresql), file, size (bytes), source (manual, schedule)
db.optimised a maintenance task (optimise, repair, analyse, check, VACUUM ANALYZE) finished account, account_id, database, engine, operation, table (empty = every table), tables, problems
ssl.issued a certificate is issued or renewed account, account_id, domain, kind (letsencrypt, wildcard, custom, renew, autossl)
backup.finished a full backup of an account is made account, account_id, backup
backup.failed a full backup of an account could not be made account, account_id, error
panelarc.finished the nightly panel backup was made and copied name, size, stores (names of the storages that got it)
panelarc.failed the panel backup failed error, stores (the storages that failed)
backup.target_added a customer adds own backup storage account, account_id, target, kind (s3, sftp)
backup.target_removed a customer removes own backup storage account, account_id, target, kind
backup.remote_uploaded a backup is copied to a customer's own storage account, account_id, target, backup
backup.remote_failed a copy to a customer's own storage failed account, account_id, target, error
backup.remote_restored a customer restores from their own storage account, account_id, target, backup, what
terminal.started a customer opens the web terminal account, account_id, ref, ip, by (a staff or team member, or empty)
terminal.ended a web terminal session ends account, account_id, ref, reason (closed, exit, idle, limit, suspended, signed_out), seconds
ssh.key_added an SSH key is added (also by import or a key pair made on the server) account, account_id, name, type, fingerprint
ssh.key_removed an SSH key is removed account, account_id, name, fingerprint
ssh.access_changed a customer switches SSH and SFTP logins on or off account, account_id, enabled
wordpress.updated a safe WordPress update installed its updates and every check passed account, account_id, site, url, items (kind, slug, from, to), backup, source (auto, manual, api)
wordpress.rollback a safe WordPress update broke a check and the backup was put back account, account_id, site, url, item (kind, slug, from, to), reason, restored, source
app.installed an app of the App installer was installed on a website account, account_id, app, version, url, domain
site.cloned a website was copied (also into another account) account, account_id (the account that got the copy), from, from_account, domain, database, kind
template.created a site template was saved account, account_id, template_id, title, scope (own, host), domain, size
site.from_template a website was made from a site template account, account_id, domain, template_id, template, scope, database
staging.pushed a staging copy was pushed to the live site account, account_id, domain, staging, what (files, db, both), backup
preview.deployed a Git branch's preview website was built account, account_id, repository, branch, url, commit, status (ready, failed)
malware.found a malware scan finds new suspicious files in an account account, account_id, files, paths (up to 50, below web/)
malware.cleaned a file is cleaned in place or moved to quarantine (by a person or the policy) account, account_id, path, signature, action (clean or quarantine), automatic
site.cache_purged a website's page cache is emptied (Speed test, PHP settings or the API) account, account_id, domain, by (panel or api)
site.speed_changed a speed setting of a website changes account, account_id, domain, setting (page_cache, cache_query_strings, compression, browser_cache_days), value
speed.tested a speed test finishes (also the weekly ones) account, account_id, domain, score, ttfb_ms, error (empty when it worked)
site.check_finished a scheduled or "Check now" site check finishes account, account_id, domain, run_id, pages, issues, new, fixed, stopped (pages, time or empty), error (empty when it worked), manual
images.optimised the image optimiser finishes account, account_id, domain, images, changed, saved_bytes, webp
login.succeeded an administrator or reseller signs in user, user_id, role, ip, method (password or passkey)
panel.updated HF Panel was updated to a new version (seen within 5 minutes) from, to
config.restored an administrator put back an earlier version of a server config file (Config history) path, version, version_time, service, by
status.incident an incident or maintenance is posted, updated, edited, started or finished on the status page incident_id, action, kind, title, impact, status, parts, url
chat.started a customer or website visitor starts a live chat conversation, source (panel or website), account and account_id, or visitor
chat.message a message is sent in a live chat conversation, source, account/account_id or visitor, from (customer, visitor or staff), text (first 500 characters)
chat.closed a live chat is closed conversation, source, account/account_id or visitor, by (customer, visitor or staff)
ticket.rated a customer rates a closed support ticket (or changes the rating) ticket, subject, score (1–5), comment, account, account_id, staff, staff_id
chat.rated a customer rates an ended live chat (or changes the rating) conversation, score (1–5), comment, account/account_id (not for a website visitor), staff, staff_id
team.member_invited a customer (or their team member, or staff) invites a team member account, account_id, member_id, email, role, by
team.member_changed a team member's role changes, or they are suspended or let in again account, account_id, member_id, email, role, status, by
team.member_removed a team member is removed account, account_id, member_id, email, by
reseller.domain_changed a reseller saves or removes their own addresses reseller, reseller_id, panel_host, webmail_host, files_host, website_url
reseller.domain_verified one of a reseller's own addresses points to the server and has its certificate reseller, reseller_id, host, kind (panel, webmail, files), dns, ssl
coupon.created a discount code is made code, kind (percent, amount, trial), value, duration, applies
coupon.used a discount code's use is paid (an order, a licence or an invoice made by hand) code, kind (order, licence, invoice), account_id, discount, currency
announcement.sent an announcement goes out (now, or when its time comes) announcement, subject, kind, audience, channels, recipients, by
kb.article_published a help article is published article, title, slug, visibility, owner_id
onboarding.completed a customer finishes the welcome wizard account, account_id, steps
privacy.export_ready a customer's personal data export is ready and emailed account, account_id, request, expires_at
privacy.deletion_requested a customer confirms deleting their account (the waiting time starts) account, account_id, request, due_at, automatic
privacy.deletion_done an account is deleted on the customer's request, after its final backup account, account_id, request, backup, backup_until, by
twostep.policy_changed the two-step sign-in policy of a role changes (also a reseller's for their customers) role, policy (required or optional), grace_days, by, reseller
twostep.reset an administrator or reseller resets a login's two-step sign-in user, user_id, role, by, passkeys
abuse.received a complaint arrives on the abuse desk complaint, source (email, arf, form, api), kind, ip, domain, account, account_id
abuse.status_changed a complaint's status changes complaint, status, from, by
abuse.action a one-click action is run from a complaint complaint, action (suspend_account, pause_mail, suspend_site, lock_mailbox), target, account, account_id, by
ipaddr.assigned a website or account gets an IPv6 address of its own account, account_id, address, domain (empty: every website)
ipaddr.released such an address is taken back account, account_id, address
dnssec.enabled DNSSEC is turned on for a zone domain, account, account_id, ds_published (the panel sent the DS records to the registrar)
dnssec.disabled DNSSEC is turned off for a zone domain, account, account_id
firewall.rules_changed a website's firewall rules are saved (screen or API) account, account_id, domain, allow, block (counts), countries, login_limit, bot, by
cron.failed a monitored cron job ends with an exit code other than 0 (once, until it works again) account, account_id, job_id, schedule, command, exit_code, duration, output (first 2,000 characters)
cron.missed a monitored cron job did not start within its schedule and grace time account, account_id, job_id, schedule, command, expected_at, last_run_at
server.capacity_low disk space, files or memory is expected to run out within a warning step (14, 7, 2 days) resource (e.g. disk:/), label, days_left, step, used, total, unit

The Events tab of the screen shows each event with an example payload.

The payload

Every event is one JSON object. These fields are always there:

Field What it is
id a random id for this event (16 hexadecimal characters)
event the event's name, for example account.created
time when it happened, in UTC (RFC 3339)
server the panel's address, for example panel.yourdomain.com
actor the username of the person who did it (the staff member when they use Log in as), or empty when the panel did it by itself

The event's own fields follow. For example:

json
{
  "id": "6f1c2a9e0b7d4c35",
  "event": "account.created",
  "time": "2026-10-25T08:00:00Z",
  "server": "panel.yourdomain.com",
  "actor": "admin",
  "account": "sarahbakes",
  "account_id": 3,
  "email": "sarah@example.com",
  "package": "Basic",
  "domain": "sarahbakes.co.uk",
  "reseller": ""
}

Note: a payload never holds a password, a secret, a token or a key. HF Panel removes any field with such a name before it sends the event.

Webhooks

A webhook sends the events you choose to an address of yours, as a POST with the payload as its body.

Add a webhook

  1. Open Plugins & hooks and click the Webhooks tab.
  2. Enter the Address (https://), tick the Events it should get (or click All events), and keep On ticked.
  3. Click Add webhook. The message at the top shows the webhook's signing secret once. Keep it in your receiving program.

To see the secret again, click Show secret. To replace it, click New secret: the old one stops working at once. Both are written to the activity log.

What HF Panel sends

http
POST /hostfog HTTP/1.1
Host: hooks.example.com
Content-Type: application/json
User-Agent: HF Panel hooks
X-HF-Event: account.created
X-HF-Delivery: 42
X-HF-Timestamp: 1792915200
X-HF-Signature: sha256=9b2f0c…

{"account":"sarahbakes","account_id":3,"actor":"admin","event":"account.created",…}
Header What it is
X-HF-Event the event's name
X-HF-Delivery the delivery's number in the log (the same number on every retry)
X-HF-Timestamp when this attempt was sent (Unix time)
X-HF-Signature sha256= and the hexadecimal HMAC-SHA256 of the raw body, made with the webhook's signing secret

Check the signature

Work out the HMAC-SHA256 of the body exactly as you received it, with the signing secret as the key, and compare it with the header in constant time. Refuse the request when they differ. To refuse old requests sent again, also check that the payload's time is recent.

php
<?php
$secret = 'whsec_…';
$body = file_get_contents('php://input');
$sig = 'sha256=' . hash_hmac('sha256', $body, $secret);
if (!hash_equals($sig, $_SERVER['HTTP_X_HF_SIGNATURE'] ?? '')) {
    http_response_code(401);
    exit;
}
$event = json_decode($body, true);
// … do your work, then answer quickly with any 2xx status
http_response_code(204);
python
import hashlib, hmac

def valid(secret: str, body: bytes, header: str) -> bool:
    sig = "sha256=" + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(sig, header)

Retries and the delivery log

  • An answer with a 2xx status counts as delivered. Answer within 10 seconds: a slower answer counts as a failure.
  • A network error, a 5xx, 408 or 429 is tried again 3 times: after 10 seconds, 1 minute and 5 minutes. Any other answer (for example 404) fails at once.
  • Redirects are not followed.
  • The Deliveries card shows the last 100 deliveries: when, the event, the address, the status (sent, retry, failed), the attempts and the answer's status code. The body of your answer is never stored.
  • Click Send test to send a hook.test event to one webhook at once (one attempt, no retries).

Addresses you can use

The address must start with https:// and point at a public address. HF Panel refuses private, loopback, link-local and reserved addresses (for example 127.0.0.1, 10.0.0.0/8, 192.168.0.0/16, 169.254.169.254, ::1) and names such as localhost, both when you save the address and each time it connects, so a name that points at such an address later is refused too. An address with a user name or password in it is refused. You can have up to 20 webhooks.

Script hooks

A script hook is a program you put on the server as root. It runs as root for each event, with the payload on its standard input.

Add a script hook

  1. Make a folder for the event, owned by root: install -d -m 755 /etc/hostfog/hooks/account.created.d

  2. Put an executable file in it, owned by root, that only root can change:

    bash
    cat >/etc/hostfog/hooks/account.created.d/10-welcome.sh <<'SH'
    #!/bin/sh
    payload="$(cat)"
    echo "$payload" >>/var/log/new-accounts.json
    SH
    chmod 755 /etc/hostfog/hooks/account.created.d/10-welcome.sh
    
  3. Open the Script hooks tab to check that its status is Runs.

How script hooks run

  • The files of a folder run one after the other, in name order (start their names with 10-, 20- … to choose).
  • Each file gets the payload on its standard input and in the file named by $HF_PAYLOAD_FILE, and the event's name in $HF_EVENT. Nothing else from the panel's environment is passed on.
  • A file may run for 30 seconds; then it is stopped. Its exit status and what it prints (up to 64 KB) are written to /var/log/hostfog-hooks.log. The end of that log is shown on the Script hooks tab.
  • Script hooks run on the main HF Panel server, also for accounts on other servers of a cluster.

A file is skipped, and the tab says why, when it:

Status Why
Not owned by root its owner is not root
Others can change it its group or other users can write it
Not executable it has no execute permission
A symlink: skipped it is a symbolic link
Name not allowed its name has characters other than letters, digits, ., _ and -, or it is a backup copy (name~, name.dpkg-old, name.bak)
Folder not safe the event's folder, or /etc/hostfog/hooks, is not owned by root or others can write it

Warning: a script hook runs as root. Never let anyone else write to /etc/hostfog/hooks, and treat the payload's text as data: quote it, and never pass it to a shell as a command.