§ For agents · Webhooks

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


§ 01What they are

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.


§ 02Subscribe

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.

§ 03Events you can subscribe to
EventWhen it fires
booking.committedAn agent hard-commits a booking. The payload carries booking_id, agent_id, user_id, start, and end.
booking.cancelledA booking is cancelled and its slot is freed. Carries the booking_id.
booking.evictedA 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_proposedAn agent counter-proposes alternative times against a winning booking. Carries the counter_id and the proposed slots.
booking.counter_acceptedA counter-proposal is accepted and re-committed onto the new slot. Carries the new_booking_id and accepted slot.
booking.counter_rejectedA counter-proposal is rejected. Carries the booking_id and counter_id.
hold.invalidatedAn external calendar event invalidates an active hold (or a still-bumpable commit). Carries the booking and the triggering external event.
mailbox.inboundAn 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.


§ 04The delivery payload

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:

HeaderValue
X-AgentDraft-Signaturet=<unix-seconds>,v1=<hex> — HMAC-SHA256 over "<t>." + the raw request body.
X-AgentDraft-EventThe event type, e.g. booking.committed — mirrors the envelope type for cheap routing.
User-AgentAgentDraft-Webhooks/0.1

§ 05Verify the signature

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.


§ 06Delivery & retries

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.


§ 07

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.


§ 08Further reading