Skip to content
machinemail

Threads and messages

How MachineMail stores email as messages grouped into threads, how labels and read state work, what extracted_text and the trust report give your agent, and how to fetch attachments and raw MIME.

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. This is an inbound reply as returned by GET /v0/inboxes/{inbox_id}/messages/{message_id}, trimmed:

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"
}
FieldNotes
message_idmsg_ prefix. MachineMail’s ID, not the RFC 5322 Message-ID header.
timestampThe Date header for inbound mail (arrival time if it is missing or invalid); send time for outbound.
fromOne string. to, cc, bcc and reply_to are arrays.
previewShort plain-text snippet of the body.
text, htmlThe bodies as sent. Untrusted.
extracted_text, extracted_htmlInbound only: the new part of the message, with quoted history and signatures stripped.
in_reply_to, referencesThreading headers, without angle brackets.
headersMessage headers as a map.
sizeBytes of the raw message.
directioninbound or outbound.
delivery_statusOutbound only: sent, delivered, bounced or complained.
trust_levelInbound only: the trust report’s level, handy in lists.
trustInbound only: the full trust report.
received_viaraw 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 is in both. Fetch the message to get bodies and the trust report.

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

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.

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.

MethodPathNotes
GET/v0/threadsThreads across every inbox in scope.
GET/v0/inboxes/{inbox_id}/threadsThreads 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:

LabelSet when
INBOXAn inbound message is accepted normally.
UNREADEvery new inbound message. Remove it to mark the message read.
SENTEvery outbound message.
QUARANTINEThe trust layer quarantined the message. Replaces INBOX.
SPAMThe spam probability is 0.9 or higher. Replaces INBOX.
UNAUTHENTICATEDDMARC failed, or SPF and DKIM both failed. Added next to INBOX.
BLOCKEDThe sender matched a receive block list. The only label.

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

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:

ParameterNotes
limit, page_tokenDefault 50, maximum 500.
ascendingtrue for oldest first. Default is newest first.
labelsOnly messages with any of these labels. Repeat the parameter or use commas.
before, afterISO 8601 timestamps.
include_spam, include_blocked, include_trash, include_quarantine, include_unauthenticatedInclude hidden messages.
qThread 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.

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.

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