drafts.md
Drafts
Drafts let an agent prepare a message without sending it, for human review or for scheduled sending with send_at.
A draft is an unsent message stored in an inbox. Use drafts when an agent should prepare a reply but a person (or another check) should review it first, or when a message should go out later.
Sending a draft turns it into a normal outbound message, deletes the draft and emits draft.sent.
Creating a draft
curl
curl https://api.machinemail.dev/v0/inboxes/support-agent@machinemail.dev/drafts \
-H "Authorization: Bearer $MACHINEMAIL_API_KEY" \
-H "Content-Type: application/json" \
-d '{\n "to": ["ada@example.com"],\n "subject": "Your refund",\n "text": "Hi Ada, the refund is on its way.",\n "in_reply_to": "msg_01m4fy8han0000y3ge3m73vw3w"\n }'
TypeScript
const inbox = encodeURIComponent('support-agent@machinemail.dev');
const res = await fetch(`https://api.machinemail.dev/v0/inboxes/${inbox}/drafts`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.MACHINEMAIL_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
to: ['ada@example.com'],
subject: 'Your refund',
text: 'Hi Ada, the refund is on its way.',
in_reply_to: 'msg_01m4fy8han0000y3ge3m73vw3w',
}),
});
const draft = await res.json();
Python
import os, requests
draft = requests.post(
"https://api.machinemail.dev/v0/inboxes/support-agent@machinemail.dev/drafts",
headers={"Authorization": f"Bearer {os.environ['MACHINEMAIL_API_KEY']}"},
json={
"to": ["ada@example.com"],
"subject": "Your refund",
"text": "Hi Ada, the refund is on its way.",
"in_reply_to": "msg_01m4fy8han0000y3ge3m73vw3w",
},
).json()
| Field | Notes |
|---|---|
to, cc, bcc |
Recipients. At most 50 across the three, checked on create and update; addresses themselves are not checked until the draft is sent. |
subject, text, html |
Content. |
reply_to |
Reply-To addresses. |
labels |
Labels for the message once sent. |
attachments |
Each { filename, content_type, content } with base64 content, or { url }, fetched when the draft is sent (its size is 0 until then). |
in_reply_to |
The message_id of a message in this inbox. Sending then sets In-Reply-To and References, so the message lands in that thread. |
send_at |
ISO 8601 time to send automatically. |
client_id |
Idempotency key. The same client_id returns the existing draft with 200. |
{
"draft_id": "drf_01m4fy8hmj00004yb08pa27yt2",
"inbox_id": "support-agent@machinemail.dev",
"pod_id": "pod_01m4fy8h150000w4xty1ehfnqa",
"labels": [],
"to": ["ada@example.com"],
"subject": "Your refund",
"preview": "Hi Ada, the refund is on its way.",
"text": "Hi Ada, the refund is on its way.",
"in_reply_to": "msg_01m4fy8han0000y3ge3m73vw3w",
"references": ["abc123@example.com"],
"created_at": "2026-10-09T09:00:16.402Z",
"updated_at": "2026-10-09T09:00:16.402Z"
}
Reviewing, editing and sending
| Method | Path | Permission |
|---|---|---|
GET |
/v0/inboxes/{inbox_id}/drafts |
draft_read |
GET |
/v0/drafts |
draft_read (all inboxes in scope) |
GET |
/v0/inboxes/{inbox_id}/drafts/{draft_id} |
draft_read |
GET |
/v0/drafts/{draft_id} |
draft_read |
PATCH |
/v0/inboxes/{inbox_id}/drafts/{draft_id} |
draft_update |
DELETE |
/v0/inboxes/{inbox_id}/drafts/{draft_id} |
draft_delete |
POST |
/v0/inboxes/{inbox_id}/drafts/{draft_id}/send |
draft_send |
PATCH accepts to, cc, bcc, subject, text, html, reply_to and send_at, plus add_labels / remove_labels and add_attachments / remove_attachments (attachment IDs). in_reply_to is fixed at creation.
POST .../send sends immediately and returns the same { message_id, thread_id, timestamp } as a normal send. The draft is deleted.
A human-in-the-loop setup gives the agent a key with draft_create but not message_send, message_reply or draft_send, and lets a person review drafts in the dashboard.
Scheduled sending
Set send_at to send the draft automatically. The draft gets send_status: "scheduled", and MachineMail sends it within a few seconds after send_at passes.
{
"draft_id": "drf_01m4fy8hmj00004yb08pa27yt2",
"subject": "Later",
"send_status": "scheduled",
"send_at": "2026-10-10T09:00:00.000Z"
}
send_status |
Meaning |
|---|---|
| absent | Not scheduled. Sent only when you call send. |
scheduled |
Waiting for send_at. |
sending |
Being sent now. |
failed |
The send failed, for example because a recipient is suppressed. The draft is kept so you can fix and resend it. |
To cancel a schedule, PATCH with "send_at": null. To reschedule, PATCH a new send_at. An edit or a reschedule that lands while the draft is about to go out wins: the draft goes out as edited, or at the new time. A failed draft is not retried automatically.