# Sync calendar events with webhooks

Source: https://developer.nylas.com/docs/cookbook/calendar/calendar-webhooks/

Polling a calendar API every few minutes to spot new meetings is slow, wasteful, and a fast way to burn through rate limits. The native push systems make it worse: Google wants you to register and renew watch channels, and Microsoft Graph wants per-resource subscriptions you renew every few days. A multi-provider app ends up running two separate systems just to learn that someone moved a meeting.

Nylas webhooks collapse that into one push channel. You subscribe a single HTTPS endpoint to the calendar and event triggers you care about, and the same `event.updated` handler fires whether the change happened in Google Calendar or Outlook. For the general webhook setup that also covers email and grant events, see [Get real-time updates with webhooks](/docs/cookbook/use-cases/build/realtime-webhooks/).

## Subscribe to calendar and event changes

A calendar webhook is a push subscription: you register an HTTPS URL and a list of `trigger_types`, and Nylas sends an HTTP `POST` with a JSON body each time a matching change occurs. The six calendar triggers cover both event lifecycle and calendar lifecycle, and one subscription can carry all of them across every connected provider.

Create the subscription with a single request to `POST /v3/webhooks/`. Pass your endpoint as `webhook_url` and the triggers in `trigger_types`. The `notification_email_addresses` field is optional and tells Nylas where to email you if the endpoint starts failing. Subscribing to all six triggers covers every calendar and event change.

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

```js [calendarWebhook-Node.js SDK]


const nylas = new Nylas({ apiKey: "<NYLAS_API_KEY>" });

const webhook = await nylas.webhooks.create({
  requestBody: {
    triggerTypes: [
      WebhookTriggers.EventCreated,
      WebhookTriggers.EventUpdated,
      WebhookTriggers.EventDeleted,
      WebhookTriggers.CalendarCreated,
      WebhookTriggers.CalendarUpdated,
      WebhookTriggers.CalendarDeleted,
    ],
    webhookUrl: "https://yourapp.com/webhooks/nylas",
    description: "Calendar and event sync",
    notificationEmailAddresses: ["ops@yourapp.com"],
  },
});

console.log("Webhook created:", webhook);
```

```python [calendarWebhook-Python SDK]
from nylas import Client
from nylas.models.webhooks import WebhookTriggers

nylas = Client("<NYLAS_API_KEY>")

webhook = nylas.webhooks.create(
    request_body={
        "trigger_types": [
            WebhookTriggers.EVENT_CREATED,
            WebhookTriggers.EVENT_UPDATED,
            WebhookTriggers.EVENT_DELETED,
            WebhookTriggers.CALENDAR_CREATED,
            WebhookTriggers.CALENDAR_UPDATED,
            WebhookTriggers.CALENDAR_DELETED,
        ],
        "webhook_url": "https://yourapp.com/webhooks/nylas",
        "description": "Calendar and event sync",
        "notification_email_addresses": ["ops@yourapp.com"],
    }
)

print(webhook)
```

## Verify the webhook challenge

Before Nylas delivers any notification, it confirms you own the endpoint with a one-time handshake. The first time you create a webhook, or set an existing one back to `active`, the API sends a `GET` request to your `webhook_url` with a single `challenge` query parameter. Your endpoint has 10 seconds to answer.

Your handler must return the exact value of `challenge` in the body of a `200 OK` response, with no quotes, JSON wrapping, or extra characters. Returning anything else fails verification and the webhook stays inactive. The snippet below echoes the parameter straight back.

```js [challengeHandler-Node.js]
app.get("/webhooks/nylas", (req, res) => {
  res.status(200).send(req.query.challenge);
});
```

```python
@app.get("/webhooks/nylas")
def verify(challenge: str):
    return Response(content=challenge, media_type="text/plain", status_code=200)
```

A successful handshake generates your `webhook_secret`, which you use to verify every notification that follows. The [challenge handshake details](/docs/v3/notifications/#the-challenge-query-parameter) cover the full verification flow.

## Handle event notifications

After verification, Nylas `POST`s a notification to your endpoint each time a subscribed trigger fires. The payload identifies the change by type and includes the affected object's `id` and `grant_id`. For an `event.updated` notification, that's the event ID and the calendar it belongs to, so your handler knows exactly what changed without re-listing the calendar.

The notification carries identifiers, not always the complete object. The reliable pattern is to read the object ID from the payload, then make one `GET` request for the full event, since that response always reflects current provider state. Always answer with a `200 OK` within 10 seconds, then process asynchronously.

```python
@app.post("/webhooks/nylas")
async def handle(request: Request):
    payload = await request.json()
    if payload["type"] == "event.updated":
        data = payload["data"]["object"]
        event = nylas.events.find(
            identifier=data["grant_id"],
            event_id=data["id"],
            query_params={"calendar_id": data["calendar_id"]},
        )
        sync_to_database(event)
    return Response(status_code=200)
```

## Things to know about calendar webhooks

Calendar webhooks behave consistently across providers, but a few details affect how you build and debug them. There are six calendar-related triggers in total, and you subscribe to any subset of them on a single endpoint.

The full trigger list:

| Trigger            | Fires when                          |
| ------------------ | ----------------------------------- |
| `event.created`    | A new event is added to a calendar  |
| `event.updated`    | An existing event changes           |
| `event.deleted`    | An event is removed                 |
| `calendar.created` | A new calendar is added             |
| `calendar.updated` | A calendar's metadata changes       |
| `calendar.deleted` | A calendar is removed               |

**Webhooks beat polling on latency.** A subscription pushes a notification within seconds of the change, while a polling loop checking every 5 minutes averages 2.5 minutes of lag and wastes one request per cycle whether anything changed or not. Across a few thousand users, that gap is the difference between a live calendar and a stale one.

**Calendar uses standard webhooks, not Pub/Sub.** Gmail real-time email sync can route through Google Cloud Pub/Sub, but calendar and event triggers always arrive over the standard Nylas webhook channel described here. You don't need a Pub/Sub topic or any Google Cloud setup to receive `event.created`.

**Verify the signature on every notification.** Each delivery carries an `X-Nylas-Signature` header: a hex-encoded HMAC-SHA256 of the raw request body, signed with your `webhook_secret`. Recompute the HMAC over the unmodified body and compare before trusting the data. The [webhook security guide](/docs/v3/notifications/#secure-a-webhook) walks through the check, including the case where the body arrives compressed.

**Sync timing differs by provider.** Google and Microsoft both deliver calendar changes quickly, but Microsoft occasionally batches updates, so a single edit can produce more than one `event.updated` for the same event. Treat your handler as idempotent: key on the event `id` and apply the latest state rather than assuming one notification per change.

**Retries are automatic.** If your endpoint doesn't return a `200 OK` within 10 seconds, Nylas retries with backoff and marks the endpoint `failing`, then `failed`, after repeated misses. Respond fast and do the heavy work in a queue. See [Get real-time updates with webhooks](/docs/v3/notifications/) for the retry and failure rules.

## What's next

- [Get real-time updates with webhooks](/docs/cookbook/use-cases/build/realtime-webhooks/) for the unified setup across email, calendar, and grant events
- [Using webhooks with Nylas](/docs/v3/notifications/) for the full setup, verification, and failure handling
- [Secure a webhook](/docs/v3/notifications/#secure-a-webhook) for signature verification on every notification
- [Checking calendar availability](/docs/v3/calendar/calendar-availability/) to combine real-time sync with free/busy lookups