# Send a draft

POST /v1/drafts/{draft_id}/send

Source: https://cherami.to/docs/api/drafts/send-draft



{/* Generated from apps/web/openapi. Edit the contract, not this file. */}

`POST /v1/drafts/{draft_id}/send`

Requires a [Claim-issued API key](https://cherami.to/docs/api#authentication): `Authorization: Bearer YOUR_CREDENTIAL`.

`POST /v1/drafts/{draft_id}/send` with `{}` or `{"idempotency_key":"YOUR_SEND_KEY"}`.

Sending takes the current saved content, not a previously retrieved copy. The draft freezes as `submitted` when an outgoing attempt is reserved, with `sent_message_id` identifying that attempt. An edit that wins before reservation must be included or cause `409 draft_busy`; a send never silently submits an older saved copy. Concurrent sends cannot reserve multiple submissions for the same draft.

Validation, ownership, permission, recipient-policy or quota failures before reservation leave it editable. After reservation it remains submitted for **every** provider outcome, including rejection and uncertainty. There is no automatic retry or return-to-draft operation. A deliberately new attempt requires a new draft; do not create one merely to resolve an unknown outcome.

The response uses the [ordinary send receipt and outcomes](https://cherami.to/docs/api/sending/send-message): `201` for a new attempt, `200` with `replayed: true` when recovering. `Location` points to `/v1/sent/{sent_message_id}`. Acceptance is not proof of delivery. If `outcome_persisted` is false, retain the stronger immediate outcome even if later reads lag.

**The draft ID itself prevents another submission, without expiry.** Repeat the same send request to recover an uncertain attempt, not to restart it. This can preserve a possibly unsent attempt rather than risk a duplicate. Deleting the sent copy does not unlock the draft; recovery then returns `409 draft_result_unavailable` (or `idempotency_result_unavailable` for an active sending key).

Optional sending keys share the ordinary account-scoped sending namespace and 24-hour lifetime. Their intent identifies this draft, not its mutable fields. Changing the draft ID or reusing a key from an ordinary send conflicts. Key expiry does not remove the draft's permanent submitted state. A replay recovered through the draft association need not include `idempotency_expires_at`; it does not allocate or renew a key.

## Parameters [#parameters]

| Parameter  | Location | Required | Type   | Meaning                                        |
| ---------- | -------- | -------- | ------ | ---------------------------------------------- |
| `draft_id` | path     | Yes      | string | Owned Cherami resource ID returned by the API. |

## Request body [#request-body]

JSON object. Total UTF-8 request body limit: 4096 bytes.

### Schema: SendDraft [#schema-senddraft]

| Field             | Required | Type   | Meaning and constraints                                                                                                                                                              |
| ----------------- | -------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `idempotency_key` | No       | string | Retain a unique key, exact payload and first request time for this intended operation. Account-scoped protection lasts 24 hours without renewal. `pattern`: `^[A-Za-z0-9_-]{1,128}$` |

Unknown fields are rejected.

## curl example [#curl-example]

Replace resource-ID placeholders with returned IDs. Supply `CHERAMI_API_KEY` through your private shell environment.

Save the intended payload as a private `request.json` file and replace illustrative values. For an uncertain response, follow the [operation-specific recovery rules](https://cherami.to/docs/api/errors#retry-by-operation).

```json
{}
```

```sh
curl --silent --show-error --include --request POST \
  "https://cherami.to/v1/drafts/DRAFT_ID/send" \
  --header "Authorization: Bearer $CHERAMI_API_KEY" \
  --header 'Content-Type: application/json' \
  --data-binary @request.json
```

## Responses [#responses]

### HTTP 200 [#http-200]

Matching replay: current resource or saved attempt, without another allocation or provider submission.

* `X-Request-ID`: Support correlation ID, not an idempotency key.
* `Location`: Relative URL of the resulting resource.

Content type: `application/json`.

All of these schemas apply.

### Part 1 [#part-1]

[SendReceipt](#schema-sendreceipt)

### Part 2 [#part-2]

| Field      | Required | Type   | Meaning and constraints |
| ---------- | -------- | ------ | ----------------------- |
| `replayed` | Yes      | `true` |                         |

```json
{
  "limited": false,
  "message": {
    "id": "33333333-3333-4333-8333-333333333333",
    "inbox_id": "11111111-1111-4111-8111-111111111111",
    "created_at": "2026-10-01T00:00:00.000Z",
    "recipient_count": 1,
    "status": "unknown",
    "provider_message_id": null,
    "error_code": null,
    "thread_id": "44444444-4444-4444-8444-444444444444",
    "in_reply_to": null,
    "labels": []
  },
  "outcome_persisted": true,
  "replayed": true
}
```

### HTTP 201 [#http-201]

Successful operation; inspect resource state and outcome fields.

* `X-Request-ID`: Support correlation ID, not an idempotency key.
* `Location`: Relative URL of the resulting resource.

Content type: `application/json`.

All of these schemas apply.

### Part 1 [#part-1-1]

[SendReceipt](#schema-sendreceipt)

### Part 2 [#part-2-1]

| Field      | Required | Type    | Meaning and constraints |
| ---------- | -------- | ------- | ----------------------- |
| `replayed` | No       | `false` |                         |

### HTTP 400 [#http-400]

`invalid_json`: Send a valid UTF-8 JSON object, not an array or scalar.

`invalid_message`: Correct the send fields, recipients, reply ID, or attachments using the message's guidance.

`invalid_name`: Correct the inbox, sender or recipient display name using the returned guidance.

`invalid_labels`: Correct label names, changes, filter groups, or discovery prefix.

`invalid_idempotency_key`: Use 1–128 ASCII letters, digits, hyphens or underscores.

`invalid_draft`: Use supported draft fields, source preparation or listing parameters. Draft cursors that are malformed or do not match the inbox/state also use this code.

* `X-Request-ID`: Support correlation ID, not an idempotency key.

Content type: `application/json`.

[Error](#schema-error)

### HTTP 401 [#http-401]

`unauthorized`: Provide a valid bearer credential. Use [human-approved recovery](https://cherami.to/docs/guides/recovery) if access is lost.

* `X-Request-ID`: Support correlation ID, not an idempotency key.
* `WWW-Authenticate`: `"Bearer"`

Content type: `application/json`.

[Error](#schema-error)

### HTTP 403 [#http-403]

`operation_not_allowed`: This account cannot send mail, delete mail, or delete inboxes. Contact support if unexpected.

`recipient_not_allowed`: The inbox’s sending rules block one or more recipients. Nothing was submitted or charged. Use allowed recipients or ask the human to review [Sending rules](https://cherami.to/account/sending-rules); do not bypass them through another inbox.

* `X-Request-ID`: Support correlation ID, not an idempotency key.

Content type: `application/json`.

[Error](#schema-error)

### HTTP 404 [#http-404]

`not_found`: Resource is absent or inaccessible to this account. Reply targets must be in the sending inbox.

* `X-Request-ID`: Support correlation ID, not an idempotency key.

Content type: `application/json`.

[Error](#schema-error)

### HTTP 409 [#http-409]

`reply_not_ready`: A received reply or forward source must be ready; a sent source must have confirmed provider acceptance.

`reply_headers_unavailable`: The target has no usable RFC Message-ID. Send a new message without `in_reply_to`.

`idempotency_conflict`: The key belongs to different input. Recover with the original inbox and payload, not a replacement key.

`idempotency_result_unavailable`: The key was used but its inbox, draft or sent copy is unavailable. No replacement was created or submitted; do not bypass protection with a new key.

`draft_busy`: An edit/send raced other changes. Retrieve current content before trying again.

`draft_result_unavailable`: The draft was already submitted but its sent copy is unavailable. Nothing was resubmitted.

* `X-Request-ID`: Support correlation ID, not an idempotency key.

Content type: `application/json`.

[Error](#schema-error)

### HTTP 413 [#http-413]

`body_too_large`: Reduce the JSON request to the endpoint's body limit.

`message_too_large`: Reduce message content and attachments. Passing local checks does not guarantee generated MIME fits the provider limit.

* `X-Request-ID`: Support correlation ID, not an idempotency key.

Content type: `application/json`.

[Error](#schema-error)

### HTTP 415 [#http-415]

`unsupported_media_type`: Send `Content-Type: application/json`.

* `X-Request-ID`: Support correlation ID, not an idempotency key.

Content type: `application/json`.

[Error](#schema-error)

### HTTP 429 [#http-429]

`outbound_limit_reached`: Read `quota`, `reason` and `sufficient_capacity_at`. Waiting cannot fix `message_exceeds_allowance`; see [sending recovery](https://cherami.to/docs/api/sending/get-outbound-quota).

* `X-Request-ID`: Support correlation ID, not an idempotency key.
* `Retry-After`: Delay in seconds when supplied. Quota uses sufficient\_capacity\_at; absent when waiting cannot make the message fit.

Content type: `application/json`.

[QuotaError](#schema-quotaerror)

### HTTP 503 [#http-503]

`content_unavailable`: Expected stored content is unavailable. Retry the read later.

`draft_unavailable`: Draft operation is uncertain. Follow [draft-specific recovery](https://cherami.to/docs/api/drafts); do not blindly create a replacement.

* `X-Request-ID`: Support correlation ID, not an idempotency key.

Content type: `application/json`.

[Error](#schema-error)

## Schema: SendReceipt [#schema-sendreceipt]

Inspect message.status even on HTTP 201. accepted is provider acceptance, not delivery. Preserve a known outcome when outcome\_persisted is false; later reads may lag. Keyed receipts add replayed and expiry. Draft-association recovery adds replayed without requiring an expiry.

| Field                    | Required | Type                                 | Meaning and constraints                                                                                                              |
| ------------------------ | -------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
| `limited`                | Yes      | `false`                              |                                                                                                                                      |
| `message`                | Yes      | [SentMetadata](#schema-sentmetadata) |                                                                                                                                      |
| `outcome_persisted`      | Yes      | boolean                              |                                                                                                                                      |
| `replayed`               | No       | boolean                              |                                                                                                                                      |
| `idempotency_expires_at` | No       | string                               | UTC service instant with milliseconds and Z suffix. `format`: `date-time` `pattern`: `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$` |

## Schema: SentMetadata [#schema-sentmetadata]

| Field                 | Required | Type                                        | Meaning and constraints                                                                                                              |
| --------------------- | -------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `id`                  | Yes      | string                                      | Cherami resource ID, distinct from the RFC Message-ID. Use the returned value.                                                       |
| `inbox_id`            | Yes      | string                                      | Cherami resource ID, distinct from the RFC Message-ID. Use the returned value.                                                       |
| `created_at`          | Yes      | string                                      | UTC service instant with milliseconds and Z suffix. `format`: `date-time` `pattern`: `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$` |
| `recipient_count`     | Yes      | integer                                     | `minimum`: `0`                                                                                                                       |
| `status`              | Yes      | `"accepted"` or `"rejected"` or `"unknown"` |                                                                                                                                      |
| `provider_message_id` | Yes      | string or null                              |                                                                                                                                      |
| `error_code`          | Yes      | string or null                              |                                                                                                                                      |
| `thread_id`           | Yes      | string or null                              |                                                                                                                                      |
| `in_reply_to`         | Yes      | string or null                              |                                                                                                                                      |
| `labels`              | Yes      | array of string                             |                                                                                                                                      |

## Schema: Error [#schema-error]

| Field   | Required | Type   | Meaning and constraints |
| ------- | -------- | ------ | ----------------------- |
| `error` | Yes      | object |                         |

### `error` fields [#error-fields]

| Field     | Required | Type   | Meaning and constraints                                                                       |
| --------- | -------- | ------ | --------------------------------------------------------------------------------------------- |
| `code`    | Yes      | string | Programmatic error code. Handle unrecognized codes by status and operation-specific recovery. |
| `message` | Yes      | string | Human-readable context, not a stable string to match.                                         |

## Schema: QuotaError [#schema-quotaerror]

| Field                    | Required | Type                                                      | Meaning and constraints |
| ------------------------ | -------- | --------------------------------------------------------- | ----------------------- |
| `error`                  | Yes      | object                                                    |                         |
| `quota`                  | Yes      | [Quota](#schema-quota)                                    |                         |
| `requested_recipients`   | Yes      | integer                                                   | `minimum`: `0`          |
| `reason`                 | Yes      | `"temporary_exhaustion"` or `"message_exceeds_allowance"` |                         |
| `sufficient_capacity_at` | Yes      | string or null                                            |                         |
| `guidance`               | Yes      | string                                                    |                         |

### `error` fields [#error-fields-1]

| Field     | Required | Type                       | Meaning and constraints |
| --------- | -------- | -------------------------- | ----------------------- |
| `code`    | Yes      | `"outbound_limit_reached"` |                         |
| `message` | Yes      | string                     |                         |

## Schema: Quota [#schema-quota]

| Field                  | Required | Type                     | Meaning and constraints |
| ---------------------- | -------- | ------------------------ | ----------------------- |
| `allowance`            | Yes      | integer                  | `minimum`: `0`          |
| `used`                 | Yes      | integer                  | `minimum`: `0`          |
| `remaining`            | Yes      | integer                  | `minimum`: `0`          |
| `next_capacity_at`     | Yes      | string or null           |                         |
| `next_capacity_amount` | Yes      | integer                  | `minimum`: `0`          |
| `window_hours`         | Yes      | `24`                     |                         |
| `unit`                 | Yes      | `"recipient_deliveries"` |                         |
| `increase_request`     | Yes      | string                   |                         |
| `policy_url`           | Yes      | string                   |                         |

[HTTP conventions, errors and pagination](https://cherami.to/docs/api/errors) · [Download OpenAPI 3.1](https://cherami.to/openapi.json)
