Calendar
List and manage calendars and events, answer invitations and find a time when attendees are free.
Read and write an account's calendars and events, answer invitations, and ask when people are free. To let other people book time with your users, see Scheduling links.
Calendars and sync
Calendar routes read Workwae's index of the account's calendars.
- Google and Microsoft 365 accounts sync calendars while the account's
calendarEnabledsetting is on. - IMAP accounts sync over CalDAV once their IMAP settings name a CalDAV server.
- The index covers the sync window: by default 90 days back to 365 days ahead.
| Status | Reason | When |
|---|---|---|
| 409 | calendar_disabled | The account does not sync calendars. |
| 403 | plan_module_unavailable | The plan has no calendar module. |
curl "https://api.workwae.com/v1/accounts/$ACCOUNT_ID/calendars" \
-H "Authorization: Bearer $WORKWAE_API_KEY"Each calendar has canEdit (whether the account can write to it, from access), its color, time zone and, for a shared calendar, its owner.
Manage calendars
POST, PATCH and DELETE /v1/accounts/{accountId}/calendars create, change and delete calendars. Providers store different fields:
| Provider | Stores | Refused as provider_unsupported |
|---|---|---|
| Microsoft 365 | Name, color (#rrggbb) | Description, time zone (the mailbox zone applies to every calendar) |
| Name, description, time zone, color | — | |
| CalDAV | Name, description, color (Apple calendar-color) | Time zone |
- The primary calendar can be renamed where the provider allows it, but not deleted (
calendar_primary). - A calendar the account cannot write answers
calendar_read_only. - Deleting a calendar removes its events from the index without announcing them one by one. A provider that still holds events deletes them with the calendar.
List events
Events are listed for a time range [start, end), ordered by start time. The list holds singles, occurrences of recurring series and exceptions that overlap the range, never series definitions. The range spans up to 400 days; pages hold up to 500 events (default 100).
curl "https://api.workwae.com/v1/accounts/$ACCOUNT_ID/calendar-events?start=2026-11-01T00:00:00Z&end=2026-12-01T00:00:00Z&calendarId=$CALENDAR_ID" \
-H "Authorization: Bearer $WORKWAE_API_KEY"kind | Meaning |
|---|---|
single | A one-off event. |
series | A recurring event's definition: the times of its first occurrence and its recurrence rule. Read it with GET .../calendar-events/{seriesId}. |
occurrence | An unchanged instance of a series; seriesId names the series and originalStart its slot. |
exception | An instance changed on its own. |
start and end carry the instant (dateTime), the zone it was scheduled in and, for all-day events, the date (an end date is exclusive). GET .../calendar-events/{eventId} adds the event's body, fetched from the provider.
Create events
Give times as wall-clock time in a zone ({ dateTime, timeZone }, no offset) or as a whole day ({ date }). Both ends must be the same kind. A recurrence of RFC 5545 lines (without DTSTART) makes a series, evaluated in the start's zone.
curl -X POST "https://api.workwae.com/v1/accounts/$ACCOUNT_ID/calendar-events" \
-H "Authorization: Bearer $WORKWAE_API_KEY" \
-H "content-type: application/json" \
-d '{
"calendarId": "'"$CALENDAR_ID"'",
"subject": "Weekly sync",
"start": { "dateTime": "2026-11-02T10:00:00", "timeZone": "Europe/Berlin" },
"end": { "dateTime": "2026-11-02T10:30:00", "timeZone": "Europe/Berlin" },
"attendees": [{ "name": "Ada", "address": "ada@example.com" }],
"recurrence": ["RRULE:FREQ=WEEKLY;BYDAY=MO;COUNT=10"],
"onlineMeeting": true
}'For a video link, set one of these; more than one answers multiple_meeting_options:
| Field | Link |
|---|---|
onlineMeeting | The provider's own: Teams on Microsoft 365, Meet on Google. |
onlineMeetingUrl | A link you already have. |
onlineMeetingService | A Zoom or Webex meeting Workwae creates. See Meetings. |
notify (default true) decides whether attendees hear about the change.
Change and delete events
PATCH .../calendar-events/{eventId}changes the fields given;attendeesreplaces the whole list.- Addressing an occurrence changes that instance only (it becomes an exception); addressing the series changes them all.
DELETE .../calendar-events/{eventId}takes its options in the query (notify,comment). When the account organizes the event, it cancels the meeting for the attendees.
curl -X PATCH "https://api.workwae.com/v1/accounts/$ACCOUNT_ID/calendar-events/$EVENT_ID" \
-H "Authorization: Bearer $WORKWAE_API_KEY" \
-H "content-type: application/json" \
-d '{
"start": { "dateTime": "2026-11-02T11:00:00", "timeZone": "Europe/Berlin" },
"end": { "dateTime": "2026-11-02T11:30:00", "timeZone": "Europe/Berlin" }
}'
curl -X DELETE "https://api.workwae.com/v1/accounts/$ACCOUNT_ID/calendar-events/$EVENT_ID?notify=true&comment=Cancelled%20for%20the%20holidays" \
-H "Authorization: Bearer $WORKWAE_API_KEY"Answer invitations
response is accepted, tentative or declined. The account's own answer is the event's selfResponse. An event the account organizes has no invitation to answer (organizer_cannot_respond).
curl -X POST "https://api.workwae.com/v1/accounts/$ACCOUNT_ID/calendar-events/$EVENT_ID/respond" \
-H "Authorization: Bearer $WORKWAE_API_KEY" \
-H "content-type: application/json" \
-d '{ "response": "tentative", "comment": "May run late" }'Find a time
POST /v1/accounts/{accountId}/availability/find asks the provider for the busy times of up to 20 attendees over a range of up to 62 days and suggests common free slots, optionally inside working hours.
curl -X POST "https://api.workwae.com/v1/accounts/$ACCOUNT_ID/availability/find" \
-H "Authorization: Bearer $WORKWAE_API_KEY" \
-H "content-type: application/json" \
-d '{
"attendees": ["ada@example.com", "alan@example.com"],
"start": "2026-11-02T00:00:00Z",
"end": "2026-11-07T00:00:00Z",
"durationMinutes": 45,
"timeZone": "Europe/Berlin",
"workingHours": { "start": "09:00", "end": "17:00" }
}'| Option | Default | Meaning |
|---|---|---|
durationMinutes | 30 | Slot length, 5 to 1440. |
stepMinutes | 30 | Distance between suggested starts. |
timeZone | UTC | Zone the working hours are read in. |
maxSuggestions | 10 | Up to 100. |
includeAccount | true | Count the account's own calendar. |
requireAll | false | Suggest nothing while an attendee is unresolved. |
- One provider request covers every attendee; the answer is cached briefly per account, attendees and range.
- Each attendee comes back with its
busyblocks, or anerrorwhen the provider could not tell (an unknown address, a calendar not shared). Such an attendee is listed inunresolved,completeis false, and it does not constrain the suggestions. - With
requireAll: true, an unresolved attendee withholds every suggestion (withheldReason: "unresolved_attendees").
Across several accounts
POST /v1/availability/find reads up to 20 of the tenant's accounts, each through its own calendar. With mode: "all" it suggests slots where every account is free; with mode: "any" (round-robin) slots where at least one is, naming the free accounts per slot. Extra attendees are looked up through the first account and must be free in either mode.
curl -X POST "https://api.workwae.com/v1/availability/find" \
-H "Authorization: Bearer $WORKWAE_API_KEY" \
-H "content-type: application/json" \
-d '{
"accountIds": ["'"$ALICE_ACCOUNT_ID"'", "'"$BOB_ACCOUNT_ID"'"],
"mode": "any",
"start": "2026-11-02T00:00:00Z",
"end": "2026-11-07T00:00:00Z",
"timeZone": "Europe/Berlin"
}'An account that cannot be read (calendar sync off, paused, needing a new sign-in, a provider failure) is listed in unresolved with its error. In mode all it does not constrain the suggestions; in mode any it is never offered as free. requireAll withholds every suggestion instead. Keys limited to specific accounts cannot use this route.
To offer these slots to outside people and book them, create a scheduling link instead.
Calendar events in webhooks
Changes made anywhere arrive as events: calendar.created, calendar.updated, calendar.deleted, calendar_event.created, calendar_event.updated (naming the changed fields) and calendar_event.deleted. calendar.initialized marks the end of the first calendar sync. Payloads are in Calendar and contacts events.