concepts
Webhooks
Event-driven notifications with HMAC signing and retry logic.
Creating a Webhook
POST /api/v1/orgs/{orgId}/teams/{teamId}/webhooks{ "url": "https://example.com/webhook", "events": ["email.received", "inbox.created", "inbox.expired"]}The response includes a secret — the HMAC-SHA256 signing key. Shown only once.
Events
| Event | Trigger |
|---|---|
email.received | New email delivered to an inbox |
inbox.created | New inbox created |
inbox.expired | Inbox TTL expired |
Note: inbox.expired is emitted by the cleanup worker as it removes a lapsed inbox, so it fires on the first cleanup cycle after the TTL elapses rather than at the exact expiry instant.
HMAC-SHA256 Signing
Every delivery includes these headers:
X-BurnerByte-Signature: <hex-encoded-hmac>X-BurnerByte-Event: <event-name>X-BurnerByte-Delivery-ID: <delivery-uuid>Retry Logic
Failed deliveries (non-2xx or timeout) are retried with exponential backoff:
| Attempt | Delay before it |
|---|---|
| 1 | Immediate |
| 2 | 5 seconds |
| 3 | 25 seconds |
The attempt count comes from defaults.webhook_max_retries (default 3). Each
attempt is bounded by defaults.webhook_timeout (default 10s).
After all attempts fail, the webhook's failure_count is incremented. That counter is bookkeeping only — it does not disable the webhook, and every subsequent event is still dispatched. The webhook_retry background worker clears the counter for active webhooks sitting at 1 or 2 failures.
Delivery Logs
Every attempt is logged with HTTP status, response time, success/failure, attempt number, and timestamp. View via the UI or GET /api/v1/orgs/{orgId}/teams/{teamId}/webhooks/{webhookId}/deliveries.
SSRF Protection
Validating the URL at create time is not enough on its own, because DNS can be
rebound between the check and the request. The dispatcher re-resolves the target
host at dial time and refuses the connection if any resolved address is
private (10/8, 172.16/12, 192.168/16), loopback (127/8, ::1),
link-local (169.254/16), unique-local (fc00::/7) or unspecified. It then
dials the resolved IP directly, so the address that was checked is the address
connected to. A refused dial is logged as a failed delivery like any other error.
Create and update apply the same rules up front: http/https only, no
localhost, 127.0.0.1, ::1, 0.0.0.0, no .internal/.local hostname, and
169.254.169.254 is refused by name.
Aggregate delivery health for one webhook — totals, success and failure counts, average response time — is available at:
GET /api/v1/orgs/{orgId}/teams/{teamId}/webhooks/{webhookId}/stats