# Get iCloud email threads with Nylas

Source: https://developer.nylas.com/docs/cookbook/email/threads/list-threads-icloud/

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](/docs/reference/api/threads/) 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.

## How do I list iCloud email threads with the Nylas API?

Send a `GET` request to `/v3/grants/{grant_id}/threads` using the iCloud connector and your API key. The endpoint returns the most recent threads by default and up to 200 per page. iCloud has no native threading, so Nylas builds threads from `In-Reply-To` and `References` headers. See [List threads](#list-threads) for the request and response.

## 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

You'll need:

- A [Nylas application](/docs/v3/getting-started/) with a valid API key
- A [grant](/docs/v3/auth/) for an iCloud Mail account
- An iCloud connector configured in your Nylas application


> **Info:** 
> **New to Nylas?** Start with the [quickstart guide](/docs/v3/getting-started/) to set up your app and connect a test account before continuing here.


### 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:

1. Go to [appleid.apple.com](https://appleid.apple.com/) and sign in
2. Navigate to **Sign-In and Security** then **App-Specific Passwords**
3. Generate a new app password
4. Use that password (not their regular iCloud password) when authenticating

> **Warn:** 
> **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](/docs/provider-guides/icloud/) and the [app passwords guide](/docs/provider-guides/app-passwords/).

## List threads

Make a [List Threads request](/docs/reference/api/threads/get-threads/) with the grant ID. By default, Nylas returns the most recent threads. These examples limit results to 5:

```bash
curl --compressed --request GET \
  --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/threads?limit=5" \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>' \
  --header 'Content-Type: application/json'

```

```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",
            "email": "nyla@example.com"
          }
        ],
        "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": [
          {
            "email": "nyla@example.com"
          }
        ],
        "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": [
        {
          "email": "nylas@nylas.com"
        }
      ],
      "snippet": "Send Email with Nylas APIs",
      "subject": "Learn how to Send Email with Nylas APIs",
      "message_ids": ["<MESSAGE_ID>"]
    }
  ],
  "next_cursor": "123"
}


```

```js [listThreads-Node.js SDK]

import Nylas from "nylas";

const nylas = new Nylas({
  apiKey: "<NYLAS_API_KEY>",
  apiUri: "<NYLAS_API_URI>",
});

async function fetchRecentThreads() {
  try {
    const identifier = "<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();


```

```python [listThreads-Python SDK]

from nylas import Client

nylas = Client(
    "<NYLAS_API_KEY>",
    "<NYLAS_API_URI>"
)

grant_id = "<NYLAS_GRANT_ID>"

threads = nylas.threads.list(
  grant_id,
  query_params={
    "limit": 5
  }
)

print(threads)

```


```ruby [listThreads-Ruby SDK]
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(', ')}"
}
```

```java [listThreads-Java SDK]
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());
    }
  }
}
```

```kt [listThreads-Kotlin SDK]
import com.nylas.NylasClient
import 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.

## List iCloud threads from the terminal

Apple offers no threading API for iCloud Mail, so conversations come from message headers rather than a native thread field. The [Nylas CLI](https://cli.nylas.com/docs/commands) presents them as threads anyway: once an app-specific password connects the account, `nylas email threads list` groups each exchange into one row.


The Nylas CLI groups messages into conversations the same way the Threads API does. After `nylas init` and `nylas auth login`, `email threads list` returns the 10 most recent threads by default, each collapsing a full back-and-forth into a single row:

```bash
# List the 10 most recent threads
nylas email threads list

# Only unread threads, filtered by subject
nylas email threads list --unread --subject "invoice"

# Open one thread to see every message in the conversation
nylas email threads show <thread-id>
```

Thread search works differently from message search: it filters by field rather than free text, so you match on `--subject`, `--from`, or `--unread` instead of passing a bare query string:

```bash
# Find threads by subject and sender
nylas email threads search --subject "contract renewal" --from "legal@vendor.com"
```

Both commands accept `--json` for scripting. See the [`email threads list`](https://cli.nylas.com/docs/commands/email-threads-list) and [`email threads search`](https://cli.nylas.com/docs/commands/email-threads-search) command reference for every flag.


Header-based threading means a reply that drops the `References` chain can land outside its conversation, the same tradeoff as any IMAP account. iCloud keeps roughly 90 days of mail searchable, and [`nylas email threads show`](https://cli.nylas.com/docs/commands/email-threads-show) reads a single conversation end to end.

## 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              | `?from=alex@example.com`      |
| `to`              | Filter by recipient           | `?to=team@company.com`        |
| `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:


```bash
curl --request GET \
  --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/threads?from=alex@example.com&unread=true&limit=10" \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>'
```

```js [filterThreads-Node.js SDK]
const threads = await nylas.threads.list({
  identifier: grantId,
  queryParams: {
    from: "alex@example.com",
    unread: true,
    limit: 10,
  },
});
```

```python [filterThreads-Python SDK]
threads = nylas.threads.list(
    grant_id,
    query_params={
        "from": "alex@example.com",
        "unread": True,
        "limit": 10,
    }
)
```


### 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.

```bash
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>'
```

```js [nativeSearchIcloudThreads-Node.js SDK]
const threads = await nylas.threads.list({
  identifier: grantId,
  queryParams: {
    searchQueryNative: "subject:invoice",
    limit: 10,
  },
});
```

```python [nativeSearchIcloudThreads-Python SDK]
threads = nylas.threads.list(
    grant_id,
    query_params={
        "search_query_native": "subject:invoice",
        "limit": 10,
    }
)
```

> **Warn:** 
> **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](/docs/dev-guide/best-practices/search/) for more on `search_query_native` across providers.

## 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

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 `References` header
- **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


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


Thread-level fields are computed from all cached messages in the conversation:

- `unread` is `true` if any message in the thread is unread
- `starred` is `true` if any message is starred
- `has_attachments` is `true` if any message has attachments
- `participants` is the union of all senders and recipients
- `earliest_message_date` reflects the oldest cached message, not necessarily the start of the conversation


### 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

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


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:

```bash
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>'
```

```js [paginateThreads-Node.js SDK]
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);
```

```python [paginateThreads-Python SDK]
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:
        break
```

Keep paginating until the response comes back without a `next_cursor`.


## What's next

- [Threads API reference](/docs/reference/api/threads/) for full endpoint documentation and all available parameters
- [Using the Threads API](/docs/v3/email/threads/) for thread concepts and additional operations
- [Messages API reference](/docs/reference/api/messages/) to fetch individual message content from threads
- [List iCloud messages](/docs/cookbook/email/messages/list-messages-icloud/) for message-level operations on iCloud accounts
- [Search best practices](/docs/dev-guide/best-practices/search/) for advanced search with `search_query_native` across providers
- [Webhooks](/docs/v3/notifications/) for real-time notifications instead of polling
- [iCloud provider guide](/docs/provider-guides/icloud/) for full iCloud setup including authentication
- [App passwords guide](/docs/provider-guides/app-passwords/) for generating app-specific passwords