Webhooks

Get notified when something happens to your runner — most usefully, when work arrives for one that is stopped.

Webhooks let Evident tell your own systems when something happens to a runner, so they can react automatically. The one most people want: start a stopped runner when work arrives for it.

These are one-way, outbound notifications. Evident POSTs a small, signed event to your endpoint; your system decides what to do about it. There is no inbound HTTP surface here — you cannot send a message to a runner, or reply to a conversation, by responding to a webhook.

Add a webhook

Webhooks live on the runner's own page, under Connectors — there is no separate settings page and no CLI command. You give it an HTTPS endpoint URL and pick which events it should receive. A runner can have several, each with its own events, its own on/off switch and its own secret.

Endpoints must be reachable on the public internet. An address that is obviously internal is rejected when you add it, and rejected again if you try to change an existing webhook to one — in which case the webhook keeps its previous endpoint.

Events

There are three, and you subscribe to each explicitly:

  • agent.message_queued — work arrived for a runner that is not online. This is the automatic wake, and the only one subscribed by default.
  • agent.wake_requested — someone asked Evident to wake a stopped runner. Opt in to this one to enable the one-click wake.
  • runner.suspend_requested — a machine-backed runner reported itself idle, so your controller can suspend it.

A webhook is only notified about the kinds it subscribed to, and adding a new kind to Evident never retroactively subscribes an existing webhook. A disabled webhook is never notified about anything.

What arrives

{
  "type": "agent.message_queued",
  "agent_id": "6f1c…",
  "runner_id": "6f1c…",
  "occurred_at": "2026-08-12T09:41:02.311Z",
  "data": { "queued_count": 1 }
}

An event is a doorbell — it carries no message content. The queue holds the mail: the payload tells you something happened to a runner, and your system reacts. Do not read queued_count as a count of waiting messages; it is not one. agent_id and runner_id are the same public runner identifier.

Verifying a notification

Each webhook has its own secret, shown to you once when you create it and never again. Every request carries an X-Evident-Signature header: a hex HMAC-SHA256 of the raw request body, keyed with that webhook's secret. Compute the same HMAC over the bytes you received — not over a re-serialised copy of the parsed JSON, which may not be byte-identical — and compare.

You can rotate a secret, which shows you the new one once. Rotation is a clean cutover: the previous secret stops verifying immediately, so deploy the new one promptly. Cancelling a rotation leaves the existing secret working.

Deliveries are at-least-once — make your handler idempotent. A notification that doesn't get through is retried, so the same event can arrive twice. Handling a duplicate should be a no-op.

Waking a stopped runner

When a message arrives for a stopped runner, Evident holds the message and notifies your webhook that there is work. Your system starts the runner; when it comes online the held message is delivered and the reply goes back to whichever channel it came from. You are notified once per stopped period, however many messages arrive, and again the next time the runner needs waking.

You can also ask Evident to wake a runner from its page. Evident notifies your webhook and tells you the request was accepted — that is genuinely all it can say yet, and the wait is visible on the runner's page to anyone looking, surviving a reload. If the runner never comes online you are told so, and offered to try again or check your webhook. The one-click wake is only offered when a webhook subscribed to agent.wake_requested exists; otherwise you are guided to configure one.

Evident only rings the doorbell. Your own system is what actually starts the runner, and that can take a few minutes. With no webhook configured nothing breaks — the message is still held, and the runner works normally once you start it by hand.

Checking deliveries

Each webhook keeps a short history of recent deliveries on the runner's page, newest first, showing where each was sent, how your endpoint responded, and when. Both accepted and failed deliveries are listed, and you can expand one to inspect the response and debug it. A redirect is never followed, and is recorded as failed rather than delivered.

One case deliberately leaves no row here: if the endpoint has since become unreachable-by-policy — an address the guard rejects — the delivery is abandoned before it is attempted, so nothing appears in this list. Look in the runner's activity log instead, which records why the delivery was blocked. If a webhook has simply stopped firing and its history has gone quiet, that is where the reason is.

Next steps