Skip to Content
Events

Events

Morgen maps Calendar Events from different providers to a common data model, inspired by the JSCalendar standard . Some differences with the JSCalendar standard are documented below.

List events

Morgen offers an endpoint to list events from a given calendar. Events are retrieved in a given time window, and recurring events are automatically expanded to their individual occurrences. Deleted or cancelled events are not included in the response.

fetch( "https://api.morgen.so/v3/events/list?accountId=<ACCOUNT_ID>&calendarIds=<CALENDAR_IDS>&start=<START_DATETIME>&end=<END_DATETIME>", { method: "GET", headers: { accept: "application/json", Authorization: "ApiKey <API_KEY>", }, } );
ParameterTypeDefaultRequiredDescription
accountIdQuery-YesThe calendar account ID to retrieve events from.
calendarIdsQuery-YesComma-separated list of calendar IDs to retrieve events from. Notice that these calendars must all belong to the same account identified by accountId.
startQuery-YesStart of the time window in ISO 8601 format, e.g. 2023-03-01T00:00:00Z
endQuery-YesEnd of the time window in ISO 8601 format, e.g. 2023-04-01T00:00:00Z. This must be greater than start. The interval cannot be longer than 6 months. It is recommended to retrieve no more than 2 months of events at the same time.
⚠️

Unknown query parameters are rejected. Sending a parameter that is not in the table above returns HTTP 400, it is not ignored.

Response

Returns HTTP 200 OK. The events are returned as an array under data.events:

{ "data": { "events": [] } }

See Event schema below for the shape of each event.

⚠️

Listing events costs 10 points per request. If you only need to read back a single event you already have the ID for, use Get an event instead, which costs 1 point. See Rate Limits for details.

Event schema

Here is an example response for the /events/list endpoint described above:

{ "data": { "events": [ { "@type": "Event", "id": "WyJBUU1rQURaa1lXWXpOel...", "uid": "kki3mce...@google.com", "calendarId": "WyI2NDBhNjJjOW...", "accountId": "640a62c9aa5b7e06cf420000", "integrationId": "o365", "baseEventId": "AAkALgAAAAAAHYQDEapmEc2by...", "masterEventId": "WyI2NDBhNjJjOWFhNWI3ZTA2Y2Y0MjAwMDA...", "masterBaseEventId": "AAkALgAAAAAAHYQDEapmEc2by...", "created": "2023-02-28T11:50:57", "updated": "2023-02-28T17:56:44", "recurrenceId": "2023-08-29T16:00:00", "recurrenceIdTimeZone": "Europe/Zurich", "title": "Chat about Morgen", "description": "<html><body>Description of event, possibly in HTML format.</body></html>", "descriptionContentType": "text/html", "start": "2023-03-01T10:15:00", "timeZone": "Europe/Berlin", "duration": "PT25M", "showWithoutTime": false, "privacy": "public", "freeBusyStatus": "free", "locations": { "1": { "@type": "Location", "name": "Morgen HQ, Forrlibuckstrasse 223" } }, "participants": { "doe@morgen.so": { "@type": "Participant", "name": "John Doe", "email": "doe@morgen.so", "roles": { "attendee": true, "owner": true }, "participationStatus": "needs-action" }, "bWFyY29AbW9yZ2Vu0000": { "@type": "Participant", "name": "Willie White", "email": "white@morgen.so", "roles": { "attendee": true }, "accountOwner": true, "participationStatus": "needs-action" } }, "alerts": { "eyJhIjoiZGlzcGxheSIsInRvIjoiLVBUMTVNIn0=": { "@type": "Alert", "trigger": { "@type": "OffsetTrigger", "offset": "-PT15M", "relativeTo": "start" }, "action": "display" } }, "useDefaultAlerts": false, "recurrenceRules": [ { "@type": "RecurrenceRule", "frequency": "weekly", "interval": 1, "byDay": [{ "@type": "NDay", "day": "mo" }] } ], "google.com:colorId": "5", "google.com:hangoutLink": "https://meet.google.com/abc-defg-hij", "morgen.so:derived": { "virtualRoom": { "url": "https://us02web.zoom.us/j/82...?pwd=..." } }, "morgen.so:metadata": { "updated": "2023-09-25T08:53:36.793Z", "categoryId": "9b5f823f-d690-4781-8783-95052ac05740@morgen.so", "categoryName": "Morgen", "categoryColor": "#CCEACD", "progress": "needs-action", "taskId": "0d464578-08ff-4df6-88b2-19083b296df7" } } ] } }

