Guides
Sending mail
Sends and replies are idempotent by design. Learn how to retry safely, what the limits are, and how to read delivery outcomes.
The idempotency key
Every send and reply requires an idempotencyKey, a string of 1 to 200 characters that you generate. Goshen Email uses it to make sure one intended email is sent at most once:
- A new intended email gets a new key. A UUID is a good choice.
- If the request times out, the connection drops, or you get a 5xx, retry with the same key and the same contents. The server returns the original result, and
deduplicated: truetells you it was a replay. - Never mint a new key because a response was lost. That is how duplicate emails happen.
- A retry with the same key but different contents is refused with
idempotency_conflict(409).
// Save the key before the first attempt so a crash and restart can still retry safely.
const idempotencyKey = crypto.randomUUID()
await saveDraftKey(taskId, idempotencyKey)
await email.messages.send({ inboxId, to, subject, text, idempotencyKey })
REST callers may pass the key in an Idempotency-Key header instead of the body. If both are present they must match.
The CLI and MCP server never retry on their own. If a send gets no response, for example a timeout or a network_error from the CLI, the outcome is unknown. Retry with the saved key; the failure does not prove that nothing was sent.
Send
POST /v1/inboxes/{inboxId}/messages/send
| Field | Notes |
|---|---|
to | Required. 1 to 50 addresses. |
cc, bcc | Optional. The total across to, cc, and bcc is capped at 50. |
subject | Up to 998 characters, no line breaks. Defaults to empty. |
text, html | At least one is required. Together they must fit in 512 KiB. |
attachments | Up to 10 files, 2 MiB combined. See Attachments. |
labels | Up to 50 labels to apply to the sent message. |
idempotencyKey | Required. |
The response is { "messageId", "threadId", "deduplicated"? }. The message appears in the inbox with the sent label right away; delivery happens asynchronously.
The sender name on outgoing mail is the inbox's displayName; the address is the inbox address.
Reply
POST /v1/inboxes/{inboxId}/messages/{messageId}/reply
A reply keeps the thread. By default it goes to the original sender, or to the Reply-To address when the sender set one; replying to a message the inbox itself sent addresses that message's recipients. Set replyAll: true to also cc everyone else on the original message, or pass your own to, cc, and bcc. The inbox's own address is never added. Body, attachments, labels, and idempotencyKey work as in send.
You cannot reply to a quarantined message (message_quarantined, 403) until a person releases it.
Limits
| Limit | Value |
|---|---|
| Recipients per message | 50 across to, cc, and bcc |
| Body size | 512 KiB, text and html combined |
| Attachments | 10 files, 2 MiB combined |
| Subject | 998 characters |
| Sends per inbox | 250 in any rolling 24-hour window by default |
Hitting the send limit returns rate_limited (429) with transient: true. Wait and retry with the same idempotency key; the send is not lost. See Limits.
Delivery outcomes
Sending returns as soon as the message is accepted for delivery. The recipient's server answers later, and that answer is recorded on the message as delivery with one entry per recipient:
| Status | Meaning |
|---|---|
queued, accepted | Handed to the outbound path; no remote answer yet |
delivered | The recipient's server accepted the message (SMTP 250) |
deferred | The remote server asked us to try again later |
bounced | The remote server rejected it permanently; see reason and the SMTP codes |
failed, rejected | Delivery could not be completed |
complained | The recipient reported the message as spam |
delivered means the receiving server took the message, not that it reached the inbox or was read. The dashboard shows the same outcomes on each sent message.
Pending sends
If a send is still being processed when you retry, the API answers send_pending (409). The first request is in flight; wait a moment and retry with the same key.
Who decides a send happens
The operation descriptions, which the MCP server passes to every connected model, ask the agent to send only with the user's authorization. What "authorization" means is yours to define in code: a person approving each draft, a policy that lets the agent reply within threads it started, or full autonomy for an inbox that only ever confirms sign-ups. Scope the key to match. See Building agents on email.