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.
- 01CHeKTWebhook deliveryEvent firesalarm.created, device.offline, etc.
- 02Webhook deliveryWebhook deliverySign with HMAC-SHA256Headers: X-CHeKT-Signature, X-CHeKT-Timestamp
- 03Webhook deliveryYour endpointPOST eventJSON body. 30s connect + read timeout.
- 04Your endpointWebhook delivery200 OK within 30sAnything other than 2xx triggers retry.
- 05Webhook deliveryWebhook deliveryOn failure: retry with exp. backoffUp to 24 hours total.
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.
{
"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.
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).
Dedupe pattern
Store the event ID for each event you process. On retry, the lookup hits and you skip the work without side effects.
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.