# How to list Microsoft email messages

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

If you're building an app that reads email from Microsoft 365 or Outlook accounts, you have two choices: 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 messages is the same whether the account is Microsoft, Google, or IMAP. The differences show up in folder naming, message ID formats, admin consent requirements, and rate limiting. This guide covers all of that.

## How do I list Microsoft email messages with the Nylas API?

Send a `GET` request to `/v3/grants/{grant_id}/messages` with your API key. The endpoint returns the 50 most recent Outlook or Microsoft 365 messages by default and up to 200 per page, with base64-encoded message IDs and internal folder names like `sentitems` normalized into one schema. The same call works across Gmail, Yahoo, and IMAP. See [List messages](#list-messages) for the request and response.

## Why use Nylas instead of Microsoft Graph directly?

The Microsoft Graph API is capable, but integrating it requires significant setup. You need to register an app in Azure AD, configure OAuth scopes, manage token refresh with MSAL, handle admin consent flows for enterprise tenants, and deal with Microsoft-specific data formats like base64-encoded message IDs and internal folder names like `sentitems`.

Nylas handles all of that behind a single REST API. Your code stays the same whether you're reading from Outlook, Gmail, or Yahoo. No Azure AD registration, no MSAL token lifecycle, no mapping `deleteditems` to "Trash" in your UI. If you need to support multiple providers or want to skip the Graph onboarding, Nylas is the faster path.

If you only need Microsoft and already have Graph experience, you can integrate directly.

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

Make a [List Messages request](/docs/reference/api/messages/get-messages/) with the grant ID. Nylas returns the 50 most recent messages by default. These examples limit results to 5:

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

```

```json
{
  "request_id": "d0c951b9-61db-4daa-ab19-cd44afeeabac",
  "data": [
    {
      "starred": false,
      "unread": true,
      "folders": ["UNREAD", "CATEGORY_PERSONAL", "INBOX"],
      "grant_id": "1",
      "date": 1706811644,
      "attachments": [
        {
          "id": "1",
          "grant_id": "1",
          "filename": "invite.ics",
          "size": 2504,
          "content_type": "text/calendar; charset=\"UTF-8\"; method=REQUEST"
        },
        {
          "id": "2",
          "grant_id": "1",
          "filename": "invite.ics",
          "size": 2504,
          "content_type": "application/ics; name=\"invite.ics\"",
          "is_inline": false,
          "content_disposition": "attachment; filename=\"invite.ics\""
        }
      ],
      "from": [
        {
          "name": "Nylas DevRel",
          "email": "nylasdev@nylas.com"
        }
      ],
      "id": "1",
      "object": "message",
      "snippet": "Send Email with Nylas APIs",
      "subject": "Learn how to Send Email with Nylas APIs",
      "thread_id": "1",
      "to": [
        {
          "name": "Nyla",
          "email": "nyla@nylas.com"
        }
      ],
      "created_at": 1706811644,
      "body": "Learn how to send emails using the Nylas APIs!"
    }
  ],
  "next_cursor": "123"
}


```

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

import Nylas from "nylas";

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

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

    console.log("Messages:", messages);
  } catch (error) {
    console.error("Error fetching emails:", error);
  }
}

fetchRecentEmails();


```

```python [listMessages-Python SDK]

from nylas import Client

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

grant_id = "<NYLAS_GRANT_ID>"

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

print(messages)

```


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

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

messages.each {|message|
  puts "[#{Time.at(message[:date]).strftime("%d/%m/%Y at %H:%M:%S")}] \
      #{message[:subject]}"
}
```

```java [listMessages-Java SDK]
import com.nylas.NylasClient;
import com.nylas.models.*;
import java.text.SimpleDateFormat;

public class ListMessages {
  public static void main(String[] args) throws NylasSdkTimeoutError, NylasApiError {
    NylasClient nylas = new NylasClient.Builder("<NYLAS_API_KEY>").build();
    ListMessagesQueryParams queryParams = new ListMessagesQueryParams.Builder().limit(5).build();
    ListResponse<Message> message = nylas.messages().list("<NYLAS_GRANT_ID>", queryParams);

    for(Message email : message.getData()) {
      String date = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").
          format(new java.util.Date((email.getDate() * 1000L)));

      System.out.println("[" + date + "] | " + email.getSubject());
    }
  }
}
```

```kt [listMessages-Kotlin SDK]
import com.nylas.NylasClient
import com.nylas.models.*
import java.text.SimpleDateFormat
import java.util.*

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

    for (message in messages) {
        val date = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
            .format(Date(message.date.toLong() * 1000))
        println("[$date] | ${message.subject}")
    }
}
```


## List Microsoft messages from the terminal

Admin consent and Azure AD registration are the usual friction points on Microsoft 365, so proving a grant syncs is the first thing worth doing. The [Nylas CLI](https://cli.nylas.com/docs/commands) does it from your terminal: `nylas email list` returns the account's 10 most recent inbox messages through the Messages API, with no integration code required.


The Nylas CLI mirrors the Messages API, so you can read the same inbox from your terminal without writing any code. After `nylas init` and `nylas auth login`, the `email list` command returns the 10 most recent inbox messages by default and pages through everything automatically once `--limit` goes over 200:

```bash
# List the 10 most recent inbox messages
nylas email list

# Show only unread messages from a specific sender
nylas email list --unread --from billing@vendor.com

