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>",
},
}
);| Parameter | Type | Default | Required | Description |
|---|---|---|---|---|
accountId | Query | - | Yes | The calendar account ID to retrieve events from. |
calendarIds | Query | - | Yes | Comma-separated list of calendar IDs to retrieve events from. Notice that these calendars must all belong to the same account identified by accountId. |
start | Query | - | Yes | Start of the time window in ISO 8601 format, e.g. 2023-03-01T00:00:00Z |
end | Query | - | Yes | End 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
| Field | Type | Description |
|---|---|---|
@type | String | Always "Event" |
id | String | Morgen’s identifier for the event. See Event IDs. |
uid | String | The provider’s iCalendar UID. This is not the id, and no endpoint accepts it in place of id. |
calendarId | String | Morgen ID of the calendar the event belongs to. |
accountId | String | Morgen ID of the account the event belongs to. |
integrationId | String | The provider the event came from, e.g. "google", "o365", "caldav". |
baseEventId | String | The 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. |
masterEventId | String | Recurring instances only: the Morgen ID of the master recurring event. Can be used with recurrenceId to identify and update a specific instance. |
masterBaseEventId | String | Recurring instances only: the provider’s ID of the master recurring event. Not returned for non-recurring events. |
created | String | Creation timestamp. |
updated | String | Last update timestamp. |
recurrenceId | String | Recurring instances only: the LocalDateTime that identifies this occurrence within the series, e.g. "2023-08-29T16:00:00". |
recurrenceIdTimeZone | String | IANA timezone for recurrenceId. |
title | String | Event title/summary. |
description | String | Event description, plain text or HTML. |
descriptionContentType | String | Either "text/plain" or "text/html", depending on the content of description. |
start | String | Start time in LocalDateTime format. Note that this carries no timezone offset, the zone is given separately in timeZone. |
timeZone | String | IANA timezone the start is expressed in. |
duration | String | Duration in ISO 8601 format, e.g. "PT25M". |
showWithoutTime | Boolean | true for an all-day event, false for an event at a specific time of day. |
privacy | String | "public", "private" or "secret". Whether the event details can be shown to other users with access to the calendar. |
freeBusyStatus | String | "free" or "busy". Whether the event marks the participants as busy. |
locations | Object | Map of location IDs to location objects. |
participants | Object | Map 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. |
alerts | Object | Map 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. |
useDefaultAlerts | Boolean | If true, the calendar’s default alerts apply instead of the alerts in alerts. |
recurrenceRules | Array | Recurrence rules, for recurring events. |
google.com:colorId | String | Google Calendar only. Read/write, see Google Calendar Color ID. |
google.com:hangoutLink | String | Google Calendar only. Read-only Google Meet link. |
morgen.so:derived | Object | Read-only fields derived from the rest of the event, for example a virtual room URL found in the description. |
morgen.so:metadata | Object | Read-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:requestVirtualRoom | String | Write-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
idis only meaningful within the account it came from. Always pair anidwith the sameaccountIdit 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. idandcalendarIdare not interchangeable. They are different kinds of identifier, and passing one where the other is expected fails.uidis not theid.uidis the provider’s iCalendar UID and is not accepted by any endpoint that asks for anid.
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:
| Field | Type | Access | Description |
|---|---|---|---|
google.com:colorId | String | Read/Write | Google’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>",
},
});| Parameter | Type | Default | Required | Description |
|---|---|---|---|---|
id | Query | - | Yes | The 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:
POST /v3/events/deletereturns 204.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
| Field | Type | Required | Description |
|---|---|---|---|
accountId | String | Yes | The ID of the account to create the event in. |
calendarId | String | Yes | The ID of the calendar to create the event in. |
title | String | Yes | The title/summary of the event. |
start | String | Yes | Start time in LocalDateTime format (e.g., 2023-03-01T10:15:00). |
duration | String | Yes | Duration in ISO 8601 format (e.g., PT1H for 1 hour, PT30M for 30 minutes). |
timeZone | String or null | Yes | IANA timezone for timed events (e.g., Europe/Paris). Use null for all-day events (showWithoutTime: true). See the note below on floating timed events. |
showWithoutTime | Boolean | Yes | true for all-day events, false for timed events. |
description | String | No | Event description. Can be plain text or HTML (specify with descriptionContentType). |
descriptionContentType | String | No | Either text/plain or text/html. Default: text/plain. |
locations | Object | No | Map of location IDs to location objects. See JSCalendar standard . |
participants | Object | No | Map of participant IDs to participant objects. The key should be the base64-encoded email address. |
alerts | Object | No | Map of alert IDs to alert objects. See Alert structure above. Cannot be used with useDefaultAlerts: true. |
useDefaultAlerts | Boolean | No | If true, use the calendar’s default alert settings. Cannot be used together with alerts. |
privacy | String | No | public, private, or secret. Default: public. |
freeBusyStatus | String | No | free or busy. Default: busy. |
recurrenceRules | Array | No | Array 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})
});| Parameter | Type | Default | Required | Description |
|---|---|---|---|---|
seriesUpdateMode | Query | single | No | Defines 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
| Field | Type | Required | Description |
|---|---|---|---|
id | String | Yes* | The Morgen ID of the event to update. *Can be omitted if masterEventId and recurrenceId are provided (see below). |
accountId | String | Yes | The ID of the account the event belongs to. |
calendarId | String | Yes | The ID of the calendar the event belongs to. |
masterEventId | String | No | For recurring event instances: The Morgen ID of the master recurring event. Must be provided with recurrenceId if id is not provided. |
recurrenceId | String | No | For 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. |
recurrenceIdTimeZone | String | No | Optional 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:
- Using the instance’s direct
id(if you have it) - 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>})
});| Parameter | Type | Default | Required | Description |
|---|---|---|---|---|
seriesUpdateMode | Query | single | No | Defines 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
| Field | Type | Required | Description |
|---|---|---|---|
id | String | Yes | The Morgen ID of the event to delete. See Event IDs. |
accountId | String | Yes | The ID of the account the event belongs to. |
calendarId | String | Yes | The 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.
| Status | When you will see it |
|---|---|
400 | The 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. |
401 | The Authorization header is missing, malformed, or carries a key that is not valid. See Authentication. |
403 | The key is valid but your plan does not include API access. |
404 | The 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. |
429 | You have exceeded your rate limit. See Rate Limits. |
500 | An 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"
}| Field | Always present | Description |
|---|---|---|
message | Yes | A human-readable description of the failure. Intended for logs and debugging, not for matching on programmatically. |
status | No | The HTTP status code, repeated in the body. See the warning below. |
errorRef | Yes | An identifier for this specific failure. |
context | No | Present 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 hourPT30M- 30 minutesPT1H30M- 1 hour 30 minutesP1D- 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:
- The event’s direct
id, OR - Both
masterEventIdandrecurrenceId(for recurring event instances)