Skip to main content
Webhooks push events to your HTTPS endpoint so your agent reacts to inbound mail and to runs that need a human — without polling. The one you almost certainly want is run.blocked: a run that stops for a CAPTCHA, an SMS code, or a final submit is held for ten minutes, and this is how someone finds out in time.

Events

The run.* events are delivered once, with no retry. If your endpoint can miss a delivery, pair them with polling GET /v1/runs/:id — or, for blocked runs, rely on the operator email that Sente also sends. Never make a run.* webhook the only thing standing between a blocked run and its ten-minute hold expiring.

Delivery

Each delivery is an HTTP POST with content-type: application/json and a 10-second timeout. Every delivery to your org carries the org-level secret in the x-sente-secret header:
Verify it with a constant-time compare before trusting the payload. There is no HMAC over the body and no raw-body requirement — a plain parsed-JSON handler is enough.
Any non-2xx response — or a timeout — records the delivery as failed. Respond fast and do the work afterwards.
Webhook URLs must be http(s) and must resolve to a public address. URLs pointing at loopback, private ranges, link-local, or cloud-metadata addresses are rejected at registration with 400, and re-checked immediately before every delivery in case DNS changed in between.

Create a webhook

POST /v1/webhooks Idempotent per (url, identity scope): re-posting the same url and scope returns the existing subscription with its event list replaced by the one you sent. Safe to call on every app start — and note that it replaces rather than merges, so always send the full list.
Response 201 — the only response in the API that contains the secret:
The secret is org-level, not per-webhook: every delivery to your org carries the same one, so a second subscription returns the same value.

List webhooks

GET /v1/webhooks
Response 200 — no secret in list responses:
Lost the secret? Re-post the same url and scope to POST /v1/webhooks — the response returns the org secret again, and the existing subscription is reused rather than duplicated.

Delete a webhook

DELETE /v1/webhooks/:id
Response: 204 with no body. 404 {"error": "not found"} for an unknown id or another org’s subscription.

Payloads

message.received

A summary only — the body and the extracted code are not in the payload. Fetch them with GET /v1/messages/:id. channel is always email today.

run.blocked

Fired the moment a run parks for a human, and carrying everything a notifier needs to route someone to the gate:
Every blocked run also emails the org’s operators the same dashboard link, using each user’s notification preference. The webhook is for your systems; the email is the “you walked away” backstop. The sente watch CLI command is a third path — a desktop notifier.

run.completed / run.failed

run.failed has the same shape with "status": "failed", the failure code in code/detail, and "result": null. Both fire exactly once, from whichever writer wins the terminal transition — including the timeout and worker-recovery paths, so a run that dies quietly still notifies. appUrl is null when the run has no registration behind it. Note that the run payloads use code/detail at the top level, where the run object from GET /v1/runs/:id nests them under error.

Webhooks guide

Endpoint setup, verification, and retries in practice.

Human takeover

What to do when run.blocked arrives.

Messages

Fetch the body after message.received.