# How to list Exchange calendar events

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

Exchange on-premises servers are still common in enterprise environments, especially in regulated industries and government organizations. If your users run self-hosted Exchange (2007 or later), Nylas connects to their calendars through Exchange Web Services (EWS) - a separate protocol from the Microsoft Graph API used for Exchange Online and Microsoft 365.

The same [Events API](/docs/reference/api/events/) you use for Google Calendar and Outlook works for Exchange on-prem accounts. This guide covers the EWS-specific details: when to use EWS vs. Microsoft Graph, authentication and autodiscovery, recurring event restrictions, and on-prem networking considerations.

## How do I list Exchange 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 way they do for Google and Microsoft. See [List events](#list-events) for the request and response.

## EWS vs. Microsoft Graph: which one?

This is the first thing to figure out. The two provider types target different Exchange deployments:

| Provider type         | Connector   | Use when                                                |
| --------------------- | ----------- | ------------------------------------------------------- |
| Microsoft Graph       | `microsoft` | Exchange Online, Microsoft 365, Office 365, Outlook.com |
| Exchange Web Services | `ews`       | Self-hosted Exchange servers (on-premises)              |

If the user's calendar is hosted by Microsoft in the cloud, use the [Microsoft guide](/docs/cookbook/calendar/events/list-events-microsoft/) instead. The `ews` connector is specifically for organizations that run their own Exchange servers.

> **Warn:** 
> **Microsoft announced EWS retirement** and recommends migrating to Microsoft Graph. However, many organizations still run on-premises Exchange servers where EWS is the only option. Nylas continues to support EWS for these environments.

## Why use Nylas instead of EWS directly?

EWS is a SOAP-based XML API. Every request requires building XML SOAP envelopes, every response needs XML parsing, and errors come back as SOAP faults with nested XML structures. Calendar operations are particularly verbose in EWS - creating a recurring event with attendees, timezone rules, and reminders means constructing deeply nested XML payloads. You also need to handle autodiscovery to find the right server endpoint (which is frequently misconfigured), manage credential-based authentication with support for two-factor app passwords, and deal with Exchange's recurrence model that doesn't map cleanly to iCalendar standards.

Nylas replaces all of that with a JSON REST API. No XML, no WSDL, no SOAP. Authentication and autodiscovery are handled automatically. Your code stays the same whether you're reading calendar events from Exchange on-prem, Exchange Online, Google Calendar, or iCloud.

If you have deep EWS experience and only target Exchange on-prem, direct integration is an option. For multi-provider calendar support or faster time-to-integration, Nylas is the simpler 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 an Exchange on-premises account
- An EWS connector configured with the `ews.calendars` scope
- The Exchange server accessible from outside the corporate network (not behind a VPN or firewall that blocks external access)


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


### Autodiscovery and authentication

EWS uses credential-based authentication. During the auth flow, users sign in with their Exchange credentials - typically the same username and password they use for Windows login. The username format is usually `user@example.com` or `DOMAIN\username`.

If EWS autodiscovery is configured on the server, Nylas automatically locates the correct EWS endpoint. If autodiscovery is disabled or misconfigured, users can click "Additional settings" during authentication and manually enter the Exchange server address (for example, `mail.company.com`).

