> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ruber.me/llms.txt
> Use this file to discover all available pages before exploring further.

# Threads

> Read a folder as conversations rather than as a flat list of messages.

A folder, grouped into conversations. The same messages
[`GET /v1/messages`](/api/messages#list-messages) returns, arranged the way the
web app arranges them.

**Required permission:** Messages → Read (`messages:read`)

A thread is a view over messages a key can already read, so it needs no
permission of its own.

## List conversations

```
GET https://app.ruber.me/api/v1/threads?mailbox=you@yourdomain.com
```

| Parameter |              |                                                        |
| --------- | ------------ | ------------------------------------------------------ |
| `mailbox` | **required** | An address from [`GET /v1/mailboxes`](/api/mailboxes). |
| `folder`  | optional     | Defaults to `inbox`.                                   |
| `limit`   | optional     | 1–100 **conversations**. Defaults to 25.               |
| `page`    | optional     | 0-based.                                               |

```json theme={null}
{
  "data": [
    {
      "id": "PGc4LXRyYW5zcGFyZW5jeS12NEBydWJlci5tZT4",
      "subject": "Invoice 42",
      "root_message_id": "<abc123@supplier.com>",
      "folder": "inbox",
      "message_count": 3,
      "unread": true,
      "last_message_at": "2026-09-08T13:40:27.753Z",
      "participants": ["billing@supplier.com", "you@yourdomain.com"],
      "preview": "Thanks — paid this morning.",
      "uids": [42, 43, 47]
    }
  ],
  "total": 30,
  "page": 0,
  "has_more": true
}
```

| Field             |                                                                                              |
| ----------------- | -------------------------------------------------------------------------------------------- |
| `id`              | Stable for the life of the conversation. Adding a reply does not change it.                  |
| `subject`         | The subject of the message that **started** it, not the newest.                              |
| `message_count`   | How many messages are in it.                                                                 |
| `unread`          | True when **any** message in it is unread.                                                   |
| `last_message_at` | When it was last active — what the list is sorted by.                                        |
| `participants`    | Every sender, in order of first appearance.                                                  |
| `uids`            | Every message's uid, for [`PATCH /v1/messages`](/api/messages#change-many-messages-at-once). |

`uids` is the useful one: archiving a whole conversation is this list handed
straight to the bulk endpoint.

<Note>
  **Ordering is by last activity, not by when the conversation started.** An old
  thread somebody just replied to belongs at the top, which is where it appears.
</Note>

## Read one conversation

```
GET https://app.ruber.me/api/v1/threads/{id}?mailbox=you@yourdomain.com
```

Takes the same `mailbox` (required) and `folder` parameters.

```json theme={null}
{
  "id": "PGc4LXRyYW5zcGFyZW5jeS12NEBydWJlci5tZT4",
  "subject": "Invoice 42",
  "root_message_id": "<abc123@supplier.com>",
  "folder": "inbox",
  "messages": [
    {
      "uid": 42,
      "message_id": "<abc123@supplier.com>",
      "subject": "Invoice 42",
      "from": { "name": "Accounts", "address": "billing@supplier.com" },
      "received_at": "2026-09-08T13:40:27.753Z",
      "unread": true,
      "starred": false,
      "has_attachment": true,
      "preview": "Your invoice for August is attached…"
    }
  ]
}
```

Messages come back **oldest first** — reading order — and carry headers and a
preview, not bodies. Fetch a message with
[`GET /v1/messages/{uid}`](/api/messages#get-one-message) for its text; a
thirty-message conversation with full HTML would be megabytes.

## How conversations are worked out

Messages are grouped by their `Message-ID`, `In-Reply-To` and `References`
headers, transitively — so a reply joins the conversation whether it names its
parent, the whole chain, or only part of it, and two replies to the same
message stay in one conversation rather than splitting into two.

<Warning>
  **Subject is never used to group.** A common shortcut is to merge messages
  sharing a subject after "Re:" is stripped. Ruber does not: two unrelated people
  both replying "Re: invoice" would become one conversation, and a thread that
  silently contains a stranger's mail is worse than two threads that should have
  been one.

  The practical consequence: a reply sent by a client that strips threading
  headers appears as its own conversation. That is the honest answer rather than
  a guess.
</Warning>

<Note>
  **Conversations are computed per request, from the live mailbox.** They are not
  read from a stored index, so they are never stale — a message that arrived a
  second ago is in the right conversation.

  The cost is one headers-only read of the folder per request: no bodies, no
  attachments. Threading cannot be paginated at the source, because a
  conversation's messages are scattered through a folder by arrival time and any
  window smaller than the folder produces threads missing their other halves. So
  the whole folder is grouped and `limit`/`page` apply to the **conversations**.
</Note>

<Warning>
  **A thread `id` is only meaningful inside the folder it came from.** Like a
  `uid`, it describes a grouping of one folder's messages rather than an object
  the server stores. Carry the `mailbox` and `folder` alongside it, and expect an
  id to stop resolving once the conversation is moved elsewhere.
</Warning>

## Errors

| Code               | HTTP  | Cause                                                                                  |
| ------------------ | ----- | -------------------------------------------------------------------------------------- |
| `invalid_request`  | `400` | Missing `mailbox`, an unknown `folder`, a `limit` outside 1–100, or a Private mailbox. |
| `unauthorized`     | `401` | Missing, malformed, unknown or revoked key.                                            |
| `forbidden`        | `403` | The key lacks `messages:read`.                                                         |
| `not_found`        | `404` | No such mailbox, or no conversation with that id in that folder.                       |
| `rate_limited`     | `429` | Over 600 requests in a minute.                                                         |
| `mail_unavailable` | `502` | The mail host did not answer. Transient; retry with backoff.                           |

See [Errors](/api/introduction#errors) for the full response shape.
