# Get Outlook email threads with Nylas

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

If you're building an inbox UI that groups messages into conversations, the Nylas Threads API gives you that view out of the box. For Microsoft 365 and Outlook accounts, Nylas maps Microsoft's native `ConversationId` to a unified thread model, so you get conversation grouping without working directly with Microsoft Graph.

The API call is the same whether the account is Microsoft, Google, or IMAP. The differences show up in how each provider groups messages into threads, what metadata is available, and how threads interact with folders. This guide covers those details for Microsoft accounts.

## How do I list Outlook 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. Microsoft groups messages using its native `conversationId`. The same call works across Gmail, Yahoo, and IMAP. See [List threads](#list-threads) for the request and response.

## Why use Nylas for threads instead of Microsoft Graph directly?

Microsoft Graph provides conversation threading through the `conversationId` property on messages, but building a thread view from it requires significant work. You need to query messages, group them by `conversationId`, sort by date, track read/unread state per thread, and handle the `conversationIndex` binary header for sub-threading. Graph also requires Azure AD registration, MSAL token management, and admin consent for enterprise tenants.

Nylas gives you a dedicated `/threads` endpoint that returns pre-grouped conversations with metadata like `latest_draft_or_message`, participant lists, and aggregate read state. No Azure AD, no MSAL, no manual grouping logic. And the same code works across Outlook, Gmail, Yahoo, and IMAP.

If you only need Microsoft and already have Graph experience, direct integration works. For multi-provider support or faster development, Nylas handles the threading logic for you.

## 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 `Mail.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 mailbox data. If your users see a "Need admin approval" screen during auth, it means 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 it since November 2020, and without it users see an error during auth.

## List threads

Make a [List Threads request](/docs/reference/api/threads/get-threads/) with the grant ID. Nylas returns the most recent threads by default. 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, so you can render a thread preview without making a separate call to the Messages API. The same code works for Google, Yahoo, and IMAP accounts.

## List Microsoft threads from the terminal

Outlook and Microsoft 365 track conversations with a `conversationId` that holds a thread together even when its messages sit in different folders. The [Nylas CLI](https://cli.nylas.com/docs/commands) reads that grouping directly: `nylas email threads list` returns each conversation as one row from your terminal.


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.


A Microsoft conversation can span Inbox, Sent, and Archive at once, so one thread pulls messages from several folders. `nylas email threads list` returns 10 threads by default; raise `--limit` to fetch more, then expand one with [`nylas email threads show`](https://cli.nylas.com/docs/commands/email-threads-show).

## Filter threads

You can narrow results with query parameters. The same filters available for messages work on threads:


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


Combining filters works the way you'd expect. 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`

Microsoft supports the `search_query_native` parameter, which maps to the [`$search` query parameter](https://learn.microsoft.com/en-us/graph/search-query-parameter) in Microsoft Graph. This uses Microsoft's [Keyword Query Language (KQL)](https://learn.microsoft.com/en-us/sharepoint/dev/general-development/keyword-query-language-kql-syntax-reference) syntax.

```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 [nativeSearchMicrosoftThreads-Node.js SDK]
const threads = await nylas.threads.list({
  identifier: grantId,
  queryParams: {
    searchQueryNative: "subject:invoice",
    limit: 10,
  },
});
```

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

Microsoft restricts which query parameters you can combine with `search_query_native`. You can only use it with `in`, `limit`, and `page_token`. Other query parameters return an error.

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

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

## Things to know about Microsoft threads

A few provider-specific details that matter when you're working with threads on Microsoft accounts.

### Microsoft has native conversation grouping

Outlook groups messages into conversations using a `ConversationId` that Microsoft assigns internally. Nylas maps this directly to the `thread_id` field, which means Microsoft threads are based on the same grouping logic that Outlook uses in its own UI. This is more reliable than subject-line matching because it tracks the actual reply chain.

The grouping considers the subject line, recipients, and `In-Reply-To`/`References` headers together. Two messages with the same subject but different participants won't end up in the same thread.

### Threads span multiple folders

A single Microsoft thread can include messages across Inbox, Sent Items, Drafts, and Deleted Items. The thread object's `folders` array reflects all folders that contain at least one message from that conversation. This is expected behavior since a conversation includes both received and sent messages.

If you're building a folder-filtered view (like "show threads in Inbox"), use the `in` query parameter. The API returns threads that have at least one message in the specified folder.

### The `latest_draft_or_message` field

Each thread includes a `latest_draft_or_message` object containing the most recent message or draft in the conversation, including its body content. This is useful for rendering thread previews in an inbox view without making a separate call to the Messages API for each thread.

The object includes `from`, `to`, `subject`, `snippet`, `body`, `date`, and attachment metadata. If the latest item is a draft, it appears here instead of the most recent received message.

### Thread-level vs. message-level metadata

Some fields on the thread object are aggregated from all messages in the conversation:

- `unread` is `true` if any message in the thread is unread
- `starred` is `true` if any message in the thread is starred
- `has_attachments` is `true` if any message in the thread has attachments
- `participants` is the union of all senders and recipients across the thread
- `earliest_message_date` and `latest_message_received_date` span the full conversation timeline

To get per-message read/starred state, fetch individual messages using the `message_ids` from the thread response.

### Rate limits are per-mailbox

Microsoft throttles API requests at the mailbox level, same as with the Messages API. Nylas handles retries automatically on `429` responses. For apps that need real-time thread updates, use [webhooks](/docs/v3/notifications/) instead of polling.

## 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 Microsoft messages](/docs/cookbook/email/messages/list-messages-microsoft/) for message-level operations on Microsoft accounts
- [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