Most engineering teams are not purely Google or purely Microsoft. One person lives in Gmail Calendar, another in Outlook, and scheduling a four-person meeting means tab-hopping and mental timezone math. A unified calendar view sounds simple until you try to keep it accurate without hammering two different APIs all day.
At ChronoFlow, we pull Google Calendar and Microsoft Outlook into one workspace. The hard part is not fetching events once — it is staying in sync incrementally, handling deletes, and making both providers look the same to everything downstream. This post explains the patterns we use, at the level of public API docs, not our internal implementation.
Why one calendar view is hard
Google and Microsoft expose different JSON shapes, pagination models, and change-tracking mechanisms. Google uses sync tokens; Microsoft Graph uses delta links. All-day events, time zones, and deleted meetings are represented differently. If you normalize poorly, you get duplicate events, stale blocks, or meetings that disappear silently.
OAuth without over-scoping
Calendar sync starts with OAuth. The mistake most teams make is requesting every scope the product might ever need on first connect. Users read the consent screen carefully; a wall of permissions kills trust.
For calendar-only features, request calendar read (and write only if you create events). For Google, that means the Calendar API scope plus offline access so you receive a refresh token. For Microsoft, include offline_access and calendar read scopes — without it, access tokens expire in about an hour and the integration silently breaks.
Store tokens per user and per provider. Encrypt at rest, rotate refresh tokens when the provider re-issues them, and surface a clear “Reconnect account” state when refresh fails — especially for Microsoft accounts that can go inactive for long periods.
Google incremental sync
Google Calendar supports incremental sync via a syncToken. On the first fetch, you list events in a window and Google returns a nextSyncToken. On later runs, pass that token back and Google returns only what changed.
// Pseudocode — first sync
const response = await calendar.events.list({
calendarId,
timeMin,
timeMax,
singleEvents: true,
orderBy: "startTime",
})
const syncToken = response.data.nextSyncToken
// persist syncToken for this user + calendar
// Later — incremental sync
const changes = await calendar.events.list({
calendarId,
syncToken: savedSyncToken,
singleEvents: true,
})The footgun: sync tokens expire. If a calendar has not been synced in a while, Google may respond with 410 Gone. Your code should catch that, discard the stale token, and run a full sync to obtain a fresh one. Without this fallback, users see frozen calendars after a vacation.
Microsoft delta queries
Microsoft Graph uses delta queries. The first call hits a delta endpoint; the response includes an @odata.deltaLink URL. On the next sync, call that URL directly — you receive only changes since the last run.
Gotchas worth planning for
Pagination: the delta link may appear only on the last page of a multi-page first sync. Grab it too early and you miss events.
Deletes: removed events often come back as minimal objects with an @removed flag, not a full event body. Your sync loop must delete locally when you see that flag.
Time zones: request UTC (via the Prefer: outlook.timezone="UTC" header) so you are not parsing ambiguous local offsets from each mailbox.
One event shape for both providers
Define an internal event model with fields your product actually uses: title, start, end, all-day flag, location, attendees, and a stable external ID. Map Google's summary and Microsoft's subject to the same field. Map htmlLink and webLink to one “open in provider” URL.
Use a composite unique key — user, provider, external event ID — so upserts are idempotent. Sync jobs overlap, webhooks retry, and users refresh aggressively; duplicates are inevitable unless the database enforces uniqueness.
What actually breaks in production
Rate limits are asymmetric. Google Calendar quotas are generous for most apps; Microsoft Graph is stricter during bulk initial syncs. Back off on 429 responses and avoid syncing every calendar in parallel on first connect.
Refresh tokens need UX. When Microsoft refresh fails with invalid_grant, do not fail silently. Show a reconnect prompt before the user notices missing meetings.
Incremental sync is worth the complexity. Moving from full re-fetch to sync tokens and delta links dramatically cuts API volume for active users and makes the calendar feel live instead of “synced five minutes ago.”
ChronoFlow ships a unified Google + Outlook calendar for engineering teams. Compare us to Motion, Reclaim, and Clockwise, or join the beta waitlist.