Outbound webhooks.
Don't poll the audit log — let AgentDraft push. Subscribe a URL to events like booking.committed or hold.invalidated and we POST a signed JSON envelope the moment they happen. Deliveries are persisted before the first attempt, HMAC-signed, and retried with exponential backoff, so a brief outage on your side never drops an event.
Updated
The audit log records every state change AgentDraft makes, but reading it means polling. Webhooks invert that: you register an HTTPS endpoint once, and AgentDraft calls it whenever a subscribed event fires. Same events, pushed to you in near real time — useful for syncing a CRM, notifying a human, kicking off a downstream agent, or reacting to an inbound email the instant it lands.
Subscriptions are managed from your dashboard session (not an agent key) and scoped to your workspace. Create one with the event type and the URL to call. The endpoint URL must be public HTTPS — private and loopback addresses are rejected.
curl -X POST https://api.agentdraft.io/v1/dashboard/webhooks \
-H "Content-Type: application/json" \
--cookie "ad_session=..." \
-d '{
"event_type": "booking.committed",
"url": "https://your-app.example.com/hooks/agentdraft"
}'- GET /v1/dashboard/webhooks — list your subscriptions.
- DELETE /v1/dashboard/webhooks/{sub_id} — remove one.
- GET /v1/dashboard/webhooks/events — the canonical, machine-readable catalog of subscribable events.
- GET /v1/dashboard/webhooks/{sub_id}/deliveries — recent delivery attempts with status, attempt count, and the last error.
| Event | When it fires |
|---|---|
| booking.committed | An agent hard-commits a booking. The payload carries booking_id, agent_id, user_id, start, and end. |
| booking.cancelled | A booking is cancelled and its slot is freed. Carries the booking_id. |
| booking.evicted | A committed booking is bumped by a higher-priority agent (or invalidated by an external calendar event). Includes the reason and the evicting agent's identity. |
| booking.counter_proposed | An agent counter-proposes alternative times against a winning booking. Carries the counter_id and the proposed slots. |
| booking.counter_accepted | A counter-proposal is accepted and re-committed onto the new slot. Carries the new_booking_id and accepted slot. |
| booking.counter_rejected | A counter-proposal is rejected. Carries the booking_id and counter_id. |
| hold.invalidated | An external calendar event invalidates an active hold (or a still-bumpable commit). Carries the booking and the triggering external event. |
| mailbox.inbound | An agent mailbox receives an inbound email. Carries sender, subject, SPF/DKIM/DMARC verdicts, and any matched booking_id — the body is fetched separately. |
GET /v1/dashboard/webhooks/events is the source of truth — query it for the live catalog rather than hard-coding this list.
Every event is delivered as the same envelope: a unique id, the type, an ISO-8601 created timestamp, and the event-specific data. The id is stable across retries — use it to deduplicate, since delivery is at-least-once.
{
"id": "evt_01JABC...",
"type": "booking.committed",
"created": "2026-06-08T02:00:00+00:00",
"data": {
"booking_id": "bkg_01JABC...",
"agent_id": "agt_01JABC...",
"user_id": "usr_01JABC...",
"start": "2026-06-15T22:00:00+00:00",
"end": "2026-06-15T22:30:00+00:00"
}
}Each request also carries these headers:
| Header | Value |
|---|---|
| X-AgentDraft-Signature | t=<unix-seconds>,v1=<hex> — HMAC-SHA256 over "<t>." + the raw request body. |
| X-AgentDraft-Event | The event type, e.g. booking.committed — mirrors the envelope type for cheap routing. |
| User-Agent | AgentDraft-Webhooks/0.1 |
Every delivery is signed so you can confirm it came from AgentDraft and wasn't replayed. The X-AgentDraft-Signature header is t=<timestamp>,v1=<hmac>, where the HMAC is SHA-256 over the string "<t>." followed by the raw request body, keyed with your workspace's webhook signing secret. Compute it over the bytes as received — parsing and re-serializing the JSON will change them and break the check. Reject any delivery whose timestamp is more than five minutes from now.
import hmac, hashlib, time
def verify(secret: str, raw_body: bytes, header: str, tolerance: int = 300) -> bool:
# header looks like: t=1718000000,v1=9f86d0818...
parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
ts, sig = parts.get("t"), parts.get("v1")
if not ts or not sig:
return False
# reject deliveries outside a 5-minute window (replay protection)
if abs(int(time.time()) - int(ts)) > tolerance:
return False
signed = f"{ts}.".encode() + raw_body # "<timestamp>." + raw body bytes
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(sig, expected)Getting your signing secret. The signing secret is provisioned at the workspace level. A self-serve view of it in the dashboard is rolling out — until then, reach out and we'll provide it so you can turn on verification.
AgentDraft writes each pending delivery to durable storage before the first network call, so an event is never lost if the sending process restarts. Delivery semantics:
- At-least-once. Retries can re-send an event; deduplicate on the envelope id.
- Retry on 5xx and network errors with exponential backoff — 2s, 4s, 8s, 16s, 32s, 64s — for up to six attempts.
- A 4xx is treated as a permanent rejection and is not retried. Return 2xx only once you've durably accepted the event.
- Each attempt times out after 5 seconds and redirects are not followed. Acknowledge fast and do heavy work asynchronously.
- No ordering guarantee between events — use created and the event data to reconcile, not arrival order.
Inspect what happened to any subscription with GET /v1/dashboard/webhooks/{sub_id}/deliveries — each row shows status (PENDING · DELIVERED · FAILED), attempts, and the last error.
Frequently asked
How do I subscribe to a webhook?
From your dashboard session, POST /v1/dashboard/webhooks with an event_type (e.g. booking.committed) and a public HTTPS url. Query GET /v1/dashboard/webhooks/events for the full list of event types you can subscribe to.
How do I verify a delivery came from AgentDraft?
Recompute the HMAC-SHA256 of "<t>." plus the raw request body using your workspace signing secret, and compare it to the v1 value in the X-AgentDraft-Signature header with a constant-time comparison. Reject deliveries whose t timestamp is more than five minutes old to block replays.
What happens if my endpoint is down?
The delivery is persisted before the first attempt and retried with exponential backoff (2s up to 64s) for up to six attempts on 5xx or network failures. A 4xx is treated as a permanent rejection and not retried. You can replay history from the deliveries endpoint.
Will I ever get the same event twice?
Possibly — delivery is at-least-once, so a retry after a slow or dropped response can re-send an event. Every envelope has a stable id that does not change across retries; deduplicate on it.
- The audit log — the same state changes, queryable on demand.
- The protocol spec — primitives, resolution semantics, and the webhook envelope.
- Calendar API for AI agents — the booking and availability surface these events report on.