---
title: "Drafts"
description: "Drafts let an agent prepare a message without sending it, for human review or for scheduled sending with send_at."
canonical: "https://machinemail.dev/docs/concepts/drafts"
---

# Drafts

> Drafts let an agent prepare a message without sending it, for human review or for scheduled sending with send_at.

Source: https://machinemail.dev/docs/concepts/drafts

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**

```bash title="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"
  }'
```

**TypeScript**

```ts title="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();
```

**Python**

```python title="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()
```

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

```json title="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

| 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 at [machinemail.dev/app/drafts](https://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. [Approve drafts before they send](https://machinemail.dev/docs/guides/approve-drafts) walks the reviewer through 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.

```json title="Scheduled draft"
{
  "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.

Creating or updating a draft with `send_at` can answer `503` with `error.type: "unavailable"` if the scheduler is briefly unavailable. Nothing was saved: retry with backoff.

A scheduled draft can go out later than `send_at`:

- If the organization has used up its daily or monthly sending allowance, the draft stays `scheduled` and goes out when the allowance resets, or as soon as the plan changes.
- If the email service cannot take the message for now (rate limits, a pause or an outage), the draft stays `scheduled` and MachineMail tries again with growing gaps, from a minute up to an hour, for about a day. If it still cannot send, the draft becomes `failed`.

## Next

- [Sending](https://machinemail.dev/docs/concepts/sending)
- [Webhooks](https://machinemail.dev/docs/concepts/webhooks)
