Google Calendar, Outlook and Zoom APIs side by side
Updated
TL;DR
Google Calendar and Microsoft Graph both use OAuth, push notifications that expire and incremental sync, but almost every detail differs: Google watch channels default to 7 days and can't be renewed, Graph subscriptions on events last at most 10,080 minutes; Google sends RRULE and IANA zones, Graph sends recurrence patterns and Windows zone names. Zoom is meetings only, capped at 100 meeting creates or updates per user per day, with access tokens that last an hour and refresh tokens that rotate.
At a glance
| Google Calendar API | Microsoft Graph (Outlook) | Zoom API | |
|---|---|---|---|
| Sign-in | OAuth 2.0; Google reviews apps that ask for calendar scopes | OAuth 2.0 through Microsoft Entra; work tenants may need admin consent | OAuth 2.0; Zoom reviews apps other accounts install |
| Access token | About an hour | About an hour | One hour |
| Refresh token | Expires in 7 days while the app is in Testing; dies after 6 months unused | "Typically valid for 90 days" | Expires after 90 days, and a new one comes with every refresh |
| Rate limits | 10,000 requests a minute per project, 600 a minute per user | 10,000 requests per 10 minutes and 4 at a time, per app per mailbox | 100 meeting creates or updates per user per day, plus per-second limits by plan |
| Change notifications | Watch channels, default 604,800 seconds, no renew call | Subscriptions, at most 10,080 minutes on events, 1,440 with resource data | App-wide webhooks to one URL |
| Incremental sync | syncToken; 410 means start over |
Delta query on calendarView with a deltaLink | None |
| Recurrence | RFC 5545 lines: RRULE, EXDATE, RDATE | patternedRecurrence: daily, weekly, monthly and yearly patterns |
One recurring meeting per series |
| Time zones | IANA names | Windows names, plus some IANA names | IANA names |
| All-day events | start.date, no zone |
isAllDay, stored as midnight to midnight UTC |
Not applicable |
| Meeting links | Meet, with conferenceDataVersion=1 |
Teams, on Microsoft 365 calendars only | Zoom meetings, copied onto the event yourself |
| Deleted event read by ID | 200 with "status": "cancelled" |
404 | 404 |
| Concurrency check | etag with If-Match |
@odata.etag or changeKey with If-Match |
None |
| Batch | Up to 1,000 calls per batch | Up to 20 requests per batch | None |
The sections below give the sources and what each difference means for your code.
Sign-in and tokens
Google. Calendar scopes are sensitive, so Google reviews the app before it can serve the public; until then only 100 new users can ever sign in, and in Testing every refresh token "expires in 7 days". A refresh token also stops working after six months unused, and an account can hold 100 refresh tokens per client ID, after which the oldest is invalidated without warning. Every one of those failures arrives as invalid_grant. See Google's OAuth review and refresh tokens that expire after 7 days.
Microsoft. You register an app in Microsoft Entra and sign users in with MSAL. Access tokens last about an hour, and the refresh token, which needs the offline_access scope, is "typically valid for 90 days". Microsoft's default consent policy keeps users from approving calendar access on their own in many work tenants, so they see "Need admin approval". One thing we found while testing: a refresh that names a scope the account never granted fails, so ask only for what was granted.
Zoom. "Access tokens expire after one hour" and "Refresh tokens expire after 90 days", and Zoom says to "always use the latest refresh token", because each refresh returns a new one. Store it every time. An app other Zoom accounts install goes through Zoom's app review.
Rate limits
Google allows 10,000 requests per minute per project and 600 per minute per user per project, with a daily billing threshold of 1,000,000 requests per project. Going over returns 403 userRateLimitExceeded, 403 or 429 rateLimitExceeded, or 403 quotaExceeded. Google plans to charge above its limits later in 2026. Details in Google Calendar API quotas and pricing.
Microsoft Graph limits Outlook to "10,000 API requests in a 10-minute period" and "Four concurrent requests" per app per mailbox, plus 150 MB of uploads per 5 minutes. A fifth request at the same time to one mailbox is throttled even when you are far under the 10,000. On a 429, wait for the Retry-After seconds.
Zoom caps meetings at "100 create/update requests per day (UTC) per user", reset at 00:00 UTC. Other endpoints have per-second limits by plan, from 4, 2 and 1 requests a second for light, medium and heavy endpoints on a Free account to 80, 60 and 40 on Business and above. One shared host account creating every meeting for your app runs out fast.
Change notifications
Google watch channels deliver a header-only notification (X-Goog-Resource-State of sync, exists or not_exists) to an HTTPS address. A channel's ttl defaults to 604,800 seconds, "there's no automatic way to renew a notification channel", and Google says "Notifications are not 100% reliable". You open a new channel before the old one expires and sync on every notification.
Graph subscriptions on Outlook events last at most 10,080 minutes, or 1,440 if they carry the event's data, and Microsoft says requests beyond the maximum will start to fail. Creating one starts a validation request your endpoint must answer within 10 seconds, each notification must be acknowledged within 3, and "Dropped notifications can't be recovered". Add a lifecycle notification URL when you create the subscription, because it can't be added later: Graph sends reauthorizationRequired, subscriptionRemoved and missed there.
Zoom webhooks are configured once on the app and go to one URL for every account that authorized it. Zoom validates the URL with an endpoint.url_validation request, answered with an HMAC-SHA256 of the token it sends, and signs each request in the x-zm-signature header.
The pattern that survives both calendar providers: treat each notification as a signal to run an incremental sync, and run a scheduled sync as well to catch what was dropped. More in calendar webhooks that keep working.
Incremental sync
Google returns nextSyncToken on the last page of a list. Send it back as syncToken to get only what changed, deletions included as cancelled entries. A sync token can't be combined with timeMin, timeMax, a search or an order, and an expired one answers 410 Gone: wipe your copy and sync again.
Graph uses a delta query on calendarView with a start and end, so a sync covers a window of time. Follow @odata.nextLink until you get an @odata.deltaLink, and send that next time. Deletions arrive as @removed, $select is not supported, and you keep one delta per calendar.
Recurring events
Google stores RFC 5545 lines: RRULE, EXDATE and RDATE. Graph stores a patternedRecurrence: daily, weekly, absolute or relative monthly, absolute or relative yearly, with weekday positions from first to fourth and last. Not every RRULE has a Graph pattern. Rules more frequent than daily, or a weekday counted from the end of the month other than "last", can't be expressed, so code that translates between the two has to refuse those rather than save a different rule.
Zoom has its own recurring meetings. For a recurring calendar event, one recurring Zoom meeting with no fixed time can serve every occurrence.
Time zones and all-day events
Google uses IANA names such as America/New_York. Graph uses Windows names such as Eastern Standard Time, accepts a listed set of IANA names, and warns that "create or update might not support all". Converting with the standard CLDR mapping leaves three Canadian Windows zones unmapped, and some mailboxes report names no list has, such as "Customized Time Zone". Send Prefer: outlook.timezone to get times in a zone you choose.
All-day events differ too. Google gives start.date with no time or zone. Outlook sets isAllDay and stores the event as midnight to midnight UTC, so reading it as UTC puts a holiday on the wrong evening in the Americas.
Meeting links
Meet needs conferenceData.createRequest with a new requestId and conferenceDataVersion=1 on the URL. Without the parameter, Google's reference says version 0 "ignores conference data in the event's body", and the link can still be pending in the first response.
Teams needs isOnlineMeeting: true and onlineMeetingProvider: "teamsForBusiness". Only Microsoft 365 calendars allow it; on Outlook.com the event is saved with no meeting and no error. Read the link from onlineMeeting.joinUrl, and keep it when you change the description.
Zoom creates the meeting with POST /users/me/meetings; you copy its join_url onto the calendar event and keep the two in step. All three, with code, are in meeting links from an API.
Deleted events and conflicts
A deleted Google event read by its ID returns 200 with "status": "cancelled", and an update to it still succeeds. Graph answers 404.
Both calendar APIs refuse a stale write with 412 when you send If-Match: Google's etag, or Graph's @odata.etag or changeKey. On Outlook.com, a new event's changeKey moves seconds after it is created, so a check against the key the create returned can fail on an event nobody touched.
Pagination and batching
Google pages with pageToken, up to 2,500 events and 250 calendars per page. Graph pages with @odata.nextLink, sized with Prefer: odata.maxpagesize.
Google batches up to 1,000 calls in one request; each still counts against your quota, and Google "might perform your calls in any order". Graph batches up to 20 requests, throttles each one separately, and runs Outlook requests 4 at a time, so a batch is no way around the mailbox limit.
What it means to support all three
Every row of the table is code: two sign-in flows and a third for Zoom, two notification systems with different lifetimes, two sync tokens, two recurrence models, two time zone vocabularies and three ways to add a meeting link. Each provider also reviews or approves the app before the public can use it.
Pyas puts one API over the three. Your users connect through Pyas Auth, which Google, Microsoft and Zoom have already verified. Events come back in one shape with UTC, local time and the IANA zone. Pyas keeps the channels and subscriptions renewed and sends one signed webhook naming what changed, and one conferencing field adds Meet, Teams or Zoom. See Pyas vs. building it yourself, or try it in the sandbox.