Guide to Cronofy Webhooks: Features and Best Practices
Cronofy is calendar API and scheduling infrastructure. Its webhooks are called push notifications, and they arrive at a callback URL that belongs to a notification channel you create per account rather than to a dashboard setting.
This guide covers how Cronofy push notifications work, how to verify them, why a change notification tells you nothing about what changed, and the best practices for production.
What are Cronofy webhooks?
A Cronofy push notification tells you that something happened in a connected calendar account. It does not tell you what. For the notification type you'll receive most often, change, the payload carries a timestamp and nothing else, and you follow it with an API read to fetch the delta.
The callback URL is a property of a notification channel, created through the API against a specific account's access token. There's no global webhook URL in a dashboard, which means channel creation is part of your onboarding flow rather than a one-time setup step.
Cronofy webhook features
| Feature | Details |
|---|---|
| Configuration | POST /v1/channels per account. No dashboard-configured URL |
| Signature header | Cronofy-HMAC-SHA256, a comma-separated list of base64 digests |
| Signing key | Your application's OAuth client secret, prefixed CRN_. There is no separate webhook secret |
| Signed content | The raw request body, and nothing else |
| Discriminator | notification.type, a body field. There is no event-type header |
| Response deadline | 5 seconds |
| Retries | For 24 hours, after which the channel is closed permanently |
| Replay protection | None. No timestamp, nonce, or delivery id is sent |
| Regions | Six data centres, each with its own API host |
Cronofy publishes no source-IP allowlist, so don't build one into your receiver.
Common events
The discriminator is notification.type. Six types arrive on a notification channel:
| Type | Fires when | What to do |
|---|---|---|
verification | Immediately after a channel is created | Return 2xx. There's no token to echo and no challenge to reflect |
change | Something changed in the account's events | Call Read Events with last_modified set to changes_since |
profile_disconnected | A calendar profile needs reauthorization | Prompt the user to reconnect |
conferencing_profile_disconnected | A conferencing profile disconnected | Prompt the user to reconnect |
profile_initial_sync_completed | Initial calendar sync finished | Do a follow-up sync |
gdpr_requested | The account invoked right-to-be-forgotten | Delete their data on your side |
Two more values share the same notification.type field but arrive from other Cronofy callback surfaces: smart_invite from Smart Invite callbacks, and event_subscription from conferencing and event subscriptions. If you route on notification.type and only account for the six above, those two fall through your switch.
event_subscription is a container. The interaction that actually fired is nested at notification.interactions[].type, as conferencing_assigned or conferencing_failing, with an optional subtype giving the reason. Routing on the top-level type alone collapses every conferencing callback into one bucket.
The envelope is consistent across all of them:
{
"notification": { "type": "change", "changes_since": "2026-08-26T09:24:16Z" },
"channel": {
"channel_id": "chn_54cf7c7cb4ad4c1027000001",
"callback_url": "https://example.com/webhooks/cronofy",
"filters": { "calendar_ids": ["cal_n23kjnwrw2_sakdnawerd3"], "only_managed": false }
}
}
notification.changes_since appears only on change. channel.filters reflects non-default filters and may be absent.
Cronofy's docs ask that your code tolerate types it doesn't recognise by ignoring them, so give your handler a default branch that returns 2xx rather than falling through to an error.
See Cronofy webhook payloads in action. Inspect and replay sample push notifications in the Hookdeck Console — no account or setup required.
Setting up Cronofy webhooks
Create a channel with the account's access token, against the data centre that account belongs to:
curl -X POST "$CRONOFY_DATA_CENTER_URL/v1/channels" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"callback_url":"https://example.com/webhooks/cronofy"}'
Cronofy sends a verification notification immediately, so a failure here shows up straight away rather than the first time a calendar changes.
Hosts differ per region: api.cronofy.com (US), api-uk.cronofy.com, api-de.cronofy.com, api-au.cronofy.com, api-ca.cronofy.com and api-sg.cronofy.com. Channel creation and the follow-up Read Events call must both hit the account's own data centre.
Securing Cronofy webhooks
Cronofy computes HMAC-SHA256 over the raw request body, keyed with your application's OAuth client secret, and base64-encodes it. The header holds a comma-separated list of digests, one per active client secret, so that secrets can be rotated without dropping deliveries. Accept the request if any element matches:
const crypto = require('crypto');
function verifyCronofyWebhook(rawBody, hmacHeader, clientSecret) {
if (!hmacHeader || !clientSecret) return false;
const expected = Buffer.from(
crypto.createHmac('sha256', clientSecret).update(rawBody).digest('base64')
);
// reduce rather than some, so every candidate is compared and timing
// doesn't reveal which one matched.
return hmacHeader.split(',').reduce((matched, candidate) => {
const buf = Buffer.from(candidate.trim());
const ok = buf.length === expected.length && crypto.timingSafeEqual(buf, expected);
return matched || ok;
}, false);
}
The digest is standard base64 rather than base64url. Cronofy's own published test vector, BmQmWVuZ70ILWjr1CAt5oC7YOolgnku4WZtlrKfx/6k=, contains a /, which is a quick way to catch an encoding mistake.
Because the same scheme covers Cronofy's other callback surfaces, one verifier handles Smart Invite callbacks and Meeting Agent notifications too.
Make Cronofy webhooks production-ready. Hookdeck Event Gateway verifies the signature, acknowledges within Cronofy's deadline, and durably queues every notification.
Cronofy webhook limitations and pain points
A whole-string signature compare works until you rotate
The Problem: Comparing the Cronofy-HMAC-SHA256 header against a single computed digest passes every test you write and keeps working in production, right up until a second client secret becomes active. From that moment the header holds two comma-joined digests, the whole-string compare never matches, and every delivery is rejected.
Why It Happens: With one active secret the header looks exactly like a single value, so the list is invisible until rotation begins.
Workarounds:
- Split on commas and treat verification as a membership test from the start.
- Test with a two-element header, using Cronofy's two published test vectors.
How Hookdeck Can Help: Hookdeck verifies Cronofy signatures at the edge, so rotation is handled in one place instead of in every receiver.
change doesn't tell you what changed
The Problem: Handlers get written to read the changed event out of the payload. There's nothing there to read. change carries changes_since and the channel object, and that's all.
Why It Happens: Cronofy sends a thin notification by design. The delta lives behind an API call.
Workarounds:
- Treat
changeas a ping and follow it withGET /v1/events?tzid=Etc/UTC&last_modified={changes_since}against the account's data centre. - Don't build reconciliation that expects your own writes to echo back. Cronofy doesn't send notifications for changes your API calls caused.
How Hookdeck Can Help: Hookdeck's transformations can enrich the thin notification before it reaches your service, so your handler receives the delta rather than a ping.
A slow handler eventually destroys the channel
The Problem: Cronofy retries failed deliveries for 24 hours. If nothing succeeds in that window, the channel is closed automatically and no further notifications are sent for that account. Recovery means creating a new channel, and until you do, that account is silently stale.
Why It Happens: The response deadline is 5 seconds and the retry window has a hard end. A handler that does its work synchronously can sit just over the line for a day without anyone noticing.
Workarounds:
- Acknowledge with 2xx immediately and process asynchronously.
- Monitor your channels. A closed channel produces silence, not errors.
How Hookdeck Can Help: Hookdeck acknowledges within the deadline on your behalf and retries against your service on its own schedule, so a slow consumer can't cost you the channel.
There is nothing to replay-check against
The Problem: Cronofy signs the body and only the body, so a captured notification stays valid forever and can be replayed at your endpoint.
Why It Happens: The signing scheme has no replay-protection material, which also means the usual timestamp tolerance check is impossible to implement.
Workarounds:
- Key idempotency on
channel_idpluschanges_since, or on the result of the follow-up read. - Make the follow-up read itself idempotent, since that's where the real work happens.
How Hookdeck Can Help: Hookdeck deduplicates deliveries before they reach your service.
Getting the region wrong looks like an auth failure
The Problem: Creating a channel against one data centre and reading events from another produces authorization errors that read like a bad token.
Why It Happens: Cronofy is multi-region and accounts belong to a specific data centre. The hosts differ but the API shape doesn't.
Workarounds:
- Store the account's data centre URL alongside its tokens and use it for both channel creation and the follow-up read.
How Hookdeck Can Help: Hookdeck routes each account's traffic to the right destination, so region stays a configuration detail rather than a branch in your handler.
Best practices
Verify against the list, not the value
Split Cronofy-HMAC-SHA256 on commas and accept any match. This costs nothing today and is the difference between a working and a broken integration on the day you rotate a secret.
Acknowledge in well under five seconds
Return 2xx before doing any work. The deadline is 5 seconds, and the consequence of missing it for long enough is a closed channel rather than a lost message. See why to process webhooks asynchronously.
Give your switch a default branch
Cronofy explicitly asks you to ignore unrecognised notification types. Two documented values already arrive from other callback surfaces, and more may follow.
Dedupe without a timestamp
There's no replay-protection material to check, so idempotency has to come from your application. See our guide to webhook idempotency.
Reconcile your channel list on a schedule
Compare active channels against the accounts that should have one. This is the only way a channel Cronofy closed on you shows up as something other than an account that mysteriously stopped syncing.
Conclusion
Cronofy push notifications are thin by design. Verify the comma-separated Cronofy-HMAC-SHA256 header as a list rather than a value, acknowledge within five seconds so a slow handler never costs you the channel, follow every change with a Read Events call on changes_since, and build idempotency in your application because the signing scheme gives you nothing to replay-check against.
Hookdeck Event Gateway verifies the signature, acknowledges inside Cronofy's deadline, deduplicates, and durably queues every notification, so your service processes each change once and on its own schedule.
Get started with Hookdeck for free and handle Cronofy webhooks reliably in minutes.