Apple doesn’t offer a public email API for iCloud Mail, let alone a threading API. The only programmatic access is raw IMAP, and even that requires each user to manually create an app-specific password. Nylas handles the IMAP connection and constructs conversation threads from message headers, exposing iCloud threads through the same Threads API you use for Gmail and Outlook.
This guide covers listing threads from iCloud accounts, including the app-specific password requirement, how threading works without native provider support, and the 90-day message cache.
Why use Nylas for threads instead of IMAP directly?
Section titled “Why use Nylas for threads instead of IMAP directly?”Building a conversation view from raw IMAP requires parsing In-Reply-To and References headers, grouping messages by conversation chain, and maintaining your own thread index. On top of that, iCloud’s biggest friction point is the app-specific password requirement, which can’t be generated programmatically. Every user must create one manually through their Apple ID settings.
Nylas wraps all of that in a REST API. The Threads API returns pre-grouped conversations with metadata, and Nylas guides users through the app password flow during authentication. Your code works across iCloud, Gmail, Outlook, Yahoo, and every other provider without modification.
Before you begin
Section titled “Before you begin”You’ll need:
- A Nylas application with a valid API key
- A grant for an iCloud Mail account
- An iCloud connector configured in your Nylas application
New to Nylas? Start with the quickstart guide to set up your app and connect a test account before continuing here.
iCloud authentication setup
Section titled “iCloud authentication setup”iCloud requires app-specific passwords for third-party email access. Unlike Google or Microsoft OAuth, there’s no way to generate these programmatically. Each user must create one manually in their Apple ID settings.
With either Hosted OAuth or BYO Authentication, users need to:
- Go to appleid.apple.com and sign in
- Navigate to Sign-In and Security then App-Specific Passwords
- Generate a new app password
- Use that password (not their regular iCloud password) when authenticating
App-specific passwords can’t be generated via API. Your app’s onboarding flow should include clear instructions telling users how to create one. Users who enter their regular iCloud password will fail authentication.
The full setup walkthrough is in the iCloud provider guide and the app passwords guide.
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. The same code works for Google, Microsoft, and Yahoo accounts.
Filter threads
Section titled “Filter threads”You can narrow results with query parameters. Here’s what works with iCloud 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 |
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, })Search with search_query_native
Section titled “Search with search_query_native”iCloud supports the search_query_native parameter for IMAP-style search. Like Yahoo and other IMAP providers, iCloud lets you combine search_query_native with any other query parameter.
curl --request GET \ --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/threads?search_query_native=subject:invoice&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, })Some IMAP providers don’t fully support the SEARCH operator. If search_query_native returns unexpected results or a 400 error, fall back to standard query parameters like subject, from, and to instead.
See the search best practices guide for more on search_query_native across providers.
Things to know about iCloud threads
Section titled “Things to know about iCloud threads”iCloud is IMAP-based with no native threading concept, which means it shares some behaviors with Yahoo and other IMAP providers. But Apple has its own quirks too.
Threading is constructed from headers
Section titled “Threading is constructed from headers”iCloud doesn’t assign a thread ID or conversation identifier to messages. Nylas builds threads by analyzing In-Reply-To and References headers, combined with subject-line matching. This works well for standard reply chains but is less precise than Gmail’s native threading.
Edge cases to be aware of:
- Forwarded messages may or may not join the original thread, depending on whether the mail client preserved the
Referencesheader - Apple Mail’s threading behavior in the UI is based on its own local algorithm that may differ slightly from what Nylas constructs server-side
- Messages without proper headers (from older clients) may not group correctly
The 90-day message cache affects threads
Section titled “The 90-day message cache affects threads”Nylas maintains a rolling cache of messages from the last 90 days for IMAP-based providers. Threads are built from cached messages, so conversations that span beyond 90 days may appear incomplete. The message_ids array only includes messages within the cache window.
To access older messages directly (not as threads), use query_imap=true on the Messages API. The Threads API does not support query_imap.
Thread metadata aggregation
Section titled “Thread metadata aggregation”Thread-level fields are computed from all cached messages in the conversation:
unreadistrueif any message in the thread is unreadstarredistrueif any message is starredhas_attachmentsistrueif any message has attachmentsparticipantsis the union of all senders and recipientsearliest_message_datereflects the oldest cached message, not necessarily the start of the conversation
Sync relies on IMAP idle
Section titled “Sync relies on IMAP idle”Nylas monitors the Inbox and Sent folders with low-bandwidth IMAP idle connections. Other folders are checked periodically. This means thread updates from Inbox activity appear quickly (within seconds), while threads with messages only in custom folders may take a few minutes to update.
iCloud connector vs. generic IMAP
Section titled “iCloud connector vs. generic IMAP”You can authenticate iCloud accounts two ways:
| Method | Provider type | What you get |
|---|---|---|
| iCloud connector | icloud | Email via IMAP plus calendar via CalDAV |
| Generic IMAP | imap | Email only, no calendar access |
Both provide full Threads API support. Use the dedicated iCloud connector if your app needs calendar access alongside email.
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 iCloud messages for message-level operations on iCloud accounts
- Search best practices for advanced search with
search_query_nativeacross providers - Webhooks for real-time notifications instead of polling
- iCloud provider guide for full iCloud setup including authentication
- App passwords guide for generating app-specific passwords