# Webhooks

> Get a signed POST request when Checkend makes a finding, sets an outcome, records an annotation or makes a report.

A webhook lets Checkend start your agent. Your agent does not need to poll. Checkend sends a signed POST request to your URL for each event.

## Add a webhook

Only an admin can manage webhooks.

```sh
ce webhooks add https://agents.example.com/checkend
```

Checkend shows the signing secret one time. Keep it in your secret store.

To get only some events, repeat `--event`:

```sh
ce webhooks add https://agents.example.com/checkend --event finding.created --event report.ready
```

Other commands:

```sh
ce webhooks list
ce webhooks test <id>          # sends a ping event
ce webhooks deliveries <id>    # the delivery log
ce webhooks delete <id> --yes
```

## Events

| Event | When | Content |
|---|---|---|
| `finding.created` | A rule makes a new finding. | The finding and the 7-day brief of the site. |
| `finding.updated` | A finding changes, for example a new outcome or a remeasure. | The finding with its `outcome`, and the brief. |
| `annotation.created` | Somebody records an annotation. | The annotation and the brief. |
| `disk.pressure` | The disk guard deleted old raw data. | `disk`: the disk use, the limit, the module, what was deleted, a note. No site and no brief. |
| `report.ready` | A scheduled report is ready. | The full report. No brief. |
| `ping` | You run `ce webhooks test`. | A test event. |

By default, a webhook gets all events.

## The request

The body is JSON: `{id, type, created_at, site, finding | annotation, brief}`. The brief is the same JSON as `GET /v1/brief`, so your agent gets the same data from push and from pull.

The request has three headers:

| Header | Content |
|---|---|
| `Checkend-Event` | The event type. |
| `Checkend-Delivery` | The event ID. Use it to drop duplicates. |
| `Checkend-Signature` | `t=<unix seconds>,v1=<hex>` |

## Check the signature

The signature is HMAC-SHA256 with the secret over `<t>.<body>`.

1. Read `t` and `v1` from the `Checkend-Signature` header.
2. Refuse the request when `t` is more than 5 minutes old.
3. Compute HMAC-SHA256 with your secret over the string `<t>.<body>`. Use the raw body.
4. Compare the hex result with `v1` in constant time.
5. Refuse the request when they are not equal.

Example in Node.js:

```js

function verify(secret, header, rawBody) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const age = Math.floor(Date.now() / 1000) - Number(parts.t);
  if (!(age >= 0 && age <= 300)) return false;
  const mac = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
  return mac.length === parts.v1.length &&
    crypto.timingSafeEqual(Buffer.from(mac), Buffer.from(parts.v1));
}
```

## Retries

A delivery needs a 2xx answer. After a failure, Checkend waits 30 seconds, 2 minutes, 10 minutes, 1 hour and 6 hours. After 6 attempts, the delivery fails. Delivery is at least once, so drop duplicates with `Checkend-Delivery`.

`ce webhooks deliveries <id>` shows each attempt.

## Private targets are refused

Checkend refuses webhook URLs on loopback, private, link-local, shared (100.64.0.0/10) and other special addresses. It checks at create time and again at connect time, after DNS. It does not follow redirects.

For a self-hosted receiver on a private network, start the server with this flag:

```sh
checkend serve --webhook-allow-private
```

> **Caution**
> Use `--webhook-allow-private` only when you need a receiver on a private network. It turns off the protection against requests to your internal services.

## The secret at rest

Checkend must keep the secret to sign each request. It seals the secret with the key file `<data>/secret.key`. The database never holds the secret in clear. A backup has `secret.key`, so the webhooks work after a restore.

> **Caution**
> Keep the webhook secret and `secret.key` safe. The secret signs the payloads, and the key file opens the stored secrets.

To change the secret, delete the webhook and add it again.

A server with another `secret.key` cannot open the secrets. Each delivery then fails with the reason, and `ce webhooks test` tells you to add the webhook again.

## Prompt for a coding agent

```text
Add a Checkend webhook receiver to this project.
1. Add an HTTPS endpoint that accepts the POST request of Checkend.
2. Check the signature of each request, as the Webhooks page below says. Refuse a request with a wrong signature or an old timestamp.
3. On `finding.created`, start the agent job of this project. Return a 2xx answer quickly.
4. Keep the signing secret in the secret store of the project, never in the code.
5. After the deploy, tell me to run `ce webhooks add <url> --event finding.created`, then `ce webhooks test <id>`.
```
