# How to list Google calendar events

Source: https://developer.nylas.com/docs/cookbook/calendar/events/list-events-google/

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](/docs/reference/api/events/) 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.

## How do I list Google Calendar events with the Nylas API?

Send a `GET` request to `/v3/grants/{grant_id}/events` with your API key and a `calendar_id` (use `primary` for the default calendar). The endpoint returns up to 50 events per page by default, sorted by start time. Recurring events expand into individual instances. The same call works for Microsoft and iCloud. See [List events](#list-events) for the request and response.

## 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

You'll need:

- A [Nylas application](/docs/v3/getting-started/) with a valid API key
- A [grant](/docs/v3/auth/) for a Google Calendar or Google Workspace account
- The appropriate [Google OAuth scopes](/docs/provider-guides/google/) configured in your GCP project


> **Info:** 
> **New to Nylas?** Start with the [quickstart guide](/docs/v3/getting-started/) to set up your app and connect a test account before continuing here.


### 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](/docs/provider-guides/google/google-verification-security-assessment-guide/).

Nylas handles token refresh and scope management, but your GCP project still needs the right scopes configured. See the [Google provider guide](/docs/provider-guides/google/) for the full setup.

## List events

Make a [List Events request](/docs/reference/api/events/get-all-events/) 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:

```bash
curl --compressed --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' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>' \
  --header 'Content-Type: application/json'

```

```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": {
        "email": "anna.molly@example.com",
        "name": ""
      },
      "description": null,
      "grant_id": "1e3288f6-124e-405d-a13a-635a2ee54eb2",
      "hide_participants": false,
      "html_link": "https://www.google.com/calendar/event?eid=NmE0dXIwabQAAAA",
      "ical_uid": "6aaaaaaame8kpgcid6hvd0q@google.com",
      "id": "6aaaaaaame8kpgcid6hvd",
      "object": "event",
      "organizer": {
        "email": "anna.molly@example.com",
        "name": ""
      },
      "participants": [
        {
          "email": "jenna.doe@example.com",
          "status": "yes"
        },
        {
          "email": "anna.molly@example.com",
          "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"
      }
    }
  ]
}


```

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

import Nylas from "nylas";

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

async function fetchAllEventsFromCalendar() {
  try {
    const events = await nylas.events.list({
      identifier: "<NYLAS_GRANT_ID>",
      queryParams: {
        calendarId: "<CALENDAR_ID>",
      },
    });

    console.log("Events:", events);
  } catch (error) {
    console.error("Error fetching calendars:", error);
  }
}

fetchAllEventsFromCalendar();


```

```python [listEvents-Python SDK]

from nylas import Client

nylas = Client(
    "<NYLAS_API_KEY>",
    "<NYLAS_API_URI>"
)

grant_id = "<NYLAS_GRANT_ID>"

events = nylas.events.list(
    grant_id,
    query_params={
      "calendar_id": "<CALENDAR_ID>"
    }
)

print(events)

```


```ruby [listEvents-Ruby SDK]

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 time
events, _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"
}

```

```java [listEvents-Java SDK]

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");
    }
  }
}

```

```kt [listEvents-Kotlin SDK]

import com.nylas.NylasClient
import 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.email + " Name: " + participant.name +
          " 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

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)   | `?attendees=alex@example.com`      |
| `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                | `?ical_uid=abc123@google.com` |
| `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:


```bash
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>'
```

```js [filterEvents-Node.js SDK]
const events = await nylas.events.list({
  identifier: grantId,
  queryParams: {
    calendarId: "primary",
    title: "standup",
    start: 1706000000,
    end: 1706100000,
    limit: 10,
  },
});
```

```python [filterEvents-Python SDK]
events = nylas.events.list(
    grant_id,
    query_params={
        "calendar_id": "primary",
        "title": "standup",
        "start": 1706000000,
        "end": 1706100000,
        "limit": 10,
    }
)
```


### 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). |

```bash
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>'
```

```js [eventType-Node.js SDK]
const events = await nylas.events.list({
  identifier: grantId,
  queryParams: {
    calendarId: "primary",
    eventType: "outOfOffice",
  },
});
```

```python [eventType-Python SDK]
events = nylas.events.list(
    grant_id,
    query_params={
        "calendar_id": "primary",
        "event_type": ["outOfOffice"],
    }
)
```

> **Info:** 
> **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

A few provider-specific details that matter when you're building against Google Calendar and Google Workspace accounts.

### 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

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:

```json
{
  "conferencing": {
    "provider": "Google Meet",
    "details": {
      "url": "https://meet.google.com/abc-defg-hij"
    }
  }
}
```

You can also [manually add conferencing](/docs/v3/calendar/add-conferencing/) when creating events through Nylas, including Google Meet, Zoom, and Microsoft Teams links.

### 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

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](/docs/v3/calendar/recurring-events/).

### 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

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](/docs/v3/notifications/) instead of polling so Nylas notifies your server when calendar events change
- Set up [Google Pub/Sub](/docs/provider-guides/google/connect-google-pub-sub/) for real-time sync, which gives faster notification delivery for Google accounts

### 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](/docs/provider-guides/google/google-workspace-service-accounts/).
- **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


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:

```bash
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>'
```

```js [paginateEvents-Node.js SDK]
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);
```

```python [paginateEvents-Python SDK]
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:
        break
```

Keep paginating until the response comes back without a `next_cursor`.


## What's next

- [List Microsoft 365 calendar events](/docs/cookbook/calendar/events/list-events-microsoft/) for the same task on Microsoft accounts
- [List iCloud calendar events](/docs/cookbook/calendar/events/list-events-icloud/) for the same task on iCloud accounts
- [Events API reference](/docs/reference/api/events/) for full endpoint documentation and all available parameters
- [Using the Events API](/docs/v3/calendar/using-the-events-api/) for creating, updating, and deleting events
- [Recurring events](/docs/v3/calendar/recurring-events/) for working with repeating events and their occurrences
- [Add conferencing](/docs/v3/calendar/add-conferencing/) to attach Google Meet, Zoom, or Teams links to events
- [Availability](/docs/v3/calendar/calendar-availability/) to check free/busy status before creating events
- [Webhooks](/docs/v3/notifications/) for real-time notifications instead of polling
- [Google Pub/Sub](/docs/provider-guides/google/connect-google-pub-sub/) for real-time sync with Google accounts
- [Google provider guide](/docs/provider-guides/google/) for full Google setup including OAuth scopes and verification
- [Google verification and security assessment](/docs/provider-guides/google/google-verification-security-assessment-guide/), required for restricted scopes in production
- [Manage calendar from the terminal](https://cli.nylas.com/guides/manage-calendar-from-terminal) -- list events, check availability, and create events using the Nylas CLI