Notion is where a lot of teams plan their week, but it doesn’t pull in calendar events on its own. People copy meetings over by hand, the database drifts, and the “this week” view stops matching reality. You want each meeting to land as a row in a Notion database the moment it’s booked or changed, with a real date property you can sort and filter on.
This recipe wires the Nylas Calendar API to a Notion database. You subscribe to event webhooks, fetch the changed event, map it to Notion’s page schema, and write a database page through the Notion API. One pipeline covers Google, Outlook, and Exchange, so you don’t write provider-specific calendar code. If you’re pushing into a sales tool instead, the sync calendar events to a CRM recipe is a closer fit, and two-way calendar sync covers writing changes back to the calendar.
What you’ll build
Section titled “What you’ll build”The pipeline is one webhook-driven flow with four steps. Each calendar change becomes exactly one Notion database page, keyed so a reschedule updates the same row instead of adding a duplicate. The loop touches two APIs and stays under the Notion limit of 3 requests per second.
- Subscribe to the
event.createdandevent.updatedwebhook triggers on your Nylas application. - Fetch the full event from the Nylas Events API when a notification arrives.
- Map Nylas event fields to Notion properties, converting Unix timestamps to ISO dates for a Notion date property.
- Write the page to your Notion database with a create or update call.
Before you begin
Section titled “Before you begin”Make sure you have the following before starting this tutorial:
- A Nylas account with an active application
- A valid API key from your Nylas Dashboard
- At least one connected grant (an authenticated user account) for the provider you want to work with
- Node.js 18+ or Python 3.8+ installed (depending on which code samples you follow)
You also need:
- A connected grant with calendar read access for at least one user
- A Notion internal integration and its API token
- A Notion database shared with that integration, with a title property and a date property
- A publicly reachable HTTPS endpoint to receive webhook notifications
Set up the Notion database
Section titled “Set up the Notion database”Create a database in Notion with at least two columns: a Title property (the default Name column works) and a Date property. The Notion API can only write to a database that’s explicitly shared with your integration, so open the database, click the menu, and add your integration as a connection. Copy the 32-character database ID from the database URL. According to the Notion API docs, property keys in your write requests must match the database’s column names exactly, so note the date column’s name now.
How do you sync calendar events to a Notion database?
Section titled “How do you sync calendar events to a Notion database?”Subscribe to Nylas event webhooks, then write each change to Notion as a database page. When the event.created or event.updated trigger fires, Nylas sends a POST to your endpoint, you fetch the full event, and you call the Notion API to create or update a page whose date property holds the meeting time. One subscription covers every connected grant.
The mapping is the part worth getting right. A Nylas event stores its start and end as Unix timestamps inside a when object, while a Notion date property expects ISO 8601 strings. You convert once, in your handler, and store the Nylas event id somewhere on the Notion page so a later event.updated finds the same row. The two systems speak different field names, so here are the 5 fields you’ll map across both providers.
| Nylas event field | Notion property type | Notes |
|---|---|---|
title | title | The default Name column in most databases |
when.start_time / when.end_time | date | Unix seconds; convert to ISO start and end |
description | rich_text | Optional column for the meeting agenda |
location or conferencing.details.url | url or rich_text | Join link for video meetings |
id | rich_text | Store it to match future updates to the same page |
Subscribe to calendar event webhooks
Section titled “Subscribe to calendar event webhooks”Create one webhook subscription that listens for both event lifecycle triggers. A single subscription covers every grant in your Nylas application, so you don’t register per-user webhooks. The POST /v3/webhooks request below registers the 2 event triggers and points them at your callback URL. Nylas sends a challenge request to confirm the endpoint before it goes live.
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"], "webhook_url": "https://your-app.com/webhooks/nylas", "description": "Sync calendar events to Notion", "notification_email_addresses": ["[email protected]"] }'import Nylas from "nylas";
const nylas = new Nylas({ apiKey: "<NYLAS_API_KEY>" });
const webhook = await nylas.webhooks.create({ requestBody: { triggerTypes: ["event.created", "event.updated"], webhookUrl: "https://your-app.com/webhooks/nylas", description: "Sync calendar events to Notion", },});
console.log("Webhook secret:", webhook.data.webhookSecret);from nylas import Client
nylas = Client("<NYLAS_API_KEY>")
webhook = nylas.webhooks.create( request_body={ "trigger_types": ["event.created", "event.updated"], "webhook_url": "https://your-app.com/webhooks/nylas", "description": "Sync calendar events to Notion", })
print("Webhook secret:", webhook.data.webhook_secret)Save the webhook_secret from the response. You use it to verify the X-Nylas-Signature header on every incoming notification so an attacker can’t forge calendar changes into your Notion workspace. For the full subscription lifecycle and signature verification, see sync calendar events with webhooks.
Fetch the changed event from Nylas
Section titled “Fetch the changed event from Nylas”A webhook notification carries the event id, grant_id, and calendar_id, but not the full event body. Fetch the complete record with GET /v3/grants/{grant_id}/events/{event_id}, which takes the grant ID and event ID positionally in the SDKs and needs the calendar_id from the webhook payload as a query parameter. The same endpoint covers all 3 providers and returns the 4 fields you’ll map into Notion: the when timestamps, title, description, and participants.
curl --request GET \ --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/events/<EVENT_ID>?calendar_id=<CALENDAR_ID>' \ --header 'Accept: application/json' \ --header 'Authorization: Bearer <NYLAS_API_KEY>'const { data: event } = await nylas.events.find({ identifier: grantId, eventId: eventId, queryParams: { calendarId: calendarId },});event = nylas.events.find( grant_id, event_id, query_params={"calendar_id": calendar_id},).dataThe when object reports start_time and end_time as Unix seconds (for example, 1701977400), with separate start_timezone and end_timezone strings. Convert the 2 timestamps to ISO 8601 before you hand them to Notion, because the Notion date property rejects raw epoch integers.
Write the event to a Notion database page
Section titled “Write the event to a Notion database page”Create the Notion page with POST /v1/pages, the Notion create-page endpoint. Set parent.database_id to your database ID, then fill properties with a title and a date. The Notion date property takes ISO 8601 strings in start and end, which is why you converted the Unix timestamps in the previous step. Every Notion request needs the Notion-Version header; the current version is 2026-03-11 per the Notion versioning docs.
curl --request POST \ --url 'https://api.notion.com/v1/pages' \ --header 'Authorization: Bearer <NOTION_API_TOKEN>' \ --header 'Notion-Version: 2026-03-11' \ --header 'Content-Type: application/json' \ --data '{ "parent": { "database_id": "<NOTION_DATABASE_ID>" }, "properties": { "Name": { "title": [{ "text": { "content": "Quarterly planning sync" } }] }, "Date": { "date": { "start": "2025-11-12T09:30:00-08:00", "end": "2025-11-12T10:30:00-08:00" } }, "Nylas Event ID": { "rich_text": [{ "text": { "content": "6aaaaaaame8kpgcid6hvd" } }] } } }'const startIso = new Date(event.when.startTime * 1000).toISOString();const endIso = new Date(event.when.endTime * 1000).toISOString();
await fetch("https://api.notion.com/v1/pages", { method: "POST", headers: { Authorization: `Bearer ${process.env.NOTION_API_TOKEN}`, "Notion-Version": "2026-03-11", "Content-Type": "application/json", }, body: JSON.stringify({ parent: { database_id: process.env.NOTION_DATABASE_ID }, properties: { Name: { title: [{ text: { content: event.title ?? "Untitled event" } }] }, Date: { date: { start: startIso, end: endIso } }, "Nylas Event ID": { rich_text: [{ text: { content: event.id } }] }, }, }),});import osimport requestsfrom datetime import datetime, timezone
start_iso = datetime.fromtimestamp(event.when.start_time, tz=timezone.utc).isoformat()end_iso = datetime.fromtimestamp(event.when.end_time, tz=timezone.utc).isoformat()
requests.post( "https://api.notion.com/v1/pages", headers={ "Authorization": f"Bearer {os.environ['NOTION_API_TOKEN']}", "Notion-Version": "2026-03-11", "Content-Type": "application/json", }, json={ "parent": {"database_id": os.environ["NOTION_DATABASE_ID"]}, "properties": { "Name": {"title": [{"text": {"content": event.title or "Untitled event"}}]}, "Date": {"date": {"start": start_iso, "end": end_iso}}, "Nylas Event ID": {"rich_text": [{"text": {"content": event.id}}]}, }, },)For an event.updated trigger, you patch the existing page instead of creating a second one. Query the database with POST /v1/databases/{database_id}/query, filtering the Nylas Event ID column for the event you stored, then send the new times to PATCH /v1/pages/{page_id}. This keeps one Notion row per meeting even after three or four reschedules.
Things to know about the Notion API
Section titled “Things to know about the Notion API”A few Notion-specific limits shape this integration, and they differ from how the Nylas Calendar API behaves. The Notion API rate-limits each integration to an average of 3 requests per second and returns HTTP 429 with a Retry-After header when you exceed it, so a burst of calendar changes needs a small queue between your webhook handler and the Notion write.
- Sharing is mandatory. The integration can only read or write a database that a workspace member has explicitly shared with it. A
404from/v1/pagesalmost always means the database isn’t connected to the integration, not that the ID is wrong. - Property keys are case-sensitive. The keys in your
propertiesobject must match the database column names exactly, including capitalization. A typo drops that property without returning an error. - Date properties take ISO 8601, not epochs. Notion’s
datetype expects strings like2025-11-12T09:30:00-08:00. Pass thestart_timezonefrom the Nylaswhenobject through to the offset so the meeting shows at the right local time. - Title is required. Every Notion page needs a non-empty
titleproperty. Calendar events occasionally have a blanktitle, so fall back to a placeholder likeUntitled eventbefore you write. - Versioning is pinned by header. Notion ties breaking changes to the
Notion-Versiondate you send. Pin it explicitly rather than relying on a default, and review the Notion changelog before bumping it.
For the deletion path, Notion’s API archives pages rather than hard-deleting them. Handle a future event.deleted trigger by patching the page with "archived": true.
What’s next
Section titled “What’s next”- Sync calendar events with webhooks for the full webhook subscription and signature verification flow
- Using the Events API for the complete event schema and query parameters
- Two-way calendar sync to write Notion changes back to the calendar
- Notion API create-page reference for the full Notion page and property schema