A webhook is a POST request the API sends to the registered endpoint when a subscribed event occurs. Delivery is asynchronous: the operation that originates the event does not wait for the receiver to respond.
Each endpoint is registered with POST /v1/webhooks, which returns the signing secret. A subscription accepts between 1 and 10 events per endpoint.
Delivery signing
The body of every delivery is signed with HMAC-SHA256 using the endpoint's secret. The signature travels in hexadecimal, prefixed by the algorithm:
X-{Brand}-Signature: sha256=<hex>Verification consists of computing the HMAC-SHA256 of the body exactly as received, without re-serialising the JSON, and comparing it against the header value. A constant-time comparison avoids leaking information through response timing.
Delivery headers
| header | content |
|---|---|
X-{Brand}-Signature | sha256= followed by the HMAC in hexadecimal |
X-{Brand}-Event | Event type that originated the delivery, for example validation.completed |
X-{Brand}-Delivery-Id | Delivery identifier, useful for deduplicating retries |
X-{Brand}-Timestamp | Send time, in ISO 8601 with a Z suffix |
Content-Type | application/json; charset=utf-8 |
The Delivery-Id stays stable across retries of the same delivery: it is the value that lets the receiver discard duplicates.
Timeouts and retries
The connection is allowed 5 seconds and the complete request 10 seconds. Past that margin the delivery counts as failed.
A failed delivery is retried up to 5 times, with growing waits:
| attempt | wait since the previous one |
|---|---|
| 1st retry | 1 minute |
| 2nd retry | 5 minutes |
| 3rd retry | 30 minutes |
| 4th retry | 2 hours |
| 5th retry | 6 hours |
Not every response is retried. The distinction is whether the failure can resolve itself over time:
| receiver response | treatment |
|---|---|
2xx | Successful delivery |
5xx, network error or timeout | Retried |
408 and 429 | Retried: they signal temporary saturation |
Any other 4xx | Not retried. A 400 or a 404 signals a misconfigured receiver, and repeating the request does not fix it |
The receiver's response body is kept truncated to 2,048 bytes for the delivery history, available at GET /v1/webhooks/{id}/deliveries.
Outgoing deliveries are capped at 10 per domain per minute: a receiver with many simultaneous events gets them staggered rather than in a burst.
Automatic deactivation
The endpoint keeps a consecutive-failure counter. A successful delivery resets it to zero.
On reaching 3 consecutive failures, the endpoint moves to auto_disabled state and stops receiving deliveries. The state is available at GET /v1/webhooks alongside the consecutive_failures counter.
Reactivation is manual, via PUT /v1/webhooks/{id} setting the state to active. The auto_disabled value cannot be assigned through the API — only the system sets it.
Delivery states
| state | meaning |
|---|---|
pending | Queued, not yet attempted |
retrying | Failed with retries still pending |
success | The receiver responded 2xx |
failed | Exhausted its retries or received a non-retryable 4xx |
Receiver recommendations
- Respond
2xxas soon as the body is received and validated, and process it afterwards. A receiver that processes before responding eats into the 10-second margin and triggers unnecessary retries. - Deduplicate by
X-{Brand}-Delivery-Id: a retry repeats the same delivery. - Verify the signature before reading the content.