Webhooks & signing

All four primitives talk back to you the same way: an HTTP POST to a webhook_url you control. This page is the contract for that delivery — the envelope shape, the signature scheme, and the retry behaviour — shared by schedules, deadlines, approvals, and watches alike. For the how-to of building and testing a receiver, see Receiving webhooks; this page owns the wire format it verifies against.

The delivery envelope

Every webhook, from every primitive, has the same top-level JSON shape:

{
  "event_id": "evt_01HKQ...",
  "event_type": "approval.approved",
  "team_id": "team_...",
  "occurred_at": "2026-05-11T17:00:00.123Z",
  "data": { ... }
}
  • event_id — a stable id for the logical event. It does not change across retries; that stability is what makes safe de-duplication possible.
  • event_type — a dotted verb from the same namespace as the tools (schedule.fired, deadline.overdue, approval.approved, watch.changed), plus explicit failure events on retry exhaustion (schedule.delivery_failed, and so on).
  • team_id — the Team the event belongs to.
  • occurred_at — when the event happened, ISO-8601 UTC.
  • data — the per-event payload. Its shape is documented in each primitive's guide (see the payload section of Schedules, Deadlines, Approvals, and Watches).

Delivery is at-least-once. If the network drops between us delivering and your endpoint acknowledging, you will see the same logical event more than once. The one hard requirement this places on every integration: your handler must be idempotent on event_id — record the ids you've processed and drop repeats. See Idempotency for the mirror-image concern on the request side.

Verifying signatures

Every delivery is signed Stripe-style so you can prove it came from AIGears and was not replayed. The signature travels in a header:

X-AIGears-Signature: t=<unix_ts>,v1=<hex_hmac>

To verify:

  1. Read t (a Unix timestamp) and v1 (a hex digest) from the header.
  2. Compute HMAC-SHA256 over the exact string <unix_ts>.<raw_request_body> — the timestamp, a literal dot, then the raw request body before any JSON parsing (re-serialising changes the bytes and breaks the signature).
  3. The secret is your Team's webhook signing secret — separate from your API keys, and rotatable from the Dashboard (the old secret stays valid for a 24-hour grace window after rotation).
  4. Compare your digest to v1 with a constant-time comparison, and reject the request if t is outside a 5-minute skew window (replay protection).

Don't hand-roll this from the prose — the scheme is exactly Stripe's, and there are runnable, copy-pasteable verifiers in the repo:

Retries, backoff, and the dead-letter queue

A delivery is retried on a 5xx response or a timeout (no response within 15s). It is not retried on a 4xx — a 4xx means your code is rejecting the payload, and retrying would only mask the bug. Backoff runs 1m, 5m, 30m, 2h, 6h, 24h — six attempts over roughly 32 hours — after which the delivery is dead-lettered.

The Dashboard's Activity feed surfaces dead letters prominently, with the full attempt log (attempt count, last status code, last response body) and a manual "retry now" on paid tiers. Return a 2xx quickly and do slow work asynchronously, so a slow handler doesn't burn your retry budget.

Destination rules

A webhook_url must be a public HTTPS endpoint. An SSRF guard rejects localhost, private, and link-local addresses at registration and at delivery time, so AIGears can never be aimed at internal infrastructure. In development, where your receiver is on localhost, put a tunnel (an ngrok-style public URL) in front of it. There is no Team-level default webhook_url — every request carries its own, so different agents can post to different endpoints and a leaked key can't silently repoint deliveries.

Testing your receiver

Before you go live, use the Dashboard's Webhook Tester to fire a synthetic event at your endpoint. A test delivery is signed and rendered identically to production — your handler cannot tell it apart, which is the whole point — but it is one-shot (no retries, no dead-letter), is never counted against your usage, and is kept out of the Activity feed by default. See Receiving webhooks for the end-to-end setup.