Skip to content

Webhooks

Your alerts as signed JSON in your own system, a few minutes after they appear in your account.

Setup

Under Integrations, add a webhook with an https address of your system. Your signing secret (whsec_...) is shown once; keep it with your system. The Send a test button sends you an event of type endpoint.test right away.

The webhook receives the same alerts as your account, as your alert rules choose them, filtered by the severity and themes you pick for the webhook.

The request

One POST per alert, with a JSON body and these headers:

Content-Type
application/json; charset=utf-8
User-Agent
Checked.be-Webhooks/1.0 (+https://checked.be/nl/developers/webhooks)
webhook-id
Unique id of the message, equal to the id field. The same on every retry: use it to ignore duplicates.
webhook-timestamp
Time of this attempt, in seconds since 1970 (UTC).
webhook-signature
The signature: v1, followed by the base64 HMAC-SHA256. See below.

Example

An alert about a made-up company, exactly as Checked sends it:

{
  "id": "msg_5f0c2d6e9b1a4c7d8e3f2a1b0c9d8e7f",
  "type": "alert.created",
  "api_version": "2026-09-25",
  "created_at": "2026-09-25T07:38:12Z",
  "language": "en",
  "data": {
    "alert": {
      "id": 184223,
      "kind": "bs_capital_change",
      "label": "Capital change",
      "signal": "kapitaal",
      "theme": "financieel",
      "theme_label": "Finance",
      "severity": "med",
      "severity_label": "medium",
      "title": "Kapitaalverhoging van € 250.000",
      "body": "Het kapitaal stijgt van € 18.550 naar € 268.550.",
      "created_at": "2026-09-25T07:36:12Z"
    },
    "company": {
      "enterprise_number": "0999999999",
      "enterprise_number_display": "0999.999.999",
      "name": "Voorbeeld NV",
      "dossier_url": "https://checked.be/en/c/voorbeeld-nv-0999999999"
    },
    "alerts_url": "https://checked.be/en/alerts"
  }
}

Fields

type
alert.created (an alert), digest.created (the daily or weekly digest, if you tick it) or endpoint.test (the test button). Ignore types you do not know: more may be added.
api_version
The version of this format. Fields may be added; existing fields do not change within a version.
language
The language of the labels and links (nl, fr or en), as set on the webhook. An alert's title and text are in Dutch, as in your account.
data.alert.kind
The kind of alert, a fixed code. label is its name, signal and theme the row and theme of the alert rules (null for a summary of several deeds).
data.alert.severity
low, med or high, with severity_label in the chosen language.
data.company
The enterprise number (10 digits and in its usual notation), the name (null when unknown) and the link to the dossier on Checked.be.

A webhook only gets what the alert also shows in your account: no email address or other account data.

Verifying the signature

Checked signs following Standard Webhooks, so you can also use one of their libraries. By hand:

  1. Take the key: the part after whsec_, base64-decoded.
  2. Compute the HMAC-SHA256 of {webhook-id}.{webhook-timestamp}.{body}, with the body exactly as received.
  3. Compare v1, plus its base64, in constant time, with each value in webhook-signature (space separated).
  4. Reject a timestamp more than five minutes away from your clock.
Node.js
import crypto from "node:crypto";

export function verify(secret, headers, body) {
  const id = headers["webhook-id"];
  const ts = headers["webhook-timestamp"];
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const expected = "v1," + crypto.createHmac("sha256", key)
    .update(`${id}.${ts}.${body}`).digest("base64");
  return headers["webhook-signature"].split(" ").some(sig =>
    sig.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)));
}
Python
import base64, hashlib, hmac, time

def verify(secret: str, headers: dict, body: bytes) -> bool:
    msg_id = headers["webhook-id"]
    ts = headers["webhook-timestamp"]
    if abs(time.time() - int(ts)) > 300:
        return False
    key = base64.b64decode(secret.removeprefix("whsec_"))
    signed = f"{msg_id}.{ts}.".encode() + body
    expected = "v1," + base64.b64encode(
        hmac.new(key, signed, hashlib.sha256).digest()).decode()
    return any(hmac.compare_digest(sig, expected)
               for sig in headers["webhook-signature"].split(" "))
C#
using System.Security.Cryptography;
using System.Text;

static bool Verify(string secret, string id, string ts, string signatures, string body)
{
    if (Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - long.Parse(ts)) > 300) return false;
    var key = Convert.FromBase64String(secret["whsec_".Length..]);
    var hash = HMACSHA256.HashData(key, Encoding.UTF8.GetBytes($"{id}.{ts}.{body}"));
    var expected = Encoding.UTF8.GetBytes("v1," + Convert.ToBase64String(hash));
    return signatures.Split(' ').Any(s =>
        CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(s), expected));
}

Answers and retries

  • Answer within 10 seconds with a 2xx status. Do heavy work afterwards.
  • Any other answer, or none, is retried, later each time: after 1, 5, 15 and 30 minutes, then after 1 to 12 hours. A message is tried up to 6 times and expires after 3 days.
  • Messages arrive in order: while the oldest is not delivered, the next ones wait. A Retry-After header is honoured (up to an hour).
  • After 10 failures in a row the webhook is switched off and its owner gets an email. A 410 Gone answer switches it off without a retry.
  • Redirects (3xx) are not followed. The address must use https and be public; addresses in an internal network are refused, even if the name points there later.
  • Every attempt is in the webhook's delivery log, with the status and the duration.