API reference

Every endpoint of the v1 API. Field names and shapes match Cronofy’s for the endpoints both have; the few additions are listed under Migrating from Cronofy. Examples use made-up ids in the real formats.

Basics

  • Base URL of the API: https://api.calmonkey.com. /oauth/authorize is on https://app.calmonkey.com, because a browser opens it.
  • Bodies are JSON (Content-Type: application/json). The OAuth endpoints and /v1/application_calendars also take application/x-www-form-urlencoded.
  • Booleans in query strings are true, false, 1 or 0. Repeated parameters use brackets: calendar_ids[]=a&calendar_ids[]=b.
  • Ids: accounts acc_… (application calendars apc_…), profiles pro_…, calendars cal_…, channels chn_…, events evt_…. Treat them as opaque strings.
  • Errors and rate limits: Errors and limits.

GET /oauth/authorize

GEThttps://app.calmonkey.com/oauth/authorize

Opens the hosted connect page in the user’s browser. Not called by your server. Parameters and callbacks are described in the quickstart.

Query parameters
NameTypeDescription
response_typerequiredstringcode. Anything else returns error=unsupported_response_type to your redirect URI.
client_idrequiredstringYour client id.
redirect_urirequiredstringA registered redirect URI, exactly.
scopestringDefault read_write. A malformed scope returns error=invalid_scope.
statestringReturned unchanged.
provider_namestringgoogle, office365, live_connect or apple: go straight to that provider.
link_tokenstringAdds the connected calendar account to an existing account (link tokens).

Back at your redirect URI: ?code=…&state=…, or ?error=access_denied&state=… when the user cancels, declines, or the calendar account is already connected to another account of your application.

POST /oauth/token

POSThttps://api.calmonkey.com/oauth/token

Authentication: Your client id and client secret, in the request body.

grant_type=authorization_code

Body of an authorization code exchange
NameTypeDescription
client_idrequiredstringYour client id.
client_secretrequiredstringYour client secret.
grant_typerequiredstringauthorization_code
coderequiredstringThe code from the callback. Valid once, for 10 minutes.
redirect_urirequiredstringThe same redirect URI the authorization request used.
200 OK
{
  "token_type": "bearer",
  "access_token": "cmat_Tz5vTHRrZ5QIIK0Ng0zRJCkt9ZqmjBrNn1mv7x4DbGg",
  "expires_in": 10800,
  "refresh_token": "cmrt_D0L78QVguUuBnvOcex1VPdSP0uPRYOCtQMGeFi8Qku8",
  "scope": "read_write",
  "account_id": "acc_38da51e7b1345bb5fae3656a",
  "sub": "acc_38da51e7b1345bb5fae3656a",
  "linking_profile": {
    "provider_name": "google",
    "profile_id": "pro_7782a96ff1609061631ebc5b",
    "profile_name": "dana@example.com"
  }
}
Token response fields
NameTypeDescription
access_tokenstringFor Authorization: Bearer. Valid for expires_in seconds (10,800: 3 hours).
refresh_tokenstringGets new access tokens. Does not rotate.
scopestringread_write
sub / account_idstringThe account (acc_…). The same value under both names.
linking_profileobjectThe calendar account connected in this run: provider_name, profile_id, profile_name.

grant_type=refresh_token

Body of a refresh
NameTypeDescription
client_idrequiredstringYour client id.
client_secretrequiredstringYour client secret.
grant_typerequiredstringrefresh_token
refresh_tokenrequiredstringThe account's refresh token.
200 OK
{
  "token_type": "bearer",
  "access_token": "cmat_8yWq0eYb2Hn6R1sVdKp3LmXc5ZtA9uGf4JoQiNwE7Bv",
  "expires_in": 10800,
  "refresh_token": "cmrt_D0L78QVguUuBnvOcex1VPdSP0uPRYOCtQMGeFi8Qku8",
  "scope": "read_write"
}

Errors are 400 with {"error": "…"}: invalid_client (wrong client id or secret), invalid_grant (code used, expired or for another redirect URI; refresh token revoked), invalid_request (a field is missing), unsupported_grant_type.

POST /oauth/token/revoke

POSThttps://api.calmonkey.com/oauth/token/revoke

Authentication: Your client id and client secret, in the request body.

Body of a revoke
NameTypeDescription
client_idrequiredstringYour client id.
client_secretrequiredstringYour client secret.
tokenrequiredstringAn access token or a refresh token of the account.

200, empty body, whether or not the token was known. Revokes the whole grant: every token of the account, its channels, its stored provider credentials and its cached events. An application calendar is deleted with it; opening the same application_calendar_id again gives a new, empty calendar.

