# How to list Microsoft calendar events

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

If you're building an app that reads calendar data from Microsoft 365 or Outlook accounts, you can either work directly with the Microsoft Graph API or use Nylas as a unified layer that handles the provider differences for you.

With Nylas, the API call to list events is identical whether the account is Microsoft, Google, or iCloud. The differences show up in timezone handling, Teams conferencing data, recurring event behavior, and admin consent requirements. This guide covers all of that.

## How do I list Microsoft 365 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, in a schema shared with Google and iCloud. Recurring series expand into instances. See [List events](#list-events) for the request and response.

## Why use Nylas instead of Microsoft Graph directly?

The Microsoft Graph Calendar API is powerful, but integrating it means dealing with Azure AD app registration, OAuth scope configuration, MSAL token refresh logic, admin consent flows for enterprise tenants, and Windows timezone ID mapping (Graph returns identifiers like "Eastern Standard Time" instead of IANA timezones). You also need to handle Microsoft's specific data formats for recurring events, conferencing links, and all-day event boundaries.

Nylas normalizes all of that behind a single REST API. Your code stays the same whether you're reading from Outlook, Google Calendar, or iCloud. No Azure AD setup, no MSAL token lifecycle, no mapping Windows timezone IDs to IANA in your application code. If you need to support multiple calendar providers or want to skip the Graph onboarding, Nylas is the faster path.

That said, if you only need Microsoft calendars and already have Graph experience, direct integration works fine.

## 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 Microsoft 365 or Outlook account
- The `Calendars.Read` scope enabled in your Azure AD app registration


> **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.


### Microsoft admin consent

Microsoft organizations often require admin approval before third-party apps can access calendar data. If your users see a "Need admin approval" screen during auth, their organization restricts user consent.

You have two options:

- **Ask the tenant admin** to grant consent for your app via the Azure portal
- **Configure your Azure app** to request only permissions that don't need admin consent

Nylas has a detailed walkthrough: [Configuring Microsoft admin approval](/docs/provider-guides/microsoft/admin-approval/). If you're targeting enterprise customers, you'll almost certainly need to deal with this.

You also need to be a [verified publisher](/docs/provider-guides/microsoft/verification-guide/). Microsoft requires publisher verification since November 2020, and without it users see an error during auth.

## List events

Make a [List Events request](/docs/reference/api/events/get-all-events/) with the grant ID and a `calendar_id`. Nylas returns the most recent events by default. You can use `primary` as the `calendar_id` to target the account's default calendar. These examples limit results to 5:

```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 `calendar_id` parameter is required for all Events endpoints. For Microsoft accounts, calendar IDs are long base64-encoded strings, but you can use `primary` as a shortcut to target the default calendar. The response format is the same regardless of provider, so your parsing logic works across Microsoft, Google, and iCloud without changes.

## List Microsoft events from the terminal

Microsoft 365 calendars carry Teams conferencing data and per-event timezones, which the API normalizes into one schema. The [Nylas CLI](https://cli.nylas.com/docs/commands) reads them directly: `nylas calendar events list` prints the account's upcoming events from your terminal, with admin consent the only enterprise prerequisite.


The Nylas CLI lists calendar events from your terminal through the same Events API. After `nylas init` and `nylas auth login`, `calendar events list` displays upcoming events from the connected calendar:

```bash
# Upcoming events on the connected calendar
nylas calendar events list

# Look ahead 14 days, with times in a specific zone
nylas calendar events list --days 14 --timezone America/New_York

# Open one event to see attendees, time, and conferencing
nylas calendar events show <event-id>
```

Add `--json` to pipe results into a script. The `events list` command supports Google Calendar, Outlook, and Exchange. See the [`calendar events list`](https://cli.nylas.com/docs/commands/calendar-events-list) command reference for every flag.


Each event's `conferencing` object includes the Teams join URL, and times come back normalized so you don't reconcile per-event zones by hand. The Events API returns up to 50 events per page. Pass `--timezone` to render times in a specific zone, or `nylas calendar events show <event-id>` to expand one event.

## Filter events

You can narrow results with query parameters. Here's what works with Microsoft accounts:


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


Microsoft also supports several additional filter parameters beyond the standard set:

- `show_cancelled` - include cancelled events in results (defaults to `false`)
- `tentative_as_busy` - treat tentative events as busy when checking availability
- `updated_after` / `updated_before` - filter by last-modified timestamp, useful for incremental sync
- `ical_uid` - find a specific event by its iCalendar UID
- `master_event_id` - list all instances and overrides for a specific recurring series

Combining filters works the way you'd expect. This example pulls events in 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,
    }
)
```


