Inkbox

> # Documentation index
> Fetch the complete documentation index at: https://inkbox.ai/sitemap.xml
> Use this file to discover all available pages before exploring further.

---

# Drafts
description: Create, revise, attach files to, duplicate, and send mailbox email drafts

---


# Drafts

Drafts let you save incomplete email before sending it. Recipients, subject, and
body are optional while drafting. A saved draft belongs to one mailbox, counts
toward that mailbox's storage, and does not appear in message, thread, or search
results until it is sent.

All draft routes are under:

```
/api/v1/mail/mailboxes/{email_address}/drafts
```

The examples use `X-API-Key`; supported user and identity tokens can also access
these routes. Content mutations use optimistic concurrency: read the current
`generation`, include it in the next mutation, and replace your local value with
the generation returned by the API.

## Create draft `POST`

```
POST /mailboxes/{email_address}/drafts
```

Create an empty, incomplete, or send-ready draft. Returns a
`DraftDetailResponse` with status `201` and generation `1`.

The optional `Idempotency-Key` header makes an initial create safe to retry with
the exact same request. If the created draft still exists, a matching retry
returns it instead of creating a duplicate. Reusing the key with different
content, or after the draft was sent or deleted, returns
`409 idempotency_key_reused`; use a new key for a new logical create.

### Request body

| Field | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| `recipients` | object | No | Optional `to`, `cc`, and `bcc` arrays. Addresses are normalized and deduplicated across all groups. |
| `subject` | string \| null | No | Subject, up to 998 characters. |
| `body_text` | string \| null | No | Plain-text body. |
| `body_html` | string \| null | No | HTML body. |
| `reply_to` | string \| null | No | Address that should receive replies. |
| `thread_id` | UUID \| null | No | Existing mailbox thread for a reply draft. |
| `in_reply_to_message_id` | string \| null | No | RFC 5322 Message-ID of the reply parent. |
| `references` | string[] \| null | No | RFC 5322 Message-IDs in the reply chain. |
| `attachments` | AttachmentUpload[] \| null | No | Up to 50 attachments included in the initial draft. |
| `track_opens` | boolean | No | Apply open tracking when sent. Requires an HTML body. Default `false`. |
| `forward_message_id` | UUID \| null | No | Stored message to forward. |
| `forward_mode` | string | No | `inline` (default) or `wrapped`. Requires `forward_message_id` when supplied. |
| `include_original_attachments` | boolean | No | Include original attachments in an inline forward. Default `true`. |
| `forward_note_text` | string \| null | No | Editable plain-text note for a forward. |
| `forward_note_html` | string \| null | No | Editable HTML note for a forward. |

Forward drafts use `forward_note_text` and `forward_note_html` instead of the
generic body fields. Reply fields and forward fields cannot be combined.

### Example

```bash
curl -X POST "https://inkbox.ai/api/v1/mail/mailboxes/agent%40inkboxmail.com/drafts" \
    -H "X-API-Key: YOUR_API_KEY" \
    -H "Idempotency-Key: create-follow-up-01" \
    -H "Content-Type: application/json" \
    -d '{
      "recipients": {"to": ["customer@example.com"]},
      "subject": "Follow-up",
      "body_text": "I will finish this later."
    }'
```

To start a reply draft, pass the thread context you already hold. The create and
update request field is named `in_reply_to_message_id`; detail responses expose
the resolved header as `in_reply_to`.

## List drafts `GET`

```
GET /mailboxes/{email_address}/drafts
```

Returns drafts newest-update first in the standard cursor page shape.

| Query parameter | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| `cursor` | string | — | Opaque `next_cursor` from the previous page. |
| `limit` | integer | 50 | Page size from 1 to 100. |

```json
{
    "items": [
      {
        "id": "8e8bb1ab-b319-4ae4-8e20-bcc6c390fc5a",
        "mailbox_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "from_address": "agent@inkboxmail.com",
        "to_addresses": ["customer@example.com"],
        "cc_addresses": [],
        "bcc_addresses": [],
        "subject": "Follow-up",
        "snippet": "I will finish this later.",
        "has_attachments": false,
        "attachment_count": 0,
        "generation": 1,
        "send_state": "draft",
        "track_opens": false,
        "created_at": "2026-08-16T12:00:00Z",
        "updated_at": "2026-08-16T12:00:00Z"
      }
    ],
    "next_cursor": null,
    "has_more": false
}
```

## Get draft `GET`

```
GET /mailboxes/{email_address}/drafts/{draft_id}
```

Returns the current `DraftDetailResponse`, including bodies and attachment
descriptors. A draft may be read in any send state.

