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:

// 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

FieldNotes
toRequired. 1 to 50 addresses.
cc, bccOptional. The total across to, cc, and bcc is capped at 50.
subjectUp to 998 characters, no line breaks. Defaults to empty.
text, htmlAt least one is required. Together they must fit in 512 KiB.
attachmentsUp to 10 files, 2 MiB combined. See Attachments.
labelsUp to 50 labels to apply to the sent message.
idempotencyKeyRequired.

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

LimitValue
Recipients per message50 across to, cc, and bcc
Body size512 KiB, text and html combined
Attachments10 files, 2 MiB combined
Subject998 characters
Sends per inbox250 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:

StatusMeaning
queued, acceptedHanded to the outbound path; no remote answer yet
deliveredThe recipient's server accepted the message (SMTP 250)
deferredThe remote server asked us to try again later
bouncedThe remote server rejected it permanently; see reason and the SMTP codes
failed, rejectedDelivery could not be completed
complainedThe 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.