Receiving Webhooks
Verify, dedup, and act on signed run-status events sent to your HTTPS receiver.
Headways can push run-status events to an HTTPS endpoint you control. This guide is for the receiver — the service on your side that accepts, verifies, and acts on those events.
If you're looking for how to trigger routines in the first place, see Triggered routines via the API.
Set up an endpoint
In the product: Settings → Webhooks → Add endpoint.
| Field | What it is |
|---|---|
| URL | Your public HTTPS receiver (SSRF-checked: no private addresses, no URL credentials, no plain HTTP) |
| Event types | The events you want (run.completed, run.failed, …) |
| Origins | Which runs you care about — e.g. API-triggered only, or all origins |
| Secret | Generated and shown once. Store it; use it to verify every delivery |
Use Send test event to fire a webhook.test at your receiver before you rely on live traffic.
The envelope
Every delivery is a POST with Content-Type: application/json:
{
"type": "run.completed",
"timestamp": "2026-09-22T18:03:11.487Z",
"data": {
"org_id": "org_xcl…",
"run_id": "clx…",
"routine_id": "clx…",
"origin": "api_triggered",
"status": "completed",
"request_id": "order-8412-sync",
"error_summary": "…failures only, ≤ 500 chars…"
}
}
data.org_idis always present — route the event to the right tenant if you serve several.data.request_idechoes theIdempotency-Keyyou sent at trigger time. Use it to tie an event back to the call that started the run.- Events never contain run transcripts, agent outputs, or the original trigger payload. Fetch those from the product or the run API if you need them.
Event catalog (v1)
type | data.status | When |
|---|---|---|
run.started | running | The run began |
run.awaiting_input | awaiting_input | The run is waiting on a human (fires on every entry into this state) |
run.completed | completed | Finished successfully |
run.failed | failed or budget_exceeded | Errored, or the budget was exhausted |
run.cancelled | cancelled | Cancelled |
webhook.test | — | The admin "Send test event" button |
origin tells you who kicked the run off: api_triggered, scheduled, user_initiated, or link_triggered.
Verify the signature
Every delivery — including webhook.test — is signed with your endpoint's secret using the Standard Webhooks scheme:
webhook-id: <event id>
webhook-timestamp: <unix seconds>
webhook-signature: v1,<hex HMAC-SHA256(secret, "{timestamp}.{rawBody}")>
Use any Standard Webhooks-compatible library to verify the raw request body:
import { Webhooks } from '@standard-webhooks/standard-webhooks' // or any compatible lib
const wh = new Webhooks(secret)
wh.verify(rawBody, req.headers) // throws if the signature or timestamp is invalid
Two things to get right:
- Verify the raw bytes, before any JSON parsing or body-mutating middleware. A re-serialized body won't match.
- Check the timestamp (libraries do this for you). Reject deliveries older than your tolerance — 5 minutes is a good default — to bound replay.
Rotating the secret: after a rotation, Headways signs with the new secret only and the old secret immediately stops validating. Update your receiver promptly.
Handle at-least-once delivery
Delivery is at-least-once: you may see the same event more than once, and events aren't guaranteed to arrive in order.
- Dedup on
webhook-id. It's stable across retries and across fan-out to multiple endpoints, so it's safe as an idempotency key. - Don't assume ordering. Use
data.statusandtimestampto reason about where a run is in its life. - Return 2xx promptly. Anything else (including a slow timeout) counts as a failed attempt.
Retries
A delivery that doesn't get a 2xx is retried automatically, up to 5 attempts with exponential backoff (30s, 60s, 120s, 240s, 480s). After the last attempt the delivery is marked Failed and retained for 30 days.
Admins can inspect delivery history — status, attempt count, and the destination's response — and click Redeliver on any failed delivery to reset the retry window.
A destination that backs up does not affect other endpoints: each endpoint has its own delivery queue and retry state.
Troubleshooting
| Symptom | Likely cause |
|---|---|
| Signature verification fails | You're parsing before verifying, or still using a rotated-out secret |
| No events arrive | Endpoint disabled; origin filter excludes the run's origin; event type not subscribed |
| Events arrive but the run is missing | Event type not in your subscription, or the run belongs to a different org_id |
| Duplicate events | Expected — dedup on webhook-id |
| A burst marks deliveries failed | Your receiver returned non-2xx or timed out; check the response status in delivery history |