Webhooks
Send every change to your own server as JSON, signed so you can tell it really came from us.
A webhook sends every change we'd email about to a URL you choose, as a JSON POST. Use it to update a spreadsheet, post into another app, or trigger your own automation. Webhook deliveries never count toward your monthly email allowance.
Webhooks are part of the Pro and Business plans.
Add an endpoint
- Open a calendar and go to its Webhooks tab.
- Choose Webhook as the type and enter your URL. It must be
https, and on a public host. - Save. We show you a signing secret once. Copy it now and store it somewhere safe; we won't show it again.
- Choose Send a test to send a sample delivery to your endpoint straight away, rather than waiting for the calendar to change.
We don't follow redirects, so point the endpoint at its final URL.
What you receive
Each delivery is a JSON body like this:
{
"id": "…",
"apiVersion": "2026-08-01",
"type": "change",
"createdAt": "2026-10-02T14:02:00.000Z",
"account": { "id": "…" },
"watch": { "id": "…", "name": "School calendar" },
"feed": { "id": "…", "url": "https://example.org/…", "calendarName": "School calendar" },
"data": {
"counts": { "added": 1, "changed": 1, "cancelled": 0 },
"added": [ … ],
"changed": [ … ],
"cancelled": [ … ]
}
}typesays what happened:changefor calendar changes. We also send notices, such as a calendar we can't reach.idis the same on every retry of the same delivery.datahas the same changes your email describes. Changed events list each field with itsbeforeandaftervalues.- The feed's
urlhas its private parts removed. - We may add new fields over time, so ignore any you don't recognise. If we ever make a breaking change,
apiVersionwill change.
Each request also carries these headers:
X-CheckTheCal-Event: the delivery type.X-CheckTheCal-Delivery: an ID that stays the same across our retries. Use it to avoid processing the same delivery twice.X-CheckTheCal-Signature: the signature, explained below.
Check the signature
Anyone can send a request to a public URL, so check the signature before trusting a delivery. The header looks like t=1790000000,v1=5257a8….
- Split the header on commas to get
t(a Unix timestamp) andv1(the signature). - Take the raw request body, exactly as received, before any JSON parsing.
- Compute an HMAC-SHA256 of
t, a full stop, then the raw body (<t>.<body>), using your signing secret as the key. Write the result as lowercase hex. - Compare it to
v1using a constant-time comparison. - Reject the request if
tis more than 5 minutes from your server's current time. That stops anyone replaying an old request.
In Node.js:
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(secret, rawBody, header) {
const parts = Object.fromEntries(header.split(',').map((p) => p.trim().split('=')));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
return expected.length === parts.v1?.length &&
timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}Retries and failures
Answer with any 2xx status to tell us a delivery arrived. If your endpoint errors or doesn't answer in time, we retry with a growing gap between attempts.
If an endpoint fails many times in a row, we turn it off and email you, so a broken integration doesn't fail quietly. It shows Turned off after failures on the Webhooks tab. Once it's fixed, choose Switch on there.
Every endpoint has a delivery log on the Webhooks tab: when each delivery was sent, the status your server answered with, and the start of its reply. Look there first when a delivery doesn't seem to arrive.
Still stuck? Browse all guides or email support@checkthecal.com.