## Things to know about Microsoft

A few provider-specific details that matter when you're building against Microsoft calendar accounts.

### Timezone handling

Microsoft Graph stores timezone information using Windows timezone identifiers like "Eastern Standard Time" or "Pacific Standard Time" rather than IANA identifiers like "America/New_York" or "America/Los_Angeles". Nylas normalizes these automatically, so event times in API responses always use IANA timezone identifiers. You don't need to maintain a Windows-to-IANA mapping table in your application.

### All-day events end one day later

When Nylas returns an all-day event (a `datespan` object) from Microsoft, the `end_date` is set to the day _after_ the event actually ends. This matches how Microsoft Graph represents all-day events internally. A single-day event on December 1st comes back with `start_date: "2024-12-01"` and `end_date: "2024-12-02"`.

If you're displaying events in a calendar UI, subtract one day from the `end_date` to show the correct range. This behavior is the same for Google, so your display logic doesn't need to be provider-specific.

### Teams meeting links

Events created with Microsoft Teams conferencing include a `conferencing` object in the response with `provider` set to `"Microsoft Teams"`. The `details` array contains the join URL and any dial-in information. If you're building a meeting list or join button, check for this field on every event - it's only present when the organizer added Teams to the invite.

### Tentative events

Microsoft treats tentative events as busy by default when calculating availability. The `tentative_as_busy` query parameter controls this. If your app needs to distinguish between confirmed and tentative events, check the `status` field on each event object.

### Calendar IDs

Microsoft calendar IDs are long base64-encoded strings like `AAMkAGI2TG93AAA=`. These are stable and safe to store, but they take up more space than Google's shorter numeric IDs. For the account's default calendar, use `primary` as a shortcut instead of fetching the full ID. If you need to list events from a non-default calendar, call [List Calendars](/docs/reference/api/calendar/get-all-calendars/) first to get the available calendar IDs.

### Recurring event quirks

Microsoft handles recurring events differently from Google in a few important ways:

- **Overrides are removed on recurrence change.** If you modify a recurring series (for example, changing from weekly to daily), Microsoft removes all existing overrides. Google keeps them if they still fit the pattern.
- **No multi-day monthly BYDAY.** You can't create a monthly recurring event on multiple days of the week (like the first and third Thursday). Microsoft's recurrence model doesn't support different indices within a single rule.
- **EXDATE recovery isn't possible.** Once you remove an occurrence from a recurring series, you can't undo it through Nylas. You'd need to create a separate standalone event to fill the gap.
- **Rescheduling constraints.** Microsoft Exchange won't let you move a recurring instance to the same day as, or the day before, the previous instance. Overlapping instances within a series aren't allowed.

For the full breakdown of Google vs. Microsoft recurring event differences, see [Recurring events](/docs/v3/calendar/recurring-events/).

### Cancelled events

When the organizer deletes a recurring event instance, Microsoft and Google handle it differently. Google marks deleted occurrences as "cancelled" and keeps them retrievable with `show_cancelled=true`. Microsoft removes them entirely from the system - you can't get them back through the API.

For non-recurring events, deletion behavior is more straightforward: the event disappears from list results. Use `show_cancelled=true` if you need to see events that participants declined or that the organizer cancelled but hasn't fully removed yet.

### Rate limits

Microsoft throttles API requests at the per-mailbox level. If your app triggers a `429` response, Nylas handles the retry automatically with appropriate backoff, so you don't need to implement retry logic yourself.

If you're polling a calendar frequently, you'll burn through rate limits fast. [Webhooks](/docs/v3/notifications/) solve this by notifying you of changes in real time without any polling requests.

### Sync timing

Calendar events typically appear within seconds of being created or updated. If an event you know exists isn't showing up in list results yet, wait a moment and retry. This is a Microsoft-side sync delay, not a Nylas one.

For apps that need real-time awareness of calendar changes, use [webhooks](/docs/v3/notifications/) instead of polling. Nylas pushes a notification to your server as soon as the event syncs.

## 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 Google calendar events](/docs/cookbook/calendar/events/list-events-google/) for the same task on Google 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 series creation, overrides, and provider-specific behavior
- [Add conferencing to events](/docs/v3/calendar/add-conferencing/) to attach Teams, Zoom, or other meeting links
- [Availability](/docs/v3/calendar/calendar-availability/) to check free/busy across multiple calendars
- [Webhooks](/docs/v3/notifications/) for real-time notifications instead of polling
- [Microsoft admin approval](/docs/provider-guides/microsoft/admin-approval/) to configure consent for enterprise organizations
- [Microsoft publisher verification](/docs/provider-guides/microsoft/verification-guide/), required for production apps