Webhooks
The outbound webhook delivery contract: request shape, headers, signature, retry schedule, delivery states and the management endpoints. Business plan and above.
Fermium delivers workspace audit events to URLs you register. Business plan and above; managed by workspace owners and admins. The user-facing guide is Webhooks; this page is the contract.
Management endpoints
Authenticated with the app session like the rest of /api. See the REST reference for curl examples.
| Method and path | Body | Returns |
|---|---|---|
GET /api/webhooks |
{webhooks: [webhook]} |
|
POST /api/webhooks |
{url, events?} |
201, the webhook with secret (only here) |
PATCH /api/webhooks/{id} |
{url?, events?, active?} |
the webhook |
DELETE /api/webhooks/{id} |
{deleted: true, id} |
|
POST /api/webhooks/{id}/test |
{queued: true, delivery_id, event_id} |
|
GET /api/webhooks/{id}/deliveries?limit=50 |
{deliveries: [delivery]}, newest first, at most 200 |
A webhook:
{ "id": 4, "url": "https://example.com/hooks/fermium", "events": ["doc.*", "member.join"], "active": true, "created_at": "2026-09-15T10:00:00", "secret_set": true }
Validation: url must be http(s) with a hostname; the hostname is resolved at save time and every resulting address must be public (private, loopback, link-local, reserved, multicast and unspecified ranges are refused with a 400 naming the address). events is a list of up to 50 strings: exact action names, <family>.* prefixes, or *; an empty list means *.
Event matching
Every event written to the workspace audit log is offered to each active webhook. It is delivered when the webhook's events contains *, the exact action name, or a prefix whose family matches (doc.* matches doc.publish). Action names are listed in Audit log. A test delivery bypasses the filter.
The request
POST {url} HTTP/1.1
Content-Type: application/json
User-Agent: Fermium-Webhooks/1
X-Fermium-Event: {action}
X-Fermium-Delivery: {delivery id}
X-Fermium-Signature: t={unix seconds},v1={hex}
Body: the audit event, serialized as compact JSON with keys sorted:
{"action":"doc.publish","actor":"ada@acme.com","created_at":"2026-09-15T14:02:11.482910+00:00","id":9127,"meta":{"folder":"Customers/Acme","title":"Q3 proposal","visibility":"workspace"},"target_id":"01J7ZK3N9QW0X4B2C6D8E0F1G2","target_type":"doc","workspace_id":7}
| Field | Meaning |
|---|---|
id |
the audit event id; the same on every retry of the same event. Deduplicate on it |
action |
the event name |
target_type, target_id |
what it was about: doc and a document id, user and a user id, workspace, group, invite, webhook, folder, scim_token |
actor |
an email, system, or scim:<workspace-slug> |
workspace_id |
your workspace's numeric id |
created_at |
ISO-8601, UTC |
meta |
action-specific details, the same object the audit log shows |
Redirects are not followed; a 3xx is a failure. The request times out after 10 seconds.
Signature
v1 is the lowercase hex HMAC-SHA256 of the string <t>.<body>, keyed with the webhook's secret, where <t> is the timestamp from the same header and <body> is the raw request body. Verify over the exact bytes you received, compare in constant time, and reject timestamps outside a window you choose (300 seconds is a reasonable default). Verification snippets in Node and Python are on the Webhooks guide.
The secret is generated by Fermium (whsec_ followed by random characters), returned once at creation, and cannot be retrieved later. Rotate by creating a new webhook and deleting the old one.
Response handling and retries
| Your response | Result |
|---|---|
| any 2xx | delivered; status: ok, delivered_at set |
| anything else, redirect, timeout, connection error | failure; retried |
Retry schedule after a failure: 1 minute, 5 minutes, 30 minutes, 2 hours, 12 hours. Six attempts in all. After the sixth failure the delivery is failed and is not retried. Each attempt records last_status_code (or null for a connection error) and, for connection errors, last_error.
A delivery:
{
"id": 41, "webhook_id": 4, "event_id": 9127, "action": "doc.publish",
"status": "pending", "attempts": 2, "last_status_code": 503, "last_error": null,
"next_attempt_at": "2026-09-15T14:08:11+00:00", "created_at": "2026-09-15T14:02:11", "delivered_at": null
}
Deliveries are independent: a failing event does not hold back later ones, and events may arrive out of order. Deleting a webhook deletes its deliveries; pausing it with active: false stops new deliveries from being created, but pending retries of existing deliveries still run.
Testing
POST /api/webhooks/{id}/test writes a webhook.test audit event (meta: {url, test: true}) and queues one delivery to this webhook regardless of its filter. Read the result from the deliveries list.