## Update draft `PATCH`

```
PATCH /mailboxes/{email_address}/drafts/{draft_id}
```

Partially update the expected revision. `generation` is required. Omitted fields
are preserved; an explicit `null` clears a nullable field. Attachments are
preserved and are changed through the attachment endpoints.

| Field | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| `generation` | integer | Yes | Current generation read by the caller. |
| `recipients` | object \| null | No | Replacement recipient groups. |
| `subject` | string \| null | No | Replacement subject; `null` clears it. |
| `body_text` | string \| null | No | Replacement plain-text body; `null` clears it. If the draft has HTML, also supply or clear `body_html`. |
| `body_html` | string \| null | No | Replacement HTML body; `null` clears it. |
| `reply_to` | string \| null | No | Replacement Reply-To address; `null` clears it. |
| `thread_id` | UUID \| null | No | Replacement reply thread; `null` clears reply linkage. |
| `in_reply_to_message_id` | string \| null | No | Replacement reply parent Message-ID. |
| `references` | string[] \| null | No | Replacement reply chain. |
| `track_opens` | boolean | No | Open-tracking preference applied when sent. |
| `forward_note_text` | string \| null | No | Replacement plain-text note on a forward draft. |
| `forward_note_html` | string \| null | No | Replacement HTML note on a forward draft. |

```bash
curl -X PATCH "https://inkbox.ai/api/v1/mail/mailboxes/agent%40inkboxmail.com/drafts/DRAFT_ID" \
    -H "X-API-Key: YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "generation": 1,
      "subject": "Updated follow-up",
      "body_text": "The final draft body."
    }'
```

A semantic change increments the generation. A request that changes nothing may
return the existing generation.

## Duplicate draft `POST`

```
POST /mailboxes/{email_address}/drafts/{draft_id}/duplicate
```

Copy an exact revision into a new editable draft. The copy receives a new draft
ID, generation `1`, and a new Message-ID. It uses storage independently from the
source.

```json
{
    "generation": 3
}
```

This is the safe way to continue from a draft whose delivery status is
`uncertain`: review the copy before deciding whether to send it.

## Delete draft `DELETE`

```
DELETE /mailboxes/{email_address}/drafts/{draft_id}?generation={generation}
```

Permanently delete the expected revision and free its storage. Returns `204 No
Content`. An uncertain draft can be deleted; a draft currently being sent cannot.

## Add attachments `POST`

```
POST /mailboxes/{email_address}/drafts/{draft_id}/attachments
```

Add one or more attachments to the expected generation. Returns the updated
`DraftDetailResponse` and a new generation.

```json
{
    "generation": 2,
    "attachments": [
      {
        "filename": "report.pdf",
        "content_type": "application/pdf",
        "content_base64": "BASE64_CONTENT"
      }
    ]
}
```

`AttachmentUpload` has `filename`, `content_type`, and `content_base64`, with
filename and content type limited to 255 characters. A request can add up to 50
attachments, and decoded attachment input cannot exceed 25 MB. Requests may be
rejected for policy reasons. The complete encoded draft must remain below 9.5 MiB.

The optional `content_id` marks an inline image referenced from HTML as
`cid:<content_id>`. Content IDs must be unique, use an `image/*` content type,
and require the draft to have an HTML body. Forward drafts do not accept inline
attachments.

## Remove attachment `DELETE`

```
DELETE /mailboxes/{email_address}/drafts/{draft_id}/attachments/{part_index}?generation={generation}
```

Remove the attachment identified by its current `part_index`. Returns the
updated detail and generation. Part indexes belong to a specific generation, so
always use a descriptor from the revision you are mutating.

For a wrapped forward, the `message/rfc822` part containing the original message
cannot be removed. Other attachments remain removable.

## Download attachment `GET`

```
GET /mailboxes/{email_address}/drafts/{draft_id}/attachments/{part_index}?generation={generation}
```

Streams the decoded attachment body with its content type and download filename.
The generation is required because an edit can change part indexes.

```bash
curl "https://inkbox.ai/api/v1/mail/mailboxes/agent%40inkboxmail.com/drafts/DRAFT_ID/attachments/2?generation=4" \
    -H "X-API-Key: YOUR_API_KEY" \
    --output report.pdf
```

## Send draft `POST`

```
POST /mailboxes/{email_address}/drafts/{draft_id}/send
```

Validate and send the exact revision. The final draft must contain at least one
recipient and satisfy the ordinary outbound mail requirements.

```json
{
    "generation": 4
}
```

