Skip to content

Store your own data on calendar events: Google extended properties, Graph extensions, or metadata

Updated

TL;DR

Google Calendar stores your data in extendedProperties: up to 300 properties and 32 kB per event, keys over 44 characters silently dropped and values over 1,024 characters silently truncated. Outlook offers open extensions, which Microsoft recommends but which can't be filtered and use up a mailbox quota, or extended properties addressed by GUID and name with their own $filter syntax. Pyas keeps your data next to the event instead: up to 50 values per event or calendar, the same on every provider, and searchable.

Why you need somewhere to put it

Almost every product that writes calendar events needs to tie them back to its own records: the booking ID behind an interview, the CRM deal behind a meeting, the order behind a delivery slot. When the event comes back in a webhook or a sync, you need to know which of your records it belongs to, and you need to find your events without reading every event on someone's calendar.

Each provider has its own place for that data, with its own limits and its own way to search.

Google Calendar: extendedProperties

Google gives every event two maps of string keys to string values, private and shared:

Event bodyJSON
{
  "summary": "Interview: Sam Patel",
  "extendedProperties": {
    "private": { "bookingId": "bk_1007" },
    "shared": { "stage": "onsite" }
  }
}

The difference is who sees them. Per Google's extended properties guide, "private properties are set on one attendee's local 'copy' of the event", while "Shared properties are visible and editable by all attendees of an event." Keep your own IDs in private.

The limits are tight, and they fail quietly:

  • "The maximum size of a property's key is 44 characters, and properties with longer keys will be silently dropped."
  • "The maximum size of a property's value is 1024 characters, and properties with longer values will be silently truncated."
  • "An event can have up to 300 properties totaling up to 32kB in size", and those 300 count "shared and private properties, across all 'copies' of the event."

To find events by a property, events.list takes privateExtendedProperty=bookingId=bk_1007 or sharedExtendedProperty=.... Matching is on the exact value only, and the filters can't be combined with a syncToken, so an incremental sync can't be narrowed to your own events. Google's guide and its reference also describe repeated filters differently, one as OR and one as AND, so test the combination you rely on.

Outlook: open extensions or extended properties

Microsoft Graph has two ways to do this, and its docs steer you to the one you can't search.

Open extensions are named JSON objects on the event. Microsoft calls them "the recommended solution for most scenarios". On Outlook items, though, "each open extension ... is stored in a MAPI named property", and "MAPI named properties are a finite resource in a user's mailbox. When a user's named property quota is exhausted, you can't create anymore named properties for that user." Removing an extension "doesn't remove the named property", so Microsoft advises that "most applications should require no more than one extension." Open extensions can't be used in $filter, and delegates can't add them to events in a shared mailbox calendar.

Extended properties are the older MAPI properties, addressed by a type, a GUID and a name:

Create an event with oneHTTP
POST /me/events
{
  "subject": "Interview: Sam Patel",
  "singleValueExtendedProperties": [
    { "id": "String {66f5a359-4659-4830-9070-00047ec6ac6e} Name bookingId", "value": "bk_1007" }
  ]
}

They can be filtered, with their own syntax:

HTTP
GET /me/events?$filter=singleValueExtendedProperties/Any(ep: ep/id eq 'String {66f5a359-4659-4830-9070-00047ec6ac6e} Name bookingId' and ep/value eq 'bk_1007')

and read only by expanding them by ID, which Microsoft calls "currently the only way to get" one:

HTTP
GET /me/events/{id}?$expand=singleValueExtendedProperties($filter=id eq 'String {66f5a359-4659-4830-9070-00047ec6ac6e} Name bookingId')

The create response doesn't include the property, so confirming it takes another request. Microsoft also notes that "You can't use the REST API to get all the extended properties of a specific instance in one call", that the name in the ID is matched case-sensitively while the value is matched case-insensitively, and that once you choose an ID format, you "should access that extended property by only that format."

One more Outlook detail matters here: an event's ID can change, for example when it is moved to another calendar, so a mapping you keep elsewhere by event ID can lose its event.

Or keep it in your own database

Many teams skip both and keep a table of event ID to their own record. It works the same for every provider, but you take on what the providers' fields did for free: noticing when an event is deleted, coping with IDs that change, and finding your events again after a full resync. You still can't ask a provider for "every event with booking bk_1007" without reading the calendar.

What Pyas does: metadata

Pyas keeps your data next to the event itself, outside the provider, and returns it with every read:

book.tsTypeScript
const event = await pyas.events.create({
  accountId,
  title: 'Interview: Sam Patel',
  startsAt: '2026-11-04T15:00:00Z',
  endsAt: '2026-11-04T16:00:00Z',
  metadata: { bookingId: 'bk_1007', stage: 'onsite' },
})

// Later: every event for that booking, across the app's connected calendars
const page = await pyas.events.list({ metadata: { bookingId: 'bk_1007' } })
  • The same on every provider. Google Calendar, Outlook, Pyas calendars and the sandbox all take the same metadata field, so there is no per-provider code and no GUIDs.
  • Room for what you need. Up to 50 keys and 8 KB per event or calendar, with text, number or boolean values. Keys up to 64 characters, refused with an error rather than dropped.
  • Searchable. GET /v2/events?metadata.bookingId=bk_1007 finds events across your whole app, or narrowed to one account or one of your users, with up to five filters combined. Each result is read fresh from its provider.
  • Simple updates. A PATCH merges the keys you send; setting a key to null removes it.
  • Private to your app. Another app never sees it, and nothing is written into your users' events for attendees to see.
  • It stays with the event. Edits made in Google Calendar or Outlook keep it, and on Outlook, Pyas follows an event whose ID changes when it moves.
  • Bring what you have. If your events already carry Google extended properties or Outlook named properties, POST /v2/metadata/import copies them into metadata without changing the calendars, with a dry run first if you want one.

Changing only metadata doesn't change the event's version and doesn't send an event.updated webhook, because nothing on the calendar changed. If you still need the provider's own properties, read them with include=extendedProperties.

See the calendar API for the rest of the event model, or try metadata on sample calendars in the sandbox.

Google Calendar, Outlook and Zoom APIs side by sideHow to build calendar webhooks that keep working (Google push channels and Microsoft Graph subscriptions)