POST /v1/application_calendars

POSThttps://api.calmonkey.com/v1/application_calendars

Authentication: Your client id and client secret, in the request body. Wrong credentials answer 401 with an empty body.

Body
NameTypeDescription
client_idrequiredstringYour client id.
client_secretrequiredstringYour client secret.
application_calendar_idrequiredstringYour name for the calendar, up to 255 characters. The same id always opens the same calendar.
200 OK
{
  "token_type": "bearer",
  "access_token": "cmat_3kVb7Qx1Lr9Zs5Hd0Wn2Pc8Fy4Mj6Tg1Ae5Ku7Oi3Ys0",
  "expires_in": 10800,
  "refresh_token": "cmrt_9Xc2Vb6Nm1Lk4Jh8Gf3Ds7Aq0Pw5Oe2Iu9Yt6Rr1Ee4",
  "scope": "read_write",
  "account_id": "apc_c4a4b70dce47c34ad4d384f8",
  "sub": "apc_c4a4b70dce47c34ad4d384f8",
  "linking_profile": {
    "provider_name": "calmonkey",
    "profile_id": "pro_1b9e64c0d2a7f3e85c6d4a21",
    "profile_name": "test-calendar-1"
  },
  "application_calendar_id": "test-calendar-1"
}

Creates the calendar on first use and returns tokens for it: a token response with sub apc_… and your application_calendar_id. Each call returns a new token pair (in Cronofy compatibility mode, the pair the calendar already has while it is valid). The account has one profile and one writable, primary calendar.

GET /v1/account

GEThttps://api.calmonkey.com/v1/account

Authentication: Authorization: Bearer <access_token>

200 OK
{
  "account": {
    "account_id": "acc_38da51e7b1345bb5fae3656a",
    "type": "account",
    "email": "dana@example.com",
    "name": "",
    "scope": "read_write",
    "default_tzid": "Etc/UTC"
  }
}

type is account, or application_calendar for an application calendar.

GET /v1/userinfo

GEThttps://api.calmonkey.com/v1/userinfo

Authentication: Authorization: Bearer <access_token>

Who the token belongs to, with every connected profile and its calendars.

200 OK
{
  "sub": "acc_38da51e7b1345bb5fae3656a",
  "email": "dana@example.com",
  "zoneinfo": "Etc/UTC",
  "calmonkey.type": "account",
  "calmonkey.data": {
    "authorization": { "scope": "read_write", "status": "active" },
    "profiles": [
      {
        "provider_name": "google",
        "provider_service": "gmail",
        "profile_id": "pro_7782a96ff1609061631ebc5b",
        "profile_name": "dana@example.com",
        "profile_connected": true,
        "profile_initial_sync_required": false,
        "profile_calendars": [
          {
            "calendar_id": "cal_89645320138ae1ac547189a1",
            "calendar_name": "dana@example.com",
            "calendar_readonly": false,
            "calendar_deleted": false,
            "calendar_primary": true,
            "calendar_integrated_conferencing_available": true,
            "calendar_attachments_available": false,
            "permission_level": "unrestricted"
          }
        ]
      }
    ]
  }
}

For an application calendar, application_calendar_id takes the place of email, and the data includes application_calendar. In Cronofy compatibility mode the keys are cronofy.type and cronofy.data.

GET /v1/profiles

GEThttps://api.calmonkey.com/v1/profiles

Authentication: Authorization: Bearer <access_token>

200 OK
{
  "profiles": [
    {
      "provider_name": "office365",
      "provider_service": "office365",
      "profile_id": "pro_7782a96ff1609061631ebc5b",
      "profile_name": "dana@contoso.example",
      "profile_connected": false,
      "profile_relink_url": null,
      "profile_initial_sync_required": false,
      "profile_calendars": [
        {
          "calendar_id": "cal_89645320138ae1ac547189a1",
          "calendar_name": "Calendar",
          "calendar_readonly": false,
          "calendar_deleted": false,
          "calendar_primary": true,
          "calendar_integrated_conferencing_available": true,
          "calendar_attachments_available": false,
          "permission_level": "unrestricted"
        }
      ]
    }
  ]
}
  • profile_connected: false means the person has to connect that calendar account again (the provider refused our access, or the password was revoked). Send them through /oauth/authorize with the same provider_name: the same profile, calendars and account come back.
  • provider_service: gmail, gsuite, office365, outlook_com, icloud, or calmonkey for an application calendar.
  • profile_relink_url is null: a profile is reconnected through /oauth/authorize, as above.
  • profile_initial_sync_required is true until the profile’s first sync has finished.