Fields with no value are omitted from the response rather than returned as null, so most of the fields above are absent on any given event. An empty object, such as "alerts": {}, is a value rather than an absent field and is returned as-is.

Event Fields Reference

FieldTypeDescription
@typeStringAlways "Event"
idStringMorgen’s identifier for the event. See Event IDs.
uidStringThe provider’s iCalendar UID. This is not the id, and no endpoint accepts it in place of id.
calendarIdStringMorgen ID of the calendar the event belongs to.
accountIdStringMorgen ID of the account the event belongs to.
integrationIdStringThe provider the event came from, e.g. "google", "o365", "caldav".
baseEventIdStringThe provider’s ID for the base event. For recurring events this is the ID of the series, for non-recurring events it is the ID of the event itself.
masterEventIdStringRecurring instances only: the Morgen ID of the master recurring event. Can be used with recurrenceId to identify and update a specific instance.
masterBaseEventIdStringRecurring instances only: the provider’s ID of the master recurring event. Not returned for non-recurring events.
createdStringCreation timestamp.
updatedStringLast update timestamp.
recurrenceIdStringRecurring instances only: the LocalDateTime that identifies this occurrence within the series, e.g. "2023-08-29T16:00:00".
recurrenceIdTimeZoneStringIANA timezone for recurrenceId.
titleStringEvent title/summary.
descriptionStringEvent description, plain text or HTML.
descriptionContentTypeStringEither "text/plain" or "text/html", depending on the content of description.
startStringStart time in LocalDateTime format. Note that this carries no timezone offset, the zone is given separately in timeZone.
timeZoneStringIANA timezone the start is expressed in.
durationStringDuration in ISO 8601 format, e.g. "PT25M".
showWithoutTimeBooleantrue for an all-day event, false for an event at a specific time of day.
privacyString"public", "private" or "secret". Whether the event details can be shown to other users with access to the calendar.
freeBusyStatusString"free" or "busy". Whether the event marks the participants as busy.
locationsObjectMap of location IDs to location objects.
participantsObjectMap of participant IDs to participant objects. The key can be the participant’s email address or an internal ID. Note that the email is sometimes a group address, for example when the event lives in a secondary Google calendar.
alertsObjectMap of alert IDs to alert objects. The alert ID is derived from the alert’s action and offset, see Updating Alerts. Only relativeTo: "start" is currently supported.
useDefaultAlertsBooleanIf true, the calendar’s default alerts apply instead of the alerts in alerts.
recurrenceRulesArrayRecurrence rules, for recurring events.
google.com:colorIdStringGoogle Calendar only. Read/write, see Google Calendar Color ID.
google.com:hangoutLinkStringGoogle Calendar only. Read-only Google Meet link.
morgen.so:derivedObjectRead-only fields derived from the rest of the event, for example a virtual room URL found in the description.
morgen.so:metadataObjectRead-only Morgen-specific metadata: categoryId, categoryName, categoryColor (hex), and the task fields progress ("needs-action" or "completed") and taskId. An event with a taskId is displayed as a task in Morgen.
morgen.so:requestVirtualRoomStringWrite-only. Requests creation of a virtual meeting room: "default", "googleMeet" or "microsoftTeams". See Request the creation of a virtual meeting room.

Event IDs

