Skip to content
machinemail

Reply in thread

Reply, reply-all and forward from an inbox, keep conversations threaded with In-Reply-To and References, prepare replies as drafts, and label whole threads.

Email threading depends on two headers: In-Reply-To names the message being answered, and References lists the conversation so far. MachineMail sets both for you when you use the reply endpoints, and uses them to group inbound mail into threads.

Reply

Terminal
curl https://api.machinemail.dev/v0/inboxes/support-agent@machinemail.dev/messages/msg_01m4fy8han0000y3ge3m73vw3w/reply \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text": "Got it, thanks.", "html": "<p>Got it, thanks.</p>"}'
Response (201)
{
  "message_id": "msg_01m4fy8hbz0000k3m19n91j9va",
  "thread_id": "thd_01m4fy8h340001pp6c74x4688h",
  "timestamp": "2026-10-09T09:00:16.127Z"
}

What the endpoint does:

  • Recipients. For an inbound message it replies to the message’s reply_to addresses if present, otherwise to from. For one of your own sent messages it goes to that message’s to. Pass to to override.
  • Subject. Re: plus the original subject, unless it already starts with Re:. A subject in the body is ignored.
  • Headers. In-Reply-To is the original’s Message-ID, and References is the original’s references plus that ID.
  • Thread. The reply joins the original’s thread, so thread_id matches.

The body accepts the same fields as a send except subject: text, html, to, cc, bcc, reply_to, labels, attachments and headers. See Sending.

The key needs message_reply or message_send.

reply.ts
const inbox = 'support-agent@machinemail.dev';
const res = await fetch(`https://api.machinemail.dev/v0/inboxes/${inbox}/messages/${messageId}/reply`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.MACHINEMAIL_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ text: 'Got it, thanks.' }),
});
const { message_id, thread_id } = await res.json();
reply.py
import os, requests

inbox = "support-agent@machinemail.dev"
res = requests.post(
    f"https://api.machinemail.dev/v0/inboxes/{inbox}/messages/{message_id}/reply",
    headers={"Authorization": f"Bearer {os.environ['MACHINEMAIL_API_KEY']}"},
    json={"text": "Got it, thanks."},
)
sent = res.json()

Reply all

POST .../messages/{message_id}/reply-all sends to the original from and to, with the original cc copied, all minus your own inbox address.

Terminal
curl https://api.machinemail.dev/v0/inboxes/support-agent@machinemail.dev/messages/msg_01m4fy8han0000y3ge3m73vw3w/reply-all \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text": "Looping everyone in."}'

Note

Reply-all computes to and cc from the original message and ignores to and cc in the request. You can still add bcc. To choose recipients yourself, use reply with to and cc.

Forward

POST .../messages/{message_id}/forward requires to. The subject defaults to Fwd: plus the original subject; pass subject to change it. Your text comes first, followed by a forwarded-message header (From, Date, Subject, To) and the original plain-text body.

Terminal
curl https://api.machinemail.dev/v0/inboxes/support-agent@machinemail.dev/messages/msg_01m4fy8han0000y3ge3m73vw3w/forward \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"to": ["billing@acme.dev"], "text": "Can you handle this one?"}'

A forward starts a new thread. The original’s attachments are not copied; pass attachments if the recipient needs them. See Attachments.

Warning

Forwarding passes untrusted content to someone else. If the original is suspicious, forward it to a human reviewer only, never to an address the message itself asked for.

How threading works

Every message you send gets a Message-ID of the form <msg_id@inbox-domain>, for example <msg_01m4fy8h340000kzb2kp66c161@machinemail.dev>.

When mail arrives, MachineMail looks at its In-Reply-To and References headers and searches the same inbox for a message with a matching Message-ID, or for one of its own msg_ IDs. If it finds one, the new message joins that thread. Otherwise it starts a new thread.

So an inbound reply to your message lands in your thread, and the message records the link:

Inbound reply (trimmed)
{
  "message_id": "msg_01m4fy8han0000y3ge3m73vw3w",
  "thread_id": "thd_01m4fy8h340001pp6c74x4688h",
  "from": "ada@example.com",
  "subject": "Re: Hello",
  "in_reply_to": "msg_01m4fy8h340000kzb2kp66c161@machinemail.dev",
  "direction": "inbound"
}

Threads are per inbox. If two of your inboxes receive the same conversation, each has its own thread.

Threading a plain send

If you need to answer a message without the reply endpoint, for example a message you only know by its RFC Message-ID, set the headers on a normal send. MachineMail resolves the thread from them the same way:

Terminal
curl https://api.machinemail.dev/v0/inboxes/support-agent@machinemail.dev/messages/send \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": ["ada@example.com"],
    "subject": "Re: Hello",
    "text": "Following up.",
    "headers": {
      "In-Reply-To": "<abc123@example.com>",
      "References": "<abc123@example.com>"
    }
  }'

headers is a map of extra header names to values and is passed through to the outgoing message.

Reply as a draft

To have a person review a reply before it goes out, create a draft with in_reply_to set to the MachineMail message_id you are answering. When the draft is sent it gets the right In-Reply-To and References headers and joins the thread.

Terminal
curl https://api.machinemail.dev/v0/inboxes/support-agent@machinemail.dev/drafts \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": ["ada@example.com"],
    "subject": "Re: Hello",
    "text": "Here is the refund you asked for.",
    "in_reply_to": "msg_01m4fy8han0000y3ge3m73vw3w"
  }'

Drafts do not set the subject or recipients for you, so fill in to and subject yourself. Send it with POST /v0/inboxes/{inbox_id}/drafts/{draft_id}/send, or set send_at to schedule it. Every unsent draft is listed in the dashboard at /app/drafts, where a person can send or delete it. See Drafts.

Read and label a whole thread

GET /v0/threads/{thread_id} returns the thread summary and every message in it, oldest first. The same thread is also at /v0/inboxes/{inbox_id}/threads/{thread_id}.

Terminal
curl https://api.machinemail.dev/v0/threads/thd_01m4fy8h340001pp6c74x4688h \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
Response (trimmed)
{
  "thread_id": "thd_01m4fy8h340001pp6c74x4688h",
  "inbox_id": "support-agent@machinemail.dev",
  "labels": ["SENT", "INBOX", "UNREAD"],
  "senders": ["support-agent@machinemail.dev", "ada@example.com"],
  "recipients": ["ada@example.com", "support-agent@machinemail.dev"],
  "subject": "Hello",
  "last_message_id": "msg_01m4fy8hbz0000k3m19n91j9va",
  "message_count": 3,
  "messages": [{ "message_id": "msg_01m4fy8han0000y3ge3m73vw3w" }, { "message_id": "msg_01m4fy8h340000kzb2kp66c161" }]
}

Thread labels are the union of the labels on its messages. PATCH /v0/threads/{thread_id} applies add_labels and remove_labels to every message in the thread, which is how you mark a whole conversation read:

Terminal
curl -X PATCH https://api.machinemail.dev/v0/threads/thd_01m4fy8h340001pp6c74x4688h \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"remove_labels": ["UNREAD"], "add_labels": ["resolved"]}'

Changing thread labels needs message_update. DELETE /v0/threads/{thread_id} deletes every message in the thread and needs thread_delete.