Skip to content

Webhooks

View as Markdown

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.

Only an admin can manage webhooks.

Terminal window
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:

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

Other commands:

Terminal window
ce webhooks list
ce webhooks test <id> # sends a ping event
ce webhooks deliveries <id> # the delivery log
ce webhooks delete <id> --yes
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 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>

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:

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));
}

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.

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:

Terminal window
checkend serve --webhook-allow-private

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.