Skip to content

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

FieldTypeRequiredDescription
recipientsobjectNoOptional to, cc, and bcc arrays. Addresses are normalized and deduplicated across all groups.
subjectstring | nullNoSubject, up to 998 characters.
body_textstring | nullNoPlain-text body.
body_htmlstring | nullNoHTML body.
reply_tostring | nullNoAddress that should receive replies.
thread_idUUID | nullNoExisting mailbox thread for a reply draft.
in_reply_to_message_idstring | nullNoRFC 5322 Message-ID of the reply parent.
referencesstring[] | nullNoRFC 5322 Message-IDs in the reply chain.
attachmentsAttachmentUpload[] | nullNoUp to 50 attachments included in the initial draft.
track_opensbooleanNoApply open tracking when sent. Requires an HTML body. Default false.
forward_message_idUUID | nullNoStored message to forward.
forward_modestringNoinline (default) or wrapped. Requires forward_message_id when supplied.
include_original_attachmentsbooleanNoInclude original attachments in an inline forward. Default true.
forward_note_textstring | nullNoEditable plain-text note for a forward.
forward_note_htmlstring | nullNoEditable 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

bashbash

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 parameterTypeDefaultDescription
cursorstring—Opaque next_cursor from the previous page.
limitinteger50Page size from 1 to 100.
JSONJSON

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.

FieldTypeRequiredDescription
generationintegerYesCurrent generation read by the caller.
recipientsobject | nullNoReplacement recipient groups.
subjectstring | nullNoReplacement subject; null clears it.
body_textstring | nullNoReplacement plain-text body; null clears it. If the draft has HTML, also supply or clear body_html.
body_htmlstring | nullNoReplacement HTML body; null clears it.
reply_tostring | nullNoReplacement Reply-To address; null clears it.
thread_idUUID | nullNoReplacement reply thread; null clears reply linkage.
in_reply_to_message_idstring | nullNoReplacement reply parent Message-ID.
referencesstring[] | nullNoReplacement reply chain.
track_opensbooleanNoOpen-tracking preference applied when sent.
forward_note_textstring | nullNoReplacement plain-text note on a forward draft.
forward_note_htmlstring | nullNoReplacement HTML note on a forward draft.
bashbash

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.

JSONJSON

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.

JSONJSON

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.

bashbash

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.

JSONJSON

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

FieldTypeDescription
idUUIDStable draft identifier.
mailbox_idUUIDOwning mailbox.
from_addressstringSending mailbox address.
to_addressesstring[]Primary recipients.
cc_addressesstring[]CC recipients.
bcc_addressesstring[]BCC recipients retained in the draft.
subjectstring | nullSubject.
snippetstring | nullBody preview.
has_attachmentsbooleanWhether attachments are present.
attachment_countintegerNumber of attachment parts.
generationintegerCurrent concurrency revision.
send_statestringdraft, sending, or uncertain.
track_opensbooleanWhether open tracking will be applied when sent.
created_atstringCreation time in ISO 8601 format.
updated_atstringLast semantic update time in ISO 8601 format.

DraftDetailResponse

Includes every summary field plus:

FieldTypeDescription
body_textstring | nullCombined plain-text body.
body_htmlstring | nullCombined HTML body.
reply_tostring | nullReply-To address.
thread_idUUID | nullReply thread context.
message_idstring | nullStable RFC 5322 Message-ID for this draft.
in_reply_tostring | nullResolved RFC 5322 reply-parent header.
referencesstring[]Resolved reply-chain headers.
forward_source_message_idUUID | nullSource message for a forward draft.
forward_note_textstring | nullEditable plain-text forward note.
forward_note_htmlstring | nullEditable HTML forward note.
attachment_metadataDraftAttachmentResponse[]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

FieldTypeDescription
part_indexintegerCurrent zero-based MIME leaf index.
filenamestringDownload filename.
content_typestringMIME content type.
sizeintegerDecoded size in bytes.
content_idstring | nullContent-ID for an inline part.
is_inlinebooleanWhether the part is rendered inline.

Conflicts and delivery state

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

detail.errorMeaningClient action
draft_generation_conflictThe 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_progressAnother 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_uncertainInkbox 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.