Events
Morgen maps Calendar Events from different providers to a common data model, inspired by the JSCalendar standard (opens in a new tab). 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. |
Event schema
Here is an example response for the /events/list endpoint described above:
{
"data": {
"events": [
{
"@type": "Event",
// Morgen ID for event
"id": "WyJBUU1rQURaa1lXWXpOel...",
// 3rd-party iCalendar UID
"uid": "kki3mce...@google.com",
// Morgen ID for the calendar
"calendarId": "WyI2NDBhNjJjOW...",
// Morgen ID for the account
"accountId": "640a62c9aa5b7e06cf420000",
"integrationId": "o365",
// 3rd-party ID for the base event in the recurrence
"baseEventId": "AAkALgAAAAAAHYQDEapmEc2by...",
// For recurring event instances: Morgen ID of the master recurring event
"masterEventId": "WyI2NDBhNjJjOWFhNWI3ZTA2Y2Y0MjAwMDA...",
// For recurring event instances: 3rd-party ID of the master recurring event
"masterBaseEventId": "AAkALgAAAAAAHYQDEapmEc2by...",
"created": "2023-02-28T11:50:57",
"updated": "2023-02-28T17:56:44",
// ID + timezone that identify this event within the recurrence
"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>",
// Either "text/plain" or "text/html" depending on content
"descriptionContentType": "text/html",
// Start time of the event. Notice that this does not include a time zone offset.
// Instead, the time zone is specified in the "timeZone" field.
"start": "2023-03-01T10:15:00",
"timeZone": "Europe/Berlin",
// Duration in ISO format: 'PT[hours]H' | 'PT[minutes]M'
"duration": "PT25M",
// If true, this event is an all-day event. Otherwise it occurs at a specific time in the day.
"showWithoutTime": false,
// Whether the event details can be displayed to others users with access to the calendar.
"privacy": "public",
// Whether the event is marked "busy" for the participants. Otherwise "free".
"freeBusyStatus": "free",
"locations": {
"1": {
"@type": "Location",
"name": "Morgen HQ, Forrlibuckstrasse 223"
}
},
"participants": {
// Map of participant IDs to participants.
// The key can be the participant's email address or an internal ID.
"doe@morgen.so": {
"@type": "Participant",
"name": "John Doe",
// Email address of the participant
// NOTE: Sometimes, the participant email is a group email,
// e.g. if the event was created in a secondary Google
// calendar.
"email": "doe@morgen.so",
"roles": {
// Whether this participant will be attending
"attendee": true,
// Whether the participant owns the event and can
// therefore make changes that affect other
// participants
"owner": true
},
"participationStatus": "needs-action"
},
"bWFyY29AbW9yZ2Vu0000": {
"@type": "Participant",
"name": "Willie White",
"email": "white@morgen.so",
"roles": {
"attendee": true
},
// Indicates whether this participant is also the owner of
// the calendar account
"accountOwner": true,
"participationStatus": "needs-action"
},
...
},
"alerts": {
// Map of alert IDs to alerts.
// Alert IDs are base64-encoded: btoa(JSON.stringify({ a: action, to: offset }))
"eyJhIjoiZGlzcGxheSIsInRvIjoiLVBUMTVNIn0=": {
"@type": "Alert",
"trigger": {
"@type": "OffsetTrigger",
// Offset in ISO format: 'PT[hours]H' | 'PT[minutes]M'
// In this case, 15 minutes before the event start time.
"offset": "-PT15M",
"relativeTo": "start" // only "start" is supported at the current time
},
"action": "display"
},
...
},
// Use default calendar alerts instead of custom alerts
"useDefaultAlerts": false,
// Recurrence rules for recurring events
"recurrenceRules": [
{
"@type": "RecurrenceRule",
"frequency": "weekly",
"interval": 1,
"byDay": [{ "@type": "NDay", "day": "mo" }]
}
],
// Google Calendar specific fields
"google.com:colorId": "5", // Writable: Google's event color ID (1-11)
"google.com:hangoutLink": "https://meet.google.com/abc-defg-hij",
},
// Derived, read-only fields.
// For example a virtual room URL if found in the event description.
"morgen.so:derived": {
"virtualRoom": {
"url": "https://us02web.zoom.us/j/82...?pwd=..."
}
},
// Metadata, read-only, Morgen-specific fields.
"morgen.so:metadata": {
"updated": "2023-09-25T08:53:36.793Z"
// Category-related fields
"categoryId": "9b5f823f-d690-4781-8783-95052ac05740@morgen.so",
"categoryName": "Morgen",
"categoryColor": "#CCEACD", // Hexadecimal color code
// Task-related fields. An event with a taskId will be displayed as a task in the Morgen calendar.
"progress": "needs-action", // Possible values: "needs-action", "completed".
"taskId": "0d464578-08ff-4df6-88b2-19083b296df7",
}
]
}
}
Please refer to the JSCalendar standard (opens in a new tab) for more information about the data model. Please consider the following differences:
Differences with JSCalendar
Additional fields
Morgen returns some additional fields for convenience and to provide more information about the event:
integrationId: The ID of the integration the event belongs to.accountId: The ID of the account the event belongs to.calendarId: The ID of the calendar the event belongs to.baseEventId: The provider ID of the base event the event belongs to. This is the ID of the recurring event for recurring events, and the ID of the event itself for non-recurring events.masterEventId: The Morgen ID of the master recurring event (for recurring event instances only). Can be used withrecurrenceIdto identify and update specific instances.masterBaseEventId: The provider ID of the master base event the event belongs to. This is the ID of the master recurring event for recurring events, and will not be returned for non-recurring events.morgen.so:derived: An object containing useful fields derived from other event information. Read-only.morgen.so:metadata: An object containing Morgen-specific metadata, such as the event category and the link to a task.morgen.so:requestVirtualRoom: Writable field to request creation of a virtual meeting room ("default"|"googleMeet"|"microsoftTeams").useDefaultAlerts: Boolean flag to use the calendar's default alert settings instead of custom alerts.
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 (opens in a new tab)
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.
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 (e.g., Europe/Paris). Use null for floating events (no timezone). |
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 (opens in a new tab). |
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 (opens in a new tab) are also supported.
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). This parameter has no effect on 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 (opens in a new tab).
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"
})
});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 update an event in 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 update recurring events. Possible values are: all (update all events), future (update this and future occurrences), single (update this event only, default). This parameter has no effect on non-recurring events. |
id | Body | - | Yes | The ID of the event to update. |
accountId | Body | - | Yes | The ID of the account the event belongs to. |
calendarId | Body | - | Yes | The ID of the calendar the event belongs to. |
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.
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 (opens in a new tab).
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)