Webhooks
Signed events for findings, scores, compliance plans, test runs, consents, certificates, notices and law changes — the event types, the payload, signature verification and retries.
Checkbox POSTs an Event to each of your webhook endpoints that subscribed to its type. Use them to react without polling: open a ticket when a finding appears, tell your agent when a verification passes, or alert when the law changes.
Event types
| Event | When | data.object |
|---|---|---|
plan.updated | A site's compliance plan changed (its status or a task's status); data.object is the plan without task content — GET the plan and follow next_action. | CompliancePlanSummary |
task.verified | A compliance-plan task verification resolved (passed or failed); data.object is the verification with expected vs observed per check. | Verification |
test_run.completed | A lifecycle test run finished; data.object is the test_run with every step's observations, check results and fixes. | TestRun |
test_run.failed | A lifecycle test run stopped early; data.object.failure has the code, message and fix. | TestRun |
site.score_changed | A site's compliance score changed (data.previous_attributes.score has the old score). | Site |
site.insurable_changed | A site became insurable or stopped being insurable. | Site |
finding.created | A check started failing (new finding, or a resolved / passing one failing again). | Finding |
finding.resolved | A failing or undecided check now passes or no longer applies. | Finding |
scan.completed | A scan finished (the evaluation may follow in a separate site.score_changed event). | Scan |
scan.failed | A scan failed; data.object.error says why. | Scan |
law.changed | A law change Checkbox applies affected one of your sites (data.object is the law update with that site's impact). | LawUpdate |
certificate.issued | A monitoring certificate was issued. | Certificate |
certificate.lapsed | A certificate lapsed (the site stopped meeting the requirements). | Certificate |
certificate.revoked | A certificate was revoked. | Certificate |
notice.sent | Checkbox sent a legally required notice (acknowledgment, reminder, price change) for the merchant. | Notice |
consent.created | A subscriber gave affirmative consent to the automatic-renewal terms through Checkbox Elements (or your server recorded one); data.object is the consent. | Consent |
site.compliant | A site's compliance plan became compliant: nothing is left to apply, verify or answer; data.object is the site. | Site |
Create an endpoint
enabled_events takes event types from the table above, or ["*"] for all of them. The response includes the endpoint's secret (whsec_…) once:
curl https://app.checkbox.site/v1/webhook_endpoints \
-u "$CHECKBOX_SECRET_KEY:" \
-H "Content-Type: application/json" \
-d '{ "url": "https://joybox.example/hooks/checkbox", "enabled_events": ["finding.created", "finding.resolved", "task.verified"] }'import Checkbox from '@checkbox-site/cli';
const checkbox = new Checkbox(process.env.CHECKBOX_SECRET_KEY);
const endpoint = await checkbox.webhookEndpoints.create({
url: 'https://joybox.example/hooks/checkbox',
enabled_events: ['finding.created', 'finding.resolved', 'task.verified'],
});
console.log(endpoint.secret);import os
from checkbox import Checkbox
checkbox = Checkbox(os.environ["CHECKBOX_SECRET_KEY"])
endpoint = checkbox.webhook_endpoints.create(
url="https://joybox.example/hooks/checkbox",
enabled_events=["finding.created", "finding.resolved", "task.verified"],
)
print(endpoint["secret"])Endpoints must be public HTTPS URLs; redirects are not followed. rollWebhookEndpointSecret replaces the secret immediately and returns the new one (only then), so deploy the new secret right after rolling it.
The payload
{
"id": "evt_3f9a1c2e7b4d4e0f9a8b7c6d5e4f3a2b",
"object": "event",
"type": "finding.created",
"api_version": "2026-10-30",
"created": 1793318400,
"livemode": true,
"data": {
"object": { "id": "fnd_3f9a1c2e7b4d4e0f9a8b7c6d5e4f3a2b", "object": "finding", "check_id": "signup.consent.not_prechecked", "status": "fail" },
"previous_attributes": {}
},
"pending_webhooks": 1,
"request": { "id": null }
}Headers: Checkbox-Signature, Checkbox-Event-Id, Checkbox-Event-Type. Events are kept for 30 days and listed by listEvents (type=finding.*, created_gte…).
Verify signatures
Checkbox-Signature: t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256 of "<t>.<raw body>" with the endpoint secret. Reject anything older than 5 minutes, and accept the delivery if any v1 entry matches (a header may carry more than one). Use the raw body exactly as received — not re-serialized JSON.
Without an SDK:
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(rawBody, header, secret, toleranceSec = 300) {
const parts = header.split(',');
const t = Number(parts.find((p) => p.startsWith('t='))?.slice(2));
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest();
return parts
.filter((p) => p.startsWith('v1='))
.some((p) => {
const got = Buffer.from(p.slice(3), 'hex');
return got.length === expected.length && timingSafeEqual(got, expected);
});
}
// Self-check with a header built the same way Checkbox builds it.
const secret = 'whsec_test_secret';
const body = '{"id":"evt_123","type":"site.compliant"}';
const t = Math.floor(Date.now() / 1000);
const header = `t=${t},v1=${createHmac('sha256', secret).update(`${t}.${body}`).digest('hex')}`;
console.log(verify(body, header, secret), verify(body, header, 'whsec_wrong'));With the SDKs: checkbox.webhooks.constructEvent(rawBody, header, secret) (Node) and checkbox.webhook.construct_event(raw_body, header, secret) (Python).
A complete handler (Node, no framework):
import { createServer } from 'node:http';
import Checkbox from '@checkbox-site/cli';
const checkbox = new Checkbox(process.env.CHECKBOX_SECRET_KEY);
createServer((req, res) => {
const chunks = [];
req.on('data', (c) => chunks.push(c));
req.on('end', () => {
let event;
try {
event = checkbox.webhooks.constructEvent(Buffer.concat(chunks).toString('utf8'), req.headers['checkbox-signature'], process.env.CHECKBOX_WEBHOOK_SECRET);
} catch {
res.writeHead(400).end();
return;
}
if (event.type === 'task.verified') console.log('verified', event.data.object.id);
res.writeHead(200).end();
});
}).listen(3000);Delivery and retries
Any 2xx response acknowledges a delivery; answer quickly and do the work afterwards. Failed deliveries are retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours, 24 hours and 24 hours (about three days), then marked failed. Deliveries to private or loopback addresses are refused. Events can arrive more than once or out of order: use id to deduplicate and created to order.
Checkbox is compliance software, not a law firm; nothing in these docs is legal advice (details). For AI agents: every page is available as Markdown at the same URL plus .md; the index is /llms.txt.