A successful request returns the canonical [MessageResponse](/docs/api/mail/messages#message-object)
and removes that draft. Repeating the same request after a lost successful
response may return the same sent message rather than sending again.

A validation, policy, quota, storage, or other rejection before delivery is
attempted leaves the draft editable at the same generation. Use the returned
structured error to decide whether to edit or retry.

## Response objects

### DraftSummaryResponse

| Field | Type | Description |
| :--- | :--- | :--- |
| `id` | UUID | Stable draft identifier. |
| `mailbox_id` | UUID | Owning mailbox. |
| `from_address` | string | Sending mailbox address. |
| `to_addresses` | string[] | Primary recipients. |
| `cc_addresses` | string[] | CC recipients. |
| `bcc_addresses` | string[] | BCC recipients retained in the draft. |
| `subject` | string \| null | Subject. |
| `snippet` | string \| null | Body preview. |
| `has_attachments` | boolean | Whether attachments are present. |
| `attachment_count` | integer | Number of attachment parts. |
| `generation` | integer | Current concurrency revision. |
| `send_state` | string | `draft`, `sending`, or `uncertain`. |
| `track_opens` | boolean | Whether open tracking will be applied when sent. |
| `created_at` | string | Creation time in ISO 8601 format. |
| `updated_at` | string | Last semantic update time in ISO 8601 format. |

### DraftDetailResponse

Includes every summary field plus:

| Field | Type | Description |
| :--- | :--- | :--- |
| `body_text` | string \| null | Combined plain-text body. |
| `body_html` | string \| null | Combined HTML body. |
| `reply_to` | string \| null | Reply-To address. |
| `thread_id` | UUID \| null | Reply thread context. |
| `message_id` | string \| null | Stable RFC 5322 Message-ID for this draft. |
| `in_reply_to` | string \| null | Resolved RFC 5322 reply-parent header. |
| `references` | string[] | Resolved reply-chain headers. |
| `forward_source_message_id` | UUID \| null | Source message for a forward draft. |
| `forward_note_text` | string \| null | Editable plain-text forward note. |
| `forward_note_html` | string \| null | Editable HTML forward note. |
| `attachment_metadata` | DraftAttachmentResponse[] | Attachments in the current generation. |

`in_reply_to_message_id` is the create/update request field;
`in_reply_to` is the detail response field.

### DraftAttachmentResponse

| Field | Type | Description |
| :--- | :--- | :--- |
| `part_index` | integer | Current zero-based MIME leaf index. |
| `filename` | string | Download filename. |
| `content_type` | string | MIME content type. |
| `size` | integer | Decoded size in bytes. |
| `content_id` | string \| null | Content-ID for an inline part. |
| `is_inline` | boolean | Whether the part is rendered inline. |

## Conflicts and delivery state

Draft conflicts use `409 Conflict`. Branch on `detail.error`, not on status alone.

| `detail.error` | Meaning | Client action |
| :--- | :--- | :--- |
| `draft_generation_conflict` | The draft changed after your client loaded it. | Reload the current detail. Preserve local edits and ask before replacing them, or duplicate the current revision and apply the edits to the copy. |
| `draft_send_in_progress` | Another request is already sending this revision. | Honor `Retry-After`, then repeat the same generation-checked request to poll that attempt. Do not send a different revision. |
| `draft_delivery_uncertain` | Inkbox cannot confirm whether delivery occurred. | Do not automatically resend. Delete the draft or duplicate it to a new draft with a fresh Message-ID, review it, and choose explicitly. |

An uncertain draft is readable and deletable but cannot be edited or sent.

## Storage behavior

Draft bytes count toward mailbox storage as soon as the draft is saved.
Attachments and duplicate drafts count independently. A successful send replaces
the draft's storage usage with the final sent message's usage rather than keeping
both indefinitely. Rejected sends preserve the draft and its existing storage.

## Mail-app interoperability

The Inkbox Console, REST API, and connected mail apps share the same Drafts
mailbox. An edit from another surface may appear in a mail app as the old draft
being replaced by a new revision.

Mail apps save drafts through IMAP and send through SMTP. After SMTP reports
success, the mail app is responsible for removing its own saved draft. An
unrelated SMTP submission is not matched to a draft automatically.

IMAP APPEND accepts drafts up to 10 MB. Final send preparation can require
additional headroom, so a draft saved near that limit may be rejected when sent.
The failed send leaves the draft intact for editing or deletion.

See [Email drafts](/docs/capabilities/email/drafts) for user workflows and
[Use a mail app](/docs/capabilities/email/mail-clients) for connection setup.