# Fetch everything across all folders, paginated automatically
nylas email list --all --all-folders --max 500
```

To find specific messages rather than list them, `email search` runs a full-text query with its own filters for sender (`--from`), date range (`--after` and `--before`), and attachments (`--has-attachment`). Search returns 20 results by default and only fetches more than one page once `--limit` goes over 200:

```bash
# Full-text search restricted to one sender, attachments only
nylas email search "invoice" --from billing@vendor.com --has-attachment
```

Both commands accept `--json`, so you can pipe results into `jq` or a script. See the [`email list`](https://cli.nylas.com/docs/commands/email-list) and [`email search`](https://cli.nylas.com/docs/commands/email-search) command reference for every flag.


Microsoft sorts mail into folders rather than labels, and Nylas normalizes the internal names like `inbox` and `sentitems` into the `folders` array on each message. To filter by folder, run `nylas email folders list` first and pass the folder ID it returns, since enterprise tenants often rename or add folders. Each page of results holds up to 200 messages. To send Outlook mail from the same command line, see [Send email from the terminal](https://cli.nylas.com/guides/send-email-from-terminal).

## Filter messages

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


Combining filters works the way you'd expect. This pulls unread messages from a specific sender:


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

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

```python [filterMessages-Python SDK]
messages = nylas.messages.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>/messages?search_query_native=subject%3Ainvoice&limit=10" \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>'
```

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

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

Like Google, 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 will 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

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

### Folder names aren't what you'd expect

Microsoft uses internal names like `sentitems`, `deleteditems`, and `junkemail` instead of the display names you see in the Outlook UI. Here's the mapping:

| Outlook UI name | Microsoft internal name |
| --------------- | ----------------------- |
| Inbox           | `inbox`                 |
| Sent Items      | `sentitems`             |
| Drafts          | `drafts`                |
| Deleted Items   | `deleteditems`          |
| Junk Email      | `junkemail`             |
| Archive         | `archive`               |

If you're building a folder picker or filtering messages by folder, call the [List Folders endpoint](/docs/reference/api/folders/get-folder/) first to get the actual folder IDs and display names for the account. Don't hardcode folder names because some organizations customize them.

### Message IDs are long and base64-encoded

Microsoft message IDs look like `AAMkAGI2TG93AAA=`, which are long base64 strings compared to Google's shorter numeric IDs. These IDs are stable and persist across syncs, so you can safely store and reference them. Just be aware that they'll take more space in your database and URLs.

### Sync is fast, but not instant

New messages typically appear within a few seconds of being sent or received. If a message you know exists isn't showing up in list results yet, wait a moment and retry. This is a Microsoft-side delay, not a Nylas one.

For apps that need real-time notification of new messages, don't poll. Use [webhooks](/docs/v3/notifications/) instead. Nylas will push a notification to your server as soon as the message syncs.

### Rate limits are per-mailbox

Microsoft throttles API requests at the mailbox level. Its documented ceiling for the Outlook service is 10,000 requests per 10-minute window per app per mailbox, plus 4 concurrent requests. If your app hits a `429` response, Nylas honors the `Retry-After` header and retries automatically, so you don't need to implement backoff logic yourself. See [Microsoft Graph throttling limits](https://learn.microsoft.com/en-us/graph/throttling-limits) for the full table.

That said, if you're building something that checks a mailbox frequently (like every few seconds), 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.

### Attachments and inline images

Microsoft handles attachments differently from Google. A few things to watch for:

- **Inline images** in HTML email bodies are returned as attachments with `is_inline: true`. If you're rendering email content, you'll need to replace `cid:` references in the HTML with the actual attachment URLs.
- **Attachment size limits** vary. Microsoft allows up to 150 MB for messages with attachments, but individual files over 3 MB should use the [upload attachment flow](/docs/v3/email/attachments/).
- **Calendar invites** (`.ics` files) show up as attachments on messages. Check the `content_type` field for `text/calendar` or `application/ics`.

### Focused and Other inboxes

Microsoft 365 sorts incoming mail into Focused and Other using its own relevance model, but both sit in the same `inbox` folder. A List Messages call returns messages from both, and the API doesn't split them into separate folders. Microsoft Graph exposes the split through the `inferenceClassification` property (`focused` or `other`) if you need to replicate it. Microsoft introduced Focused Inbox in 2016, and many enterprise tenants leave it enabled, so don't assume `inbox` means "Focused only".

### Shared and delegated mailboxes

Reading a shared or delegated mailbox needs a grant authorized for that mailbox plus the right Microsoft permission: the signed-in user needs Full Access to the shared mailbox, and your Azure app needs delegated mail scopes. Shared mailboxes under 50 GB don't require their own Microsoft 365 license, which is why support and sales aliases often use them. See [Microsoft's shared mailbox overview](https://learn.microsoft.com/en-us/microsoft-365/admin/email/about-shared-mailboxes) for the access model.

## Paginate through results


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

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

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

  // Process result.data here

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

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

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

    result = nylas.messages.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 email threads](/docs/cookbook/email/threads/list-threads-microsoft/) to group these messages into conversations
- [List Gmail messages](/docs/cookbook/email/messages/list-messages-google/) for the same task on Google accounts
- [List IMAP messages](/docs/cookbook/email/messages/list-messages-imap/) for the same task on generic IMAP accounts
- [Messages API reference](/docs/reference/api/messages/) for full endpoint documentation and all available parameters
- [Using the Messages API](/docs/v3/email/messages/) for search, modification, and deletion
- [Threads](/docs/v3/email/threads/) to group related messages into conversations
- [Attachments](/docs/v3/email/attachments/) to download and upload file attachments
- [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
- [List Outlook emails from the terminal](https://cli.nylas.com/guides/list-outlook-emails) -- read and search Outlook messages using the Nylas CLI
- [Send email from the terminal](https://cli.nylas.com/guides/send-email-from-terminal) -- send email from the command line without writing code