> ## Documentation Index
> Fetch the complete documentation index at: https://docs.usepatchwork.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Events and webhooks

> What happens inside Patchwork, delivered to a realtime subscriber or to a server of yours.

Everything a run does publishes an event. Where an event goes depends on the transport: realtime subscribers get all of them, and most are also deliverable to a webhook endpoint you register.

## The events

| Event | When | Webhook |
| - | - | - |
| `run.started` | A run began. | ✓ |
| `run.completed` | The run closed. Carries the final state and what it cost. | ✓ |
| `run.failed` | The run stopped on an error. | ✓ |
| `elicitation.requested` | The agent asked the user something and is waiting. | ✓ |
| `action.proposed` | The agent prepared a write for you to commit. | ✓ |
| `write.confirmation_requested` | A write is held, waiting for approval. | ✓ |
| `handoff.requested` | The agent handed the conversation to a person. | ✓ |
| `message.completed` | An assistant message is final. | ✓ |
| `tool_call.started` | The agent began using a patch. | ✓ |
| `tool_call.completed` | It finished — check `status` for `ok`, `failed`, `declined` or `denied`. | ✓ |
| `plan.updated` | The agent revised its plan. | ✓ |
| `extract.completed` | An async [extract](/guides/extract) finished. | ✓ |
| `extract.failed` | An async extract failed. | ✓ |
| `message.delta` | A fragment of assistant content, while streaming. | — |
| `message.block` | A [rich-message block](/guides/messages) for you to render. | — |

`message.delta` and `message.block` are realtime-only. Streaming fragments over HTTP would be a poor trade, and a render block is a thing a UI draws, not a thing a server acts on.

<Note>
  The four "agent needs something" events **are** webhooked. If your integration is headless, they matter more than `run.completed` — a run parked at one of them waits until you answer, and nothing else will happen in the meantime.
</Note>

## Envelope

Every event has the same shape, whichever transport carries it:

```json theme={null}
{
  "id": "evt_9f2c81b4d7e6",
  "type": "run.completed",
  "created_at": "2026-08-30T12:00:04Z",
  "seq": 4,

  "origin": "agent",
  "source": { "kind": "run", "id": "7c9e6679-…" },
  "parent": null,

  "context": {
    "workspace_id": "0f3a…",
    "connection_id": "c4b1…",
    "agent_id": "a91e…",
    "agent_key": "support",
    "thread_id": "3fa85f64-…",
    "subject_ref": "user_9f2"
  },

  "run_id": "7c9e6679-…",
  "thread_id": "3fa85f64-…",

  "data": { "state": "closed", "cost_credits": 12.5 }
}
```

* **`source`** is the unit of work the event is about. `kind` is `run` for agent work, `request` for a bare product call like extract, and `endpoint` for a test.
* **`origin`** is which surface produced the work: `agent`, `api`, `mcp`, `console` or `playground`.
* **`context`** carries tenancy and routing. A key is left out rather than sent as null when it does not apply — a run whose agent uses no [connection](/guides/connections) has no `connection_id`.
* **`seq`** orders events within one `source`. Product requests have none.
* **`run_id`** and **`thread_id`** sit at the top level for compatibility with the SDK. They duplicate `source` and `context`; new code should read the structured fields.

`data` varies by type. `write.confirmation_requested` carries the `tool_call_id`, `patch_ref`, `summary` and `risk` you need to build an approval prompt.

## One event per unit of work

An event describes the outermost thing you asked for, not every step inside it.

If an agent extracts three documents during a run, you get **one** `run.completed` — not three `extract.completed`. The extracts are steps of that run, and the run's own event already reports it. `extract.*` fires only when you call the extract API directly.

If you do want per-step visibility inside a run, subscribe to `tool_call.completed` and read `data.name`. That is a separate choice, and it is a noisy one: you will get every tool call in every run, not just the ones you had in mind.

## Registering an endpoint

Under **Settings → Webhooks**. An endpoint is a subscription, not just a URL:

