Skip to content
Fermium

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.