Webhooks
Back to the guide
A webhook is a message Deeluge sends to your server the moment something happens — a record is created, a deal moves stage, someone fills in a form — so you do not have to keep asking the API whether anything changed.
Two ways to set one up
- In the app: an owner or admin adds one under Settings → Webhooks → Outgoing. See Webhooks in Settings.
- Through the API: an integration subscribes itself with its API key. The key
needs
webhooks:write, and the read scope of every event it asks for.
POST /api/v1/webhooks
Authorization: Bearer sf_live_…
Content-Type: application/json
{ "name": "Sync to warehouse", "url": "https://example.com/deeluge",
"events": ["record.created", "record.updated"], "objects": ["accounts"] }
The answer carries the webhook's signing secret (whsec_…) once. A key sees and
changes only the webhooks it made.
Events
GET /api/v1/event-types lists them all with the scope each needs.
| Event | When |
|---|---|
record.created |
A record is created — in the app, by import, through the API, by a form or an incoming webhook |
record.updated |
A record's values change; changed_attributes says which |
record.deleted |
A record is deleted |
list_entry.created, list_entry.deleted |
A record is added to, or taken off, a list |
note.created |
A note, call or meeting is logged |
task.created, task.updated |
A task is created, or changes |
form.submitted |
Someone submits one of the firm's forms |
Subscribe to a family with record.*, or to everything with *.
What arrives
A POST with a JSON body:
{
"id": "evt_6c1a…",
"type": "record.updated",
"version": 1,
"created_at": "2026-10-26T15:04:05.000Z",
"workspace_id": "…",
"actor": { "type": "user", "id": "…" },
"data": {
"record": { "id": "…", "object": "opportunities", "title": "Acme buyout", "values": { "stage": "…" } },
"changed_attributes": ["stage"]
}
}
data.record has exactly the shape GET /api/v1/records/{id} returns, as it was when
the message was built. A webhook carries only what its identity may see: restricted
records are left out until someone grants that webhook (or that key) access.
Checking it came from us
Every message carries three headers, following the Standard Webhooks specification:
| Header | |
|---|---|
webhook-id |
The message id. The same on every retry — use it to ignore duplicates. |
webhook-timestamp |
Unix seconds. Refuse anything more than five minutes old. |
webhook-signature |
v1, then the base64 HMAC-SHA256 of id.timestamp.body, keyed with the secret's bytes after whsec_. |
Any Standard Webhooks library does all of this:
import { Webhook } from 'standardwebhooks';
const wh = new Webhook(process.env.DEELUGE_WEBHOOK_SECRET);
const event = wh.verify(rawBody, headers); // throws if anything is wrong
Verify against the raw body, before any JSON parsing.
Answering
Answer with any 2xx within ten seconds. Do slow work afterwards, from a queue.
Anything else is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours,
12 hours and 24 hours — eight attempts over about two days. Answering 410 Gone
turns the webhook off straight away. A webhook that has failed fifty times in a row
and not succeeded for three days is turned off too; turn it back on from Settings or
with PATCH /api/v1/webhooks/{id} and {"status": "active"}.
Delivery is at least once, and messages can arrive out of order. Use webhook-id
to de-duplicate and created_at to order.
Changes you made yourself
A key's webhooks are not sent events caused by requests made with that same key.
An integration that writes back to the CRM therefore does not trigger itself in a loop.
actor says who made every other change.
Inspecting and replaying
GET /api/v1/webhooks/{id}/deliveries?status=failed— the last deliveries and what the receiver answered.POST /api/v1/webhooks/{id}/deliveries/{deliveryId}/retry— send one again, with the same body andwebhook-id.POST /api/v1/webhooks/{id}/test— send awebhook.testevent now.POST /api/v1/webhooks/{id}/secret— roll the signing secret.
Deliveries are kept for 30 days.
Receiving: incoming webhooks
The other direction — another tool creating records in Deeluge by posting to a URL — is set up in Settings → Webhooks → Incoming. See Webhooks in Settings.