Webhooks
A webhook is an HTTPS request the server sends to a URL of yours when something happens: an account is created or suspended, a backup fails, a certificate is refused, an admin user signs in. The server's endpoints get the events about the server itself and the events about every account, so one receiver sees the whole box. Typical receivers are a billing system, a SIEM, and an on-call pager or a chat channel behind a relay.
The server's endpoints are on Integrations › Webhooks in the admin app,
and in elpanctl as webhook:endpoint:* with --server. Account holders
set up their own in the account panel; the account docs'
Webhooks
page describes those, and what is the same for both: the request format, the
retry schedule, the filters, and rotation.
Every request is a Standard Webhooks POST signed with the endpoint's secret, and nothing else: a system that wants another format gets it from a relay you run. Receive a webhook shows the request, the code that verifies it, and relays to chat tools; it applies to the server's endpoints word for word. The server receives no webhooks; a system that wants to tell it something calls the Server API.
Add an endpoint
elpanctl webhook:endpoint:create --server <name> <url> [--event=<type>]... [--secret-header="<Name>: <value>"] [--allow-private-network] [--json]
or Create Endpoint on the Webhooks page, which asks for the same:
| Setting | What it does |
|---|---|
| Name | Tells you which receiver this is. |
| URL | The address that gets the requests. |
| Event types | The filter: types (backup.failed), families (backup.*) or *. --event defaults to *. |
| Secret header | Optional. One header with a secret value sent on every request, e.g. Authorization: Bearer …, for a receiver that checks a token rather than the signature. |
| Allow a private network address | Lets the endpoint post to an internal system, such as an http://10.x chat bridge. Off by default. |
The command prints the endpoint's signing secret, whsec_…; the page shows
it once after the endpoint is made. Give it to the receiver.
The server's endpoints differ from an account's in two ways, because they are your own:
- The URL may be plain
http, and with Allow a private network address it may point into a private network. An account's must behttpsand public. The server's own addresses are refused either way: the panel does not post to itself. - There is no rate budget and no backlog cap. The server's deliveries are
claimed before any account's, so a flood from one account does not delay
your
account.suspended. The one limit is 50 endpoints.
Many receivers put a credential in the URL, so the server stores and shows
it like one: masked after saving, as https://hooks.example.net/…, and kept
out of the logs. To change it, type the whole new one. The server allows one
endpoint per URL.
Reading a secret again on the page asks you to confirm it's you and writes a row to the Auth Log. The API and the CLI show a secret when they create or rotate it, never again.
Event types
The Webhooks page lists the types the server's endpoints can get, as does
GET /api/v1/event-types, each with its family and a description. The
server's endpoints get these:
| Event types | When |
|---|---|
account.created, account.updated, account.suspended, account.unsuspended, account.terminated, account.restored, account.removed | An account's life cycle. account.updated is a change to its system-level settings (its limits, locale, time zone, IP). |
account.purge.succeeded, account.purge.failed | A deletion finished, or stopped at a step. One per attempt. |
apikey.created, apikey.revoked | A server API key, or an account's key, was created or revoked. |
domain.created, domain.removed | A hostname was added to or removed from an account. |
certificate.issued, certificate.refused, certificate.expiring | A CA-signed certificate was put in use; the server can't get one for a hostname until something changes; a certificate in use has 30 days left, then 7, and nothing renews it. |
dns.publication_failed | A name server refused part of a zone, so the internet sees different records from the panel. |
quota.threshold_crossed | An account's storage reached 80 %, 95 % or 100 % of its quota. |
backup.succeeded, backup.failed, restore.succeeded, restore.failed | A backup or a restore finished, failed, or was refused before it started. |
webapp.action.succeeded, webapp.action.failed | A job on an installed web application finished or failed: an install, an upgrade, a repair. |
webapp.upgrade_run.finished | A mass upgrade started with webapp:upgrade finished (Upgrade Runs). |
webapp.vulnerable | A security advisory affects an installed web application. |
security.signed_in, security.sign_in_denied, security.password_changed, security.password_breached, security.totp_enabled, security.totp_disabled, security.passkey_added, security.passkey_removed, security.device_added | An admin user's sign-in and credential changes. |
sign_in_help.requested, sign_in_help.resolved | An account asked you for help to sign in, or the request was answered (Sign-in Help Requests). |
webhook.test | The test event. No filter matches it. |
An event about an account goes to that account's endpoints and to the
server's. An event about the server or an admin user goes to the server's
only. The exceptions: an account's own security.* events go to the account
only (you have the auth log and syslog), and a domain user's request for
sign-in help goes to the account that owns the mailbox. toolchain.task.*
events are the account's only.
A server endpoint's copy of an event links to the admin app and the server
API, and its data.summary (a title, a sentence or two, a severity, a link
to the admin app's page) is written in the server's default locale, so a
relay that posts to a chat channel needs nothing else.
Delivery
The receiver must answer within 10 seconds. A 2xx is delivered; a 410 Gone fails the delivery and disables the endpoint at once; 429, 502,
503 and 504 are tried again, obeying Retry-After up to one hour; a
redirect is a failure, because redirects are not followed; anything else is
tried again. The retries come after 1 minute, 5 minutes, 30 minutes, 2
hours, 5 hours, 10 hours and 10 hours: eight attempts in about 28 hours,
then the delivery fails.
With no success for five days and ten failed deliveries, the endpoint is
disabled, and one message goes to the server's root mailbox
(System mail). Once the receiver is fixed, enable
the endpoint again; the form offers to redeliver what failed while it was
disabled.
The webhook-id header is the same on every attempt and redelivery. Events
can arrive twice and out of order, so a receiver uses it to ignore a second
copy.
Pause, resume, enable, delete
elpanctl webhook:endpoint:<action> --server <id>
| Action | Effect |
|---|---|
pause | Nothing is queued for the endpoint while it is paused. |
resume | Deliver to a paused endpoint again. |
enable | Enable a disabled (or paused) endpoint again, with a clean health record. |
delete | What was still waiting for it is dropped; what was delivered stays in the log for 30 days. |
webhook:endpoint:list --server [--format=tsv|jsonl] gives the ids.
Test, redeliver, rotate
elpanctl webhook:endpoint:test --server <id> [--queue [--count=<n>]]
elpanctl webhook:endpoint:redeliver --server <id> --delivery=<delivery id>
elpanctl webhook:endpoint:redeliver --server <id> --since="<time>"
elpanctl webhook:endpoint:rotate --server <id>
- Test sends a
webhook.testevent at once and shows the attempt, so you see the receiver's answer. With--queueit goes through the delivery engine instead, like a real event;--countsends several. At most 5 tests a minute per endpoint. - Redeliver sends one delivery again now, or queues every failed or
abandoned delivery whose event occurred since a time (
"2026-10-29 10:00","-6 hours"): abandoned means dropped while the endpoint was disabled. A bulk redelivery is allowed once per 10 minutes per endpoint. - Rotate gives the endpoint a new signing secret, and the old one keeps signing beside it for 24 hours, so the receiver can switch without a gap.
The endpoint's page has the same actions, and its delivery log: each delivery with its attempts, status codes, the headers that are safe to keep, and up to 1 KiB of the answer. The log is kept for 30 days.
API
The Server API has every operation of the page, under
the same paths as the account API but in plain JSON. Reading needs
all:readonly, changing all:readwrite.
| Method and path | What |
|---|---|
GET, POST /api/v1/webhook-endpoints | List the endpoints, oldest first; create one. The signing secret is in the POST response only. |
GET, PATCH, DELETE /api/v1/webhook-endpoints/{id} | One endpoint. PATCH changes the fields it carries; status: paused pauses, status: enabled resumes or enables again; secretHeader: null removes the header. |
POST /api/v1/webhook-endpoints/{id}/tests | Send a test event. |
POST /api/v1/webhook-endpoints/{id}/secret-rotations | Rotate the signing secret. |
GET /api/v1/webhook-endpoints/{id}/deliveries | The delivery log, newest first, 50 at a time: pass nextBefore back as before. |
POST /api/v1/webhook-endpoints/{id}/retries | Redeliver the failed deliveries since a time. |
GET /api/v1/webhook-deliveries/{id}, POST …/{id}/retries | One delivery with its attempts; send it again. |
GET /api/v1/event-types | The catalogue, with each type's family and audiences. |