The id field is Morgen’s identifier for an event, and is what /events/update, /events/delete and GET /v3/events expect. Treat it as an opaque string:

  • Do not parse it, construct it, or transform it. Its internal format is an implementation detail and may change without notice.
  • Pass it back byte for byte, exactly as you received it. Do not URL-decode, re-encode, trim, or case-fold it.
  • An id is only meaningful within the account it came from. Always pair an id with the same accountId it was returned with. IDs are not portable between accounts, and using one against a different account fails or resolves to the wrong event rather than being silently ignored.
  • id and calendarId are not interchangeable. They are different kinds of identifier, and passing one where the other is expected fails.
  • uid is not the id. uid is the provider’s iCalendar UID and is not accepted by any endpoint that asks for an id.
⚠️

For recurring events, the id returned by /events/create is not the id returned by /events/list. Creating a recurring event returns the ID of the master series. Listing events expands the series, so each occurrence comes back with its own distinct instance ID. To match a listed occurrence back to the series you created, compare its masterEventId field against the ID that create returned, not its id.

💡

For non-recurring events, the ID returned by create is the same string that list returns and that update, delete and GET /v3/events accept.

Differences with JSCalendar

Please refer to the JSCalendar standard  for more information about the data model. Please consider the following differences:

Additional fields

Morgen returns some additional fields for convenience and to provide more information about the event: integrationId, accountId, calendarId, baseEventId, masterEventId, masterBaseEventId, morgen.so:derived, morgen.so:metadata and useDefaultAlerts.

There is also one write-only field, morgen.so:requestVirtualRoom, which you can send on create and update but will never receive back.

All of them are described in the Event Fields Reference.

⚠️

Additional fields might be added in the future. If you are storing the event in your database, please make sure to ignore unknown fields.

Google Calendar Color ID

For Google Calendar events, you can set the event color using the google.com:colorId field:

FieldTypeAccessDescription
google.com:colorIdStringRead/WriteGoogle’s event color ID. Valid values are “1” through “11”, each representing a different color in Google Calendar’s color palette.
💡

The google.com:colorId field is only available for Google Calendar events. Valid values are “1” through “11”. See the Google Calendar Colors API documentation  for details on what each color ID represents.

Example: Setting a Google Calendar event color when creating an event

{ "accountId": "640a62c9aa5b7e06cf420000", "calendarId": "WyI2NDBhNjJjOW...", "title": "Team Meeting", "start": "2023-03-15T14:00:00", "duration": "PT1H", "timeZone": "America/New_York", "showWithoutTime": false, "google.com:colorId": "5" }
⚠️

This field only works for Google Calendar accounts. Attempting to set google.com:colorId on non-Google calendars (Office 365, iCloud, CalDAV, etc.) will have no effect.

Get an event

Retrieve a single event by its ID. Unlike /events/list, this does not require a time window and does not expand recurring series, it returns exactly the event whose ID you pass.

fetch("https://api.morgen.so/v3/events?id=<EVENT_ID>", { method: "GET", headers: { accept: "application/json", Authorization: "ApiKey <API_KEY>", }, });
ParameterTypeDefaultRequiredDescription
idQuery-YesThe Morgen ID of the event to retrieve.

Response

Returns HTTP 200 OK with the event wrapped under data.event:

{ "data": { "event": { "@type": "Event", "id": "WyJBUU1rQURaa1lXWXpOel...", "calendarId": "WyI2NDBhNjJjOW...", "accountId": "640a62c9aa5b7e06cf420000", "integrationId": "o365", "title": "Chat about Morgen", "start": "2023-03-01T10:15:00", "timeZone": "Europe/Berlin", "duration": "PT25M", "showWithoutTime": false, "privacy": "public", "freeBusyStatus": "free" } } }

The event has the same shape as the entries returned by /events/list, see Event schema.

💡

This endpoint costs 1 point per request, against 10 points for /events/list. If you already have an event’s ID, for example to read back an event you just created or updated, prefer this endpoint. See Rate Limits for details.

Deleted and unknown events

An event that cannot be retrieved returns HTTP 404. It does not return 200 with a null or empty event:

{ "message": "Cancelled events cannot be retrieved from Google", "status": 404, "errorRef": "V1StGXR8Z5jd" }

