---
title: "Threads and messages"
description: "How MachineMail groups messages into threads, labels and read state, what extracted_text and the trust report give your agent, and attachments and raw MIME."
canonical: "https://machinemail.dev/docs/concepts/threads-and-messages"
---

# Threads and messages

> How MachineMail groups messages into threads, labels and read state, what extracted_text and the trust report give your agent, and attachments and raw MIME.

Source: https://machinemail.dev/docs/concepts/threads-and-messages

A **message** is one email, inbound or outbound. A **thread** is a conversation: the messages in one inbox that reply to each other. Every message belongs to exactly one thread, and every thread belongs to exactly one inbox.

## Messages

Inbound messages carry the full parsed email plus a [trust report](https://machinemail.dev/docs/trust/trust-report). This is an inbound reply as returned by `GET /v0/inboxes/{inbox_id}/messages/{message_id}`, trimmed:

```json title="Message"
{
  "message_id": "msg_01m4fy8han0000y3ge3m73vw3w",
  "inbox_id": "support-agent@machinemail.dev",
  "thread_id": "thd_01m4fy8h340001pp6c74x4688h",
  "pod_id": "pod_01m4fy8h150000w4xty1ehfnqa",
  "labels": ["INBOX", "UNREAD"],
  "timestamp": "2026-10-08T14:03:11.000Z",
  "from": "ada@example.com",
  "to": ["support-agent@machinemail.dev"],
  "subject": "Re: Hello",
  "preview": "Thanks! Your verification code is 482913.\n\nOn Thu, Oct 8, 2026 at 2:00 PM Support agent wrote:\n> Sent by an agent.",
  "attachments": [],
  "in_reply_to": "msg_01m4fy8h340000kzb2kp66c161@machinemail.dev",
  "size": 376,
  "direction": "inbound",
  "trust_level": "unknown",
  "text": "Thanks! Your verification code is 482913.\n\nOn Thu, Oct 8, 2026 at 2:00 PM Support agent wrote:\n> Sent by an agent.\n",
  "extracted_text": "Thanks! Your verification code is 482913.",
  "trust": {
    "level": "unknown",
    "score": 0.7,
    "content": { "text": "Thanks! Your verification code is 482913.", "...": "..." },
    "classification": { "category": "otp", "...": "..." },
    "reasons": ["Unknown: no authentication passed", "..."]
  },
  "created_at": "2026-10-09T09:00:16.085Z",
  "updated_at": "2026-10-09T09:00:16.085Z"
}
```

| Field                              | Notes                                                                                                           |
| ---------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `message_id`                       | `msg_` prefix. MachineMail's ID, not the RFC 5322 `Message-ID` header.                                          |
| `timestamp`                        | The `Date` header for inbound mail (arrival time if it is missing or invalid); send time for outbound.          |
| `from`                             | One string. `to`, `cc`, `bcc` and `reply_to` are arrays.                                                        |
| `preview`                          | Short plain-text snippet of the body.                                                                           |
| `text`, `html`                     | The bodies as sent. &#x2A;*Untrusted.**                                                                         |
| `extracted_text`, `extracted_html` | Inbound only: the new part of the message, with quoted history and signatures stripped.                         |
| `in_reply_to`, `references`        | Threading headers, without angle brackets.                                                                      |
| `headers`                          | Message headers as a map.                                                                                       |
| `size`                             | Bytes of the raw message.                                                                                       |
| `direction`                        | `inbound` or `outbound`.                                                                                        |
| `delivery_status`                  | Outbound only: `sent`, `delivered`, `bounced` or `complained`.                                                  |
| `trust_level`                      | Inbound only: the trust report's `level`, handy in lists.                                                       |
| `trust`                            | Inbound only: the full [trust report](https://machinemail.dev/docs/trust/trust-report).                         |
| `authentication_results`           | Inbound only: SPF, DKIM and DMARC verdicts with the domain each is about. See [below](#authentication-results). |
| `received_via`                     | `raw` when the message was injected with `POST /inbound/raw`.                                                   |

Lists return the short form (`MessageItem`): everything except `text`, `html`, `extracted_*`, `reply_to`, `received_via` and `trust`. `trust_level` and `authentication_results` are in both. Fetch the message to get bodies and the trust report.

### Authentication results

`authentication_results` is AgentMail's field of the same name: the SPF, DKIM and DMARC results recorded when the message reached MachineMail, in the vocabulary of the `Authentication-Results` header (`pass`, `fail`, `none`, `neutral`, `softfail`, `temperror`, `permerror`), each with the domain it is about:

```json title="authentication_results"
{
  "spf": "pass",
  "spf_domain": "mail.linear.app",
  "dkim": "pass",
  "dkim_domain": "linear.app",
  "dmarc": "pass",
  "dmarc_domain": "linear.app"
}
```

A mechanism that was not evaluated is left out, and the whole field is absent on sent mail, on mail injected with `POST /inbound/raw`, and when nothing was evaluated. As in AgentMail, only verdicts that speak for the sender are kept: SPF only when its domain aligns with the `From` domain, DKIM and DMARC only when `From` names one domain. The verdicts are not collapsed: to decide whether to trust a message, read `trust.level`. See [Authentication](https://machinemail.dev/docs/trust/authentication#authentication_results-on-the-message).

## What to give your agent

There are three versions of an inbound body. Pick deliberately:

1. **`trust.content.text`** is the agent-safe body: quoted history, signatures, disclaimers, hidden text, zero-width characters and tracking pixels removed. Give this to the model by default.
2. **`extracted_text`** is the reply-parsed body: the new text without quoted history or signatures, but otherwise as written. Hidden text tricks are not removed.
3. **`text`*&#x2A; and &#x2A;*`html`** are the raw bodies. Never put them in a prompt unchecked.

Also check `trust.level` before acting on what a message asks. See [Using trust in agents](https://machinemail.dev/docs/trust/using-trust-in-agents).

## Threads

MachineMail threads messages by their headers. When a message arrives or is sent, its `In-Reply-To` and `References` are matched against the messages already in the same inbox, by RFC 5322 `Message-ID` and by MachineMail message IDs. A match puts it in that thread; otherwise it starts a new one. Subjects are not used for threading.

Replies sent with the reply endpoints set these headers for you, so the conversation stays in one thread on both sides. See [Reply in thread](https://machinemail.dev/docs/guides/reply-in-thread).

```json title="Thread"
{
  "thread_id": "thd_01m4fy8h340001pp6c74x4688h",
  "inbox_id": "support-agent@machinemail.dev",
  "pod_id": "pod_01m4fy8h150000w4xty1ehfnqa",
  "labels": ["SENT", "INBOX", "UNREAD"],
  "timestamp": "2026-10-09T09:00:15.844Z",
  "received_timestamp": "2026-10-08T14:03:11.000Z",
  "sent_timestamp": "2026-10-09T09:00:15.844Z",
  "senders": ["support-agent@machinemail.dev", "ada@example.com"],
  "recipients": ["ada@example.com", "support-agent@machinemail.dev"],
  "subject": "Hello",
  "preview": "Sent by an agent.",
  "attachments": [],
  "last_message_id": "msg_01m4fy8h340000kzb2kp66c161",
  "message_count": 2,
  "size": 681,
  "created_at": "2026-10-09T09:00:15.970Z",
  "updated_at": "2026-10-09T09:00:16.105Z"
}
```

A thread's `labels` are the union of its messages' labels. `timestamp`, `preview` and `last_message_id` follow the latest message. `received_timestamp` and `sent_timestamp` are the latest inbound and outbound times.

`GET /v0/threads/{thread_id}` returns the thread plus a `messages` array with full messages, oldest first, including trust reports.

| Method   | Path                             | Notes                                                                          |
| -------- | -------------------------------- | ------------------------------------------------------------------------------ |
| `GET`    | `/v0/threads`                    | Threads across every inbox in scope.                                           |
| `GET`    | `/v0/inboxes/{inbox_id}/threads` | Threads in one inbox.                                                          |
| `GET`    | `/v0/threads/{thread_id}`        | Thread with messages. Also under `/v0/inboxes/{inbox_id}/threads/{thread_id}`. |
| `PATCH`  | `/v0/threads/{thread_id}`        | `add_labels` and `remove_labels`, applied to every message in the thread.      |
| `DELETE` | `/v0/threads/{thread_id}`        | Deletes the thread and all its messages.                                       |
| `GET`    | `/v0/threads/search?q=`          | Full-text search, returns threads. Also per inbox.                             |

## Labels

Labels are free strings on each message. MachineMail sets these system labels:

| Label             | Set when                                                                                                                                                                            |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `INBOX`           | An inbound message is accepted normally.                                                                                                                                            |
| `UNREAD`          | Every new inbound message. Remove it to mark the message read.                                                                                                                      |
| `SENT`            | Every outbound message.                                                                                                                                                             |
| `QUARANTINE`      | The trust layer quarantined the message. Replaces `INBOX`.                                                                                                                          |
| `SPAM`            | The spam probability is 0.9 or higher. Replaces `INBOX`.                                                                                                                            |
| `UNAUTHENTICATED` | DMARC failed, or SPF and DKIM both failed. Added next to `INBOX`.                                                                                                                   |
| `BLOCKED`         | The sender matched a receive block list. The only label.                                                                                                                            |
| `TRUSTED`         | The sender is one of your [trusted domains](https://machinemail.dev/docs/concepts/trusted-domains) and the message carries that domain's own DKIM signature. Added next to `INBOX`. |

`TRASH` is honoured as a hidden label but never set by MachineMail; deleting is permanent. Add your own labels freely, for example `needs-human` or `handled`.

### Hidden by default

List and thread queries leave out messages labelled `SPAM`, `BLOCKED`, `TRASH`, `QUARANTINE` or `UNAUTHENTICATED`. Ask for them explicitly with `include_spam`, `include_blocked`, `include_trash`, `include_quarantine` or `include_unauthenticated` set to `true`, or by filtering on the label itself (`labels=QUARANTINE`). Spam, blocked, unauthenticated and trash also need the matching `label_*_read` permission on restricted keys.

### Read state and other label changes

```bash title="Terminal"
curl -X PATCH https://api.machinemail.dev/v0/inboxes/support-agent@machinemail.dev/messages/msg_01m4fy8han0000y3ge3m73vw3w \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"remove_labels": ["UNREAD"], "add_labels": ["handled"]}'
```

The response is the updated message. To change many at once, `POST /v0/inboxes/{inbox_id}/messages/batch-update` takes `message_ids` (up to 100), `add_labels` and `remove_labels`.

## Listing and filtering messages

`GET /v0/inboxes/{inbox_id}/messages` and the thread list endpoints accept:

| Parameter                                                                                           | Notes                                                                       |
| --------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| `limit`, `page_token`                                                                               | Default 50, maximum 500.                                                    |
| `ascending`                                                                                         | `true` for oldest first. Default is newest first.                           |
| `labels`                                                                                            | Only messages with any of these labels. Repeat the parameter or use commas. |
| `before`, `after`                                                                                   | ISO 8601 timestamps.                                                        |
| `include_spam`, `include_blocked`, `include_trash`, `include_quarantine`, `include_unauthenticated` | Include hidden messages.                                                    |
| `q`                                                                                                 | Thread lists only: full-text filter.                                        |

`POST /v0/inboxes/{inbox_id}/messages/batch-get` with `{ "message_ids": [...] }` returns up to 100 full messages in one call. Search is covered in [Search and export](https://machinemail.dev/docs/guides/search-and-export).

## Attachments

Each message lists its attachments with `attachment_id`, `filename`, `size`, `content_type`, `content_disposition` and `content_id`. Inbound attachments are also risk-scored in `trust.attachments`.

- `GET /v0/inboxes/{inbox_id}/messages/{message_id}/attachments/{attachment_id}` returns the metadata plus `download_url` and `expires_at`. Depending on storage, `download_url` is a short-lived signed URL or an API URL that needs your Bearer key.
- `GET .../attachments/{attachment_id}/download` returns the bytes directly.

See the [attachments guide](https://machinemail.dev/docs/guides/attachments).

## Raw MIME

`GET /v0/inboxes/{inbox_id}/messages/{message_id}/raw` returns the original message as `message/rfc822`. Use it for archiving or your own parsing, not as model input.

## Deleting

`DELETE /v0/inboxes/{inbox_id}/messages/{message_id}` removes the message, its raw MIME and attachments, and rebuilds the thread. A thread with no messages left is removed.

## Next

- [Sending](https://machinemail.dev/docs/concepts/sending)
- [Trust report](https://machinemail.dev/docs/trust/trust-report)
- [Reply in thread](https://machinemail.dev/docs/guides/reply-in-thread)
