Google Calendar is the most common calendar provider developers integrate with, and the Google Calendar API comes with more setup friction than you might expect. Between the GCP project configuration, the tiered OAuth scope system, and Google’s restricted-scope security assessment, there’s a lot of overhead before you can make your first API call. On top of that, Google has its own concepts like event types (focus time, out of office), numeric color IDs, and non-standard recurring event behavior that don’t map cleanly to other providers.
Nylas normalizes all of that. You get a single Events API that works across Google, Microsoft, iCloud, and Exchange without provider-specific branching. This guide covers listing events from Google Calendar accounts and the Google-specific details you should know about.
Why use Nylas instead of the Google Calendar API directly?
Section titled “Why use Nylas instead of the Google Calendar API directly?”The Google Calendar API requires more scaffolding than most developers anticipate:
- GCP project and OAuth consent screen - You need a GCP project, an OAuth consent screen, and the correct calendar scopes configured before anything works.
- Three-tier scope system - Google classifies calendar scopes as non-sensitive, sensitive, or restricted. Restricted scopes (like full read-write) require a third-party security assessment before you can go to production.
- Google-specific data model - Event types (
outOfOffice,focusTime,workingLocation), numeric color IDs, and Google Meet auto-attachment are all concepts that don’t exist on other providers. - Recurring event quirks - Google marks cancelled occurrences as hidden events instead of removing them, and returns unsorted results when querying by
master_event_id.
Nylas handles token management, scope negotiation, and data normalization. If you only need Google Calendar and want fine-grained control over every Google-specific field, the native API works. If you need multi-provider support or want to skip the verification process, Nylas is the faster path.
Before you begin
Section titled “Before you begin”You’ll need:
- A Nylas application with a valid API key
- A grant for a Google Calendar or Google Workspace account
- The appropriate Google OAuth scopes configured in your GCP project
New to Nylas? Start with the quickstart guide to set up your app and connect a test account before continuing here.
Google OAuth scopes and verification
Section titled “Google OAuth scopes and verification”Google classifies OAuth scopes into three tiers, and each one comes with different verification requirements:
| Scope tier | Example | What’s required |
|---|---|---|
| Non-sensitive | calendar.readonly (metadata only) | No verification needed |
| Sensitive | calendar.events.readonly | OAuth consent screen verification |
| Restricted | calendar.events, calendar | Full security assessment by a third-party auditor |
If your app only needs to read events, the calendar.events.readonly scope is classified as sensitive. For full read-write access to calendars and events, you’ll need the calendar scope, which is restricted and requires a security assessment.
Nylas handles token refresh and scope management, but your GCP project still needs the right scopes configured. See the Google provider guide for the full setup.
List events
Section titled “List events”Make a List Events request with the grant ID and a calendar_id. The calendar_id parameter is required for all events requests. You can use primary to target the user’s default calendar. By default, Nylas returns the 50 most recent events sorted by start date:
curl --request GET \ --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/events?calendar_id=<CALENDAR_ID>&start=<TIMESTAMP>&end=<TIMESTAMP>' \ --header 'Accept: application/json, application/gzip' \ --header 'Authorization: Bearer <NYLAS_API_KEY>' \ --header 'Content-Type: application/json'{ "request_id": "cbd60372-df33-41d3-b203-169ad5e3AAAA", "data": [{ "busy": true, "calendar_id": "primary", "conferencing": { "details": { "meeting_code": "ist-****-tcz", "url": "https://meet.google.com/ist-****-tcz" }, "provider": "Google Meet" }, "created_at": 1701974804, "creator": { "name": "" }, "description": null, "grant_id": "1e3288f6-124e-405d-a13a-635a2ee54eb2", "hide_participants": false, "html_link": "https://www.google.com/calendar/event?eid=NmE0dXIwabQAAAA", "id": "6aaaaaaame8kpgcid6hvd", "object": "event", "organizer": { "name": "" }, "participants": [ { "status": "yes" }, { "status": "yes" } ], "read_only": true, "reminders": { "overrides": null, "use_default": true }, "status": "confirmed", "title": "Holiday check in", "updated_at": 1701974915, "when": { "end_time": 1701978300, "end_timezone": "America/Los_Angeles", "object": "timespan", "start_time": 1701977400, "start_timezone": "America/Los_Angeles" } }]}import 'dotenv/config'import Nylas from 'nylas'
const NylasConfig = { apiKey: process.env.NYLAS_API_KEY, apiUri: process.env.NYLAS_API_URI}
const nylas = new Nylas(NylasConfig)
async function fetchAllEventsFromCalendar() { try { const events = await nylas.events.list({ identifier: process.env.NYLAS_GRANT_ID, queryParams: { calendarId: process.env.CALENDAR_ID, } })
console.log('Events:', events) } catch (error) { console.error('Error fetching calendars:', error) }}
fetchAllEventsFromCalendar()from dotenv import load_dotenvload_dotenv()
import osimport sysfrom nylas import Client
nylas = Client( os.environ.get('NYLAS_API_KEY'), os.environ.get('NYLAS_API_URI'))
grant_id = os.environ.get("NYLAS_GRANT_ID")
events = nylas.events.list( grant_id, query_params={ "calendar_id": os.environ.get("CALENDAR_ID") })
print(events)require 'nylas'
nylas = Nylas::Client.new(api_key: "<NYLAS_API_KEY>")
query_params = { calendar_id: "<CALENDAR_ID>"}
# Read events from our main calendar in the specified date and timeevents, _request_ids = nylas.events.list(identifier: "<NYLAS_GRANT_ID>", query_params: query_params)
events.each {|event| case event[:when][:object] when 'timespan' start_time = Time.at(event[:when][:start_time]).strftime("%d/%m/%Y at %H:%M:%S") end_time = Time.at(event[:when][:end_time]).strftime("%d/%m/%Y at %H:%M:%S") event_date = "The time of the event is from: #{start_time} to #{end_time}" when 'datespan' start_time = event[:when][:start_date] end_time = event[:when][:end_date] event_date = "The date of the event is from: #{start_time} to: #{end_time}" when 'date' start_time = event[:when][:date] event_date = "The date of the event is: #{start_time}" end event[:participants].each {|participant| participant_details += "Email: #{participant[:email]} " \ "Name: #{participant[:name]} Status: #{participant[:status]} - " } print "Id: #{event[:id]} | Title: #{event[:title]} | #{event_date} | " puts "Participants: #{participant_details.chomp(' - ')}" puts "\n"}import com.nylas.NylasClient;import com.nylas.models.When;
import com.nylas.models.*;import java.text.SimpleDateFormat;import java.util.List;import java.util.Objects;
public class read_calendar_events { public static void main(String[] args) throws NylasSdkTimeoutError, NylasApiError { NylasClient nylas = new NylasClient.Builder("<NYLAS_API_KEY>").build();
// Build the query parameters to filter our the results ListEventQueryParams listEventQueryParams = new ListEventQueryParams.Builder("<CALENDAR_ID>").build();
// Read the events from our main calendar List<Event> events = nylas.events().list("<NYLAS_GRANT_ID>", listEventQueryParams).getData();
for (Event event : events) { System.out.print("Id: " + event.getId() + " | "); System.out.print("Title: " + event.getTitle());
// Dates are handled differently depending on the event type switch (Objects.requireNonNull(event.getWhen().getObject()).getValue()) { case "datespan" -> { When.Datespan date = (When.Datespan) event.getWhen();
System.out.print(" | The date of the event is from: " + date.getStartDate() + " to " + date.getEndDate()); } case "date" -> { When.Date date = (When.Date) event.getWhen();
System.out.print(" | The date of the event is: " +date.getDate()); } case "timespan" -> { When.Timespan timespan = (When.Timespan) event.getWhen();
String initDate = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"). format(new java.util.Date((timespan.getStartTime() * 1000L)));
String endDate = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"). format(new java.util.Date((timespan.getEndTime() * 1000L)));
System.out.print(" | The time of the event is from: " + initDate + " to " + endDate); } }
System.out.print(" | Participants: ");
for(Participant participant : event.getParticipants()){ System.out.print(" Email: " + participant.getEmail() + " Name: " + participant.getName() + " Status: " + participant.getStatus()); }
System.out.println("\n"); } }}import com.nylas.NylasClientimport com.nylas.models.*
import java.util.*
fun main(args: Array<String>) { val nylas: NylasClient = NylasClient(apiKey = "<NYLAS_API_KEY>")
val eventquery: ListEventQueryParams = ListEventQueryParams(calendarId = "<CALENDAR_ID>")
// Get a list of events val myevents: List<Event> = nylas.events().list( "<NYLAS_GRANT_ID>", queryParams = eventquery).data
// Loop through the events for(event in myevents){ print("Id: " + event.id + " | "); print("Title: " + event.title);
// Get the details of Date and Time of each event. when(event.getWhen().getObject().toString()) { "DATE" -> { val datespan = event.getWhen() as When.Date
print(" | The date of the event is: " + datespan.date); } "DATESPAN" -> { val datespan = event.getWhen() as When.Datespan
print(" | The date of the event is: " + datespan.startDate); } "TIMESPAN" -> { val timespan = event.getWhen() as When.Timespan val startDate = Date(timespan.startTime.toLong() * 1000) val endDate = Date(timespan.endTime.toLong() * 1000)
print(" | The time of the event is from: $startDate to $endDate"); } }
print(" | Participants: ");
// Get a list of the event participants val participants = event.participants
// Loop through and print their email, name and status for(participant in participants) { print(" Email: " + participant.name + " Name: " + participant.email + " Status: " + participant.status) }
println("\n") }}The same code works for Microsoft, iCloud, and Exchange accounts. Just swap the grant ID and Nylas handles the provider differences.
Filter events
Section titled “Filter events”You can narrow results with query parameters. Here’s what the Events API supports:
| Parameter | What it does | Example |
|---|---|---|
calendar_id | Required. Filter by calendar | ?calendar_id=primary |
title | Match on event title (case insensitive) | ?title=standup |
description | Match on description (case insensitive) | ?description=quarterly |
location | Match on location (case insensitive) | ?location=Room%20A |
start | Events starting at or after a Unix timestamp | ?start=1706000000 |
end | Events ending at or before a Unix timestamp | ?end=1706100000 |
attendees | Filter by attendee email (comma-delimited) | [email protected] |
busy | Filter by busy status | ?busy=true |
metadata_pair | Filter by metadata key-value pair | ?metadata_pair=project_id:abc123 |
Google accounts also support these additional filter parameters:
| Parameter | What it does | Example |
|---|---|---|
show_cancelled | Include cancelled events | ?show_cancelled=true |
updated_after | Events updated after a Unix timestamp | ?updated_after=1706000000 |
updated_before | Events updated before a Unix timestamp | ?updated_before=1706100000 |
ical_uid | Filter by iCalendar UID | [email protected] |
master_event_id | Get occurrences of a recurring event | ?master_event_id=evt_abc123 |
Here’s how to combine filters. This pulls events with “standup” in the title within a specific time range:
curl --request GET \ --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/events?calendar_id=primary&title=standup&start=1706000000&end=1706100000&limit=10" \ --header 'Accept: application/json' \ --header 'Authorization: Bearer <NYLAS_API_KEY>'const events = await nylas.events.list({ identifier: grantId, queryParams: { calendarId: "primary", title: "standup", start: 1706000000, end: 1706100000, limit: 10, },});events = nylas.events.list( grant_id, query_params={ "calendar_id": "primary", "title": "standup", "start": 1706000000, "end": 1706100000, "limit": 10, })Filter by event type
Section titled “Filter by event type”Google Calendar supports event types beyond standard calendar events. These are Google-only and won’t appear on other providers. By default, the Events API returns only default events. To retrieve other types, you must explicitly filter for them using the event_type parameter.
| Event type | Description |
|---|---|
default | Standard calendar events. Returned by default if event_type is not specified. |
outOfOffice | Out-of-office blocks set by the user. Automatically declines new invitations during the blocked time. |
focusTime | Focus time blocks. Mutes notifications during the blocked time. |
workingLocation | Working location events that indicate where the user is working from (office, home, or another location). |
curl --request GET \ --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/events?calendar_id=primary&event_type=outOfOffice" \ --header 'Accept: application/json' \ --header 'Authorization: Bearer <NYLAS_API_KEY>'const events = await nylas.events.list({ identifier: grantId, queryParams: { calendarId: "primary", eventType: "outOfOffice", },});events = nylas.events.list( grant_id, query_params={ "calendar_id": "primary", "event_type": "outOfOffice", })Event types don’t mix in a single request. You can only filter for one event type at a time. If you need both default and outOfOffice events, make two separate requests.
Things to know about Google
Section titled “Things to know about Google”A few provider-specific details that matter when you’re building against Google Calendar and Google Workspace accounts.
Event types are Google-only
Section titled “Event types are Google-only”The outOfOffice, focusTime, and workingLocation event types only exist on Google Calendar. Microsoft has similar concepts (like focus time in Viva), but they surface differently through the API. If you’re building a multi-provider app, you’ll need to handle the case where these event types simply don’t exist for non-Google accounts.
Also worth noting: these event types are only returned when you explicitly filter for them. A standard list request without event_type returns only default events, so you won’t accidentally pull in out-of-office blocks.
Google Meet conferencing
Section titled “Google Meet conferencing”When a Google Calendar user has Google Meet enabled (most do), new events often get a Meet link auto-attached. Nylas returns this in the conferencing object on the event:
{ "conferencing": { "provider": "Google Meet", "details": { "url": "https://meet.google.com/abc-defg-hij" } }}You can also manually add conferencing when creating events through Nylas, including Google Meet, Zoom, and Microsoft Teams links.
Color IDs on events
Section titled “Color IDs on events”Google supports numeric color IDs for event-level color overrides. These map to a fixed set of colors in Google Calendar’s UI. The color ID appears in the event response as a string (like "1" through "11"). Other providers handle event colors differently or not at all, so don’t rely on this field for cross-provider consistency.
Recurring events return unsorted
Section titled “Recurring events return unsorted”When you use master_event_id to fetch occurrences of a recurring event from a Google account, the results come back in a non-deterministic order. Unchanged occurrences typically appear first, followed by modified occurrences, but this isn’t guaranteed. Sort the results by start_time on your end if order matters.
For more details on how Google handles recurring event modifications and deletions compared to Microsoft, see the recurring events guide.
Cancelled events work differently
Section titled “Cancelled events work differently”When a user deletes a single occurrence from a recurring series in the Google Calendar UI, Google doesn’t actually remove the event. Instead, it marks the occurrence as cancelled and creates a hidden master event to track deleted occurrences. That master event has a different ID from the individual occurrences.
To retrieve cancelled events, pass show_cancelled=true in your request. One important detail: Google does not reflect cancelled occurrences in the EXDATE of the primary recurring event. If you’re using EXDATE to track which occurrences were removed, you’ll get incomplete results for Google accounts.
Rate limits are per-user and per-project
Section titled “Rate limits are per-user and per-project”Google enforces calendar API quotas at two levels:
- Per-user: Each authenticated user has a per-minute and daily quota for API calls
- Per-project: Your GCP project has an overall daily limit across all users
Nylas handles retries when you hit rate limits, but if your app polls aggressively for many users, you may exhaust your project quota. Two ways to reduce this:
- Use webhooks instead of polling so Nylas notifies your server when calendar events change
- Set up Google Pub/Sub for real-time sync, which gives faster notification delivery for Google accounts
Google Workspace vs. personal accounts
Section titled “Google Workspace vs. personal accounts”Both personal Google accounts and Google Workspace accounts work with Nylas, but there are differences worth knowing:
- Workspace admins can restrict which third-party apps have access. If a Workspace user can’t authenticate, their admin may need to allow your app in the Google Admin console.
- Service accounts are available for Google Workspace Calendar access, which lets you read and write events without individual user OAuth flows. See the service accounts guide.
- Domain-wide delegation lets a Workspace admin grant your service account access to all users in the organization, which is useful for enterprise calendar integrations.
Paginate through results
Section titled “Paginate through results”The Events API returns paginated responses. When there are more results, the response includes a next_cursor value. Pass it back as page_token to get the next page:
curl --request GET \ --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/events?calendar_id=primary&limit=10&page_token=<NEXT_CURSOR>" \ --header 'Accept: application/json' \ --header 'Authorization: Bearer <NYLAS_API_KEY>'let pageCursor = undefined;
do { const result = await nylas.events.list({ identifier: grantId, queryParams: { calendarId: "primary", limit: 10, pageToken: pageCursor, }, });
// Process result.data here
pageCursor = result.nextCursor;} while (pageCursor);page_cursor = None
while True: query = {"calendar_id": "primary", "limit": 10} if page_cursor: query["page_token"] = page_cursor
result = nylas.events.list(grant_id, query_params=query)
# Process result.data here
page_cursor = result.next_cursor if not page_cursor: breakKeep paginating until the response comes back without a next_cursor.
What’s next
Section titled “What’s next”- Events API reference for full endpoint documentation and all available parameters
- Using the Events API for creating, updating, and deleting events
- Recurring events for working with repeating events and their occurrences
- Add conferencing to attach Google Meet, Zoom, or Teams links to events
- Availability to check free/busy status before creating events
- Webhooks for real-time notifications instead of polling
- Google Pub/Sub for real-time sync with Google accounts
- Google provider guide for full Google setup including OAuth scopes and verification
- Google verification and security assessment, required for restricted scopes in production