---
title: Webhooks
description: Optional. Signed deliveries of ledger rows to your server, with retries. Off until a Manager adds an endpoint.
section: API
order: 7
---

Polling is the whole integration. Webhooks are for when you would rather be told.

## Set one up

A Manager adds an endpoint in the app under **Settings**, in **Webhooks**: a URL, and the events to receive, or none selected for all of them. The signing secret, `whsec_…`, is shown **once**. **Test** sends one signed delivery on the spot and shows what your server answered.

Nothing from before the endpoint existed is ever replayed.

## A delivery

```json
{
  "id": "whd-8Hq2mZ1xPq0a",
  "type": "workflow.auto_filed",
  "at": "2026-09-17T21:22:48.377Z",
  "application": "euw8ydHxTW",
  "data": {
    "actor": { "kind": "system", "name": "Workflow", "authenticatedAs": "LOS integration (API)" },
    "target": { "type": "run", "id": "run-20260917-212242-ef09" },
    "detail": { "workflow": "standard-intake v1", "approved": 1, "leftUnassigned": 0, "failed": 0 }
  }
}
```

It is one [ledger](/docs/concepts/the-ledger) row. `type` is the ledger's own action name.

## Events worth subscribing to

| Event | Meaning |
| --- | --- |
| `application.created` | A case was started. |
| `file.received` | A file landed on a case. |
| `run.ingested` | A run's results landed. |
| `workflow.auto_filed` | A workflow finished filing what it recommended. |
| `document.approved`, `document.rejected` | A filing decision, by a person or a workflow. |
| `run.failed` | A run failed, or the analyzer went silent for 3 minutes, and the case was put back. |
| `run.cancelled` | A person stopped the run and the case was put back. |

The full list is the `WebhookEvent` enum in the [contract](/api/openapi.json).

## Verify the signature

Deliveries follow the Standard Webhooks specification. Three headers arrive with each one:

| Header | Value |
| --- | --- |
| `webhook-id` | The delivery id. The same on every retry, so use it to drop duplicates. |
| `webhook-timestamp` | Seconds since the epoch. |
| `webhook-signature` | `v1,` followed by the base64 HMAC-SHA256 of `id.timestamp.body`, keyed with your secret. |

The key is the base64 part of the secret, after `whsec_`. Any Standard Webhooks library does this for you. Reject a delivery whose signature does not match or whose timestamp is more than a few minutes old.

## Retries

Answer `2xx` within 15 seconds. Otherwise the delivery is retried 14 times over about 13 hours, from 30 seconds apart up to 2 hours apart, and then marked failed. Answering `410` turns the endpoint off; add it again to resume. The endpoint's row in **Settings** shows its latest delivery.