GET /v1/calendars

GEThttps://api.calmonkey.com/v1/calendars

Authentication: Authorization: Bearer <access_token>

200 OK
{
  "calendars": [
    {
      "provider_name": "google",
      "profile_id": "pro_7782a96ff1609061631ebc5b",
      "profile_name": "dana@example.com",
      "calendar_id": "cal_89645320138ae1ac547189a1",
      "calendar_name": "dana@example.com",
      "calendar_readonly": false,
      "calendar_deleted": false,
      "calendar_primary": true,
      "calendar_integrated_conferencing_available": true,
      "calendar_attachments_available": false,
      "permission_level": "unrestricted"
    },
    {
      "provider_name": "google",
      "profile_id": "pro_7782a96ff1609061631ebc5b",
      "profile_name": "dana@example.com",
      "calendar_id": "cal_2f6c8e1a9b3d4f5a6b7c8d9e",
      "calendar_name": "Public holidays",
      "calendar_readonly": true,
      "calendar_deleted": false,
      "calendar_primary": false,
      "calendar_integrated_conferencing_available": false,
      "calendar_attachments_available": false,
      "permission_level": "unrestricted"
    }
  ],
  "sub": "acc_38da51e7b1345bb5fae3656a"
}

Calendars deleted at the provider stay in the list with calendar_deleted: true. calendar_integrated_conferencing_available is true on a calendar that adds a meeting link of its own, so conferencing.profile_id: "integrated" is accepted there: a Google calendar, and a Microsoft calendar that allows online meetings, when your application can write to it (provider table). calendar_attachments_available is false for every calendar.

GET /v1/free_busy

GEThttps://api.calmonkey.com/v1/free_busy

Authentication: Authorization: Bearer <access_token>

Query parameters of free/busy
NameTypeDescription
tzidrequiredstringTime zone the request’s dates are read in, e.g. Australia/Sydney.
fromdate or timeStart, a day (2026-11-03) in tzid or a time with an offset. Defaults to 42 days ago.
todate or timeEnd, exclusive. Defaults to 201 days from now.
calendar_ids[]string, repeatableOnly these calendars (up to 100). Default: every calendar of the account.
include_managedbooleanInclude events your application created. Default false.
include_idsbooleanAdd event_id to the periods of events your application created.
localized_timesbooleanTimes with the offset of tzid instead of UTC.
200 OK
{
  "pages": { "current": 1, "total": 1 },
  "free_busy": [
    {
      "calendar_id": "cal_89645320138ae1ac547189a1",
      "event_uid": "evt_d589a60f0b07d0e721482ee2",
      "start": "2026-11-02T22:00:00Z",
      "end": "2026-11-02T23:30:00Z",
      "free_busy_status": "busy"
    },
    {
      "calendar_id": "cal_89645320138ae1ac547189a1",
      "event_uid": "evt_6a1f2c3d4e5b6a7c8d9e0f12",
      "start": "2026-11-03T03:00:00Z",
      "end": "2026-11-03T04:00:00Z",
      "free_busy_status": "busy",
      "event_id": "booking-1042"
    }
  ]
}
Free/busy period fields
NameTypeDescription
calendar_idstringThe calendar of the event.
event_uidstringIdentifies the event (evt_…).
start / endstringTimes in UTC (or local with localized_times), or dates for all-day events with an exclusive end. Not clipped to the window.
free_busy_statusstringbusy, tentative or free (an event shown as free).
event_idstringOnly with include_ids=true, only on your application’s own events: the id you chose.

A repeating event gives one period for each occurrence in the window. Invitations the person declined are never busy and are left out.

Up to 1,000 periods per page. When there are more, pages.next_page is an absolute URL for the next page: request it with the same token, as it is.

GET /v1/events

GEThttps://api.calmonkey.com/v1/events

Authentication: Authorization: Bearer <access_token>

