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:
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
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
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
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. |
Get draft GET
Returns the current DraftDetailResponse, including bodies and attachment
descriptors. A draft may be read in any send state.
Update draft PATCH
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. |
A semantic change increments the generation. A request that changes nothing may return the existing generation.
Duplicate draft POST
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.
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
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
Add one or more attachments to the expected generation. Returns the updated
DraftDetailResponse and a new generation.
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
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
Streams the decoded attachment body with its content type and download filename. The generation is required because an edit can change part indexes.
Send draft POST
Validate and send the exact revision. The final draft must contain at least one recipient and satisfy the ordinary outbound mail requirements.
A successful request returns the canonical MessageResponse 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 for user workflows and Use a mail app for connection setup.