The same 404 covers every case where the event is not there:

  • it was deleted, by you or from the calendar directly
  • it was cancelled at the provider
  • the ID never existed on this account

The exact message varies by provider and by whether the provider still holds a cancelled copy, so treat it as diagnostic detail and branch on the status line.

This endpoint reads the connected calendar directly rather than a copy held by Morgen, so the response reflects the provider’s state at the moment you ask.

💡

A 404 here means absence, not “try again later”. Transient conditions surface as different codes: a provider outage returns 5xx, a rate limit returns 429, and an expired or revoked calendar connection returns 401 or 403. None of them degrade into a 404, so you do not need a retry loop to distinguish a deleted event from a temporarily unreachable one.

Verifying that an event was deleted

To confirm a deletion, read the event back by ID and assert on the pair:

  1. POST /v3/events/delete returns 204.
  2. GET /v3/events?id=<same id> returns 404.

Both steps matter. The 204 is the success signal for the deletion itself. The 404 confirms the event is no longer retrievable, but on its own it does not tell you that your delete caused it, since an unknown ID and an already-cancelled event return the same status.

⚠️

Do not verify a deletion by absence from /events/list. That endpoint requires a start and end window and omits cancelled events, so a missing event may have been deleted, or may simply fall outside your window or outside the calendars you passed in calendarIds. Read back by ID instead.

Create an event

Morgen offers an endpoint to create events in a given calendar. The event is created in the calendar of the account identified by the accountId parameter.

fetch("https://api.morgen.so/v3/events/create", { method: "POST", headers: { "accept": "application/json", "Authorization": "ApiKey <API_KEY>" }, body: JSON.stringify({"accountId": <ACCOUNT_ID>, "calendarId": <CALENDAR_ID>, ...eventfields}) });

Request Body Fields

FieldTypeRequiredDescription
accountIdStringYesThe ID of the account to create the event in.
calendarIdStringYesThe ID of the calendar to create the event in.
titleStringYesThe title/summary of the event.
startStringYesStart time in LocalDateTime format (e.g., 2023-03-01T10:15:00).
durationStringYesDuration in ISO 8601 format (e.g., PT1H for 1 hour, PT30M for 30 minutes).
timeZoneString or nullYesIANA timezone for timed events (e.g., Europe/Paris). Use null for all-day events (showWithoutTime: true). See the note below on floating timed events.
showWithoutTimeBooleanYestrue for all-day events, false for timed events.
descriptionStringNoEvent description. Can be plain text or HTML (specify with descriptionContentType).
descriptionContentTypeStringNoEither text/plain or text/html. Default: text/plain.
locationsObjectNoMap of location IDs to location objects. See JSCalendar standard .
participantsObjectNoMap of participant IDs to participant objects. The key should be the base64-encoded email address.
alertsObjectNoMap of alert IDs to alert objects. See Alert structure above. Cannot be used with useDefaultAlerts: true.
useDefaultAlertsBooleanNoIf true, use the calendar’s default alert settings. Cannot be used together with alerts.
privacyStringNopublic, private, or secret. Default: public.
freeBusyStatusStringNofree or busy. Default: busy.
recurrenceRulesArrayNoArray of recurrence rule objects for recurring events.

Other fields from the JSCalendar standard  are also supported.

⚠️

Floating (no-timezone) timed events are not reliably preserved. The API accepts timeZone: null on a timed event (showWithoutTime: false), but a floating time is not currently carried through end to end when the event is written to a connected calendar, and the behavior varies by provider: iCloud/CalDAV rejects a timed event that has no timezone, and Microsoft 365 fills in a concrete zone (often Africa/Abidjan, UTC+0) rather than keeping the time floating. For timed events, always provide an explicit IANA timezone. null is intended for all-day events (showWithoutTime: true), where no timezone applies.

Response

Returns HTTP 200 OK on success, not 201. The created event is returned in full, wrapped under data.event:

