# Build a unified inbox

Source: https://developer.nylas.com/docs/cookbook/email/unified-inbox/

Your users connect a Gmail account for personal mail and an Outlook account for work, then expect both to show up in one list. Building that against Gmail's API and Microsoft Graph separately means two auth flows, two data models, and two sets of folder names to reconcile. This recipe shows how to merge several connected accounts into a single inbox view using one schema and one API call shape per account.

The key idea: each connected account is a [grant](/docs/v3/auth/), you list messages from each grant with the same request, and you merge the results in your own code. There's no single endpoint that lists across grants, so you fan out per grant and combine. That's an honest tradeoff worth understanding before you build.

## List messages from each account

A unified inbox starts with one [List Messages request](/docs/reference/api/messages/get-messages/) per connected grant: `GET /v3/grants/{grant_id}/messages`. The call shape is identical for every provider, and the response uses one schema across Google, Microsoft, Yahoo, iCloud, IMAP, and EWS. You run it once per account, not once per provider type.

Because the schema is unified, there's no provider branching in your code. A Gmail message and an Outlook message come back with the same `id`, `subject`, `from`, `date`, and `folders` fields. The default page size is 50 messages and the maximum is 200. These examples limit results to 5 per account:

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

```

You call this same request for every grant in the user's account. For provider-specific details like Gmail labels or Outlook folder names, see the [Google](/docs/cookbook/email/messages/list-messages-google/) and [Microsoft](/docs/cookbook/email/messages/list-messages-microsoft/) list-messages recipes.

## Merge and sort across accounts

To build the merged view, fetch messages from each grant, concatenate the result arrays, and sort by the `date` field. The `date` value is a Unix timestamp in seconds (an integer) on every message, regardless of provider, so a single numeric sort orders the combined list correctly across all connected accounts.

Sorting descending by `date` puts the newest message first, the same ordering a single mailbox would show. The snippet below pulls 50 messages from two grants, tags each one with its source grant ID for display, then merges and sorts. With two accounts at the default page size, that's up to 100 messages in the combined view.

```js [mergeInbox-Node.js SDK]
const grantIds = ["<GRANT_ID_GMAIL>", "<GRANT_ID_OUTLOOK>"];

const perAccount = await Promise.all(
  grantIds.map(async (grantId) => {
    const { data } = await nylas.messages.list({
      identifier: grantId,
      queryParams: { limit: 50 },
    });
    return data.map((message) => ({ ...message, grantId }));
  })
);

const unifiedInbox = perAccount
  .flat()
  .sort((a, b) => b.date - a.date);
```

```python [mergeInbox-Python SDK]
grant_ids = ["<GRANT_ID_GMAIL>", "<GRANT_ID_OUTLOOK>"]

merged = []
for grant_id in grant_ids:
    response = nylas.messages.list(grant_id, query_params={"limit": 50})
    for message in response.data:
        merged.append({**message.__dict__, "grant_id": grant_id})

unified_inbox = sorted(merged, key=lambda m: m["date"], reverse=True)
```

Keep the source grant ID on each message so the UI can show which account it came from and so reply or modify actions route back to the right mailbox.

## Paginate a merged inbox

Each grant paginates on its own [`page_token`](/docs/reference/api/messages/get-messages/) cursor, and there's no shared cursor across grants. The default page size is 50 messages per grant and the maximum is 200, so a merged page from three accounts can hold up to 600 messages before you request the next page from any of them.

The reliable strategy is to track one `page_token` per grant and advance them independently. To fill a fixed merged page (say 50 rows), pull a batch from each account, merge, sort by `date`, take the top 50, and remember where each account's cursor stopped. The snippet fetches the next page for a single grant using its stored token:

```js [paginateInbox-Node.js SDK]
const { data, nextCursor } = await nylas.messages.list({
  identifier: grantId,
  queryParams: { limit: 50, pageToken: cursors[grantId] },
});

cursors[grantId] = nextCursor; // undefined when this account has no more pages
```

```python [paginateInbox-Python SDK]
response = nylas.messages.list(
    grant_id,
    query_params={"limit": 50, "page_token": cursors[grant_id]},
)

cursors[grant_id] = response.next_cursor  # None when this account is exhausted
```

Store the cursor map keyed by grant ID so a "load more" action resumes each account exactly where it left off, even when one account runs out of pages before the others.

## Things to know about a unified inbox

A unified inbox is an application-side aggregation, not a single API feature. The Nylas Email API exposes messages per grant, so you query each of the user's connected accounts and merge the results yourself. The points below cover the design decisions that matter once you move past two test accounts.

**There's no single cross-grant list endpoint.** Every list call targets one `grant_id`, so a user with 5 connected accounts means 5 requests per refresh. Run them in parallel and merge in memory. Plan request volume around the account count, because a 1,000-user app where each user connects 3 mailboxes fans out to 3,000 grants.

**Rate limits apply per grant, not per app.** Google throttles at the per-user and per-project level, and Microsoft throttles per mailbox. Polling every account every few seconds burns through those limits fast. Use [webhooks](/docs/cookbook/use-cases/build/new-email-webhook/) so the API pushes a notification when a message arrives, instead of polling each grant on a timer.

**Keep a local index for fast UI.** Fetching and merging live on every page load gets slow once a user has several accounts. Store a normalized copy of message metadata (`id`, `grant_id`, `date`, `subject`, `from`, `folders`) in your own database, render the inbox from that index, and keep it fresh with webhooks. The unified schema means one table holds messages from all six supported providers.

**Remove duplicate copies of shared threads.** When a user sends mail between their own connected accounts, the same conversation appears in two mailboxes. Group by the normalized `subject` and participant set, or key off the `thread_id` within each grant, so a message and its self-copy don't show as two separate rows.

**Provider date and folder semantics differ.** The `date` field is a consistent Unix-seconds timestamp everywhere, so sorting is safe. Folders aren't: Gmail uses labels (a message can sit in `INBOX` and `IMPORTANT` at once), while Outlook uses single folders like `inbox` and `sentitems`. Map both into your own "inbox" and "sent" concepts rather than filtering on raw provider names. See [Microsoft's folder reference](https://learn.microsoft.com/en-us/graph/api/resources/mailfolder) for the internal names Graph exposes.

## What's next

- [List Google email messages](/docs/cookbook/email/messages/list-messages-google/) for Gmail labels, search operators, and OAuth scopes
- [List Microsoft email messages](/docs/cookbook/email/messages/list-messages-microsoft/) for Outlook folder names, admin consent, and message IDs
- [Trigger a webhook on new email](/docs/cookbook/use-cases/build/new-email-webhook/) to keep a merged inbox fresh without polling
- [Email API overview](/docs/v3/email/) for messages, threads, drafts, and attachments
- [Messages API reference](/docs/reference/api/messages/) for every list parameter and the full response schema