Gareth Wilson Gareth Wilson

Guide to Cronofy Webhooks: Features and Best Practices

Published


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

FeatureDetails
ConfigurationPOST /v1/channels per account. No dashboard-configured URL
Signature headerCronofy-HMAC-SHA256, a comma-separated list of base64 digests
Signing keyYour application's OAuth client secret, prefixed CRN_. There is no separate webhook secret
Signed contentThe raw request body, and nothing else
Discriminatornotification.type, a body field. There is no event-type header
Response deadline5 seconds
RetriesFor 24 hours, after which the channel is closed permanently
Replay protectionNone. No timestamp, nonce, or delivery id is sent
RegionsSix 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:

TypeFires whenWhat to do
verificationImmediately after a channel is createdReturn 2xx. There's no token to echo and no challenge to reflect
changeSomething changed in the account's eventsCall Read Events with last_modified set to changes_since
profile_disconnectedA calendar profile needs reauthorizationPrompt the user to reconnect
conferencing_profile_disconnectedA conferencing profile disconnectedPrompt the user to reconnect
profile_initial_sync_completedInitial calendar sync finishedDo a follow-up sync
gdpr_requestedThe account invoked right-to-be-forgottenDelete 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 change as a ping and follow it with GET /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_id plus changes_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.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.