Webhooks
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
Section titled “Add a webhook”Only an admin can manage webhooks.
ce webhooks add https://agents.example.com/checkendCheckend shows the signing secret one time. Keep it in your secret store.
To get only some events, repeat --event:
ce webhooks add https://agents.example.com/checkend --event finding.created --event report.readyOther commands:
ce webhooks listce webhooks test <id> # sends a ping eventce webhooks deliveries <id> # the delivery logce webhooks delete <id> --yesEvents
Section titled “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
Section titled “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
Section titled “Check the signature”The signature is HMAC-SHA256 with the secret over <t>.<body>.
- Read
tandv1from theCheckend-Signatureheader. - Refuse the request when
tis more than 5 minutes old. - Compute HMAC-SHA256 with your secret over the string
<t>.<body>. Use the raw body. - Compare the hex result with
v1in constant time. - Refuse the request when they are not equal.
Example in Node.js:
import crypto from "node:crypto";
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
Section titled “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
Section titled “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:
checkend serve --webhook-allow-privateThe secret at rest
Section titled “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.
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.