Skip to content
machinemail

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

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": "Your refund",
    "text": "Hi Ada, the refund is on its way.",
    "in_reply_to": "msg_01m4fy8han0000y3ge3m73vw3w"
  }'
draft.ts
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();
draft.py
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()
FieldNotes
to, cc, bccRecipients. At most 50 across the three (deployment default), checked on create and update; addresses themselves are not checked until the draft is sent.
subject, text, htmlContent.
reply_toReply-To addresses.
labelsLabels for the message once sent.
attachmentsEach { filename, content_type, content } with base64 content. Fetching by url is only supported when sending directly.
in_reply_toThe message_id of a message in this inbox. Sending then sets In-Reply-To and References, so the message lands in that thread.
send_atISO 8601 time to send automatically.
client_idIdempotency key. The same client_id returns the existing draft with 200.
Response
{
  "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

MethodPathPermission
GET/v0/inboxes/{inbox_id}/draftsdraft_read
GET/v0/draftsdraft_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}/senddraft_send

PATCH accepts to, cc, bcc, subject, text, html, reply_to and send_at. Labels, attachments and in_reply_to are 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 at machinemail.dev/app/drafts, which lists every unsent draft ready to send or delete, or with a separate key. A draft with send_at sends automatically at that time, so leave it unset when a person should decide.

Scheduled sending

Set send_at to send the draft automatically. The draft gets send_status: "scheduled", and MachineMail sends it within about 15 seconds after send_at passes.

Scheduled draft
{
  "draft_id": "drf_01m4fy8hmj00004yb08pa27yt2",
  "subject": "Later",
  "send_status": "scheduled",
  "send_at": "2026-10-10T09:00:00.000Z"
}
send_statusMeaning
absentNot scheduled. Sent only when you call send.
scheduledWaiting for send_at.
sendingBeing sent now.
failedThe 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. A failed draft is not retried automatically.

Next