---
title: "GET /v1/mailboxes/{mailboxId}/threads/{threadId}"
method: GET
path: "/v1/mailboxes/{mailboxId}/threads/{threadId}"
---

# GET /v1/mailboxes/{mailboxId}/threads/{threadId}

`GET /v1/mailboxes/{mailboxId}/threads/{threadId}`

## Request

- The document declares no server URL.
- Auth: one of:
  - OAuth 2 (authorization code, authorization URL `/authorize`, token URL `/oauth/token`, scopes: `email.read`)
  - HTTP bearer

One thread, with both sides of the conversation — its most recent messages, and a cursor to the earlier ones. `limit` defaults to 50.

## Path parameters

- `mailboxId` string, required
- `threadId` string, required

## Query parameters

- `limit` integer
- `before` string

## Response `200`

The request has succeeded.

- ThreadDetail
  - `id` string, required: The STORED thread id — resolved identity, never an echo of the path parameter.
  - `subject` string, required: The starter's subject — the same titling rule the list uses, so a row and its detail never disagree.
  - `messages` Message[], required: The thread's most recent messages — at most `limit`, and fewer when they are large — oldest first, both directions merged. A long thread is read a page at a time (P47): every message carries its body, and the whole of a long enough conversation would pass the platform's 32 MiB limit on one response.
    - `id` string, required
    - `messageId` string, required
    - `threadId` string, required
    - `direction` 'inbound' | 'outbound', required: Both sides of the conversation. Without it an agent cannot tell what it already said from what it was told, and answers the same message twice — the characteristic failure of an autonomous replier.
    - `from` string, required
    - `fromName` string: Who the sender says they are — the `From:` display name, cleaned of control and bidirectional characters — when they gave one. Presentation only: a reply goes to `from` (or `replyTo`), never to a name. Absent on our own sends.
    - `to` string[], required: A list either way: inbound carries the one envelope recipient this copy was delivered to, outbound every address it went to. A broadcast's ONE Sent copy is addressed to its audience as an RFC 5322 §3.4 empty group — `Newsletter:;`, quoted when the name needs it — because naming every member would publish the list to anyone who can read the mailbox. A group is not an address: never reply to one.
    - `cc` string[], required: Everyone on the `Cc` header. ⛔ **Without this a client cannot offer reply-all**, which is why it is here: answering only the sender drops everybody the sender deliberately included, silently, and the reader finds out when somebody asks why they were left off. Populated in BOTH directions. It was empty on outbound until 2026-09-12, which was a gap rather than a fact about the message: a send that copied four people filed a Sent copy claiming it went to one, so the reader was told a different story from the one the recipients' own clients tell them. ⛔ **`Bcc` is absent in both directions and always will be.** Inbound, SMTP strips it before delivery, so a received message has no record of it. Outbound it is withheld on purpose: a blind copy is a promise not to name somebody, and a thread anyone granted this mailbox can read is the last place to break it.
    - `subject` string, required
    - `body` string, required: The message as readable text, capped at 32 KiB at ingest. ⚠️ **Always prose, never markup.** This used to be `text ?? html`, so an HTML-only newsletter put its own tags here and every client rendered them literally. Ingest now derives text from the HTML part when there is no text part, which is what makes this safe to snippet, to search and to hand to a model. The sender's own markup is a part — see `hasHtml`.
    - `hasHtml` boolean, required: Whether this message has an HTML body to render. A boolean rather than a URL for the same reason `Attachment` carries none, and it is here so the common case — plain-text mail, which is most mail — needs no second call to find out there is nothing to fetch. When true, `getMessageContent` returns where to render it from.
    - `attachments` Attachment[], required: Every attachment, in the order the message carried them. Empty for most mail.
      - `id` string, required: The part's IMAP section path (RFC 9051 §6.4.5) — `2`, `1.2` — which is its position in the message's MIME structure and RFC 8621's `partId`. Stable for the life of the message, because the message is kept as it arrived.
      - `filename` string, required: Bounds mirroring `domain/email/message-part.ts` — every one of these arrives in a stranger's MIME headers, so the cap is the contract, not tidiness.
      - `contentType` string, required
      - `sizeBytes` integer, required
      - `disposition` 'attachment' | 'inline', required: RFC 2183. `inline` means the message's HTML refers to this part by `cid:` and expects it rendered in place; `attachment` means it is a file to open.
      - `contentId` string, required: A `Content-ID` is a `Message-ID` by another name — RFC 5322 §2.1.1's 998-octet line.
    - `at` string, date-time, required: When it arrived, or when we sent it.
    - `sizeBytes` integer, required
    - `inReplyTo` string, nullable, required: RFC 5322 threading, as received or as we sent it — everything a caller needs to compose a reply into this conversation. Null when the message answered nothing, and on a message we sent before 2026-10-19, which kept neither.
    - `references` string, nullable, required
    - `replyTo` string, nullable, required: The sender's `Reply-To`, if any — where a reply should actually go.
    - `unsubscribe` 'one-click' | 'mailto', nullable, required: How the list this message came from may be left, when it says so in a way that is safe to act on (docs/design/INBOX-LATER-LIST.md §1): `one-click` (RFC 8058, the sender's DKIM signature covering it) or `mailto`. Null on our own messages and on mail that is no list mail. The address itself is never exposed — it is a credential to the sender's list.
  - `earlierCursor` string, nullable, required: Earlier messages exist: pass this as `before` to read the page before these. Null when `messages` reaches the start of the thread.

## Other responses

- `400`: The server could not understand the request due to invalid syntax.
- `401`: Access is unauthorized.
- `403`: Access is forbidden.
- `404`: The server cannot find the requested resource.
- `default`: An unexpected error response.

---

[API](https://skmtc.dev/foundermail/apis/mailbox.md) - [All operations](https://skmtc.dev/foundermail/apis/mailbox/llms.txt) - [OpenAPI document](https://skmtc.dev/foundermail/apis/mailbox/revisions/6a50628a86ed?raw)