Query parameters of events
NameTypeDescription
tzidrequiredstringTime zone the request’s dates are read in, e.g. Australia/Sydney.
fromdate or timeStart, a day (2026-11-03) in tzid or a time with an offset. Defaults to 42 days ago.
todate or timeEnd, exclusive. Defaults to 201 days from now.
calendar_ids[]string, repeatableOnly these calendars (up to 100). Default: every calendar of the account.
include_managedbooleanInclude events your application created. Default false.
only_managedbooleanOnly the events your application created.
include_deletedbooleanInclude events deleted since they were cached (deleted: true).
last_modifiedtimeOnly events changed at or after this time. Use a change notification’s changes_since.
localized_timesbooleanTimes with the offset of tzid instead of UTC.
200 OK
{
  "pages": { "current": 1, "total": 1 },
  "events": [
    {
      "calendar_id": "cal_89645320138ae1ac547189a1",
      "event_uid": "evt_6a1f2c3d4e5b6a7c8d9e0f12",
      "event_id": "booking-1042",
      "summary": "Lash lift with Grace",
      "description": "Booked through Your App",
      "start": "2026-11-03T03:00:00Z",
      "end": "2026-11-03T04:00:00Z",
      "deleted": false,
      "created": "2026-10-20T01:12:09Z",
      "updated": "2026-10-20T01:12:09Z",
      "location": { "description": "12 Harbour St, Sydney" },
      "url": "https://yourapp.example/bookings/1042",
      "participation_status": "accepted",
      "attendees": [],
      "transparency": "opaque",
      "extended_transparency": "opaque",
      "event_private": false,
      "organizer": null,
      "status": "confirmed",
      "categories": [],
      "recurring": false,
      "options": { "delete": true, "update": true, "change_participation_status": false }
    },
    {
      "calendar_id": "cal_89645320138ae1ac547189a1",
      "event_uid": "evt_0b9d4e7f2a1c3b5d6e8f9a01",
      "event_id": "consult-88",
      "summary": "Colour consultation",
      "description": "",
      "start": "2026-11-05T00:00:00Z",
      "end": "2026-11-05T00:30:00Z",
      "deleted": false,
      "created": "2026-10-20T01:15:40Z",
      "updated": "2026-10-21T22:03:11Z",
      "participation_status": "accepted",
      "attendees": [
        { "email": "grace@example.com", "display_name": "Grace Park", "status": "accepted" },
        { "email": "alex@example.com", "display_name": "Alex Rivera", "status": "needs_action" }
      ],
      "transparency": "opaque",
      "extended_transparency": "opaque",
      "event_private": false,
      "organizer": { "email": "dana@example.com", "display_name": "Dana Lee" },
      "status": "confirmed",
      "categories": [],
      "recurring": false,
      "conferencing": { "provider_name": "google_meet", "join_url": "https://meet.google.com/abc-defg-hij" },
      "options": { "delete": true, "update": true, "change_participation_status": false }
    },
    {
      "calendar_id": "cal_89645320138ae1ac547189a1",
      "event_uid": "evt_5c2e8a1f7b3d9e0a4c6f1b23",
      "event_id": "class-7",
      "summary": "Tuesday pilates",
      "description": "",
      "start": "2026-11-02T23:00:00Z",
      "end": "2026-11-03T00:00:00Z",
      "deleted": false,
      "created": "2026-10-20T01:20:02Z",
      "updated": "2026-10-20T01:20:02Z",
      "participation_status": "accepted",
      "attendees": [],
      "transparency": "opaque",
      "extended_transparency": "opaque",
      "event_private": false,
      "organizer": null,
      "status": "confirmed",
      "categories": [],
      "recurring": true,
      "recurrence": {
        "rules": [{ "frequency": "weekly", "count": 10, "by_day": [{ "day": "tuesday" }] }]
      },
      "series_identifier": "9f2c4b7e1a6d8035c2e4f6a8b0d1e3f5",
      "series_master": { "event_id": "class-7", "event_uid": "evt_5c2e8a1f7b3d9e0a4c6f1b23" },
      "occurrence_date": "2026-11-03",
      "options": { "delete": true, "update": true, "change_participation_status": false }
    }
  ]
}
  • Up to 300 events per page; follow pages.next_page.
  • event_id appears only on events your application created, and options.update / options.delete are true only for those.
  • Cancelled events are not included.
Guests, meeting links and repeating events on an event
NameTypeDescription
attendeesobject[]The guests: email (lower case), display_name (string or null) and status, their reply: needs_action, accepted, declined, tentative or unknown. Empty for an event without guests.
organizerobject or nullWho the event belongs to: email and display_name (string or null). Null when the calendar names nobody.
participation_statusstringThe calendar owner’s own reply, with the same values as a guest’s status. An invitation they declined is returned with declined, and never counts as busy in /v1/free_busy.
conferencingobjectThe event’s meeting link: { "provider_name": "…", "join_url": "…" }, or { "pending": true } from the moment you ask for one until the calendar has made it. provider_name is google_meet, ms_teams, google_hangouts, skype_for_business, skype or other. Left out when the event has no link.
recurringbooleanTrue on every occurrence of a repeating event. A repeating event is returned as its occurrences, one event each.
series_identifierstringOn every occurrence of a repeating event: the same value for all occurrences of one series, and for nothing else.
occurrence_datedateOn every occurrence: its original day in the series’ time zone (2026-11-24). An occurrence that was moved keeps it. This is the day DELETE and /events/occurrences name an occurrence by.
series_masterobjectOn the occurrences of a series your application wrote: event_id (the id you wrote the series with) and event_uid of its first occurrence.
recurrenceobjectOn the first occurrence of a series your application wrote: { "rules": [ … ] }, the rule as you sent it (interval is left out when it is 1).
  • Every occurrence of a series your application wrote carries your event_id, so only_managed and include_managed treat the whole series as yours. Tell the occurrences apart by occurrence_date or event_uid.
  • Guests, organisers, replies and meeting links are returned for every event in the calendar, whoever created it. Events are read from 42 days back to 201 days ahead, and a series gives the occurrences inside that window.