{ "data": { "event": { "@type": "Event", "id": "WyJBUU1rQURaa1lXWXpOel...", "uid": "kki3mce...@google.com", "calendarId": "WyI2NDBhNjJjOW...", "accountId": "640a62c9aa5b7e06cf420000", "integrationId": "google", "baseEventId": "kki3mce...", "created": "2023-03-01T09:04:22.000Z", "updated": "2023-03-01T09:04:22.311Z", "title": "Team Meeting", "start": "2023-03-15T14:00:00", "timeZone": "America/New_York", "duration": "PT1H", "showWithoutTime": false, "privacy": "public", "freeBusyStatus": "busy", "alerts": {}, "useDefaultAlerts": true } } }

The returned event has the same shape as the entries from /events/list, see Event schema. Fields with no value are omitted rather than returned as null, so the exact set of keys varies by provider and by what you sent. An empty object, such as the "alerts": {} above, is a value rather than an absent field and is returned as-is.

Read data.event.id to get the ID of the new event. See Event IDs, in particular the note about recurring events, before storing it or passing it to another endpoint.

⚠️

The create response cannot be sent back to /events/create or /events/update as-is. Those endpoints reject unknown fields with HTTP 400, and the response contains read-only fields (id, uid, baseEventId, created, updated, integrationId, morgen.so:derived, and others) that they do not accept. Send only the fields documented for the endpoint you are calling.

Update an event

The following endpoint can be used to update an event in a given calendar.

fetch("https://api.morgen.so/v3/events/update?seriesUpdateMode=<UPDATE_MODE>", { method: "POST", headers: { "accept": "application/json", "Authorization": "ApiKey <API_KEY>" }, body: JSON.stringify({"accountId": <ACCOUNT_ID>, "calendarId": <CALENDAR_ID>, "id": <EVENT_ID>, ...eventfields}) });
ParameterTypeDefaultRequiredDescription
seriesUpdateModeQuerysingleNoDefines how to update recurring events. Possible values are: all (update all events), future (update this and future occurrences), single (update this event only, default).
⚠️

seriesUpdateMode is a query-string parameter only. Putting it in the JSON request body returns HTTP 400. The request body only accepts the event fields documented below, and any property outside that set is rejected rather than ignored.

💡

On a non-recurring event, only omitting seriesUpdateMode or sending single is accepted. Sending all or future returns HTTP 400 with a message such as “Cannot apply ‘all’ series update mode to non-recurring events”.

Request Body Fields

FieldTypeRequiredDescription
idStringYes*The Morgen ID of the event to update. *Can be omitted if masterEventId and recurrenceId are provided (see below).
accountIdStringYesThe ID of the account the event belongs to.
calendarIdStringYesThe ID of the calendar the event belongs to.
masterEventIdStringNoFor recurring event instances: The Morgen ID of the master recurring event. Must be provided with recurrenceId if id is not provided.
recurrenceIdStringNoFor recurring event instances: The LocalDateTime identifying this instance (e.g., 2023-08-29T16:00:00). Must be provided with masterEventId if id is not provided.
recurrenceIdTimeZoneStringNoOptional timezone for the recurrenceId (not required for all-day events).

All other event fields are optional for updates. Only include the fields you want to change.

💡

When updating timing fields (start, duration, timeZone, showWithoutTime), you must provide all four together. For example, to change only the start time, you must also include the current values for duration, timeZone, and showWithoutTime.

💡

Updating Recurring Event Instances: You can update a specific instance of a recurring event in two ways:

  1. Using the instance’s direct id (if you have it)
  2. Using masterEventId + recurrenceId (the LocalDateTime of the instance)

Example: To update the August 29th instance of a recurring meeting, you can provide masterEventId (the ID of the recurring series) and recurrenceId: "2023-08-29T16:00:00" instead of the instance’s direct ID.

Other fields of an event are described in the JSCalendar standard .

💡

Updates are patch updates. Only the fields that are provided will be updated. All other fields will remain unchanged. To avoid unintended changes, it is recommended to always provide only the fields that need to be updated.

Here is an example of a request to update an event title, while leaving all other fields unchanged:

