Webhooks
Get a signed HTTP POST when something happens to your mail
Build on Airbox without polling
Register an HTTPS endpoint and Airbox will POST a signed event to it as your mail moves through the pipeline — no need to poll GET /messages. Manage endpoints in Dashboard → Settings → Webhooks or via the API below.
Event types
| Type | Fires when |
|---|---|
| message.received | Inbound mail is accepted, before routing |
| message.completed | A route finishes and the reply is sent |
| message.failed | Routing fails or is rejected |
| message.skipped | Intentionally not routed — dropped by a rule, or over the plan/trial limit |
Payload
Every delivery is a JSON envelope. data.object is the same message shape returned by the /v1/messages API.
{
"id": "evt_3f2a…",
"type": "message.completed",
"created": 1718971200,
"livemode": true,
"data": {
"object": {
"message_id": "…",
"conversation_id": "…",
"source": "email",
"status": "done",
"result": "…AI output…",
"error": null,
"usage": { "input_tokens": 812, "output_tokens": 143 },
"subject": "Re: Q3 numbers",
"created": 1718971180
}
}
}Each POST also carries these headers:
| Header | Value |
|---|---|
| Airbox-Event-Id | Stable evt_… id — use it to dedupe (deliveries retry) |
| Airbox-Event-Type | e.g. message.completed |
| Airbox-Signature | t=<unix>,v1=<hmac-sha256> |
Verifying the signature
Compute an HMAC-SHA256 over {timestamp}.{raw_body} keyed on your endpoint's signing secret (whsec_…), and compare it to the v1 value. Reject requests whose timestamp is too old to block replays.
import hmac, hashlib, time
def verify(secret: str, signature: str, body: bytes, tolerance=300) -> bool:
parts = dict(p.split("=", 1) for p in signature.split(","))
t, v1 = parts["t"], parts["v1"]
if abs(time.time() - int(t)) > tolerance:
return False # stale — likely a replay
expected = hmac.new(
secret.encode(), f"{t}.".encode() + body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, v1)Always verify against the raw request body, before any JSON parsing or re-serialization.
Delivery & retries
- Return any
2xxto acknowledge. Respond quickly — do work asynchronously. - Non-2xx, a timeout, or a connection error is retried with exponential backoff (up to 6 attempts).
- The
Airbox-Event-Idis stable across retries, so make your handler idempotent. - After several consecutive fully-failed deliveries, an endpoint is auto-disabled (a success or manual re-enable resets it). You'll see this in the dashboard.
- Endpoints must be
https://on a public host.
Test events
From Dashboard → Settings → Webhooks, “Send test” (or POST /v1/webhooks/{id}/test) delivers a synthetic message.completed with livemode: false — verify reachability and your signature check before real mail depends on the endpoint.
Managing endpoints (API)
| Method | Path | Description |
|---|---|---|
| POST | /v1/webhooks | Create an endpoint — returns the signing secret once |
| GET | /v1/webhooks | List endpoints |
| PATCH | /v1/webhooks/{id} | Update url / event_types / enabled |
| DELETE | /v1/webhooks/{id} | Delete an endpoint |
| POST | /v1/webhooks/{id}/test | Send a synthetic test event |
| GET | /v1/webhooks/{id}/secret | Reveal the signing secret |
| GET | /v1/webhooks/{id}/deliveries | Recent delivery log |
| POST | /v1/webhooks/{id}/deliveries/{delivery_id}/retry | Replay a failed delivery |
curl -s https://api.airbox.fyi/v1/webhooks \
-H "X-API-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://your-app.com/webhooks/airbox",
"event_types": ["message.completed", "message.failed"]}'