POST /v1/calendars/{calendar_id}/events

POSThttps://api.calmonkey.com/v1/calendars/{calendar_id}/events

Authentication: Authorization: Bearer <access_token>

Creates the event with this event_id, or replaces it. The same call invites guests, makes the event repeat and adds a meeting link.

Body of an event upsert
NameTypeDescription
event_idrequiredstringYour id for the event, up to 512 characters. Unique per calendar and application.
summaryrequiredstringTitle, up to 2,000 characters.
descriptionstringUp to 50,000 characters.
startrequiredstring or objectA time with an offset (2026-11-03T03:00:00Z), a date for an all-day event, or { "time": "…", "tzid": "…" }.
endrequiredstring or objectAs start. A date when start is a date (exclusive), a time when start is a time, not before start.
tzidstringThe event's time zone, used by the provider to show it.
location.descriptionstringUp to 2,000 characters.
urlstringA link, returned on reads. Up to 2,048 characters.
transparencystringopaque (busy, the default) or transparent (shown as free).
attendees.inviteobject[]Guests to add: { "email": "…", "display_name": "…" }, the name optional. See Guests.
attendees.removeobject[]Guests to take off: { "email": "…" }.
notify_attendeesbooleanWhether the calendar tells the guests about this write. Default true.
recurrenceobject or nullHow the event repeats: rules and exceptions. null stops it repeating. See Repeating events.
conferencing.profile_idstringdefault, integrated or none. See Meeting links.
Request body
{
  "event_id": "booking-1042",
  "summary": "Lash lift with Grace",
  "description": "Booked through Your App",
  "start": "2026-11-03T03:00:00Z",
  "end": "2026-11-03T04:00:00Z",
  "tzid": "Australia/Sydney",
  "location": { "description": "12 Harbour St, Sydney" },
  "url": "https://yourapp.example/bookings/1042",
  "transparency": "opaque"
}

202 Accepted, empty body. Fields the API does not have (reminders, for example) are accepted and ignored. 404 for a calendar the account does not have, 403 for a read-only one, 422 for an invalid body or for something the calendar cannot do (the keys).

Guests

Add guests to an event with attendees.invite. The calendar’s own service sends each guest the invitation, sends an update when you change the event and a cancellation when you delete it, exactly as if the person had invited them from their calendar. Guests answer in their own calendar or mail, and the answers come back as each attendee’s status in GET /v1/events; a change notification tells you when one arrives.

Invite two guests and add a meeting link
{
  "event_id": "consult-88",
  "summary": "Colour consultation",
  "start": "2026-11-05T00:00:00Z",
  "end": "2026-11-05T00:30:00Z",
  "tzid": "Australia/Sydney",
  "attendees": {
    "invite": [
      { "email": "grace@example.com", "display_name": "Grace Park" },
      { "email": "sam@example.com" }
    ]
  },
  "conferencing": { "profile_id": "default" }
}
A later write: one guest added, one taken off
{
  "event_id": "consult-88",
  "summary": "Colour consultation",
  "start": "2026-11-05T00:00:00Z",
  "end": "2026-11-05T00:30:00Z",
  "tzid": "Australia/Sydney",
  "attendees": {
    "invite": [{ "email": "alex@example.com", "display_name": "Alex Rivera" }],
    "remove": [{ "email": "sam@example.com" }]
  }
}
  • attendees describes a change, not the whole list. The event’s guests after the write are the guests it had, plus invite, minus remove. An address in both lists is removed. Inviting a guest again keeps them once; a display_name sent with it replaces the name.
  • A write without attendees keeps the guests as they are, so you can move or rename an event without sending the list again. To take every guest off, name them in remove.
  • notify_attendees: false makes the write without telling anybody: the guests are on the event and nobody is emailed. It also applies to DELETE. Which calendars take it is in the provider table.
  • Limits: 100 guests on an event, and 100 entries in each of invite and remove; an email address up to 254 characters, a name up to 256. Addresses are trimmed and compared in lower case, and one named twice counts once.
  • A guest’s status is the answer the calendar holds, and your later writes keep it. Moving an event to another time can ask the guests again: Google sets them back to needs_action when the time changes.
  • A calendar can add a guest of its own: somebody who answers from another address than the one you invited, or who was forwarded the invitation. They appear in attendees with their answer, and your later writes keep them. To take one off, name the address in remove.
  • The calendar’s owner is the event’s organizer.

