# Sync calendars across providers

Source: https://developer.nylas.com/docs/cookbook/calendar/cross-provider-calendar-sync/

Keeping events in sync across Google Calendar, Outlook, and iCloud usually means three separate integrations, each with its own OAuth project, event shape, and quirks. The Google Calendar API, Microsoft Graph, and CalDAV disagree on how they represent event IDs, recurrence rules, and change notifications. Write a sync engine against all three directly and most of your code becomes provider translation, not sync logic.

This guide shows how to sync events across providers through one schema and one webhook channel, then maps the differences that still leak through, so your sync stays correct when a recurring Outlook event and an iCloud event land in the same database table.

## Which calendar API supports both Google Calendar and Outlook Calendar?

The Nylas Calendar API reads Google Calendar, Outlook, Microsoft 365, iCloud, and Exchange through one schema. You list events with `GET /v3/grants/{grant_id}/events?calendar_id=<CALENDAR_ID>`, and every provider returns the same JSON: a `when` object, `participants`, and a `recurrence` array. One grant per connected account replaces three separate API clients.

The same call works whether the account is a personal Gmail address, a Microsoft 365 mailbox behind admin consent, or an iCloud calendar authenticated with an app-specific password. Because the response shape is identical across all 4 supported calendar providers (Google, Microsoft, iCloud, and Exchange), your parser, your database schema, and your sync diff logic stay provider-agnostic. The differences that remain (covered below) are narrow and predictable, not a full per-provider rewrite. For a comparison of the unified layer against calling Microsoft directly, see [Outlook vs Microsoft Graph calendar](/docs/cookbook/calendar/outlook-vs-graph-calendar/).

## How do I sync calendar events across multiple providers using one API?

Sync through three moving parts: list events per calendar with `GET /v3/grants/{grant_id}/events`, subscribe to `event.created`, `event.updated`, and `event.deleted` webhooks through `POST /v3/webhooks`, and key your local records on the stable event ID. The webhook channel pushes changes from every provider into one handler, so you never poll three APIs on a timer.

A reliable sync starts with a one-time backfill, then runs incrementally off webhooks. List each calendar's events, store them keyed by the Nylas event ID, then let webhooks drive every later change. The first call below lists events from a connected calendar. The `calendar_id` parameter is required; pass `primary` for the account's default calendar, and use `limit` (max 200) to size each page. This same request returns Google, Outlook, and iCloud events in one shape.

```bash
curl --request GET \
  --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/events?calendar_id=primary&limit=200' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>'
```

```python [crossSync-Python SDK]
from nylas import Client

nylas = Client(api_key="<NYLAS_API_KEY>")

events = nylas.events.list(
    "<NYLAS_GRANT_ID>",
    query_params={"calendar_id": "primary", "limit": 200},
)

for event in events.data:
    print(event.id, event.title, event.when)
```

To find every calendar on an account before you back-fill, call `GET /v3/grants/{grant_id}/calendars`. It returns each calendar with a `read_only` flag, so you can skip calendars your app can't write back to. For the deeper two-way pattern, including conflict handling and write-back, see [build a two-way calendar sync](/docs/cookbook/use-cases/sync/two-way-calendar-sync/).

## How do I handle calendar provider differences when syncing across Google, Outlook, and iCloud?

Three differences matter most: event ID stability, recurrence representation, and update detection. The API normalizes the JSON shape, but it can't change how each provider issues IDs or expresses recurring series. Store the provider name alongside every event and branch on these three behaviors, and your cross-provider sync stays correct.

Event IDs behave differently per provider. Google keeps the same event ID for an event no matter which user queries it, so the same meeting in two attendees' calendars shares one ID. Microsoft and iCloud issue IDs that are unique per user, so the same meeting carries different IDs in each mailbox. Treat the Nylas event ID plus the `grant_id` as your composite key, not the event ID alone, or you'll collide Google events that legitimately share an ID across two grants you sync.

