# Drafts and scheduled send

Source: https://agentboxd.com/docs/drafts

> Let an agent write email that a person approves before it goes out, and schedule sends for later with send_at.

A **draft** is an email an agent (or you) wrote that hasn’t been sent. It waits in the inbox’s **Drafts** tab in the [dashboard](https://agentboxd.com/app/inboxes) until someone edits it, approves it, schedules it or cancels it. Use drafts for a human in the loop: the agent can write, but only a person, or a supervising agent with `messages:send`, can send.

POST /v1/inboxes/:id/drafts:

```bash
curl -s -X POST https://api.agentboxd.com/v1/inboxes/$INBOX_ID/drafts \
  -H "Authorization: Bearer $AGENTBOXD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reply_to_message_id":"'$MESSAGE_ID'","text":"Hi Dana, the duplicate charge is refunded."}'

# approve it
curl -s -X POST https://api.agentboxd.com/v1/inboxes/$INBOX_ID/drafts/$DRAFT_ID/send \
  -H "Authorization: Bearer $AGENTBOXD_API_KEY"
```

## Statuses

| Status | Means | You can |
| --- | --- | --- |
| `draft` | Waiting for review. | Edit, send, schedule, cancel, delete. |
| `scheduled` | Approved: will be sent at `send_at`. | Send now, reschedule, unschedule (`send_at: null`), cancel, delete. Changing what it sends puts it back to `draft`, to approve again. |
| `sending` | Being handed to the outbound queue right now. | Wait a moment. |
| `sent` | Approved and handed to the outbound queue as a message: `sent_message_id` points at it, and the message’s `status` says where it is now. The draft’s text and files are cleared; the message keeps them. | Delete. |
| `failed` | A scheduled send was refused; `error` says why. | Send again, schedule again, cancel, delete. |
| `cancelled` | Will never be sent. | Delete. |

## Replies

Create a draft with `reply_to_message_id` (or `thread_id`, which replies to the thread’s latest message) and it is filled in like a reply: the recipients (`reply_all` like the reply call), the `Re:` subject, and `In-Reply-To` and `References` when it is sent. Anything you pass explicitly wins. A draft may also start incomplete; send checks that it has a recipient and some text (`400 draft_incomplete`).

Reply drafts from the model can go straight into a draft: `POST /v1/messages/:id/draft-reply` with `{ "save": true }`. See [Reply drafts](https://agentboxd.com/docs/reply-drafts).

## Approving a draft

In the dashboard, [Approvals](https://agentboxd.com/app/approvals) lists the drafts of every inbox that wait for a person (`GET /v1/drafts?status=draft,failed`), with a count in the sidebar. Each one opens as a card: the message it answers, quoted, then the reply, then **Approve & send**, **Edit** and **Reject** (cancel). Cc, Bcc, labels and sending later are under More options. To get an email for each new draft, turn on the `draft_created` trigger in [Human on call](https://agentboxd.com/docs/human-on-call).

`POST /v1/inboxes/:id/drafts/:draftId/send` sends it through exactly the same path as a normal send: allow/block lists, suppressions, the plan’s monthly emails, the burst limit and the daily caps are all checked, and it honours `Idempotency-Key`. It answers `202 { draft, message }`. If a check refuses it, you get that error and the draft stays as it was, with the reason in `error`.

## Who approved it

Every draft that goes out records who approved it: `approved_by` (`{ type, id, label }`), `approved_at`, and `approved_content_sha256`, a hash of exactly what it approved. It shows in the dashboard, in `GET /v1/drafts` and in the `draft.sent` event. The message it becomes keeps the same record in `approval` (`{ draft_id, by, at, content_sha256 }`), which stays after the draft is deleted.

| `type` | Who | `label` |
| --- | --- | --- |
| `user` | A person in the dashboard. | Their email. |
| `api_key` | An API key. | The key’s name. |
| `connector` | The hosted MCP connector, for example Claude. | The client’s name. |
| `policy` | An approval rule in our code, not a person or a key. Only the Inbox Heist vault uses one today. | The policy’s name, for example `Inbox Heist vault policy`. |

- Sending a draft records who sent it. Scheduling one records who scheduled it.
- An approval covers exactly the content it saw. Changing what a scheduled draft sends (recipients, subject, body or files) puts it back to `draft`, so it needs a new approval. Labels and metadata don’t count as changes.
- Unscheduling clears the approval.
- If a send is refused (a limit, a list, the quota), the draft keeps the approval it had before.
- A scheduled draft goes out only while whoever approved it still can: the person is still in the workspace, the API key isn’t revoked or expired, the connection hasn’t ended. Otherwise it fails with `approver_gone` and a `draft.failed` event, and someone has to send or schedule it again. A key whose creator left the workspace still counts. While the inbox is paused, the workspace is stopped or sending is paused, the draft waits as usual, and this is checked after the resume.
- The label is a copy, so the record stays after the key or the account is deleted.

To approve only the version you reviewed, pass the draft’s `content_sha256` (a hash of what it sends now) as `expected_content_sha256` to send or schedule. If the draft changed since, for example because an agent edited it, you get `409 draft_changed` and nothing happens. Read the draft again and review the new version. The dashboard always does this.

Approval is not delivery. After it, the message’s `status` tells you where it is:

| Stage | Where to see it |
| --- | --- |
| Approved | The draft’s `approved_at`, and the message’s `approval.at`. |
| Queued | Message `status: queued`: waiting in our outbound queue (an [emergency stop](https://agentboxd.com/docs/emergency-stop) or a paused inbox holds it here), or the receiving server asked us to try again later: we retry for about 48 hours ([how](https://agentboxd.com/docs/api#the-message-object)). |
| Left our server | Message `status: sent`, and the `message.sent` event. |
| Delivered | Message `status: delivered`, and the `message.delivered` event: every recipient is an Agentboxd inbox and got it, or the recipient’s server confirmed it where the transport reports it. With one recipient elsewhere it stays `sent`. Nothing tells us more (for example whether it reached the inbox or the spam folder). |

## Scheduled send

Set `send_at` (ISO 8601, at least 1 minute and at most 30 days ahead) when you create or update a draft, or call `POST /v1/inboxes/:id/drafts/:draftId/schedule { send_at }`. The checks run again when it is sent, so a send that became impossible meanwhile (quota used up, recipient blocked, a file no longer stored, whoever approved it no longer able to) ends `failed` with a `draft.failed` webhook. A send that hits the burst limit is retried a few minutes later. `POST …/cancel` stops it.

Plain sends can be scheduled too: `send_at` on `POST /v1/inboxes/:id/messages/send` or `…/reply` creates a scheduled draft and returns it (202) instead of a message. Draft limits apply: `text` and `html` are at most 1,000,000 characters each.

schedule a send:

```bash
curl -s -X POST https://api.agentboxd.com/v1/inboxes/$INBOX_ID/messages/send \
  -H "Authorization: Bearer $AGENTBOXD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"to":"dana@example.com","subject":"Your weekly report","text":"…","send_at":"2026-10-01T08:00:00Z"}'
```

## Meeting drafts

The [meeting calls](https://agentboxd.com/docs/api#meetings) (answer an invite, send, change or cancel a meeting) take `draft: true`: then they send nothing and save the message as a draft, with the calendar file it will carry. Approve it like any draft. Its `calendar` says what sending it does: `{ action: "respond" | "create" | "update" | "cancel", method, event_id, removed_attendees }` (for a change, the attendees taken off, who get a cancellation with it); other drafts have `calendar: null`. The meeting changes only when the draft is sent, in the same step that stores the message: a new meeting exists from then on, under `event_id`.

- The approval covers the calendar file too (`content_sha256`). You can edit the subject and the text; the recipients come from the meeting, so changing `to`, `cc`, `bcc`, `type` or `data` is `422 calendar_draft_fixed`. To change the meeting, cancel the draft and call the meeting API again.
- If the meeting changed after the draft was made (the organizer sent a new version, or another change went out first), the send is refused with `409 calendar_event_changed`: nothing goes out and the draft keeps the error. Make a new one.
- Making one needs `calendar:write` and `drafts:write`; sending it needs `messages:send`, as for every draft.

## Webhooks and permissions

Drafts emit `draft.created`, `draft.updated`, `draft.sent`, `draft.failed` and `draft.cancelled` ([Webhooks](https://agentboxd.com/docs/webhooks)). Reading drafts needs `drafts:read`; creating, editing, cancelling and deleting need `drafts:write`; sending or scheduling one also needs `messages:send`. An inbox-scoped key sees only its inbox’s drafts.

MCP clients have `create_draft`, `list_drafts`, `get_draft`, `send_draft`, `schedule_draft` and `cancel_draft` ([MCP server](https://agentboxd.com/docs/mcp)).
