Send a message
POST /v1/inboxes/{inbox_id}/sent
Read as Markdown ↗POST /v1/inboxes/{inbox_id}/sent
Requires a Claim-issued API key: Authorization: Bearer YOUR_CREDENTIAL.
Follow permitted sending.
Inspect the inbox’s sending rules before preparing a message. All send, reply, reply-all and forward paths enforce every To/Cc/Bcc destination. A new blocked attempt returns 403 recipient_not_allowed without submission or quota use. An existing keyed attempt remains replayable after a policy edit, without resubmission.
For saved preparation and later submission, use the draft API. Draft sending uses the outcomes below, but the draft ID itself prevents another submission without expiry.
Request fields
Recipient inputs are named mailbox objects, never bare strings or assembled header syntax. Names are display metadata, not identity verification; addresses determine delivery.
The owned inbox supplies From and its configured sender name. Bcc addresses and names remain in the sender’s private saved copy but are not exposed in delivered recipient headers.
At most 50 combined To/Cc/Bcc entries are accepted. Unknown top-level and attachment fields are rejected. You cannot override From or supply arbitrary headers, remote attachment URLs, inline attachments, or raw MIME.
JSON is limited to 8 MiB. The total email must fit the provider's 5 MiB limit, including generated MIME and attachments. Local size checks do not guarantee that generated MIME will fit.
Cherami appends “Sent via Cherami” to plain text and supplied HTML, after your body including quoted history. Do not add it yourself. Returned sent bodies include the attribution.
Reply targets
in_reply_to is a resource ID, not an RFC Message-ID or thread ID. Received parents must be ready; sent parents must be accepted. Both need a usable Message-ID and must belong to the sending inbox. Cherami sets In-Reply-To and accumulated References, shortening long ancestry as needed.
On this explicit-send endpoint, supply recipients and subject yourself. For derived recipients and subject, use reply or reply-all. There is no implicit latest-message selection. Unready, unaccepted, or headerless targets return 409; missing, deleted, other-account, or other-inbox targets return 404.
Response and outcomes
A created sent resource returns 201 and Location: /v1/sent/{id}.
Inspect message.status, not just HTTP status: accepted means provider acceptance, not delivery; rejected means explicit pre-acceptance rejection; unknown means acceptance could not be confirmed. Accepted and unknown attempts retain their sending charge. A retry recovers the reserved attempt without resubmitting it.
provider_message_id, error_code, thread_id, and in_reply_to can be null. in_reply_to identifies the Cherami parent resource when available.
When outcome_persisted is false, the response reports a known provider outcome that could not be saved. Later reads may still say unknown; do not resend because of that mismatch. There is no automatic provider-submission retry or delivery/bounce tracking.
If a response is lost or an infrastructure error reports uncertainty, follow the same-key recovery contract below. Without a key, inspect sent messages before considering another send. Repeating an unkeyed POST can send a duplicate. An absent match in a short list alone is not proof that retrying is safe.
Provider errors such as E_RECIPIENT_SUPPRESSED, E_RATE_LIMIT_EXCEEDED, or E_DAILY_LIMIT_EXCEEDED are reported as rejected sent outcomes, not necessarily HTTP errors. A suppressed recipient rejects the whole submission.
Retry a send with an idempotency key
Keys are scoped to the authenticated account across HTTP and MCP, not to a connection or inbox. Protection lasts 24 hours from the first reserved attempt, without renewal on retries. Keyed results include replayed and idempotency_expires_at (ISO timestamp). A new attempt returns 201 with replayed: false; a matching retry returns 200 with replayed: true, the original message.id and its current saved outcome. Both include Location. A replay adds no submission or quota charge.
Use the same key, inbox and message fields for a retry. JSON property order does not matter; omitted and empty optional recipient/attachment arrays are equivalent. Recipient addresses, names and ordering, attachment ordering, body text, HTML, subject and reply target do matter. Address spelling is retained; changing its case changes the request fingerprint. Display names are trimmed, with omitted and blank names equivalent. Changes to the inbox’s configured sender name do not change a replay: the original submission retains the identity used at that time. Initial labels also matter, but their order and duplicates do not; omitted and empty label arrays are equivalent. Later label edits do not change the original retry input, and a replay does not reapply initial labels. Reusing an active key for different input returns 409 idempotency_conflict without sending. Deleting the sent copy does not free its active key: a matching retry returns 409 idempotency_result_unavailable. Ordinary access and inbox-ownership checks still apply.
The first request may still be running when a retry returns unknown. It may also have stopped before submitting or lost the provider's response. Cherami never resumes that reserved attempt on a retry. This prevents a second application submission but may leave an email unsent; it does not guarantee delivery or resolve uncertainty. Use GET /v1/sent/{message_id} to inspect it later. outcome_persisted: true on a replay describes the saved state being returned, not proof that it captures the provider's final outcome.
Validation, authorization and quota failures before reservation do not consume the key. An uncertain infrastructure failure may have reserved it, so reuse the original key and unchanged payload to recover. Never generate a replacement key to bypass uncertainty. After expiry, the same key can create a new send: do not retry an uncertain email after the window. If the initial response was lost, measure the window conservatively from your first request time. A deliberately new email needs a new key.
Requests without a key retain their existing behavior: every POST can create a separate send.
Parameters
| Parameter | Location | Required | Type | Meaning |
|---|---|---|---|---|
inbox_id | path | Yes | string | Owned Cherami resource ID returned by the API. |
Request body
JSON object. Total UTF-8 request body limit: 8388608 bytes.
Schema: SendInput
At most 50 combined To/Cc/Bcc entries. Local body/encoded-attachment sum is bounded to 5 MiB; generated MIME must also fit the provider's 5 MiB limit. No From override, arbitrary headers or inline attachment inputs.
| Field | Required | Type | Meaning and constraints |
|---|---|---|---|
to | Yes | array of Mailbox | maxItems: 50 minItems: 1 |
cc | No | array of Mailbox | maxItems: 50 |
bcc | No | array of Mailbox | maxItems: 50 |
subject | Yes | string | No control characters; at most 998 UTF-8 bytes. Must not be blank. x-max-utf8-bytes: 998 |
text | Yes | string | Required nonblank plain text. |
html | No | string | HTML alternative. Untrusted content, not sanitized markup. |
attachments | No | array of AttachmentInput or null | Omitted or null means no attachments; an empty array also clears draft attachments. |
in_reply_to | No | string | Received or accepted sent resource ID in this inbox. The source must have usable reply headers. pattern: ^[a-f0-9-]{36}$ |
labels | No | array of Label | Trimmed, deduplicated and case-sensitive. Order is not significant. maxItems: 32 |
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
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.
{
"to": [
{
"address": "recipient@example.com",
"name": "Alex"
}
],
"subject": "Notes",
"text": "Here are the notes.",
"idempotency_key": "RETAIN_A_UNIQUE_SEND_KEY"
}curl --silent --show-error --include --request POST \
"https://cherami.to/v1/inboxes/INBOX_ID/sent" \
--header "Authorization: Bearer $CHERAMI_API_KEY" \
--header 'Content-Type: application/json' \
--data-binary @request.jsonResponses
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 2
| Field | Required | Type | Meaning and constraints |
|---|---|---|---|
replayed | Yes | true | |
idempotency_expires_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$ |
{
"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,
"idempotency_expires_at": "2026-10-02T00:00:00.000Z"
}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 2
| Field | Required | Type | Meaning and constraints |
|---|---|---|---|
replayed | No | false |
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.
X-Request-ID: Support correlation ID, not an idempotency key.
Content type: application/json.
HTTP 401
unauthorized: Provide a valid bearer credential. Use human-approved recovery if access is lost.
X-Request-ID: Support correlation ID, not an idempotency key.WWW-Authenticate:"Bearer"
Content type: application/json.
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; do not bypass them through another inbox.
X-Request-ID: Support correlation ID, not an idempotency key.
Content type: application/json.
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.
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.
X-Request-ID: Support correlation ID, not an idempotency key.
Content type: application/json.
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.
HTTP 415
unsupported_media_type: Send Content-Type: application/json.
X-Request-ID: Support correlation ID, not an idempotency key.
Content type: application/json.
HTTP 429
outbound_limit_reached: Read quota, reason and sufficient_capacity_at. Waiting cannot fix message_exceeds_allowance; see sending recovery.
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.
HTTP 503
content_unavailable: Expected stored content is unavailable. Retry the read later.
outbound_unavailable: Outbound operation failed and a send's outcome may be unknown. Recover using the original key and unchanged payload within its window, or inspect sent messages.
X-Request-ID: Support correlation ID, not an idempotency key.
Content type: application/json.
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 | |
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
| 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
| Field | Required | Type | Meaning and constraints |
|---|---|---|---|
error | Yes | object |
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
| Field | Required | Type | Meaning and constraints |
|---|---|---|---|
error | Yes | object | |
quota | Yes | 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
| Field | Required | Type | Meaning and constraints |
|---|---|---|---|
code | Yes | "outbound_limit_reached" | |
message | Yes | string |
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 |
Schema: Mailbox
| Field | Required | Type | Meaning and constraints |
|---|---|---|---|
address | Yes | string | Bare ASCII address, at most 254 characters, local part at most 64. No display-name header syntax. maxLength: 254 |
name | No | string | Unicode name, trimmed; blank means unnamed. No control characters. At most 256 UTF-8 bytes after trimming. x-max-utf8-bytes: 256 |
Unknown fields are rejected.
Schema: AttachmentInput
| Field | Required | Type | Meaning and constraints |
|---|---|---|---|
filename | Yes | string | Nonempty, no control characters, slash or backslash. At most 255 UTF-8 bytes. minLength: 1 x-max-utf8-bytes: 255 |
type | Yes | string | MIME type without parameters. maxLength: 127 pattern: ^[A-Za-z0-9!#$&^_.+-]+/[A-Za-z0-9!#$&^_.+-]+$ |
content | Yes | string | Padded base64 original bytes, no whitespace; encoded length must be a multiple of four. Empty files are accepted. contentEncoding: base64 pattern: ^[A-Za-z0-9+/]*={0,2}$ |
Unknown fields are rejected.
Schema: Label
1–128 UTF-8 bytes after trimming. Well-formed Unicode without control characters; case-sensitive.
string. x-max-utf8-bytes: 128
HTTP conventions, errors and pagination · Download OpenAPI 3.1