fetch("https://api.morgen.so/v3/events/update", { method: "POST", headers: { accept: "application/json", Authorization: "ApiKey <API_KEY>", }, body: JSON.stringify({ accountId: "0123123", calendarId: "WyJhbmNvbmEubXJjQGdtYWl", id: "WyJhbmNvbmEubXJjQGdtYWlsLmNvbSI", title: "Title updated", }), });

Response

Returns HTTP 200 OK on success. The updated event is returned as the top-level response body.

⚠️

Note the asymmetry: unlike /events/create, /events/list and GET /v3/events, the update response is not wrapped in a data envelope. The event is the response body itself.

{ "@type": "Event", "id": "WyJhbmNvbmEubXJjQGdtYWlsLmNvbSI", "calendarId": "WyJhbmNvbmEubXJjQGdtYWl", "accountId": "640a62c9aa5b7e06cf420000", "integrationId": "google", "title": "Title updated", "start": "2023-03-15T14:00:00", "timeZone": "America/New_York", "duration": "PT1H", "showWithoutTime": false, "privacy": "public", "freeBusyStatus": "busy" }

The event has the same shape as the entries returned by /events/list, see Event schema.

💡

When seriesUpdateMode is all or future, the update may act on the master recurring event rather than the occurrence whose ID you sent, so the id in the response is not necessarily the id you passed in. Read the id back from the response rather than assuming it is unchanged.

Special cases

Updating Alerts

Alerts can be updated for Google Calendar events using the /events/update endpoint:

Add a new alert: Include the alert with its properties. The alert ID must be calculated using the encoding formula below:

