# Creating a draft

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

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

```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();
```

### draft.py

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

Response

```json
{
  "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](/content/app/drafts/index.html), 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](/content/docs/guides/approve-drafts/index.html) 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.

### Scheduled draft

```json
{
  "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`.
