airbox.fyi

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

TypeFires when
message.receivedInbound mail is accepted, before routing
message.completedA route finishes and the reply is sent
message.failedRouting fails or is rejected
message.skippedIntentionally 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:

HeaderValue
Airbox-Event-IdStable evt_… id — use it to dedupe (deliveries retry)
Airbox-Event-Typee.g. message.completed
Airbox-Signaturet=<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 2xx to 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-Id is 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)

MethodPathDescription
POST/v1/webhooksCreate an endpoint — returns the signing secret once
GET/v1/webhooksList endpoints
PATCH/v1/webhooks/{id}Update url / event_types / enabled
DELETE/v1/webhooks/{id}Delete an endpoint
POST/v1/webhooks/{id}/testSend a synthetic test event
GET/v1/webhooks/{id}/secretReveal the signing secret
GET/v1/webhooks/{id}/deliveriesRecent delivery log
POST/v1/webhooks/{id}/deliveries/{delivery_id}/retryReplay 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"]}'