Repeating events

recurrence makes the event a series. start and end are its first occurrence, and rules holds one rule that says how it repeats. exceptions skips occurrences (add) and gives single occurrences their own time or text (change).

Tuesdays at 10 am Sydney time, ten times; one skipped, one moved to the afternoon
{
  "event_id": "class-7",
  "summary": "Tuesday pilates",
  "start": "2026-11-02T23:00:00Z",
  "end": "2026-11-03T00:00:00Z",
  "tzid": "Australia/Sydney",
  "recurrence": {
    "rules": [
      { "frequency": "weekly", "interval": 1, "count": 10, "by_day": [{ "day": "tuesday" }] }
    ],
    "exceptions": {
      "add": [{ "date": "2026-11-17" }],
      "change": [
        {
          "date": "2026-11-24",
          "start": "2026-11-24T03:00:00Z",
          "end": "2026-11-24T04:00:00Z",
          "summary": "Tuesday pilates (afternoon)"
        }
      ]
    }
  }
}
Fields of a recurrence rule
NameTypeDescription
frequencyrequiredstringdaily, weekly, monthly or yearly.
intervalintegerEvery how many days, weeks, months or years: 1 to 99. Default 1.
countintegerHow many occurrences in all, the first included: 1 to 730.
untildateThe last day an occurrence may fall on (2027-12-31, inclusive, in the event’s time zone). Not together with count. With neither, the series has no end.
by_dayobject[]Weekly: the days of the week, [{ "day": "tuesday" }, { "day": "thursday" }]. Monthly: a day with its place in the month, [{ "day": "monday", "nth_of_period": 1 }] for the first Monday; nth_of_period is 1 to 4, or -1 for the last.
All day on the first Monday of every month, until the end of 2027
{
  "event_id": "stocktake",
  "summary": "Stocktake",
  "start": "2026-11-02",
  "end": "2026-11-03",
  "recurrence": {
    "rules": [
      { "frequency": "monthly", "until": "2027-12-31", "by_day": [{ "day": "monday", "nth_of_period": 1 }] }
    ]
  }
}

How a rule is read

  • The start is the first occurrence, so it has to fit the rule: a weekly rule for Tuesdays needs a start on a Tuesday. A weekly rule without by_day repeats on the start’s weekday.
  • A monthly rule without by_day repeats on the start’s day of the month, and a yearly rule on the start’s date. Start a monthly series on the 1st to the 28th, and a yearly one on any date but 29 February; use by_day with nth_of_period: -1 for “the last Friday of the month”.
  • A series with a time needs tzid, and repeats at the same local time all year: 10 am stays 10 am when the clocks change. An all-day series needs none.
  • until is not before the start. A rule has the five fields above and no others.

Skipped and changed occurrences

  • An occurrence is named by its original day in the event’s time zone, YYYY-MM-DD: the day the rule puts it on, also after it was moved. Reads return it as occurrence_date.
  • exceptions.add: up to 64 days to skip, { "date": "2026-11-17" }.
  • exceptions.change: up to 64 occurrences with something of their own: date, and any of start + end (both, of the same kind as the series: times for a timed series, dates for an all-day one), summary, description and location.description. What is left out is the series’ own.
  • A day is skipped or changed, not both, and named once.

Writing a series again

  • Send the event again with the same event_id to move it, rename it or change its rule: every occurrence follows.
  • Leave recurrence out and the event keeps repeating as it does (and keeps its time zone when you send none). "recurrence": null makes it a single event again.
  • Leave exceptions.add or exceptions.change out and that list is kept; send one and it replaces the series’ list. Exceptions on days the new rule no longer has are dropped.
  • For one occurrence there are two shorter calls: DELETE with occurrence_date skips it, and POST …/events/occurrences changes it.
  • Deleting the event deletes the whole series. A series can have guests and a meeting link like any other event.