Recurrence is the second difference. The `recurrence` array holds `RRULE` and `EXDATE` strings, and it appears only on the master event, not on individual instances. Two provider-specific facts will bite you: Microsoft Graph adds one day to the `UNTIL` date in a rule, so a series that should end June 30 reports July 1, and iCloud won't let you convert a recurring event to non-recurring at all. Use the `master_event_id` parameter to fetch a series' instances, and store the `ical_uid` to correlate the same meeting across systems. Note that `ical_uid` can be `null` for some synced events, and recurring events may share one `ical_uid`.

## How do I get real-time updates from every calendar at once?

Subscribe one webhook to the calendar triggers and every provider's changes arrive at the same endpoint. Create a subscription with `POST /v3/webhooks`, set `trigger_types` to `event.created`, `event.updated`, and `event.deleted`, and point `webhook_url` at your handler. Nylas watches Google, Outlook, and iCloud server-side and posts a notification when any event changes, so you stop polling three APIs.

The request below registers a single subscription covering all three event triggers across every connected account. One webhook channel replaces per-resource subscriptions like Microsoft Graph's, which expire and need renewal roughly every few days. After registration, verify the endpoint with the challenge handshake Nylas sends, then process each notification by event ID.

```bash
curl --request POST \
  --url 'https://api.us.nylas.com/v3/webhooks' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>' \
  --header 'Content-Type: application/json' \
  --data '{
    "trigger_types": ["event.created", "event.updated", "event.deleted"],
    "webhook_url": "https://example.com/nylas/webhooks",
    "notification_email_addresses": ["alerts@example.com"]
  }'
```

For the full webhook setup, including signature verification and the challenge response, see [calendar webhooks](/docs/cookbook/calendar/calendar-webhooks/). When you can't expose a public endpoint yet, fall back to polling `GET /v3/grants/{grant_id}/events` with the `updated_after` filter to pull only events changed since your last sync run.

## Unified sync vs per-provider integrations

The table compares building cross-provider sync on the native APIs against one unified schema. Native APIs give you the deepest provider-specific control; the unified path collapses three integrations into one code path. The right choice depends on how many providers you support and how much provider-specific surface you actually touch.

| Task | Native APIs (Google + Graph + CalDAV) | Nylas unified API |
| --- | --- | --- |
| **Auth** | 3 OAuth projects, 3 token flows | 1 connector per provider, one grant per account |
| **Event schema** | 3 different shapes to map | One shape across all providers |
| **Recurrence** | Per-provider `RRULE` parsing | One `recurrence` array, documented differences |
| **Change notifications** | Graph subscriptions renew ~every 3 days | One webhook, no renewal |
| **Providers reached** | Google, Microsoft, CalDAV separately | Google, Microsoft, iCloud, Exchange |

Be honest about the tradeoff: if you support a single provider and need surfaces beyond calendar, like Microsoft Teams or SharePoint, call that provider's API directly. The unified layer pays off the moment you add a second provider or only need calendar data. The official [RFC 5545 iCalendar spec](https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.5) defines the `RRULE` grammar both approaches rely on.

## How do I keep sync incremental and avoid duplicates?

Run incremental sync off the `updated_after` filter and dedupe on a composite key. After the initial backfill, query `GET /v3/grants/{grant_id}/events` with `updated_after` set to your last successful sync timestamp, so you fetch only changed events instead of every event each run. This keeps each sync cheap and well under the 200-event page limit for most accounts.

Deduplication is the other half. Because Google shares one event ID across attendees while Microsoft and iCloud don't, key your records on `grant_id` plus event ID, never the event ID alone. Page through results with the `next_cursor` value from each response, passing it as `page_token` on the next call until the field is absent. Pair `updated_after` with webhook notifications: webhooks handle real-time changes, and a periodic `updated_after` sweep catches anything a missed or delayed webhook left behind, which is the standard safety net for production sync.

## What's next

- [Build a two-way calendar sync](/docs/cookbook/use-cases/sync/two-way-calendar-sync/) for write-back and conflict handling
- [Calendar webhooks](/docs/cookbook/calendar/calendar-webhooks/) for signature verification and the challenge handshake
- [Outlook vs Microsoft Graph calendar](/docs/cookbook/calendar/outlook-vs-graph-calendar/) for the Microsoft-specific comparison
- [Getting started with Nylas](/docs/v3/getting-started/) to create a project, connector, and your first grant