Skip to content

Time zones in calendar APIs: IANA vs Windows names, clock changes and all-day events

Updated

TL;DR

Google returns times with an offset and IANA zone names such as Europe/Zurich; Microsoft Graph returns times with no offset, in UTC unless you send Prefer: outlook.timezone, and Windows zone names such as Pacific Standard Time. Store each event as a UTC instant plus its IANA zone, convert Windows names with CLDR's mapping, read all-day events as dates, and expect zone rules to change: in 2026 alone, British Columbia, Alberta and Manitoba moved to permanent time.

Two providers, two ways to say "when"

Google Calendar gives start.dateTime an offset, and a zone name in start.timeZone, "Formatted as an IANA Time Zone Database name, e.g. 'Europe/Zurich'". When you write, "A time zone offset is required unless a time zone is explicitly specified in timeZone." List responses default to "the time zone of the calendar", or the zone you pass as timeZone.

Microsoft Graph uses a dateTimeTimeZone: a dateTime with no offset, such as 2017-08-29T04:00:00.0000000, and a timeZone such as "Pacific Standard Time". Reads come back in UTC: "If the Prefer: outlook.timezone header is not specified, the start and end times are returned in UTC." The zone the organizer chose is in originalStartTimeZone, and a value of tzone://Microsoft/Custom means "a legacy custom time zone was set in desktop Outlook."

So the same meeting reads as 2026-11-03T10:00:00-05:00 with America/New_York from Google, and 2026-11-03T15:00:00.0000000 with zone UTC (and originalStartTimeZone: Eastern Standard Time) from Outlook.

Windows zone names

Outlook's zones are Windows names. To turn them into IANA names, the standard source is the Unicode CLDR windowsZones mapping, using the "001" territory entry for each Windows zone.

The mapping lags real changes. Windows added British Columbia Standard Time, Alberta Standard Time and Manitoba Standard Time before CLDR mapped them; CLDR 49, released on October 21, 2026, finally does, but any library built on an older CLDR still falls back for those three. Some mailboxes also report names no list has, such as "Customized Time Zone". Keep a short table of your own for the gaps, and fall back to the mailbox's zone from /me/mailboxSettings, not to UTC.

Writing to Outlook has its own catch. Graph accepts Windows names plus a fixed list of "Additional time zones" in IANA form, and warns that "Methods such as create or update might not support all dateTimeTimeZone time zones." GET /me/outlook/supportedTimeZones(TimeZoneStandard=microsoft.graph.timeZoneStandard'Iana') lists what a mailbox accepts.

All-day events

An all-day event is a date, not a time. Google sends start.date with an exclusive end.date, and "the timezone field has no significance for all-day events". Graph sets isAllDay, and requires start and end to be "set to midnight and be in the same time zone."

In practice, Outlook keeps all-day events created through Graph as midnight to midnight UTC. Read as UTC instants, a holiday on Tuesday starts on Monday evening in the Americas. Read all-day events as dates in the calendar's or mailbox's own zone.

Clock changes

A recurring 9:00 meeting in New York is 14:00 UTC in winter and 13:00 UTC in summer. If you store the series as a UTC time, every occurrence after the change is an hour off. Google requires a zone on recurring events for this reason: "For recurring events, you must always specify a single time zone to expand the recurrences of the event."

Two moments each year are ambiguous. At the spring change, a local time like 2:30 doesn't exist; at the autumn change, 1:30 happens twice. RFC 5545, the iCalendar standard, settles both: a repeated time "refers to the first occurrence", and a skipped one is "interpreted using the UTC offset before the gap", which moves it forward.

Zone rules also change by law, sometimes with weeks of notice. The IANA database recorded British Columbia moving to permanent UTC-7 in March 2026, Alberta to permanent UTC-6 in June, and Manitoba to permanent UTC-5 on October 31, 2026, and Cuba deciding days beforehand not to fall back on November 1. Your runtime's zone data has to be current for future events in those places to land at the right time, so update it like any other dependency.

How to store times

  1. Store an instant and a zone. Keep each event's start and end as UTC instants, plus the IANA zone it belongs to. Show it with the zone, never with the server's.
  2. Keep the zone for recurring events. Expand the series in its zone, not in UTC.
  3. Store all-day events as dates.
  4. Convert Windows names once, at the edge, with CLDR plus your own table for gaps.
  5. Send times with an offset or Z. A time without one is ambiguous, and Graph treats a calendarView bound without an offset as UTC.

How Pyas does it

Every Pyas event, from Google, Outlook or a Pyas calendar, carries:

  • startsAt and endsAt in UTC,
  • startsAtLocal and endsAtLocal in the event's own zone,
  • timezone, always an IANA name: America/New_York, never Eastern Standard Time.

Pyas maps Outlook's Windows names to IANA zones, including the Canadian ones, and falls back to the mailbox's zone. All-day events are read in the calendar's own zone, so Tuesday stays Tuesday. When you write, Pyas takes times with an offset and an IANA zone, refuses abbreviations such as EST with a clear error, and sends each provider the form it expects. See the calendar API, or read sample events in several zones in the sandbox.

Three Canadian time zones Outlook uses that older Windows-to-IANA mappings lackOutlook stores every all-day event in UTCRecurring events across Google Calendar and Outlook: RRULE vs Graph recurrence patterns