# Create a draft without sending it

Source: https://docs.emelia.io/docs/inbox-api/create-draft

Guide: [Inbox API](https://docs.emelia.io/docs/inbox-api.md)

`POST https://api.emelia.io/inbox/api/v1/drafts`

Creates a draft in one of the workspace mailboxes. It appears immediately in the Drafts folder of the inbox, where a person reviews, edits and sends it: the way to prepare an email from an integration or an agent while keeping a human in the loop.

Signatures: by default the default signature of the mailbox address is inserted (`signature` omitted or `"default"`). Pass `"none"` for no signature, or a signature id from `GET /signatures`. In Emelia, signatures come from the Emelia sending accounts.

Important: when `inReplyTo` is set and the mailbox already holds a draft replying to the same message (including an AI auto-draft), that draft is overwritten and returned with 201, instead of a new one being created. If that draft is being sent (scheduled send or auto-reply in progress) the call fails with `409 SCHEDULED_SEND_IN_PROGRESS`; if it was synchronised from the provider, with `409 DRAFT_ALREADY_EXISTS`.

Attachments cannot be added through the API. Supports `Idempotency-Key`.

## Authentication

Send your API key in the `Authorization` header: `Authorization: <your API key>`.

## Request body

Content type: `application/json`, required.

| Field | Type | Required | Description |
|---|---|---|---|
| `mailboxId` | string | yes | Min length 1. |
| `to` | array of object | no | Default: `[]`. |
| `to[].email` | string (email) | yes |  |
| `to[].name` | string | no |  |
| `cc` | array of object | no |  |
| `cc[].email` | string (email) | yes |  |
| `cc[].name` | string | no |  |
| `bcc` | array of object | no |  |
| `bcc[].email` | string (email) | yes |  |
| `bcc[].name` | string | no |  |
| `subject` | string | no | Default: `""`. |
| `body` | object | yes | `text` feeds the preview snippet. |
| `body.html` | string | no |  |
| `body.text` | string | no |  |
| `inReplyTo` | string | no | Joins the conversation; overwrites an existing draft reply to the same message. |
| `references` | array of string | no |  |
| `signature` | string | no | `default` (or omitted): default signature of the sending identity. `none`: no signature, and a signature already in the body is removed. Otherwise a signature id from `GET /signatures` (unknown id: 404 SIGNATURE_NOT_FOUND). A body already signed is not signed twice. Default: `"default"`. Min length 1. |

```json
{
  "mailboxId": "cmbxacme0000000000000001",
  "to": [
    {
      "email": "jane.roe@example.com",
      "name": "Jane Roe"
    }
  ],
  "subject": "Re: Partnership with Acme",
  "inReplyTo": "<CAJacme.0001@mail.example.com>",
  "references": [
    "<20261008091500.acme01@acme.com>",
    "<CAJacme.0001@mail.example.com>"
  ],
  "body": {
    "text": "Hello Jane,\n\nTuesday 10am works for me.",
    "html": "<p>Hello Jane,</p><p>Tuesday 10am works for me.</p>"
  }
}
```

## Example request

**cURL**

```bash
curl -X POST "https://api.emelia.io/inbox/api/v1/drafts" \
  -H "Authorization: $EMELIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "mailboxId": "cmbxacme0000000000000001",
  "to": [
    {
      "email": "jane.roe@example.com",
      "name": "Jane Roe"
    }
  ],
  "subject": "Re: Partnership with Acme",
  "inReplyTo": "<CAJacme.0001@mail.example.com>",
  "references": [
    "<20261008091500.acme01@acme.com>",
    "<CAJacme.0001@mail.example.com>"
  ],
  "body": {
    "text": "Hello Jane,\n\nTuesday 10am works for me.",
    "html": "<p>Hello Jane,</p><p>Tuesday 10am works for me.</p>"
  }
}'
```

**JavaScript**

```js
const response = await fetch('https://api.emelia.io/inbox/api/v1/drafts', {
  method: 'POST',
  headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    "mailboxId": "cmbxacme0000000000000001",
    "to": [
      {
        "email": "jane.roe@example.com",
        "name": "Jane Roe"
      }
    ],
    "subject": "Re: Partnership with Acme",
    "inReplyTo": "<CAJacme.0001@mail.example.com>",
    "references": [
      "<20261008091500.acme01@acme.com>",
      "<CAJacme.0001@mail.example.com>"
    ],
    "body": {
      "text": "Hello Jane,\n\nTuesday 10am works for me.",
      "html": "<p>Hello Jane,</p><p>Tuesday 10am works for me.</p>"
    }
  }),
});
console.log(await response.json());
```

**Python**

```python
import os
import requests

response = requests.post(
    "https://api.emelia.io/inbox/api/v1/drafts",
    headers={"Authorization": os.environ["EMELIA_API_KEY"]},
    json={
        "mailboxId": "cmbxacme0000000000000001",
        "to": [
            {
                "email": "jane.roe@example.com",
                "name": "Jane Roe"
            }
        ],
        "subject": "Re: Partnership with Acme",
        "inReplyTo": "<CAJacme.0001@mail.example.com>",
        "references": [
            "<20261008091500.acme01@acme.com>",
            "<CAJacme.0001@mail.example.com>"
        ],
        "body": {
            "text": "Hello Jane,\n\nTuesday 10am works for me.",
            "html": "<p>Hello Jane,</p><p>Tuesday 10am works for me.</p>"
        }
    },
)
print(response.json())
```

## Responses

### 201: Created (or an existing draft reply was overwritten).

| Field | Type | Required | Description |
|---|---|---|---|
| `draft` | object | yes | Full metadata of an email or a draft (`GET /emails/{emailId}`, `PATCH /emails/{emailId}`, drafts). Has `bcc` and provider `labels`, has no `tags`. Never contains the body. |
| `draft.id` | string | yes | Inbox id of the email. |
| `draft.mailboxId` | string | yes |  |
| `draft.folderId` | string | yes |  |
| `draft.messageId` | string | yes | RFC 5322 Message-ID header. Needed to reply in a thread. |
| `draft.threadId` | string or null | yes | Provider conversation id (Gmail, Microsoft), null for IMAP. |
| `draft.subject` | string | yes |  |
| `draft.from` | object | yes | An address as returned by the API. `name` is null when unknown. |
| `draft.to` | array of object | yes |  |
| `draft.cc` | array of object | yes |  |
| `draft.date` | string (date-time) | yes |  |
| `draft.snippet` | string | yes | Short text preview (about 200 characters). |
| `draft.isRead` | boolean | yes |  |
| `draft.isStarred` | boolean | yes |  |
| `draft.isDraft` | boolean | yes |  |
| `draft.waitingForReply` | boolean | yes | Estimate that a sent email still expects an answer. |
| `draft.hasAttachments` | boolean | yes |  |
| `draft.attachments` | array of object | yes |  |
| `draft.inReplyTo` | string or null | yes |  |
| `draft.references` | array of string | yes |  |
| `draft.bcc` | array of object | yes | Only known on sent messages and drafts. |
| `draft.labels` | array of string | yes | Provider labels (for example Gmail `INBOX`, `UNREAD`, `IMPORTANT`), NOT inbox labels. |
| `draft.isAutoReply` | boolean | yes | Sent automatically by an auto-reply. |
| `draft.isAutoForward` | boolean | yes | Sent automatically by an auto-forward rule. |
| `draft.scheduledStatus` | enum: `PENDING`, `QUEUED`, `SENDING`, `FAILED`, `null` | yes | For a scheduled draft: its sending state. |
| `draft.scheduledSendAt` | string or null (date-time) | yes |  |

```json
{
  "draft": {
    "id": "cmdracme0000000000000001",
    "mailboxId": "cmbxacme0000000000000001",
    "folderId": "cmfdacme0000000000000001",
    "messageId": "<draft-cmdracme0000000000000001@acme.com>",
    "threadId": "18c2acme0001",
    "subject": "Re: Partnership with Acme",
    "from": {
      "email": "john.doe@acme.com",
      "name": null
    },
    "to": [
      {
        "email": "jane.roe@example.com",
        "name": "Jane Roe"
      }
    ],
    "cc": [],
    "bcc": [],
    "date": "2026-10-08T09:12:44.000Z",
    "snippet": "Hello Jane, Tuesday 10am works for me.",
    "isRead": true,
    "isStarred": false,
    "isDraft": true,
    "hasAttachments": false,
    "attachments": [],
    "inReplyTo": "<CAJacme.0001@mail.example.com>",
    "references": [
      "<20261008091500.acme01@acme.com>",
      "<CAJacme.0001@mail.example.com>"
    ],
    "labels": [],
    "waitingForReply": false,
    "isAutoReply": false,
    "isAutoForward": false,
    "scheduledStatus": null,
    "scheduledSendAt": null
  }
}
```

### 400: `VALIDATION_FAILED`: missing or invalid field; `details` maps each field to its errors.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | enum: `INVALID_API_KEY`, `WORKSPACE_SUSPENDED`, `FORBIDDEN`, `PLAN_REQUIRED`, `VALIDATION_FAILED`, `REQUEST_ERROR`, `PAYLOAD_TOO_LARGE`, `NOT_FOUND`, `EMAIL_NOT_FOUND`, `DRAFT_NOT_FOUND`, `MAILBOX_NOT_FOUND`, `LABEL_NOT_FOUND`, `VIEW_NOT_FOUND`, `RULE_NOT_FOUND`, `FILTER_NOT_FOUND`, `SIGNATURE_NOT_FOUND`, `MAILBOX_INACTIVE`, `INVALID_SENDER_ALIAS`, `NO_TRASH_FOLDER`, `INVALID_TAG`, `CONFLICT`, `DUPLICATE_TAG_NAME`, `DUPLICATE_RULE`, `DUPLICATE_FILTER`, `SCHEDULED_SEND_IN_PROGRESS`, `DRAFT_ALREADY_EXISTS`, `QUERY_TIMEOUT`, `RATE_LIMIT_EXCEEDED`, `QUOTA_EXCEEDED`, `INTERNAL_ERROR` | yes | Codes that the Inbox API can return, as found in the code. Branch on `code`, never on `error`. The HTTP status is derived from the code (unknown business codes are 400). |
| `error` | string | yes | Human readable. |
| `details` | object | no | Optional context: faulty fields, bounds, ids. |

```json
{
  "code": "INVALID_API_KEY",
  "error": "string",
  "details": {}
}
```

### 401: Missing, malformed, unknown or disabled key. No `X-RateLimit-*` headers.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | enum: `INVALID_API_KEY`, `WORKSPACE_SUSPENDED`, `FORBIDDEN`, `PLAN_REQUIRED`, `VALIDATION_FAILED`, `REQUEST_ERROR`, `PAYLOAD_TOO_LARGE`, `NOT_FOUND`, `EMAIL_NOT_FOUND`, `DRAFT_NOT_FOUND`, `MAILBOX_NOT_FOUND`, `LABEL_NOT_FOUND`, `VIEW_NOT_FOUND`, `RULE_NOT_FOUND`, `FILTER_NOT_FOUND`, `SIGNATURE_NOT_FOUND`, `MAILBOX_INACTIVE`, `INVALID_SENDER_ALIAS`, `NO_TRASH_FOLDER`, `INVALID_TAG`, `CONFLICT`, `DUPLICATE_TAG_NAME`, `DUPLICATE_RULE`, `DUPLICATE_FILTER`, `SCHEDULED_SEND_IN_PROGRESS`, `DRAFT_ALREADY_EXISTS`, `QUERY_TIMEOUT`, `RATE_LIMIT_EXCEEDED`, `QUOTA_EXCEEDED`, `INTERNAL_ERROR` | yes | Codes that the Inbox API can return, as found in the code. Branch on `code`, never on `error`. The HTTP status is derived from the code (unknown business codes are 400). |
| `error` | string | yes | Human readable. |
| `details` | object | no | Optional context: faulty fields, bounds, ids. |

```json
{
  "code": "INVALID_API_KEY",
  "error": "string",
  "details": {}
}
```

### 403: `WORKSPACE_SUSPENDED`: the Emelia subscription has expired.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | enum: `INVALID_API_KEY`, `WORKSPACE_SUSPENDED`, `FORBIDDEN`, `PLAN_REQUIRED`, `VALIDATION_FAILED`, `REQUEST_ERROR`, `PAYLOAD_TOO_LARGE`, `NOT_FOUND`, `EMAIL_NOT_FOUND`, `DRAFT_NOT_FOUND`, `MAILBOX_NOT_FOUND`, `LABEL_NOT_FOUND`, `VIEW_NOT_FOUND`, `RULE_NOT_FOUND`, `FILTER_NOT_FOUND`, `SIGNATURE_NOT_FOUND`, `MAILBOX_INACTIVE`, `INVALID_SENDER_ALIAS`, `NO_TRASH_FOLDER`, `INVALID_TAG`, `CONFLICT`, `DUPLICATE_TAG_NAME`, `DUPLICATE_RULE`, `DUPLICATE_FILTER`, `SCHEDULED_SEND_IN_PROGRESS`, `DRAFT_ALREADY_EXISTS`, `QUERY_TIMEOUT`, `RATE_LIMIT_EXCEEDED`, `QUOTA_EXCEEDED`, `INTERNAL_ERROR` | yes | Codes that the Inbox API can return, as found in the code. Branch on `code`, never on `error`. The HTTP status is derived from the code (unknown business codes are 400). |
| `error` | string | yes | Human readable. |
| `details` | object | no | Optional context: faulty fields, bounds, ids. |

```json
{
  "code": "INVALID_API_KEY",
  "error": "string",
  "details": {}
}
```

### 404: `MAILBOX_NOT_FOUND` (unknown `mailboxId`) or `SIGNATURE_NOT_FOUND` (unknown signature id).

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | enum: `INVALID_API_KEY`, `WORKSPACE_SUSPENDED`, `FORBIDDEN`, `PLAN_REQUIRED`, `VALIDATION_FAILED`, `REQUEST_ERROR`, `PAYLOAD_TOO_LARGE`, `NOT_FOUND`, `EMAIL_NOT_FOUND`, `DRAFT_NOT_FOUND`, `MAILBOX_NOT_FOUND`, `LABEL_NOT_FOUND`, `VIEW_NOT_FOUND`, `RULE_NOT_FOUND`, `FILTER_NOT_FOUND`, `SIGNATURE_NOT_FOUND`, `MAILBOX_INACTIVE`, `INVALID_SENDER_ALIAS`, `NO_TRASH_FOLDER`, `INVALID_TAG`, `CONFLICT`, `DUPLICATE_TAG_NAME`, `DUPLICATE_RULE`, `DUPLICATE_FILTER`, `SCHEDULED_SEND_IN_PROGRESS`, `DRAFT_ALREADY_EXISTS`, `QUERY_TIMEOUT`, `RATE_LIMIT_EXCEEDED`, `QUOTA_EXCEEDED`, `INTERNAL_ERROR` | yes | Codes that the Inbox API can return, as found in the code. Branch on `code`, never on `error`. The HTTP status is derived from the code (unknown business codes are 400). |
| `error` | string | yes | Human readable. |
| `details` | object | no | Optional context: faulty fields, bounds, ids. |

```json
{
  "code": "INVALID_API_KEY",
  "error": "string",
  "details": {}
}
```

### 409: `CONFLICT` (Idempotency-Key reused with another body, or original call in progress), `SCHEDULED_SEND_IN_PROGRESS` or `DRAFT_ALREADY_EXISTS` (an existing draft reply cannot be overwritten).

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | enum: `INVALID_API_KEY`, `WORKSPACE_SUSPENDED`, `FORBIDDEN`, `PLAN_REQUIRED`, `VALIDATION_FAILED`, `REQUEST_ERROR`, `PAYLOAD_TOO_LARGE`, `NOT_FOUND`, `EMAIL_NOT_FOUND`, `DRAFT_NOT_FOUND`, `MAILBOX_NOT_FOUND`, `LABEL_NOT_FOUND`, `VIEW_NOT_FOUND`, `RULE_NOT_FOUND`, `FILTER_NOT_FOUND`, `SIGNATURE_NOT_FOUND`, `MAILBOX_INACTIVE`, `INVALID_SENDER_ALIAS`, `NO_TRASH_FOLDER`, `INVALID_TAG`, `CONFLICT`, `DUPLICATE_TAG_NAME`, `DUPLICATE_RULE`, `DUPLICATE_FILTER`, `SCHEDULED_SEND_IN_PROGRESS`, `DRAFT_ALREADY_EXISTS`, `QUERY_TIMEOUT`, `RATE_LIMIT_EXCEEDED`, `QUOTA_EXCEEDED`, `INTERNAL_ERROR` | yes | Codes that the Inbox API can return, as found in the code. Branch on `code`, never on `error`. The HTTP status is derived from the code (unknown business codes are 400). |
| `error` | string | yes | Human readable. |
| `details` | object | no | Optional context: faulty fields, bounds, ids. |

```json
{
  "code": "INVALID_API_KEY",
  "error": "string",
  "details": {}
}
```

### 413: `PAYLOAD_TOO_LARGE`: JSON body above 256 KB.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | enum: `INVALID_API_KEY`, `WORKSPACE_SUSPENDED`, `FORBIDDEN`, `PLAN_REQUIRED`, `VALIDATION_FAILED`, `REQUEST_ERROR`, `PAYLOAD_TOO_LARGE`, `NOT_FOUND`, `EMAIL_NOT_FOUND`, `DRAFT_NOT_FOUND`, `MAILBOX_NOT_FOUND`, `LABEL_NOT_FOUND`, `VIEW_NOT_FOUND`, `RULE_NOT_FOUND`, `FILTER_NOT_FOUND`, `SIGNATURE_NOT_FOUND`, `MAILBOX_INACTIVE`, `INVALID_SENDER_ALIAS`, `NO_TRASH_FOLDER`, `INVALID_TAG`, `CONFLICT`, `DUPLICATE_TAG_NAME`, `DUPLICATE_RULE`, `DUPLICATE_FILTER`, `SCHEDULED_SEND_IN_PROGRESS`, `DRAFT_ALREADY_EXISTS`, `QUERY_TIMEOUT`, `RATE_LIMIT_EXCEEDED`, `QUOTA_EXCEEDED`, `INTERNAL_ERROR` | yes | Codes that the Inbox API can return, as found in the code. Branch on `code`, never on `error`. The HTTP status is derived from the code (unknown business codes are 400). |
| `error` | string | yes | Human readable. |
| `details` | object | no | Optional context: faulty fields, bounds, ids. |

```json
{
  "code": "INVALID_API_KEY",
  "error": "string",
  "details": {}
}
```

### 429: `RATE_LIMIT_EXCEEDED`: per minute limit of the workspace (wait `Retry-After`). `QUOTA_EXCEEDED`: monthly quota exhausted until the 1st of next month UTC (in Emelia the plan cannot be upgraded).

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | enum: `INVALID_API_KEY`, `WORKSPACE_SUSPENDED`, `FORBIDDEN`, `PLAN_REQUIRED`, `VALIDATION_FAILED`, `REQUEST_ERROR`, `PAYLOAD_TOO_LARGE`, `NOT_FOUND`, `EMAIL_NOT_FOUND`, `DRAFT_NOT_FOUND`, `MAILBOX_NOT_FOUND`, `LABEL_NOT_FOUND`, `VIEW_NOT_FOUND`, `RULE_NOT_FOUND`, `FILTER_NOT_FOUND`, `SIGNATURE_NOT_FOUND`, `MAILBOX_INACTIVE`, `INVALID_SENDER_ALIAS`, `NO_TRASH_FOLDER`, `INVALID_TAG`, `CONFLICT`, `DUPLICATE_TAG_NAME`, `DUPLICATE_RULE`, `DUPLICATE_FILTER`, `SCHEDULED_SEND_IN_PROGRESS`, `DRAFT_ALREADY_EXISTS`, `QUERY_TIMEOUT`, `RATE_LIMIT_EXCEEDED`, `QUOTA_EXCEEDED`, `INTERNAL_ERROR` | yes | Codes that the Inbox API can return, as found in the code. Branch on `code`, never on `error`. The HTTP status is derived from the code (unknown business codes are 400). |
| `error` | string | yes | Human readable. |
| `details` | object | no | Optional context: faulty fields, bounds, ids. |

```json
{
  "code": "INVALID_API_KEY",
  "error": "string",
  "details": {}
}
```

### 500: `INTERNAL_ERROR`: unexpected failure. The message is generic in production.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | enum: `INVALID_API_KEY`, `WORKSPACE_SUSPENDED`, `FORBIDDEN`, `PLAN_REQUIRED`, `VALIDATION_FAILED`, `REQUEST_ERROR`, `PAYLOAD_TOO_LARGE`, `NOT_FOUND`, `EMAIL_NOT_FOUND`, `DRAFT_NOT_FOUND`, `MAILBOX_NOT_FOUND`, `LABEL_NOT_FOUND`, `VIEW_NOT_FOUND`, `RULE_NOT_FOUND`, `FILTER_NOT_FOUND`, `SIGNATURE_NOT_FOUND`, `MAILBOX_INACTIVE`, `INVALID_SENDER_ALIAS`, `NO_TRASH_FOLDER`, `INVALID_TAG`, `CONFLICT`, `DUPLICATE_TAG_NAME`, `DUPLICATE_RULE`, `DUPLICATE_FILTER`, `SCHEDULED_SEND_IN_PROGRESS`, `DRAFT_ALREADY_EXISTS`, `QUERY_TIMEOUT`, `RATE_LIMIT_EXCEEDED`, `QUOTA_EXCEEDED`, `INTERNAL_ERROR` | yes | Codes that the Inbox API can return, as found in the code. Branch on `code`, never on `error`. The HTTP status is derived from the code (unknown business codes are 400). |
| `error` | string | yes | Human readable. |
| `details` | object | no | Optional context: faulty fields, bounds, ids. |

```json
{
  "code": "INVALID_API_KEY",
  "error": "string",
  "details": {}
}
```
