Webhooks

React to what happens in CHeKT, the moment it happens.

Subscribe to events. Receive signed, retried, idempotent payloads. Build pipelines that react in real time.

What are webhooks?

Webhooks are CHeKT-to-you events. Instead of polling our API every few seconds to check whether something happened, you register a URL and we POST to it when an event fires. Your app reacts in real time, and we both save load.

Every CHeKT App can subscribe to one or more event types. Subscriptions are configured per-app in dealer.chekt.com → Settings → CHeKT Apps → [your app] → Events.

How delivery works

When an event fires inside CHeKT, our delivery system signs the payload, posts it to your endpoint, and retries with exponential backoff if you don't respond with a 2xx within 30 seconds.

CHeKT
Webhook delivery
Your endpoint
  1. 01
    CHeKTWebhook delivery
    Event firesalarm.created, device.offline, etc.
  2. 02
    Webhook deliveryWebhook delivery
    Sign with HMAC-SHA256Headers: X-CHeKT-Signature, X-CHeKT-Timestamp
  3. 03
    Webhook deliveryYour endpoint
    POST eventJSON body. 30s connect + read timeout.
  4. 04
    Your endpointWebhook delivery
    200 OK within 30sAnything other than 2xx triggers retry.
  5. 05
    Webhook deliveryWebhook delivery
    On failure: retry with exp. backoffUp to 24 hours total.
Figure 1. The five-step path from event firing to your endpoint receiving it.

Request headers

Every webhook POST carries five CHeKT-specific headers. Two are for security, three are for traceability and dedupe.

Payload structure

Every event uses the same envelope. The fields outside data are stable across event types; the data object varies by event.

id
Stable event ID. Use this to dedupe on your side.
type
Dot-separated event name like alarm.created or device.offline.
created_at
ISO-8601 UTC timestamp when the event was generated.
data
Event-specific payload. Schema documented per event type.
POST /your-endpoint
{
  "id": "evt_2hKqx9",
  "type": "alarm.created",
  "created_at": "2026-05-26T10:21:34Z",
  "data": {
    "alarm_id": "A-10293",
    "site_id": "site_29snd",
    "priority": "HIGH",
    "trigger": "motion",
    "device_id": "dev_8h2j3kfm",
    "location": "Main Entrance",
    "snapshot_url": "https://media.chekt.com/snap/..."
  }
}

Verifying signatures

Every webhook is signed with HMAC-SHA256 over the concatenation "{timestamp}.{body}". Computing the HMAC over the timestamp + body gives you replay protection for free: even if an attacker captures a previous webhook, they can't replay it after a 5-minute window because they can't forge a fresh timestamp.

middleware/verifyChektSignature.ts
1import crypto from "crypto";
2
3// Use express.raw() for /webhooks/* so we can verify against the exact bytes
4app.post(
5  "/webhooks/chekt",
6  express.raw({ type: "application/json" }),
7  (req, res) => {
8    const sig = req.headers["x-chekt-signature"] as string;
9    const ts = req.headers["x-chekt-timestamp"] as string;
10
11    // 1) Reject if older than 5 minutes (replay protection)
12    if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) {
13      return res.status(401).end();
14    }
15
16    // 2) Compute HMAC over "{ts}.{body}"
17    const signed = `${ts}.${req.body.toString("utf8")}`;
18    const expected = crypto
19      .createHmac("sha256", process.env.CHEKT_WEBHOOK_SECRET!)
20      .update(signed)
21      .digest("hex");
22
23    // 3) Timing-safe compare
24    const ok = crypto.timingSafeEqual(
25      Buffer.from(sig, "hex"),
26      Buffer.from(expected, "hex"),
27    );
28
29    if (!ok) return res.status(401).end();
30
31    // 4) Now safe to parse the JSON
32    const event = JSON.parse(req.body.toString("utf8"));
33
34    // 5) Acknowledge fast, queue the work
35    queue.publish(event.type, event.data);
36    return res.sendStatus(200);
37  },
38);

Delivery guarantees & retries

CHeKT guarantees at-least-once delivery. If your endpoint returns anything other than a 2xx within 30 seconds, we retry on the schedule below. We give up after 8 attempts (~24 hours total).

AttemptDelay after previousElapsed
10s0s
21m1m
35m6m
415m21m
51h1h 21m
63h4h 21m
76h10h 21m
812h22h 21m

Dedupe pattern

Store the event ID for each event you process. On retry, the lookup hits and you skip the work without side effects.

handle.ts
1// Dedupe on event.id — at-least-once delivery means duplicates happen
2async function handle(event: ChektEvent) {
3  const seen = await db.events.findOne({ id: event.id });
4  if (seen) return; // Already processed — skip silently
5
6  await db.events.insertOne({ id: event.id, processed_at: new Date() });
7  await processEvent(event);
8}

Troubleshooting

The webhook delivery log lives in the dealer portal at Settings → CHeKT Apps → [your app] → Webhook log. Each row shows the attempt number, status code, response time, and the response body's first 200 bytes — usually enough to diagnose without reaching for a separate observability tool.

Signature mismatch
You're likely verifying against parsed JSON instead of raw bytes, or using the wrong secret. Check that you copied the secret exactly from the app settings.
Timeout (no response in 30s)
Acknowledge with 200 first, then do the work in a queue. Synchronous processing in the handler is the most common cause of timeouts.
Duplicate processing
You're not deduping on event.id. Insert a unique index on event_id in your processed-events table.
Out-of-order events
Webhook delivery is not strictly ordered. Sort by created_at on your side if order matters for your domain logic.

Next steps