Checkbox Docs

Webhooks

Signed events for findings, evaluations, 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

EventWhendata.object
plan.updatedA 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.verifiedA compliance-plan task verification resolved (passed or failed); data.object is the verification with expected vs observed per check.Verification
notice.dueAn email the merchant sends itself ("You send" mode) reached its recommended send date: data.object is the due_notice with the window, send_at, the copy-ready email and its BCC address. Send it now (inside the window), then call markNoticeSent (or BCC the copy). required tells a legally required email from a recommended one.Notice
notice.overdueA due_notice's window closed without proof it was sent (no markNoticeSent call, no matching BCC copy). For a required notice the compliance check fails until it is sent (late) or recorded as missed.Notice
receipt.createdA consent receipt was recorded: what a subscriber saw next to the pay button and agreed to (or, for Stripe-hosted checkouts, what Stripe recorded); data.object is the receipt.Receipt
test_run.completedA lifecycle test run finished; data.object is the test_run with every step's observations, check results and fixes.TestRun
test_run.failedA lifecycle test run stopped early; data.object.failure has the code, message and fix.TestRun
site.compliance_changedA re-evaluation changed the site's check results; data.object is the site, with its current compliance status and label in data.object.compliance.Site
finding.createdA check started failing (new finding, or a resolved / passing one failing again).Finding
finding.resolvedA failing or undecided check now passes or no longer applies.Finding
scan.completedA scan finished (the evaluation may follow in a separate site.compliance_changed event).Scan
scan.failedA scan failed; data.object.error says why.Scan
law.changedA law change Checkbox applies affected one of your sites (data.object is the law update with that site's impact).LawUpdate
certificate.issuedA monitoring certificate was issued.Certificate
certificate.lapsedA certificate lapsed (the site stopped meeting the requirements).Certificate
certificate.revokedA certificate was revoked.Certificate
notice.sentCheckbox sent a legally required notice (acknowledgment, reminder, price change) for the merchant.Notice
consent.createdA subscriber gave affirmative consent to the automatic-renewal terms through Checkbox Elements (or your server recorded one); data.object is the consent.Consent
site.compliantA 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://acme.example/hooks/checkbox", "enabled_events": ["finding.created", "finding.resolved", "task.verified"] }'

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

JSON
{
  "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:

Node
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):

Node
import { createServer } from 'node:http';
import Checkbox from '@checkbox-site/node';

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). You remain solely responsible for your subscription program, and Checkbox takes on no legal liability for it (Terms). Checkbox is a System1 company; Trust center · Status. For AI agents: every page is available as Markdown at the same URL plus .md; the index is /llms.txt.