Alert ID = base64(JSON.stringify({ "a": action, "to": offset }), with keys sorted alphabetically

You can use standard base64 encoding libraries in your language of choice (e.g., btoa() in JavaScript, base64 module in Python, Buffer.from().toString('base64') in Node.js).

Example calculation for an alert with offset: "-PT30M" and action: "display":

// JavaScript example const alertId = btoa(JSON.stringify({ a: "display", to: "-PT30M" })); // Result: "eyJhIjoiZGlzcGxheSIsInRvIjoiLVBUMzBNIn0="
{ "accountId": "640a62c9aa5b7e06cf420000", "calendarId": "WyI2NDBhNjJjOW...", "id": "WyJhbmNvbmEubXJjQGdtYWl", "alerts": { "eyJhIjoiZGlzcGxheSIsInRvIjoiLVBUMzBNIn0=": { "@type": "Alert", "trigger": { "@type": "OffsetTrigger", "offset": "-PT30M", "relativeTo": "start" }, "action": "display" } } }

Remove an alert: Calculate the alert ID using the same formula above, then set that alert to null:

{ "accountId": "640a62c9aa5b7e06cf420000", "calendarId": "WyI2NDBhNjJjOW...", "id": "WyJhbmNvbmEubXJjQGdtYWl", "alerts": { "eyJhIjoiZGlzcGxheSIsInRvIjoiLVBUMzBNIn0=": null } }

Use default calendar alerts: Set useDefaultAlerts to true to use the calendar’s default alert settings instead of custom alerts:

{ "accountId": "640a62c9aa5b7e06cf420000", "calendarId": "WyI2NDBhNjJjOW...", "id": "WyJhbmNvbmEubXJjQGdtYWl", "useDefaultAlerts": true }

When useDefaultAlerts is true, the alerts field should not be set, as the calendar’s default alerts will be used instead.

Updating Participants

Participants can be updated using the /events/update endpoint with patch operations:

Add a new participant: Include the participant with their details. The participant ID can be the participant’s email address:

{ "accountId": "640a62c9aa5b7e06cf420000", "calendarId": "WyI2NDBhNjJjOW...", "id": "WyJhbmNvbmEubXJjQGdtYWl", "participants": { "new@example.com": { "@type": "Participant", "name": "New Attendee", "email": "new@example.com", "roles": { "attendee": true }, "participationStatus": "needs-action" } } }
💡

Participant updates are patch operations: When you update participants, existing participants not mentioned remain unchanged. Only participants you explicitly include (to add/modify) or set to null (to remove) are affected.

Remove a participant: Set the participant to null using their email address:

{ "accountId": "640a62c9aa5b7e06cf420000", "calendarId": "WyI2NDBhNjJjOW...", "id": "WyJhbmNvbmEubXJjQGdtYWl", "participants": { "existing@example.com": null } }

Request the creation of a virtual meeting room

Both the create and update endpoints support the creation of a virtual meeting room for the event. To request the creation of a virtual meeting room, set the morgen.so:requestVirtualRoom field to default in the body of the request. This will create a virtual meeting room with the default settings of the integration. Google Meet will be added if the event is saved in a Google Calendar, while Microsoft Teams will be added if the event is saved in a Office 365 Calendar.

fetch("https://api.morgen.so/v3/events/create", { method: "POST", headers: { "accept": "application/json", "Authorization": "ApiKey <API_KEY>" }, body: JSON.stringify({"accountId": <ACCOUNT_ID>, "calendarId": <CALENDAR_ID>, "morgen.so:requestVirtualRoom": "default", ...eventfields}) });

Please notice that once a room has been attached to an event it cannot be removed with an update.

💡

Notice that Google Meet cannot be used on an Office 365 calendar. Similarly Microsoft Teams cannot be used on a Google Calendar.

💡

Support for Zoom and Webex is planned. It will be possible to request a virtual meeting room with Zoom and Webex and attach the meeting room to events created in any calendar.

Delete an event

The following endpoint can be used to delete an event from a given calendar.

fetch("https://api.morgen.so/v3/events/delete?seriesUpdateMode=<UPDATE_MODE>", { method: "POST", headers: { "accept": "application/json", "Authorization": "ApiKey <API_KEY>" }, body: JSON.stringify({"accountId": <ACCOUNT_ID>, "calendarId": <CALENDAR_ID>, "id": <EVENT_ID>}) });
ParameterTypeDefaultRequiredDescription
seriesUpdateModeQuerysingleNoDefines how to delete recurring events. Possible values are: all (delete the whole series), future (delete this and future occurrences), single (delete this occurrence only, default).
⚠️

seriesUpdateMode is a query-string parameter only. Putting it in the JSON request body returns HTTP 400. The delete body accepts only the fields listed below, and any property outside that set is rejected rather than ignored.

💡

On a non-recurring event, only omitting seriesUpdateMode or sending single is accepted. Sending all or future returns HTTP 400 with a message such as “Cannot apply ‘all’ series update mode to non-recurring events”.

Request Body Fields

FieldTypeRequiredDescription
idStringYesThe Morgen ID of the event to delete. See Event IDs.
accountIdStringYesThe ID of the account the event belongs to.
calendarIdStringYesThe ID of the calendar the event belongs to.

These three fields are sufficient. Sending any other property in the body returns HTTP 400.

Response

Returns HTTP 204 No Content on success, with an empty response body.

⚠️

There is no JSON body on a successful delete. Clients that unconditionally parse the response as JSON will fail on the empty payload, check for 204 first. In particular, the response does not tell you which event was removed, so when using seriesUpdateMode=all or future you cannot learn from the response which occurrences were affected.

⚠️

Deleting is not idempotent. Deleting an event that is already deleted returns 404, not another 204. The endpoint reads the event before removing it, and that read fails on an event the provider has already cancelled. Cleanup code that may run twice, and retries of a delete that timed out, should therefore treat a 404 from this endpoint as success: it means the event is gone. Note that this differs from deleting a task, where a repeated delete returns 204 again.

Errors

Errors are produced by a single shared handler, so the status codes and body shapes below apply uniformly to every endpoint on this page, and to the rest of the Morgen API.

StatusWhen you will see it
400The request failed validation: a missing or malformed field, an unknown property in the body or query string, or an ID that could not be resolved.
401The Authorization header is missing, malformed, or carries a key that is not valid. See Authentication.
403The key is valid but your plan does not include API access.
404The event could not be retrieved from the connected calendar: it was deleted or cancelled, or the ID never existed on this account. Returned by Get an event and by a repeated Delete.
429You have exceeded your rate limit. See Rate Limits.
500An unexpected server-side failure.

Error body

Errors that Morgen raises deliberately return this shape. In practice that is every case in the table above other than an unexpected 500:

{ "message": "Cannot apply 'all' series update mode to non-recurring events", "status": 400, "errorRef": "V1StGXR8Z5jd" }
FieldAlways presentDescription
messageYesA human-readable description of the failure. Intended for logs and debugging, not for matching on programmatically.
statusNoThe HTTP status code, repeated in the body. See the warning below.
errorRefYesAn identifier for this specific failure.
contextNoPresent on some errors, with extra structured detail about what failed.

Fields with no value are removed from the body entirely rather than sent as null, so do not expect a fixed set of keys.

For request-validation failures, message is a JSON-encoded list describing each field that failed, so it can be long and is not a stable, human-friendly string.

Unexpected errors return a shorter body, with the underlying message replaced by a generic one so that internal detail is not exposed:

{ "message": "Something went wrong. Please retry or contact us at connect@morgen.so.", "errorRef": "V1StGXR8Z5jd" }
⚠️

Do not rely on status being present in the error body. It appears only on errors Morgen raises deliberately. On an unexpected failure the field is absent entirely, so a client written as if (body.status >= 400) reads undefined, the comparison is false, and the request is treated as having succeeded, precisely at the moment something has actually gone wrong. Branch on the HTTP status line, and treat the body as diagnostic detail only. Note that this is not purely a 500 concern: a request whose body is not valid JSON, or which exceeds the maximum payload size, is rejected before it reaches the endpoint and also returns the shorter shape, despite carrying a 4xx status line.

💡

Every error carries an errorRef, in both shapes. Quote it when you contact support: it identifies the exact failure in our logs, and it is the fastest way for us to diagnose a problem you cannot reproduce.

Common Validation Errors

When creating or updating events, you may encounter validation errors. Here are the most common errors and how to resolve them:

Missing Required Timing Fields

Error Message:

Properties `start`, `duration`, `timeZone` and `showWithoutTime` must be provided together. `timeZone` can be `null` to indicate floating events.

Cause: You provided some but not all of the required timing fields.

Solution: Always provide all four fields together: start, duration, timeZone, and showWithoutTime.

Note: Although the error message mentions that timeZone can be null, reserve null for all-day events (showWithoutTime: true). Do not use it for floating (no-timezone) timed events: as described in the warning under Create an event, those are not reliably preserved across calendar backends (providers may reject them or normalize them to a concrete zone). For timed events, always provide an explicit IANA timezone.

Example:

{ "start": "2023-03-01T10:15:00", "duration": "PT1H", "timeZone": "Europe/Berlin", "showWithoutTime": false }

Invalid Timezone

Error Message:

Event 'timeZone' should be a valid IANA time zone (e.g. Europe/Paris)

Cause: The provided timezone is not a valid IANA timezone identifier.

Solution: Use valid IANA timezone names. See the IANA Time Zone Database .

Valid Examples: America/New_York, Europe/London, Asia/Tokyo Invalid Examples: EST, PST, GMT+1


Invalid Duration Format

Error Message:

Event `duration` should be a valid ISO8601 duration (e.g. P1D)

Cause: The duration is not in valid ISO 8601 format.

Solution: Use ISO 8601 duration format: PT[hours]H[minutes]M or P[days]D

Valid Examples:

  • PT1H - 1 hour
  • PT30M - 30 minutes
  • PT1H30M - 1 hour 30 minutes
  • P1D - 1 day

Event Identification Error

Error Message:

Event must have either 'id' or both 'masterEventId' and 'recurrenceId'

Cause: When updating/deleting an event, you didn’t provide a valid way to identify it.

Solution: Provide either:

  1. The event’s direct id, OR
  2. Both masterEventId and recurrenceId (for recurring event instances)
Last updated on