ICS Push Notifications PRERELEASE

Read as Markdown

When creating or updating a partner event, the event_invite_mechanism parameter controls how attendees are notified. When unspecified, the default mechanism is used - this is by pushing an event directly into the calendar for Read-Write Calendar Access Mode connections, or via an email invite for Free/Busy Only and No Connection Calendar Access Modes.

The subscriptions mechanism instead delivers a representation of the event and Smart Invite ICS payloads via a webhook subscription. This lets you take full control of how the invite is presented and which medium you use to send it.

The ICS content is generated using the same logic as a smart_invite, so the METHOD, attendees, organizer, and reminders are identical to what would be produced by that endpoint, so you can refer to those docs for more details.

The feature has two halves which work together:

  • event_invite_mechanism: { "type": "subscriptions" } on the event describes which mechanism is responsible for delivering the invite (over a webhook subscription, or the default where Cronofy handle adding directly into a Read-Write calendar or by invite email for Free/Busy calendars).
  • A subscription with an event_invite_updates interaction tells Cronofy where to deliver the invite (which URI accepts the callback).

Both halves must be present on the event for an invite to be pushed to the subscription.

Receiving invites via webhook subscription #

  1. Set event_invite_mechanism to { "type": "subscriptions" } on the Create or Update Event request
  2. Add a subscription with an event_invite_updates interaction:
POST /v1/calendars/cal_n23kjnwrw2_jsdfjksn234/events HTTP/1.1
Host: {data_center_url}
Authorization: Bearer {ACCESS_TOKEN}
Content-Type: application/json; charset=utf-8

{
  "event_id": "qTtZdczOccgaPncGJaCiLg",
  "summary": "Board meeting",
  "description": "Discuss plans for the next quarter.",
  "start": "2026-07-28T15:30:00Z",
  "end": "2026-07-28T17:00:00Z",
  "tzid": "Etc/UTC",
  "event_invite_mechanism": { "type": "subscriptions" },
  "attendees": {
    "invite": [
      { "email": "attendee@example.com", "display_name": "Example Attendee" }
    ]
  },
  "subscriptions": [
    {
      "type": "webhook",
      "uri": "https://example.com/cronofy/event_invite_updates",
      "interactions": [
        { "type": "event_invite_updates" }
      ]
    }
  ]
}

Request parameters #

event_invite_mechanism optional ALPHA  #

An object controlling how attendees are notified about the event. The object is open-ended so additional configuration can be added in future without breaking integrators.

event_invite_mechanism.type required ALPHA  #

A String selecting the delivery mechanism. Supported values:

  • default — Put an event directly in the calendar if connected with Write permissions, else send an email with an invite attached. This is the existing behaviour and the default when event_invite_mechanism is omitted.
  • smart_invite — Email a Smart Invite to the attendees. No event is written directly into the calendar, event if Write permissions have been granted. The recipient must accept the invite to add it to their calendar.
  • subscriptions — POST Smart Invite ICS payloads to subscriptions for the event_invite_updates interaction. The Push Notification includes an ICS is identical to the one the smart_invite mechanism would email. No email is sent or event is written directly into the calendar, it is the receiving apps responsibility to send.

If event_invite_mechanism is omitted entirely, the default mechanism is used.

subscriptions.interactions optional ALPHA  #

An array of interaction objects used to route notifications. The array itself is optional, in line with other event subscriptions, but to receive a subscription invite at least one subscription on the event must include an entry with type set to event_invite_updates.

When event_invite_mechanism.type is subscriptions, every request that sets the mechanism must also include a subscription with an event_invite_updates interaction. Requests that do not are rejected with a 422 response. See Validation below.

Subscriptions without an event_invite_updates interaction are unaffected by the subscriptions mechanism and continue to behave as before.

subscriptions.interactions.type optional ALPHA  #

The String identifying the interaction. For subscription invites the value is event_invite_updates.

Example callback #

For each subscription with an event_invite_updates interaction, Cronofy will POST a JSON payload containing the event details and an event_invites array. Each entry in event_invites bundles one or more attendees who should receive the same ICS file. Active recipients on a create or update typically share a single METHOD:REQUEST ICS (one entry, many attendees), while an attendee who has just been removed from the event receives a dedicated METHOD:CANCEL ICS (its own entry):

POST /cronofy/event_invite_updates HTTP/1.1
Host: example.com
Content-Type: application/json; charset=utf-8
Cronofy-HMAC-SHA256: {Base64(HmacSHA256(body_bytes, CLIENT_SECRET))}

{
  "notification": {
    "type": "event_subscription",
    "interactions": [
      { "type": "event_invite_updates" }
    ]
  },
  "event": {
    "calendar_id": "cal_n23kjnwrw2_jsdfjksn234",
    "event_id": "qTtZdczOccgaPncGJaCiLg",
    "event_uid": "evt_partner_60ba0f2968ed0f6e66e1ab68",
    "summary": "Board meeting",
    "description": "Discuss plans for the next quarter.",
    "start": "2026-07-28T15:30:00Z",
    "end": "2026-07-28T17:00:00Z",
    "deleted": false,
    "attendees": [
      { "email": "attendee@example.com", "display_name": "Example Attendee", "status": "needs_action" }
    ],
    "organizer": {
      "email": "host@example.com",
      "display_name": "Event Host"
    },
    "transparency": "opaque",
    "status": "confirmed"
  },
  "event_invites": [
    {
      "attendees": [
        { "email": "host@example.com", "display_name": "Event Host" },
        { "email": "attendee@example.com", "display_name": "Example Attendee" }
      ],
      "icalendar_operation": "request",
      "icalendar": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nMETHOD:REQUEST\r\n..."
    },
    {
      "attendees": [
        { "email": "removed@example.com" }
      ],
      "icalendar_operation": "cancel",
      "icalendar": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nMETHOD:CANCEL\r\n..."
    }
  ]
}

