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— theTeamthe 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:
- Read
t(a Unix timestamp) andv1(a hex digest) from the header. - Compute
HMAC-SHA256over 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). - 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). - Compare your digest to
v1with a constant-time comparison, and reject the request iftis 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.