> **Info:** 
> **Users with two-factor authentication** must generate an app password instead of using their regular password. See [Microsoft's app password documentation](https://support.microsoft.com/en-us/help/12409/) for instructions.

Create an EWS connector with the scopes your app needs:

| Scope           | Access                                |
| --------------- | ------------------------------------- |
| `ews.messages`  | Email API (messages, drafts, folders) |
| `ews.calendars` | Calendar API                          |
| `ews.contacts`  | Contacts API                          |

The full setup walkthrough is in the [Exchange on-premises provider guide](/docs/provider-guides/exchange-on-prem/).

## 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=primary` shortcut works for EWS accounts, targeting the user's default calendar. The response format is identical across providers, so your parsing logic works the same for Exchange on-prem, Exchange Online, Google, and iCloud.

## List Exchange events from the terminal

Exchange on-premises calendars come through EWS rather than a modern REST API, with tighter recurring-event handling than the cloud providers. The [Nylas CLI](https://cli.nylas.com/docs/commands) hides the protocol: `nylas calendar events list` prints an Exchange grant's upcoming events as plain rows from your terminal.


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.


Recurring events expand into instances the same way they do on Google and Microsoft, though EWS restricts some recurrence edits. The Events API returns up to 50 events per page. Open any event with `nylas calendar events show <event-id>` to see its attendees, time, and location.

## Filter events

You can narrow results with query parameters. Here's what works with Exchange 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` |


Exchange (EWS) supports several additional filter parameters:

- `tentative_as_busy` - treat tentative events as busy when checking availability (defaults to `true`)
- `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

A few parameters are **not supported** on EWS:

- `show_cancelled` - Exchange does not support retrieving cancelled events
- `event_type` - this filter is Google-only

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 Exchange

Exchange on-prem behaves differently from Exchange Online (Microsoft Graph) in several ways that matter for calendar integrations.

### Tentative events

Exchange treats tentative events as busy by default when calculating availability, the same behavior as Microsoft Graph. 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 rather than relying on the default availability calculation.

### No cancelled event retrieval

The `show_cancelled` parameter is not supported on EWS. When an organizer cancels an event or removes an occurrence from a recurring series, Exchange deletes it entirely rather than marking it as cancelled. You cannot retrieve cancelled events from Exchange on-prem accounts through the Nylas API.

This is important if you're building audit or compliance features that need to track event cancellations. Consider using [webhooks](/docs/v3/notifications/) to capture deletion events in real time before they become unrecoverable.

### Recurring event restrictions

Microsoft Exchange has specific constraints around recurring events that don't apply to Google Calendar:

- **No overlapping instances.** You cannot reschedule an instance of a recurring event to fall on the same day as, or the day before, the previous instance. Exchange rejects the update to prevent overlapping occurrences within a series.
- **Overrides removed on recurrence change.** If you modify the recurrence pattern of a series (for example, changing from weekly to daily), Exchange removes all existing overrides. Google keeps them if they still fit the new pattern.
- **EXDATE recovery is not possible.** Once you remove an occurrence from a recurring series, there is no way to restore it. You would need to create a standalone event to fill the gap.
- **No multi-day monthly BYDAY.** You cannot create a monthly recurring event on multiple days of the week (like the first and third Thursday). Exchange's recurrence model does not support different indices within a single rule.

For the full breakdown of provider-specific recurring event behavior, see [Recurring events](/docs/v3/calendar/recurring-events/).

### On-prem networking

The Exchange server's EWS endpoint must be reachable from Nylas infrastructure. This is the most common source of connection failures for on-prem deployments.

- **EWS must be enabled** on the server and exposed outside the corporate network
- If the server is behind a **firewall**, you need to allow Nylas's IP addresses. [Static IP routing](/docs/dev-guide/platform/#static-ips) requires an annual contract. [Contact sales](https://www.nylas.com/contact-sales/) to upgrade your plan
- A **reverse proxy** in front of the Exchange server is a common workaround if direct firewall rules are not feasible
- Accounts in admin groups are not supported

If calendar data is not syncing for an Exchange account, verify that the EWS endpoint is accessible before investigating other causes.

### Autodiscovery

Nylas uses Exchange autodiscovery to locate the EWS endpoint automatically during authentication. This works well when autodiscovery is properly configured on the Exchange server. When it is not, users must manually provide the server address.

If users report authentication failures, the Exchange administrator can test autodiscovery using Microsoft's [Remote Connectivity Analyzer](https://testconnectivity.microsoft.com/). Misconfigured autodiscovery is one of the most common issues with Exchange on-prem integrations.

### Timezone handling

Exchange stores timezone information using Windows timezone identifiers like "Eastern Standard Time" or "Pacific Standard Time." Nylas normalizes these to IANA identifiers (like "America/New_York" or "America/Los_Angeles") automatically, so event times in API responses always use IANA format. You do not need to maintain a Windows-to-IANA mapping table in your application.

### Sync timing

Calendar sync performance for Exchange on-prem depends on the EWS server's responsiveness and network latency between Nylas infrastructure and the Exchange server. On-prem servers with high load or limited bandwidth may introduce noticeable sync delays compared to cloud-hosted Exchange.

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, regardless of the underlying server speed.

### Rate limits are admin-configured

Unlike Google and Microsoft's cloud services, Exchange on-prem rate limits are set by the server administrator. Nylas cannot predict what they will be. If the Exchange server throttles a request, Nylas returns a `Retry-After` header with the number of seconds to wait.

For apps that check calendars frequently, [webhooks](/docs/v3/notifications/) are the best way to avoid hitting rate limits. Let Nylas notify you of changes instead of polling.

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

- [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
- [Availability](/docs/v3/calendar/calendar-availability/) to check free/busy across multiple calendars
- [Webhooks](/docs/v3/notifications/) for real-time notifications instead of polling
- [Exchange on-premises provider guide](/docs/provider-guides/exchange-on-prem/) for full Exchange setup including authentication and network requirements
- [Microsoft guide](/docs/cookbook/calendar/events/list-events-microsoft/) for cloud-hosted Exchange (Microsoft 365, Exchange Online)