* **Events** — exactly the types you pick. Nothing is selected for you, and types we add later never start arriving on an existing endpoint.
* **Where the work came from** — `agent`, `api`, `mcp`, `console`, `playground`. Playground is off unless you ask for it, so test messages never reach a production server.
* **Which backend** — one or more [connections](/guides/connections). This is how you keep local, staging and production apart; there is no separate test mode.
* **Which agent** — by agent, covering all its versions.
* **Send ids only** — omits `data`, leaving you the ids to read the resource back. Keeps run content out of your ingress logs.

A filter is ignored by a category it cannot describe, so filtering by agent never silently swallows an `extract.completed`. Within a category it can describe, an absent value fails a filter you set — an agent that uses no connection has no connection, so a connection filter will not match its runs.

## Verifying a delivery

Every delivery carries three headers:

| Header | Meaning |
| - | - |
| `Patchwork-Signature` | `t=<unix>,v2=<hmac>` over the endpoint's own secret. |
| `Patchwork-Event` | The event type, so you can route before parsing. |
| `Patchwork-Delivery` | The event id, for idempotency. |

The signature is computed exactly as for tool calls — see [Signing and verification](/guides/signing) — but keyed with the **webhook endpoint's** secret, not your workspace request secret. A delivery goes to the URL you registered and carries no query string, so the signed target is just the path.

Two rules that are easy to miss:

1. **Reject a timestamp more than five minutes old.** The signature alone does not stop a captured delivery being replayed at you; the timestamp does.
2. **Accept if *any* `v2` matches.** A header can carry more than one. During a secret rotation we sign with both the old and the new secret for 24 hours so you do not need an atomic deploy, and a verifier that reads only the first will break halfway through.

```js theme={null}
import crypto from "node:crypto";

function verify(rawBody, header, secret, path) {
  const parts = header.split(",").map((p) => p.split("=", 2));
  const timestamp = Number(parts.find(([k]) => k === "t")?.[1]);
  const signatures = parts.filter(([k]) => k === "v2").map(([, v]) => v);

  if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;

  const digest = crypto.createHash("sha256").update(rawBody).digest("base64");
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`v2.${timestamp}.POST.${path}.${digest}`)
    .digest("hex");

  return signatures.some((candidate) =>
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(candidate)),
  );
}
```

Verify against the **raw request body**, before any JSON parsing — re-serializing changes the bytes and the digest with them.

## Delivery behaviour

**Respond `2xx` quickly and do the work asynchronously.** Anything `300` or above is a failure, redirects are not followed, and we give up on the response after 8 seconds. A slow handler burns your retry budget.

Failures retry on a fixed schedule — immediately, then after 30s, 2m, 10m, 1h and 6h. Six attempts over roughly eight hours, with a little jitter so a server coming back up is not hit by everything at once.

**Deliveries are not ordered**, and a retry can land after a later event. Deduplicate on `Patchwork-Delivery` and treat each event as a statement about a run you can re-fetch, rather than as a state machine of its own. `seq` orders events within a single run if you need it.

**An endpoint that fails continuously for 24 hours is switched off** and workspace owners are emailed. Switching it off does not delete anything — the configuration, the secret and the delivery history all stay, and turning it back on does not resend the backlog.

## When something is wrong

The endpoint's page shows every delivery: the type, the state, the status we got, the attempt count, and — when you open a row — **the exact payload we sent and the response we got back**. That is usually enough to tell whether the problem is on your side or ours.

From there you can **replay** a delivery, which re-sends the stored payload under the same event id, so your handler dedupes it exactly as it would a retry.

**Send test** delivers a synthetic `endpoint.test` event to one endpoint. It ignores every filter and works on an endpoint that is switched off, which is when you most need it. It never counts toward the failure that disables an endpoint, and nothing can subscribe to it.

## Rotating the secret

Rotating gives you a new secret immediately and keeps the old one valid for 24 hours, signing every delivery with both. Take the new secret, deploy it, and the old one stops mattering on its own — provided your verifier checks every `v2`.

Only workspace owners can see or rotate a signing secret.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.