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.
Why use Nylas for threads instead of Microsoft Graph directly?
Section titled “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
Section titled “Before you begin”You’ll need:
- A Nylas application with a valid API key
- A grant for a Microsoft 365 or Outlook account
- The
Mail.Readscope enabled in your Azure AD app registration
New to Nylas? Start with the quickstart guide to set up your app and connect a test account before continuing here.
Microsoft admin consent
Section titled “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. If you’re targeting enterprise customers, you’ll almost certainly need to deal with this.
You also need to be a verified publisher. Microsoft requires it since November 2020, and without it users see an error during auth.
List threads
Section titled “List threads”Make a List Threads request with the grant ID. Nylas returns the most recent threads by default. These examples limit results to 5:
curl --request GET \ --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/threads?limit=5" \ --header 'Accept: application/json, application/gzip' \ --header 'Authorization: Bearer <NYLAS_API_KEY>' \ --header 'Content-Type: application/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", }], "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": [{ }], "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": [{ }], "snippet": "Send Email with Nylas APIs", "subject": "Learn how to Send Email with Nylas APIs", "message_ids": [ "<MESSAGE_ID>" ] } ], "next_cursor": "123"}import 'dotenv/config'import Nylas from 'nylas'
const NylasConfig = { apiKey: process.env.NYLAS_API_KEY, apiUri: process.env.NYLAS_API_URI,}
const nylas = new Nylas(NylasConfig)
async function fetchRecentThreads() { try { const identifier = process.env.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()from dotenv import load_dotenvload_dotenv()
import osimport sysfrom nylas import Client
nylas = Client( os.environ.get('NYLAS_API_KEY'), os.environ.get('NYLAS_API_URI'))
grant_id = os.environ.get("NYLAS_GRANT_ID")
threads = nylas.threads.list( grant_id, query_params={ "limit": 5 })
print(threads)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(', ')}"}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()); } }}import com.nylas.NylasClientimport 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.
Filter threads
Section titled “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 | [email protected] |
to | Filter by recipient | [email protected] |
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:
curl --request GET \ --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/[email protected]&unread=true&limit=10" \ --header 'Accept: application/json' \ --header 'Authorization: Bearer <NYLAS_API_KEY>'const threads = await nylas.threads.list({ identifier: grantId, queryParams: { unread: true, limit: 10, },});threads = nylas.threads.list( grant_id, query_params={ "unread": True, "limit": 10, })Search with search_query_native
Section titled “Search with search_query_native”Microsoft supports the search_query_native parameter, which maps to the $search query parameter in Microsoft Graph. This uses Microsoft’s Keyword Query Language (KQL) syntax.
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>'const threads = await nylas.threads.list({ identifier: grantId, queryParams: { searchQueryNative: "subject:invoice", limit: 10, },});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.
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 for more on search_query_native across providers.
Things to know about Microsoft threads
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “Thread-level vs. message-level metadata”Some fields on the thread object are aggregated from all messages in the conversation:
unreadistrueif any message in the thread is unreadstarredistrueif any message in the thread is starredhas_attachmentsistrueif any message in the thread has attachmentsparticipantsis the union of all senders and recipients across the threadearliest_message_dateandlatest_message_received_datespan 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
Section titled “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 instead of polling.
Paginate through results
Section titled “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:
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>'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);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: breakKeep paginating until the response comes back without a next_cursor.
What’s next
Section titled “What’s next”- Threads API reference for full endpoint documentation and all available parameters
- Using the Threads API for thread concepts and additional operations
- Messages API reference to fetch individual message content from threads
- List Microsoft messages for message-level operations on Microsoft accounts
- Webhooks for real-time notifications instead of polling
- Microsoft admin approval to configure consent for enterprise organizations
- Microsoft publisher verification, required for production apps