The event stops repeating and becomes one event
{
  "event_id": "class-7",
  "summary": "Tuesday pilates",
  "start": "2026-11-02T23:00:00Z",
  "end": "2026-11-03T00:00:00Z",
  "tzid": "Australia/Sydney",
  "recurrence": null
}

Meeting links

conferencing.profile_id asks the calendar to add its own meeting link to the event: Google Meet on Google calendars, Microsoft Teams on Microsoft 365 and Outlook.com.

Values of conferencing.profile_id
NameTypeDescription
defaultvalueAdd the calendar's own link where it has one. On a calendar without one the event is written without a link, and the write still succeeds.
integratedvalueAdd the calendar’s own link, and answer 422 (errors.conferencing_unavailable) on a calendar that has none.
nonevalueTake the link off the event.
  • Reads show "conferencing": { "pending": true } as soon as the write is accepted, then { "provider_name", "join_url" } once the calendar has made the link, usually within seconds. You get no notification for your own write, so read the event again a moment later.
  • A link stays on the event through later writes, also ones that leave conferencing out. The same link is kept when the event moves.
  • Guests get the link with their invitation. On Microsoft calendars the join details are also added to the event’s description in the person’s calendar; reads return your description without them.

DELETE /v1/calendars/{calendar_id}/events

DELETEhttps://api.calmonkey.com/v1/calendars/{calendar_id}/events

Authentication: Authorization: Bearer <access_token>

Body of an event delete
NameTypeDescription
event_idrequiredstringThe id the event was written with.
occurrence_datedateCancel only this occurrence of a repeating event: its original day in the event’s time zone (2026-12-01). The rest of the series stays.
notify_attendeesbooleanWhether guests are sent the cancellation. Default true.
Cancel the occurrence of 1 December
{
  "event_id": "class-7",
  "occurrence_date": "2026-12-01"
}
Delete an event and send no cancellations
{
  "event_id": "consult-88",
  "notify_attendees": false
}

202 Accepted, empty body, also when there is no such event. Without occurrence_date a repeating event is deleted with all its occurrences. With it, 422 on occurrence_date when the event does not repeat (errors.not_recurring) or has no occurrence on that day (errors.not_an_occurrence); skipping a day that is already skipped is fine.

POST /v1/calendars/{calendar_id}/events/occurrences

POSThttps://api.calmonkey.com/v1/calendars/{calendar_id}/events/occurrences

Authentication: Authorization: Bearer <access_token>

Gives one occurrence of a repeating event its own time, text or both, without sending the series again.

Body of an occurrence change
NameTypeDescription
event_idrequiredstringThe id the series was written with.
occurrence_daterequireddateThe occurrence’s original day in the event’s time zone.
start / endstring or objectA new time for this occurrence: both or neither, times for a timed series and dates for an all-day one.
tzidstringThe time zone of the new time. Default: the series' own.
summarystringA title for this occurrence, up to 2,000 characters.
descriptionstringUp to 50,000 characters.
location.descriptionstringUp to 2,000 characters.
notify_attendeesbooleanWhether guests are told. Default true.
The class of 8 December moves to midday and gets its own title
{
  "event_id": "class-7",
  "occurrence_date": "2026-12-08",
  "start": "2026-12-08T01:00:00Z",
  "end": "2026-12-08T02:00:00Z",
  "summary": "Tuesday pilates (midday)"
}
  • 202 Accepted, empty body. The body is everything that differs for this occurrence: it replaces an earlier change of the same occurrence, and a field left out is the series’ own again. At least one of the new time, summary, description and location is needed.
  • Changing an occurrence that was skipped brings it back.
  • 404 for an event_id your application has not written to the calendar. 422 on occurrence_date with errors.not_recurring or errors.not_an_occurrence, as for DELETE.
  • The same change can be made with the series itself, in recurrence.exceptions.change.

422 keys of event writes

Event writes answer 422 in the usual shape (field errors). These are the keys of the guest, repeating-event and meeting-link fields; the field name is the path into your body, such as attendees.invite[0].email or recurrence.rules[0].until.