When the partner event itself is deleted, event.deleted is true and every entry in event_invites is a METHOD:CANCEL so that all attendees, including the host, can be sent a cancellation:

POST /cronofy/event_invite_updates HTTP/1.1
Host: example.com
Content-Type: application/json; charset=utf-8
Cronofy-HMAC-SHA256: {Base64(HmacSHA256(body_bytes, CLIENT_SECRET))}

{
  "notification": {
    "type": "event_subscription",
    "interactions": [
      { "type": "event_invite_updates" }
    ]
  },
  "event": {
    "calendar_id": "cal_n23kjnwrw2_jsdfjksn234",
    "event_id": "qTtZdczOccgaPncGJaCiLg",
    "event_uid": "evt_partner_60ba0f2968ed0f6e66e1ab68",
    "summary": "Board meeting",
    "description": "Discuss plans for the next quarter.",
    "start": "2026-07-28T15:30:00Z",
    "end": "2026-07-28T17:00:00Z",
    "deleted": true,
    "attendees": [
      { "email": "attendee@example.com", "display_name": "Example Attendee", "status": "cancelled" }
    ],
    "organizer": {
      "email": "host@example.com",
      "display_name": "Event Host"
    },
    "transparency": "opaque",
    "status": "cancelled"
  },
  "event_invites": [
    {
      "attendees": [
        { "email": "host@example.com", "display_name": "Event Host" },
        { "email": "attendee@example.com", "display_name": "Example Attendee" }
      ],
      "icalendar_operation": "cancel",
      "icalendar": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nMETHOD:CANCEL\r\n..."
    }
  ]
}

Callback parameters #

notification.type  #

Always event_subscription. This matches the envelope used by sibling subscriptions such as conferencing services, meeting agents, and event triggers.

notification.interactions  #

An array containing the interaction(s) this callback was delivered for. For subscription invites the array contains a single entry with type set to event_invite_updates.

event  #

Details of the partner event for which the invite has been generated. The fields present map to those documented for our Read Events API. The event.deleted field indicates whether the event itself has been deleted; per-attendee outcomes are reported on each entry in event_invites.

The event.organizer is the account whose access_token authenticated the Create or Update Event request. This account is the host of the event and is the ORGANIZER in the generated ICS, so it also appears in the event_invites bundles.

When the event has been deleted, each event.attendees[].status is cancelled. This value is specific to this callback and extends the participation statuses documented for the Read Events API.

event_invites  #

An array of invite envelopes. Each entry groups one or more attendees who should receive the same ICS file together with the operation that ICS represents. A typical update produces one entry for the bulk METHOD:REQUEST (all active attendees and the host) plus one additional entry per attendee removed by the update (each with their own METHOD:CANCEL). When the event itself is deleted, every entry is a METHOD:CANCEL.

event_invites[].attendees  #

The attendees this invite is intended for. Each entry has an email and (where known) a display_name. When the array contains more than one attendee, every recipient should be sent the same icalendar — your application can choose whether to send a single email with multiple To/Cc addresses or separate emails with the same body.

event_invites[].icalendar_operation  #

The action this invite represents, surfaced so receivers can route REQUEST vs CANCEL without parsing the ICS payload.

  • request - the attached icalendar is a METHOD:REQUEST (new invitation or update for active attendees).
  • cancel - the attached icalendar is a METHOD:CANCEL (the partner event has been deleted, or these specific attendees have been removed from the event).

The value matches the METHOD line inside the icalendar string.

event_invites[].icalendar  #

The raw ICS String for the bundled attendees. This is the payload you would attach to an email if you were sending the invite yourself.

Callback headers #

Cronofy-HMAC-SHA256  #

Can optionally be used to verify that the notification was sent by Cronofy.

This HMAC uses the SHA256 algorithm, keyed with the application's client secret, to generate a Base64 encoded hash of the request body.

When an application has multiple active client secrets, a value is generated for each active secret, separated by commas. For example:

Cronofy-HMAC-SHA256: {HMAC_FROM_SECRET_1},{HMAC_FROM_SECRET_2}

Examples are available in our cronofy/notification-hmac-examples Github repository, and our API libraries include methods to verify this header.

HMACs are generated in the same way for all callback events.

Validation #

The wiring between event_invite_mechanism and the event_invite_updates interaction is validated up front:

  • Every request that sets event_invite_mechanism.type to subscriptions must include at least one subscription with an event_invite_updates interaction. Requests that fail this check are rejected with a 422 response and an event_invite_mechanism: event_invite_updates_subscription_required error.
  • Updates that do not set event_invite_mechanism are not affected by this check, but if no matching subscription is present at delivery time the notification is dropped; no callback is sent and no email is dispatched.
  • Setting event_invite_mechanism.type to a value other than subscriptions leaves subscriptions with event_invite_updates interactions in place but unused.

Delivery behaviour #

  • Deliveries are debounced. Rapid updates to the same partner event are coalesced into a single notification.
  • Each notification is POSTed independently to every matching subscription URI. A non-2xx response from one URI does not prevent delivery to the others.
  • To be considered successfully delivered, your application must respond in the 2xx range of HTTP status codes within 7 seconds.
  • Failed deliveries are retried with exponential backoff.