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/authorizeis onhttps://app.calmonkey.com, because a browser opens it. - Bodies are JSON (
Content-Type: application/json). The OAuth endpoints and/v1/application_calendarsalso takeapplication/x-www-form-urlencoded. - Booleans in query strings are
true,false,1or0. Repeated parameters use brackets:calendar_ids[]=a&calendar_ids[]=b. - Ids: accounts
acc_…(application calendarsapc_…), profilespro_…, calendarscal_…, channelschn_…, eventsevt_…. Treat them as opaque strings. - Errors and rate limits: Errors and limits.
POST /oauth/token
POSThttps://api.calmonkey.com/oauth/token
Authentication: Your client id and client secret, in the request body.
grant_type=authorization_code
| Name | Type | Description |
|---|---|---|
client_idrequired | string | Your client id. |
client_secretrequired | string | Your client secret. |
grant_typerequired | string | authorization_code |
coderequired | string | The code from the callback. Valid once, for 10 minutes. |
redirect_urirequired | string | The same redirect URI the authorization request used. |
{
"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"
}
}
| Name | Type | Description |
|---|---|---|
access_token | string | For Authorization: Bearer. Valid for expires_in seconds (10,800: 3 hours). |
refresh_token | string | Gets new access tokens. Does not rotate. |
scope | string | read_write |
sub / account_id | string | The account (acc_…). The same value under both names. |
linking_profile | object | The calendar account connected in this run: provider_name, profile_id, profile_name. |
grant_type=refresh_token
| Name | Type | Description |
|---|---|---|
client_idrequired | string | Your client id. |
client_secretrequired | string | Your client secret. |
grant_typerequired | string | refresh_token |
refresh_tokenrequired | string | The account's refresh token. |
{
"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.
| Name | Type | Description |
|---|---|---|
client_idrequired | string | Your client id. |
client_secretrequired | string | Your client secret. |
tokenrequired | string | An 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.
| Name | Type | Description |
|---|---|---|
client_idrequired | string | Your client id. |
client_secretrequired | string | Your client secret. |
application_calendar_idrequired | string | Your name for the calendar, up to 255 characters. The same id always opens the same calendar. |
{
"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>
{
"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.
{
"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>
{
"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: falsemeans the person has to connect that calendar account again (the provider refused our access, or the password was revoked). Send them through/oauth/authorizewith the sameprovider_name: the same profile, calendars and account come back.provider_service:gmail,gsuite,office365,outlook_com,icloud, orcalmonkeyfor an application calendar.profile_relink_urlisnull: a profile is reconnected through/oauth/authorize, as above.profile_initial_sync_requiredis true until the profile’s first sync has finished.
GET /v1/calendars
GEThttps://api.calmonkey.com/v1/calendars
Authentication: Authorization: Bearer <access_token>
{
"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>
| Name | Type | Description |
|---|---|---|
tzidrequired | string | Time zone the request’s dates are read in, e.g. Australia/Sydney. |
from | date or time | Start, a day (2026-11-03) in tzid or a time with an offset. Defaults to 42 days ago. |
to | date or time | End, exclusive. Defaults to 201 days from now. |
calendar_ids[] | string, repeatable | Only these calendars (up to 100). Default: every calendar of the account. |
include_managed | boolean | Include events your application created. Default false. |
include_ids | boolean | Add event_id to the periods of events your application created. |
localized_times | boolean | Times with the offset of tzid instead of UTC. |
{
"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"
}
]
}
| Name | Type | Description |
|---|---|---|
calendar_id | string | The calendar of the event. |
event_uid | string | Identifies the event (evt_…). |
start / end | string | Times in UTC (or local with localized_times), or dates for all-day events with an exclusive end. Not clipped to the window. |
free_busy_status | string | busy, tentative or free (an event shown as free). |
event_id | string | Only 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>
| Name | Type | Description |
|---|---|---|
tzidrequired | string | Time zone the request’s dates are read in, e.g. Australia/Sydney. |
from | date or time | Start, a day (2026-11-03) in tzid or a time with an offset. Defaults to 42 days ago. |
to | date or time | End, exclusive. Defaults to 201 days from now. |
calendar_ids[] | string, repeatable | Only these calendars (up to 100). Default: every calendar of the account. |
include_managed | boolean | Include events your application created. Default false. |
only_managed | boolean | Only the events your application created. |
include_deleted | boolean | Include events deleted since they were cached (deleted: true). |
last_modified | time | Only events changed at or after this time. Use a change notification’s changes_since. |
localized_times | boolean | Times with the offset of tzid instead of UTC. |
{
"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_idappears only on events your application created, andoptions.update/options.deleteare true only for those.- Cancelled events are not included.
| Name | Type | Description |
|---|---|---|
attendees | object[] | 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. |
organizer | object or null | Who the event belongs to: email and display_name (string or null). Null when the calendar names nobody. |
participation_status | string | The 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. |
conferencing | object | The 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. |
recurring | boolean | True on every occurrence of a repeating event. A repeating event is returned as its occurrences, one event each. |
series_identifier | string | On every occurrence of a repeating event: the same value for all occurrences of one series, and for nothing else. |
occurrence_date | date | On 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_master | object | On the occurrences of a series your application wrote: event_id (the id you wrote the series with) and event_uid of its first occurrence. |
recurrence | object | On 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, soonly_managedandinclude_managedtreat the whole series as yours. Tell the occurrences apart byoccurrence_dateorevent_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.
| Name | Type | Description |
|---|---|---|
event_idrequired | string | Your id for the event, up to 512 characters. Unique per calendar and application. |
summaryrequired | string | Title, up to 2,000 characters. |
description | string | Up to 50,000 characters. |
startrequired | string or object | A time with an offset (2026-11-03T03:00:00Z), a date for an all-day event, or { "time": "…", "tzid": "…" }. |
endrequired | string or object | As start. A date when start is a date (exclusive), a time when start is a time, not before start. |
tzid | string | The event's time zone, used by the provider to show it. |
location.description | string | Up to 2,000 characters. |
url | string | A link, returned on reads. Up to 2,048 characters. |
transparency | string | opaque (busy, the default) or transparent (shown as free). |
attendees.invite | object[] | Guests to add: { "email": "…", "display_name": "…" }, the name optional. See Guests. |
attendees.remove | object[] | Guests to take off: { "email": "…" }. |
notify_attendees | boolean | Whether the calendar tells the guests about this write. Default true. |
recurrence | object or null | How the event repeats: rules and exceptions. null stops it repeating. See Repeating events. |
conferencing.profile_id | string | default, integrated or none. See Meeting links. |
{
"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.
{
"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" }
}
{
"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" }]
}
}
attendeesdescribes a change, not the whole list. The event’s guests after the write are the guests it had, plusinvite, minusremove. An address in both lists is removed. Inviting a guest again keeps them once; adisplay_namesent with it replaces the name.- A write without
attendeeskeeps 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 inremove. notify_attendees: falsemakes 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
inviteandremove; 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
statusis 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 toneeds_actionwhen 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
attendeeswith their answer, and your later writes keep them. To take one off, name the address inremove. - 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).
{
"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)"
}
]
}
}
}
| Name | Type | Description |
|---|---|---|
frequencyrequired | string | daily, weekly, monthly or yearly. |
interval | integer | Every how many days, weeks, months or years: 1 to 99. Default 1. |
count | integer | How many occurrences in all, the first included: 1 to 730. |
until | date | The 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_day | object[] | 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. |
{
"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_dayrepeats on the start’s weekday. - A monthly rule without
by_dayrepeats 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; useby_daywithnth_of_period: -1for “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. untilis 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 asoccurrence_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 ofstart+end(both, of the same kind as the series: times for a timed series, dates for an all-day one),summary,descriptionandlocation.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_idto move it, rename it or change its rule: every occurrence follows. - Leave
recurrenceout and the event keeps repeating as it does (and keeps its time zone when you send none)."recurrence": nullmakes it a single event again. - Leave
exceptions.addorexceptions.changeout 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_dateskips 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.
{
"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.
| Name | Type | Description |
|---|---|---|
default | value | Add 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. |
integrated | value | Add the calendar’s own link, and answer 422 (errors.conferencing_unavailable) on a calendar that has none. |
none | value | Take 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
conferencingout. 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>
| Name | Type | Description |
|---|---|---|
event_idrequired | string | The id the event was written with. |
occurrence_date | date | Cancel 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_attendees | boolean | Whether guests are sent the cancellation. Default true. |
{
"event_id": "class-7",
"occurrence_date": "2026-12-01"
}
{
"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.
| Name | Type | Description |
|---|---|---|
event_idrequired | string | The id the series was written with. |
occurrence_daterequired | date | The occurrence’s original day in the event’s time zone. |
start / end | string or object | A new time for this occurrence: both or neither, times for a timed series and dates for an all-day one. |
tzid | string | The time zone of the new time. Default: the series' own. |
summary | string | A title for this occurrence, up to 2,000 characters. |
description | string | Up to 50,000 characters. |
location.description | string | Up to 2,000 characters. |
notify_attendees | boolean | Whether guests are told. Default true. |
{
"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,descriptionandlocationis needed.- Changing an occurrence that was skipped brings it back.
404for anevent_idyour application has not written to the calendar.422onoccurrence_datewitherrors.not_recurringorerrors.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.
| Name | Type | Description |
|---|---|---|
errors.invalid_format | attendees | attendees 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_email | attendees.…email | Not an email address. |
errors.required | any | A guest without email; conferencing without profile_id; a changed occurrence with only one of start and end. |
errors.too_big | any | Over 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_attendees | attendees | The event would have more than 100 guests. |
errors.attendees_unsupported | attendees | Guests 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_unsupported | notify_attendees | notify_attendees: false on an event with guests in a calendar that always tells them (Microsoft). |
errors.invalid_value | conferencing.profile_id | Not default, integrated or none. |
errors.conferencing_unavailable | conferencing.profile_id | integrated on a calendar that adds no meeting link of its own. |
errors.max_recurrence_rules_exceeded | recurrence.rules | More than one rule. |
errors.too_small | recurrence.rules | No rule. (To stop a series send "recurrence": null.) |
errors.not_recognized | …frequency | Not daily, weekly, monthly or yearly. |
errors.not_supported | recurrence.… | 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, …count | Not a whole number above zero. |
errors.must_be_date | …until | Not a date (YYYY-MM-DD). |
errors.not_supported_count_until_combination | …until | count and until together. |
errors.invalid | recurrence.…, tzid | The 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_recurring | occurrence_date | The event does not repeat. |
errors.not_an_occurrence | occurrence_date | The 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>
| Name | Type | Description |
|---|---|---|
callback_urlrequired | string | https, port 443, a public host. No credentials in the URL. |
filters.calendar_ids | string[] | Only changes in these calendars (up to 200). Default: every calendar of the account. |
filters.only_managed | boolean | Only changes to events your application created. |
{
"callback_url": "https://yourapp.example/calmonkey/notifications",
"filters": {
"calendar_ids": ["cal_89645320138ae1ac547189a1"],
"only_managed": false
}
}
{
"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>
{
"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":"…"}.
POST /v1/link_tokens
POSThttps://api.calmonkey.com/v1/link_tokens
Authentication: Authorization: Bearer <access_token>
{ "link_token": "cmlt_Hy5_QceiTdbfCgu7Ou9ok59Aa8OC_OAZmwEbQLOo3io" }
Pass it to /oauth/authorize as link_token to add another calendar account to the same account, for example a work calendar next to a personal one. Single use, valid for 5 minutes. If the calendar account the person connects already belongs to a different account of your application, the flow ends with error=access_denied.
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.
| Name | Type | Description |
|---|---|---|
verification | type | Sent once when the channel is created. |
change | type | Something changed in the account’s calendars. changes_since (UTC, whole seconds) is where to read from. |
profile_disconnected | type | A connected calendar account needs to be connected again. |
profile_initial_sync_completed | type | A 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.