422 keys of event writes
NameTypeDescription
errors.invalid_formatattendeesattendees is not an object with invite and / or remove. Also on recurrence that is not an object or null, and on a by_day entry that is not an object.
errors.invalid_emailattendees.…emailNot an email address.
errors.requiredanyA guest without email; conferencing without profile_id; a changed occurrence with only one of start and end.
errors.too_biganyOver a limit: more than 100 entries in invite or remove, an address over 254 or a name over 256 characters, interval over 99, count over 730, more than 64 entries in exceptions.add or exceptions.change.
errors.too_many_attendeesattendeesThe event would have more than 100 guests.
errors.attendees_unsupportedattendeesGuests were added on a calendar that sends no invitations (provider table). On an application calendar, send notify_attendees: false to store them without telling anybody.
errors.notify_attendees_unsupportednotify_attendeesnotify_attendees: false on an event with guests in a calendar that always tells them (Microsoft).
errors.invalid_valueconferencing.profile_idNot default, integrated or none.
errors.conferencing_unavailableconferencing.profile_idintegrated on a calendar that adds no meeting link of its own.
errors.max_recurrence_rules_exceededrecurrence.rulesMore than one rule.
errors.too_smallrecurrence.rulesNo rule. (To stop a series send "recurrence": null.)
errors.not_recognized…frequencyNot daily, weekly, monthly or yearly.
errors.not_supportedrecurrence.…A field a rule does not have (by_month_day, by_month, by_set_pos, week_start…), a field of a by_day entry other than day and nth_of_period, or a list under exceptions other than add and change. Nothing in a rule is ever dropped silently.
errors.must_be_integer_greater_than_zero…interval, …countNot a whole number above zero.
errors.must_be_date…untilNot a date (YYYY-MM-DD).
errors.not_supported_count_until_combination…untilcount and until together.
errors.invalidrecurrence.…, tzidThe rule does not fit the event: the start is not an occurrence of it (…by_day), by_day is wrong for the frequency, a monthly series starts on the 29th to 31st or a yearly one on 29 February (…frequency), until is before the start, a timed series has no tzid, or an exception’s date is not a date or is named twice. The description says which.
errors.not_recurringoccurrence_dateThe event does not repeat.
errors.not_an_occurrenceoccurrence_dateThe series has no occurrence on that day.

The body is checked first and the calendar second: a body with a mistake in it answers with the mistake, and a well-formed body the calendar cannot carry out answers with the calendar’s key. Nothing is written in either case.

POST /v1/channels

POSThttps://api.calmonkey.com/v1/channels

Authentication: Authorization: Bearer <access_token>

Body of a channel
NameTypeDescription
callback_urlrequiredstringhttps, port 443, a public host. No credentials in the URL.
filters.calendar_idsstring[]Only changes in these calendars (up to 200). Default: every calendar of the account.
filters.only_managedbooleanOnly changes to events your application created.
Request body
{
  "callback_url": "https://yourapp.example/calmonkey/notifications",
  "filters": {
    "calendar_ids": ["cal_89645320138ae1ac547189a1"],
    "only_managed": false
  }
}
200 OK
{
  "channel": {
    "channel_id": "chn_633d9797c6e79a681d8ba688",
    "callback_url": "https://yourapp.example/calmonkey/notifications",
    "filters": { "calendar_ids": ["cal_89645320138ae1ac547189a1"] },
    "signing_secret": "whsec_jHsZ7xVMwTVOJBnkmfDpreMncVmdh-pg_mWfWApj36M"
  }
}

A verification notification goes to the callback URL at once. signing_secret is returned only here (not in Cronofy compatibility mode). An account has at most 100 open channels. A URL that is not https answers 422 with errors.invalid_callback_url; a host CalMonkey will not call (local, private or example addresses) with errors.invalid_callback_url_blocked.

GET /v1/channels

GEThttps://api.calmonkey.com/v1/channels

Authentication: Authorization: Bearer <access_token>

200 OK
{
  "channels": [
    {
      "channel_id": "chn_633d9797c6e79a681d8ba688",
      "callback_url": "https://yourapp.example/calmonkey/notifications",
      "filters": { "calendar_ids": ["cal_89645320138ae1ac547189a1"] }
    }
  ]
}

The account’s open channels. Filters that were not set are left out.

DELETE /v1/channels/{channel_id}

DELETEhttps://api.calmonkey.com/v1/channels/{channel_id}

Authentication: Authorization: Bearer <access_token>

Closes the channel: 202, empty body, also when it was already closed. 404 for an id the account never had, with the body {"error":"not_found","description":"…"}.

Notifications

What CalMonkey POSTs to a channel’s callback URL: { "notification": { "type", "changes_since" }, "channel": { … } }. Headers, signatures and verification code are in the quickstart; retries in Errors and limits.

Notification types
NameTypeDescription
verificationtypeSent once when the channel is created.
changetypeSomething changed in the account’s calendars. changes_since (UTC, whole seconds) is where to read from.
profile_disconnectedtypeA connected calendar account needs to be connected again.
profile_initial_sync_completedtypeA newly connected calendar account has finished its first sync.

Your application is never notified of changes it made itself through the API. Several changes close together are sent as one notification.