# Get Exchange email threads with Nylas

Source: https://developer.nylas.com/docs/cookbook/email/threads/list-threads-ews/

Exchange on-premises servers support conversation grouping through a `ConversationId` property that EWS assigns to each message. Nylas maps this directly to the Threads API, giving you a conversation view for self-hosted Exchange accounts through the same endpoint you'd use for Gmail or Outlook online.

This guide covers listing threads from Exchange on-prem accounts and the EWS-specific details: when to use EWS vs. Microsoft Graph, how `ConversationId` maps to threads, message ID behavior, and search limitations.

## How do I list Exchange email threads with the Nylas API?

Send a `GET` request to `/v3/grants/{grant_id}/threads` with your API key. The endpoint returns the most recent threads by default and up to 200 per page, each with a `latest_draft_or_message` object. Exchange groups messages using its native conversation tracking. The same call works across Gmail, Outlook, and IMAP. See [List threads](#list-threads) 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, personal Microsoft accounts |
| Exchange Web Services | `ews`       | Self-hosted Exchange servers (on-premises)                                           |

If the user's mailbox is hosted by Microsoft in the cloud, use the [Microsoft thread guide](/docs/cookbook/email/threads/list-threads-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 for threads instead of EWS directly?

EWS is a SOAP-based XML API. Building a conversation view requires constructing XML SOAP envelopes for `FindConversation` operations, parsing nested XML responses, handling EWS-specific `ConversationId` and `ConversationIndex` fields, and managing autodiscovery to find the right server endpoint. Error responses come back as SOAP faults with nested XML structures.

Nylas replaces all of that with a JSON REST API. The `/threads` endpoint returns pre-grouped conversations with metadata. No XML, no WSDL, no SOAP. Authentication and autodiscovery are handled automatically.

## 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.messages` 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.


### Exchange authentication setup

Create an EWS connector with the scopes your app needs:

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

During authentication, 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`.

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

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

### Network requirements

The Exchange server must be accessible from Nylas's infrastructure:

- **EWS must be enabled** on the server and exposed outside the corporate network
- If the server is behind a **firewall**, you'll need to allow Nylas's [static IP addresses](/docs/dev-guide/platform/#static-ips). Static IP routing requires an annual contract. [Contact sales](https://www.nylas.com/contact-sales/) to upgrade your plan

Accounts in admin groups are not supported.

## List threads

Make a [List Threads request](/docs/reference/api/threads/get-threads/) with the grant ID. By default, Nylas returns the most recent threads. These examples limit results to 5:

```bash
curl --compressed --request GET \
  --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/threads?limit=5" \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>' \
  --header 'Content-Type: application/json'

```

```json
{
  "request_id": "1",
  "data": [
    {
      "starred": false,
      "unread": true,
      "folders": ["CATEGORY_PERSONAL", "INBOX", "UNREAD"],
      "grant_id": "<NYLAS_GRANT_ID>",
      "id": "<THREAD_ID>",
      "object": "thread",
      "latest_draft_or_message": {
        "starred": false,
        "unread": true,
        "folders": ["UNREAD", "CATEGORY_PERSONAL", "INBOX"],
        "grant_id": "<NYLAS_GRANT_ID>",
        "date": 1707836711,
        "from": [
          {
            "name": "Nyla",
            "email": "nyla@example.com"
          }
        ],
        "id": "<MESSAGE_ID>",
        "object": "message",
        "snippet": "Send Email with Nylas APIs",
        "subject": "Learn how to Send Email with Nylas APIs",
        "thread_id": "<THREAD_ID>",
        "to": [
          {
            "email": "nyla@example.com"
          }
        ],
        "created_at": 1707836711,
        "body": "Learn how to send emails using the Nylas APIs!"
      },
      "has_attachments": false,
      "has_drafts": false,
      "earliest_message_date": 1707836711,
      "latest_message_received_date": 1707836711,
      "participants": [
        {
          "email": "nylas@nylas.com"
        }
      ],
      "snippet": "Send Email with Nylas APIs",
      "subject": "Learn how to Send Email with Nylas APIs",
      "message_ids": ["<MESSAGE_ID>"]
    }
  ],
  "next_cursor": "123"
}


```

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

import Nylas from "nylas";

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

async function fetchRecentThreads() {
  try {
    const identifier = "<NYLAS_GRANT_ID>";
    const threads = await nylas.threads.list({
      identifier: identifier,
      queryParams: {
        limit: 5,
      },
    });

    console.log("Recent Threads:", threads);
  } catch (error) {
    console.error("Error fetching threads:", error);
  }
}

fetchRecentThreads();


```

```python [listThreads-Python SDK]

from nylas import Client

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

grant_id = "<NYLAS_GRANT_ID>"

threads = nylas.threads.list(
  grant_id,
  query_params={
    "limit": 5
  }
)

print(threads)

```


```ruby [listThreads-Ruby SDK]
require 'nylas'

nylas = Nylas::Client.new(api_key: '<NYLAS_API_KEY>')
query_params = { limit: 5 }
threads, _ = nylas.threads.list(identifier: '<NYLAS_GRANT_ID>', query_params: query_params)

threads.each {|thread|
  puts "#{thread[:subject]} | Participants: #{thread[:participants].map { |p| p[:email] }.join(', ')}"
}
```

```java [listThreads-Java SDK]
import com.nylas.NylasClient;
import com.nylas.models.*;
import com.nylas.models.Thread;
import java.util.List;

public class ListThreads {
  public static void main(String[] args) throws NylasSdkTimeoutError, NylasApiError {
    NylasClient nylas = new NylasClient.Builder("<NYLAS_API_KEY>").build();
    ListThreadsQueryParams queryParams = new ListThreadsQueryParams.Builder().limit(5).build();
    ListResponse<Thread> threads = nylas.threads().list("<NYLAS_GRANT_ID>", queryParams);

    for(Thread thread : threads.getData()) {
      System.out.println(thread.getSubject());
    }
  }
}
```

```kt [listThreads-Kotlin SDK]
import com.nylas.NylasClient
import com.nylas.models.*

fun main(args: Array<String>) {
    val nylas = NylasClient(apiKey = "<NYLAS_API_KEY>")
    val queryParams = ListThreadsQueryParams(limit = 5)
    val threads = nylas.threads().list("<NYLAS_GRANT_ID>", queryParams).data

    for (thread in threads) {
        println(thread.subject)
    }
}
```


The response includes a `latest_draft_or_message` object with the most recent message's content. The same code works for Google, Yahoo, and IMAP accounts.

## List Exchange threads from the terminal

Exchange on-premises assigns every message a `ConversationId`, so threading is native rather than reconstructed. The [Nylas CLI](https://cli.nylas.com/docs/commands) maps that straight through: `nylas email threads list` returns each Exchange conversation as one row, with no XML or SOAP on your side.


The Nylas CLI groups messages into conversations the same way the Threads API does. After `nylas init` and `nylas auth login`, `email threads list` returns the 10 most recent threads by default, each collapsing a full back-and-forth into a single row:

```bash
# List the 10 most recent threads
nylas email threads list

# Only unread threads, filtered by subject
nylas email threads list --unread --subject "invoice"

# Open one thread to see every message in the conversation
nylas email threads show <thread-id>
```

Thread search works differently from message search: it filters by field rather than free text, so you match on `--subject`, `--from`, or `--unread` instead of passing a bare query string:

```bash
# Find threads by subject and sender
nylas email threads search --subject "contract renewal" --from "legal@vendor.com"
```

Both commands accept `--json` for scripting. See the [`email threads list`](https://cli.nylas.com/docs/commands/email-threads-list) and [`email threads search`](https://cli.nylas.com/docs/commands/email-threads-search) command reference for every flag.


Because the server tracks conversations itself, EWS threading stays stable as messages move between folders. The Threads API returns up to 200 threads per page, and [`nylas email threads show`](https://cli.nylas.com/docs/commands/email-threads-show) expands any conversation into its messages.

## Filter threads

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


| Parameter         | What it does                  | Example                       |
| ----------------- | ----------------------------- | ----------------------------- |
| `subject`         | Match on subject line         | `?subject=Weekly standup`     |
| `from`            | Filter by sender              | `?from=alex@example.com`      |
| `to`              | Filter by recipient           | `?to=team@company.com`        |
| `unread`          | Unread only                   | `?unread=true`                |
| `in`              | Filter by folder or label ID  | `?in=INBOX`                   |
| `received_after`  | After a Unix timestamp        | `?received_after=1706000000`  |
| `received_before` | Before a Unix timestamp       | `?received_before=1706100000` |
| `has_attachment`  | Only results with attachments | `?has_attachment=true`        |


Here's how to combine filters. This pulls threads with unread messages from a specific sender:


```bash
curl --request GET \
  --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/threads?from=alex@example.com&unread=true&limit=10" \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>'
```

```js [filterThreads-Node.js SDK]
const threads = await nylas.threads.list({
  identifier: grantId,
  queryParams: {
    from: "alex@example.com",
    unread: true,
    limit: 10,
  },
});
```

```python [filterThreads-Python SDK]
threads = nylas.threads.list(
    grant_id,
    query_params={
        "from": "alex@example.com",
        "unread": True,
        "limit": 10,
    }
)
```


### Search with `search_query_native`

Exchange supports the `search_query_native` parameter using Microsoft's [Advanced Query Syntax (AQS)](https://learn.microsoft.com/en-us/windows/win32/lwef/-search-2x-wds-aqsreference). You can combine `search_query_native` with any query parameter **except** `thread_id`.

```bash
curl --request GET \
  --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/threads?search_query_native=subject%3Ainvoice&limit=10" \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>'
```

```js [nativeSearchEwsThreads-Node.js SDK]
const threads = await nylas.threads.list({
  identifier: grantId,
  queryParams: {
    searchQueryNative: "subject:invoice",
    limit: 10,
  },
});
```

```python [nativeSearchEwsThreads-Python SDK]
threads = nylas.threads.list(
    grant_id,
    query_params={
        "search_query_native": "subject:invoice",
        "limit": 10,
    }
)
```

> **Warn:** 
> **AQS queries must be URL-encoded.** For example, `subject:invoice` becomes `subject%3Ainvoice` in the URL. The SDKs handle this automatically, but you'll need to encode manually in curl requests.

The Exchange server must have the **AQS parser enabled** and **search indexing active** for `search_query_native` to work. If queries aren't returning expected results, the Exchange admin should verify these settings.

See the [search best practices guide](/docs/dev-guide/best-practices/search/) for more on `search_query_native` across providers.

## Things to know about Exchange threads

Exchange on-prem has native conversation support through `ConversationId`, but it behaves differently from Microsoft Graph in several important ways.

### Exchange has native ConversationId

Like Exchange Online (Microsoft Graph), on-premises Exchange assigns a `ConversationId` to each message. Nylas maps this to the thread's `id` field. This means Exchange threading is based on the server's own grouping logic, not heuristic header matching. The grouping is more reliable than IMAP-based providers like Yahoo or iCloud.

### Message IDs change when messages move, but thread IDs don't

This is the most important Exchange-specific behavior. When a message moves from one folder to another, its Nylas message ID changes (this is expected EWS behavior). However, the `thread_id` remains stable because it's based on the `ConversationId`, which doesn't change when messages move.

If your app stores message IDs from the `message_ids` array, treat them as folder-specific pointers. The thread ID itself is safe to use as a permanent reference.

### Threads span multiple folders

Like Microsoft Graph, Exchange threads can include messages across multiple folders (Inbox, Sent Items, Deleted Items). The thread's `folders` array reflects all folders containing messages from that conversation. Use the `in` parameter to filter threads by folder.

### Date filtering is day-level precision

Exchange processes `received_before` and `received_after` filters at the **day level**, not second-level. Even though Nylas accepts Unix timestamps, Exchange rounds to the nearest day. Results are inclusive of the specified day.

### Search indexing affects query accuracy

Exchange relies on a search index for queries. If a message has just arrived but the search index hasn't refreshed yet, threads containing that message may not appear in filtered results. The refresh interval is controlled by the Exchange administrator. Unfiltered list requests always return the latest threads.

### Rate limits are admin-configured

Unlike cloud services, Exchange on-prem rate limits are set by the server administrator. If the Exchange server throttles a request, Nylas returns a `Retry-After` header. Use [webhooks](/docs/v3/notifications/) to avoid hitting rate limits.

### EWS fallback to IMAP

If EWS isn't enabled, Nylas can still connect via IMAP for email-only access. When using IMAP fallback, threading uses header-based grouping instead of `ConversationId`, and the 90-day message cache applies.

## Paginate through results


The Threads 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>/threads?limit=10&page_token=<NEXT_CURSOR>" \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>'
```

```js [paginateThreads-Node.js SDK]
let pageCursor = undefined;

do {
  const result = await nylas.threads.list({
    identifier: grantId,
    queryParams: {
      limit: 10,
      pageToken: pageCursor,
    },
  });

  // Process result.data here

  pageCursor = result.nextCursor;
} while (pageCursor);
```

```python [paginateThreads-Python SDK]
page_cursor = None

while True:
    query = {"limit": 10}
    if page_cursor:
        query["page_token"] = page_cursor

    result = nylas.threads.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

- [Threads API reference](/docs/reference/api/threads/) for full endpoint documentation and all available parameters
- [Using the Threads API](/docs/v3/email/threads/) for thread concepts and additional operations
- [Messages API reference](/docs/reference/api/messages/) to fetch individual message content from threads
- [List Exchange messages](/docs/cookbook/email/messages/list-messages-ews/) for message-level operations on Exchange accounts
- [Search best practices](/docs/dev-guide/best-practices/search/) for advanced search with `search_query_native` and AQS for Exchange
- [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