Gmail invented the conversation view that most email clients now copy. Every message in Gmail belongs to a thread, and Google assigns a stable thread_id that persists across the entire conversation. Nylas maps this directly to the Threads API, giving you the same conversation grouping you see in the Gmail UI through a simple REST call.
This guide walks through listing threads from Google accounts and covers the Google-specific details: native thread_id behavior, how labels interact with threads, search operators, and OAuth scopes.
Why use Nylas for threads instead of the Gmail API directly?
Section titled “Why use Nylas for threads instead of the Gmail API directly?”The Gmail API has a native Threads resource that returns conversation-grouped messages. But using it requires configuring a GCP project, navigating Google’s three-tier OAuth scope system, and for anything beyond basic metadata, going through OAuth verification or a full security assessment. On top of that, the Gmail Threads resource returns message IDs that you then need to fetch individually to get content, requiring multiple API calls per thread.
Nylas gives you a /threads endpoint that includes latest_draft_or_message with the most recent message’s content inline. No extra calls needed. Token refresh, scope management, and the GCP project overhead are handled automatically. And your code works across Gmail, Outlook, Yahoo, and IMAP without any provider-specific branches.
If you only need Gmail and want full control, the Gmail API works well for threads. For multi-provider support or faster development, Nylas simplifies the integration.
Before you begin
Section titled “Before you begin”You’ll need:
- A Nylas application with a valid API key
- A grant for a Gmail or Google Workspace account
- The appropriate Google OAuth scopes configured in your GCP project
New to Nylas? Start with the quickstart guide to set up your app and connect a test account before continuing here.
Google OAuth scopes and verification
Section titled “Google OAuth scopes and verification”Google classifies OAuth scopes into three tiers, and each one comes with different verification requirements:
| Scope tier | Example | What’s required |
|---|---|---|
| Non-sensitive | gmail.labels | No verification needed |
| Sensitive | gmail.readonly, gmail.compose | OAuth consent screen verification |
| Restricted | gmail.modify | Full security assessment by a third-party auditor |
If your app needs to read thread content (not just metadata), you’ll need at least the gmail.readonly scope, which is classified as sensitive. For read-write access, gmail.modify is restricted and requires a security assessment.
Nylas handles the token management, but your GCP project still needs the right scopes configured. See the Google provider guide for the full setup.
List threads
Section titled “List threads”Make a List Threads request with the grant ID. By default, Nylas returns the most recent threads. 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 a separate Messages API call. The same code works for Microsoft, Yahoo, and IMAP accounts.
Filter threads
Section titled “Filter threads”You can narrow results with query parameters. Here’s what works with Google accounts:
| 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 |
When using the in parameter with Google accounts, you must use the folder (label) ID, not the display name. Nylas does not support filtering by label name on Google accounts. Use the List Folders endpoint to get the correct IDs.
Here’s how to combine filters. 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, })Use Gmail search operators
Section titled “Use Gmail search operators”For more advanced filtering, you can use Gmail’s native search syntax through the search_query_native parameter. This supports the same search operators you’d use in the Gmail search bar:
curl --request GET \ --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/threads?search_query_native=subject%3Ainvoice%20OR%20subject%3Areceipt" \ --header 'Accept: application/json' \ --header 'Authorization: Bearer <NYLAS_API_KEY>'const threads = await nylas.threads.list({ identifier: grantId, queryParams: { searchQueryNative: "subject:invoice OR subject:receipt", limit: 10, },});threads = nylas.threads.list( grant_id, query_params={ "search_query_native": "subject:invoice OR subject:receipt", "limit": 10, })Some useful Gmail search operators:
| Operator | What it does | Example |
|---|---|---|
from: | Threads from a sender | from:[email protected] |
to: | Threads to a recipient | to:[email protected] |
subject: | Subject line contains | subject:invoice |
has:attachment | Has file attachments | has:attachment |
filename: | Attachment filename | filename:report.pdf |
after: | Threads after a date | after:2025/01/01 |
before: | Threads before a date | before:2025/02/01 |
is:unread | Unread threads | is:unread |
label: | Threads with a label | label:important |
OR | Combine conditions | from:alex OR from:priya |
When using search_query_native, you can only combine it with the in, limit, and page_token parameters. Other query parameters will return an error. See the search best practices guide for more details.
Things to know about Google threads
Section titled “Things to know about Google threads”A few provider-specific details that matter when you’re working with threads on Gmail and Google Workspace accounts.
Gmail has the most precise threading
Section titled “Gmail has the most precise threading”Gmail assigns a native thread_id to every message, and Nylas maps this directly to the thread object’s id field. Gmail’s threading algorithm uses In-Reply-To and References headers combined with subject matching to group messages. This is the most accurate threading of any provider because it’s based on Google’s own conversation logic rather than heuristic matching.
The thread_id is stable and persistent. It doesn’t change when messages are archived, labeled, or moved to trash. You can safely store it as a permanent reference to a conversation.
Labels apply to messages, not threads
Section titled “Labels apply to messages, not threads”This is the biggest conceptual difference from how threads work visually in Gmail. Labels are assigned to individual messages, but the Gmail UI shows a thread in a label’s view if any message in the thread has that label. Nylas mirrors this: the thread object’s folders array is the union of all labels across every message in the conversation.
A thread might show:
{ "folders": ["INBOX", "SENT", "UNREAD", "CATEGORY_PERSONAL", "IMPORTANT"]}If you archive a thread in Gmail, the INBOX label is removed from all messages. But the thread still exists and is accessible through other labels or by its ID.
Thread metadata is aggregated
Section titled “Thread metadata is aggregated”Like Microsoft, thread-level fields on Google are computed from all messages:
unreadistrueif any message in the thread is unreadstarredistrueif any message is starredhas_attachmentsistrueif any message has attachmentsparticipantsis the union of all senders and recipientsmessage_idslists every message in the thread
To change read/starred state on individual messages, use the Messages API with specific message IDs from the thread.
Google Workspace vs. personal Gmail
Section titled “Google Workspace vs. personal Gmail”Threading works identically on both. The only differences are administrative:
- Workspace admins can restrict third-party app access. If a Workspace user can’t authenticate, their admin may need to allow your app in the Google Admin console.
- Delegated mailboxes (shared mailboxes in Workspace) require special handling. See the shared accounts guide.
Rate limits are per-user and per-project
Section titled “Rate limits are per-user and per-project”Google enforces quotas at two levels, same as with the Messages API:
- Per-user: Each authenticated user has a daily quota for API calls
- Per-project: Your GCP project has an overall daily limit across all users
Use webhooks or Google Pub/Sub instead of polling to avoid hitting limits.
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 Google messages for message-level operations on Google accounts
- Search best practices for advanced search with
search_query_nativeand Gmail operators - Webhooks for real-time notifications instead of polling
- Google Pub/Sub for real-time sync with Gmail accounts
- Google provider guide for full Google setup including OAuth scopes and verification