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, 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
Section titled “List messages from each account”A unified inbox starts with one List Messages request 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:
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'{ "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", } ], "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", } ], "created_at": 1706811644, "body": "Learn how to send emails using the Nylas APIs!" } ], "next_cursor": "123"}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();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 and Microsoft list-messages recipes.
Merge and sort across accounts
Section titled “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.
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);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
Section titled “Paginate a merged inbox”Each grant paginates on its own page_token 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:
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 pagesresponse = 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 exhaustedStore 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
Section titled “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 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 for the internal names Graph exposes.
What’s next
Section titled “What’s next”- List Google email messages for Gmail labels, search operators, and OAuth scopes
- List Microsoft email messages for Outlook folder names, admin consent, and message IDs
- Trigger a webhook on new email to keep a merged inbox fresh without polling
- Email API overview for messages, threads, drafts, and attachments
- Messages API reference for every list parameter and the full response schema