Skip to content
BurnerByte

concepts

Webhooks

Event-driven notifications with HMAC signing and retry logic.

Creating a Webhook

bash
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

EventTrigger
email.receivedNew email delivered to an inbox
inbox.createdNew inbox created
inbox.expiredInbox 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:

text
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:

AttemptDelay before it
1Immediate
25 seconds
325 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:

text
GET /api/v1/orgs/{orgId}/teams/{teamId}/webhooks/{webhookId}/stats