Events

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>"
    }
});
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.

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 with recurrenceId to 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:

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 (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

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 (e.g., Europe/Paris). Use null for floating events (no timezone).
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 (opens in a new tab).
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 (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})
});
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). This parameter has no effect on 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 (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>})
    });
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). This parameter has no effect on non-recurring events.
idBody-YesThe ID of the event to update.
accountIdBody-YesThe ID of the account the event belongs to.
calendarIdBody-YesThe 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 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)