Docs

Sending and replying

Send new messages, reply in thread and retry safely with Idempotency-Key.

View as Markdown

Send a new message

POST /v0/inboxes/{inbox}/messages/send starts a new thread. to needs at least one address; text or html is required. The message is created and then attempted once, inside the request: 200 with the message_id and thread_id means the transport accepted it, 502 email_delivery_failed means it did not, and the body says why in delivery_error_code and delivery_error. Delivery after acceptance is reported as message.sent, message.delivered, message.delivery_delayed, message.rejected or message.failed, and the delivery timeline section below says which one and why. The bounce and complaint breaker pauses sending when a window crosses a limit: sending_health on the inbox carries the counts, the thresholds and paused_scope, and the call returns 403 sending_paused until support resumes it.

cURL
curl -s -X POST "https://api.mueslimail.com/v0/inboxes/billing@agents.your-domain.eu/messages/send" \
  -H "Authorization: Bearer $MAI_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042-confirmation" \
  -d '{"to": ["customer@example.com"], "subject": "Order 1042", "text": "Your order has shipped."}'

Reply

POST /v0/inboxes/{inbox}/messages/{message}/reply answers in the same thread. Recipients, subject, In-Reply-To and References are set for you. Set reply_all to include the other recipients: your own address and the original Bcc are never among them, so a reply all cannot hand a hidden recipient to everybody else.

Forward

POST /v0/inboxes/{inbox}/messages/{message}/forward sends a message on with to, optional cc/bcc and an optional comment. The recipients are the ones you name and nobody else: the addresses of the original message are never the default, because that is how a conversation leaks. The subject becomes Fwd: <original> once, without stacking prefixes.

  • The attachments of the original are re-shared unchanged: same bytes, same filenames, same Content-ID, so an inline image keeps resolving from the quoted HTML.
  • The forward stays in the conversation: References accumulates the chain plus the forwarded message, and In-Reply-To is that message, the same rule a reply follows.
  • The body is your comment, then the signature of the address, then the original under a ---------- Forwarded message ---------- header.
  • A message held by screening cannot be forwarded: the answer is 404, because its content must not leave before somebody reads it.
Forward a message
curl -s -X POST "https://api.mueslimail.com/v0/inboxes/billing@agents.your-domain.eu/messages/$MESSAGE_ID/forward" \
  -H "Authorization: Bearer ***" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: forward-invoice-1042" \
  -d '{"to": ["accounting@example.com"], "comment": "For the March close."}'

Signature per address

A signature belongs to an address, not to the organization: PATCH /v0/inboxes/{inbox} with signature_html writes it for that inbox, and GET returns it. An empty string removes it. It is appended to every message the address composes: a send, a reply, a forward and a draft. In a reply and a forward it sits under your own words and above the quoted message; in a text only message the HTML is rendered to plain text, so a reader that shows no HTML still sees who wrote it.

Undo send

Undo send is a state of the message, not a timer in a client. With MAI_UNDO_SEND_SECONDS set (5, 10 or 20; 0 turns the feature off) a send is created held with cancel_window_ends_at, and nothing is attempted until that moment: the answer carries status: held and the deadline. The scheduler releases the message when the window closes, and the sender attempts it then.

  • POST /v0/inboxes/{inbox}/messages/{message}/cancel inside the window withdraws the message: it becomes cancelled, message.cancelled fires, and message.sent never does, because it was never sent.
  • Outside the window, on a message that is not held, or on inbound mail the answer is 409 invalid_state naming the state the message is in. A cancel that quietly did nothing would tell you your mail did not leave when it did.
  • message.sent fires at the delivery that actually happened, never at the request: a held send has no event until it leaves. A held send whose window closes while sending is paused becomes failed with the reason of the pause in last_error, rather than waiting for a resume that may never come.
  • The window lives on the server: reload the page, close the console, open another device, the deadline is the same one.

Idempotency-Key

Agents retry. To make retries safe, send an Idempotency-Key header (up to 256 characters) on send and reply:

  • The same key with the same request within 24 hours returns the stored result and sends nothing new.
  • The same key with a different request returns 409 conflict.
  • While the first request with a key is still running, a second one returns 409 conflict: retry to get the result.

Derive the key from your own state (an order id, a task id, the id of the message you answer), not from a random value generated at retry time.

Delivery

Messages are DKIM signed with your domain's key. Mail to another inbox on the platform is delivered locally (agent to agent) without leaving the system. Other mail goes out through an EU SMTP relay.

Deliverability test addresses

Four local parts on the service domain test.<shared domain> produce an outcome without an SMTP attempt, so a webhook, a rule or a console state can be exercised without spending the reputation of a real domain and without a second environment: delivered@, bounced@, complained@ and suppressed@. A +label after the local part is ignored (delivered+invoice@test.<shared domain>).

  • Nothing addressed this way leaves the platform: the recipients are removed before any MX is resolved, so no relay and no recipient sees the message.
  • Each one writes the timeline row and the webhook event it names, with source test, and adds the bounced, complained or suppressed label to the message.
  • A simulated outcome is never an outcome of a real recipient: it does not count towards the bounce or complaint rate, so a test bounce can never pause your sending. The delivery test of the production paths stays a real send to a mailbox you control.

When a message is not delivered

A send is attempted once, in the request, and the answer is what the mail server said. 502 email_delivery_failed carries delivery_error_code, the reason in machine terms, and delivery_error, the reply of the server in one line (at most 500 characters, never a credential). The same two fields are on the message row, so a refused message stays visible in its thread as status: failed and the console marks it Not delivered.

  • unauthorized_ip: an SMTP 525. The relay does not allow this server's address, so nothing can be delivered until the provider authorises it. Contact support.
  • auth_failed: an SMTP 535. The relay rejected the credentials.
  • relay_unreachable, connection_refused, tls_failed: the relay could not be reached, refused the connection, or the encrypted session failed.
  • unknown: the server refused the message for another reason, for example a recipient address that does not exist. delivery_error carries the reply.

A permanent refusal is not retried: the message is failed, so nothing is sent twice. A temporary one (a 4xx reply, a relay that is momentarily down) keeps the message queued and retried after 1, 5 and 30 minutes, then 1 and 2 hours, then every 3 hours within 5 days, before it is marked failed. GET /v0/organization reports the state of outbound mail for the whole organization: ok, degraded after three refused sends in a row, or blocked when every attempt of the last 24 hours was refused for an authorization reason. The console shows that state at the top of the inbox and above the composer.

Delivery timeline and reputation

GET /v0/inboxes/{inbox}/messages/{message}/timeline returns every state of one message in order, each with the source that knew it: smtp (a reply from the mail server), dsn (a delivery status notification), arf (a feedback loop report), breaker (the sending health breaker), rspamd (the spam filter), rule (a deterministic rule) or test (a deliverability test address, see below). code is the status and text the reply in one line, at most 500 characters; an unknown code always carries a written reason.

  • message.delivered is the transport accepting the message; message.sent reports the same moment as the outcome of the send.
  • message.delivery_delayed is an attempt the remote server asked to repeat, or a delay report that came back. It is never a rejection.
  • message.rejected is a definitive 5xx refusal at send time, never a retry. message.dropped is a message discarded by policy without an attempt, such as the breaker pausing sending.
  • message.bounced comes from a delivery status notification and message.complained from a feedback loop report. Both count against the reputation of the domain.
  • message.suppressed is a send to an address on a suppression list: the message was not attempted. message.delivered, message.bounced, message.complained and message.suppressed with source test are simulated by a test address and never count against the reputation.

GET /v0/domains/{domain}/reputation reports bounce_rate, complaint_rate, the window and the thresholds of the sending health breaker, plus breach_state (ok or breached) and the pause the breaker applied (paused_scope, paused_at, paused_reason). The window and the thresholds are the ones sending_health returns on an inbox, not a copy: a number that would pause the mail is the number this endpoint reads.

Limits

  • Up to 50 recipients per message across to, cc and bcc.
  • Daily and monthly send limits depend on the plan; going over returns 429 limit_exceeded. See Limits.
  • On the free plan a short footer is appended: "Sent via Mueslimail".

Drafts

A draft is a message saved for later: for a human to review, or to send at send_at. Create it with the same fields as a send; recipients and a body are only needed when it is sent. With in_reply_to (a message id) the draft is a reply: to, subject and the threading headers default to those of that message, and the sent message joins its thread. POST .../drafts/{draft}/send sends it now through the normal send path (plan limits, 403 sending_paused, Idempotency-Key) and deletes the draft.

  • send_at schedules the draft; the scheduler checks every minute. PATCH with send_at: null cancels it.
  • If a scheduled send is refused (for example a plan limit), the draft gets status: failed and last_error. Edit it or send it by hand to retry.
  • Creating a draft needs message_send; reading drafts needs message_read. draft.created fires for every new draft.

Endpoints

POST/v0/inboxes/{inbox}/messages/send

Send a new message (new thread)

Parameters

NameTypeNotes
inboxstring (email)in path, required
Idempotency-Keystringin header, max length 256

Body (Send)

NameTypeNotes
tostring (email)[]required, at least 1 item
ccstring (email)[]
bccstring (email)[]
subjectstring
textstring
htmlstring
labelsstring[]
attachmentsAttachment[]

Responses

  • 200 Accepted by the transport, or queued when the install has no transport: SendResult
  • 403 Error envelope: Error
  • 409 Error envelope: Error
  • 422 Error envelope: Error
  • 429 Error envelope: Error
  • 502 email_delivery_failed: the transport did not accept the message, with delivery_error_code and delivery_error: Error
  • 503 Error envelope: Error

GET/v0/organization

The organization and the state of its outbound mail

Responses

  • 200 OK: Organization
  • 401 Error envelope: Error
  • 403 Error envelope: Error

POST/v0/inboxes/{inbox}/messages/{message}/reply

Reply in the same thread

Parameters

NameTypeNotes
inboxstring (email)in path, required
messagestringin path, required, Message UUID or RFC Message-ID (URL encoded)
Idempotency-Keystringin header, max length 256

Body (Reply)

NameTypeNotes
textstring
htmlstring
reply_allboolean
labelsstring[]
attachmentsAttachment[]

Responses

  • 200 Accepted by the transport, or queued when the install has no transport: SendResult
  • 403 Error envelope: Error
  • 422 Error envelope: Error
  • 429 Error envelope: Error
  • 502 email_delivery_failed: the transport did not accept the reply, with delivery_error_code and delivery_error: Error
  • 503 Error envelope: Error

POST/v0/inboxes/{inbox}/messages/{message}/forward

Forward a message with its original attachments and an optional comment

Parameters

NameTypeNotes
inboxstring (email)in path, required
messagestringin path, required, Message UUID or RFC Message-ID (URL encoded)
Idempotency-Keystringin header, max length 256

Body (Forward)

NameTypeNotes
tostring (email)[]required, at least 1 item
ccstring (email)[]
bccstring (email)[]
commentstringOptional text above the quoted message, below the signature
labelsstring[]

Responses

  • 200 Accepted by the transport, held by the undo window, or queued when the install has no transport: SendResult
  • 400 Error envelope: Error
  • 403 Error envelope: Error
  • 404 Error envelope: Error
  • 429 Error envelope: Error
  • 502 email_delivery_failed: the transport refused the forward, with delivery_error_code and delivery_error: Error

POST/v0/inboxes/{inbox}/messages/{message}/cancel

Cancel a held send inside its undo window (needs message_send)

Parameters

NameTypeNotes
inboxstring (email)in path, required
messagestringin path, required, Message UUID or RFC Message-ID (URL encoded)

Responses

  • 200 Cancelled: Message
  • 403 Error envelope: Error
  • 404 Error envelope: Error
  • 409 Error envelope: Error

PATCH/v0/inboxes/{inbox}

Update the display name, the signature, or the support intake of an inbox

Parameters

NameTypeNotes
inboxstring (email)in path, required

Body

NameTypeNotes
display_namestringmax length 200
ticket_intakebooleanNew threads on this inbox become tickets in the support queue.
signature_htmlstringHTML signature of this address. Empty string removes it., max length 20000

Responses

  • 200 OK: Inbox

POST/v0/inboxes/{inbox}/drafts

Create a draft (needs message_send)

Parameters

NameTypeNotes
inboxstring (email)in path, required

Body (CreateDraft)

NameTypeNotes
tostring (email)[]
ccstring (email)[]
bccstring (email)[]
subjectstring
textstring
htmlstring
labelsstring[]
attachmentsAttachment[]
in_reply_tostringMessage UUID or RFC Message-ID to reply to
reply_allbooleanWith in_reply_to: copy the other recipients
send_atstring (date-time)Send automatically at this time (future, within 365 days)

Responses

  • 201 Created: Draft
  • 400 Error envelope: Error
  • 404 Error envelope: Error

GET/v0/inboxes/{inbox}/drafts

List drafts, newest first (needs message_read)

Parameters

NameTypeNotes
inboxstring (email)in path, required
limitintegerin query, min 1, max 100, default 20
page_tokenstringin query

Responses

  • 200 OK: DraftList

GET/v0/inboxes/{inbox}/drafts/{draft}

Get a draft (needs message_read)

Parameters

NameTypeNotes
inboxstring (email)in path, required
draftstring (uuid)in path, required

Responses

  • 200 OK: Draft
  • 404 Error envelope: Error

PATCH/v0/inboxes/{inbox}/drafts/{draft}

Update a draft (needs message_send)

Parameters

NameTypeNotes
inboxstring (email)in path, required
draftstring (uuid)in path, required

Body (UpdateDraft)

NameTypeNotes
tostring (email)[]
ccstring (email)[]
bccstring (email)[]
subjectstring
textstring
htmlstring
labelsstring[]
attachmentsAttachment[]
send_atstring,null (date-time)null cancels a scheduled send

Responses

  • 200 OK: Draft
  • 400 Error envelope: Error
  • 404 Error envelope: Error

DELETE/v0/inboxes/{inbox}/drafts/{draft}

Delete a draft (needs message_send)

Parameters

NameTypeNotes
inboxstring (email)in path, required
draftstring (uuid)in path, required

Responses

  • 204 Deleted

POST/v0/inboxes/{inbox}/drafts/{draft}/send

Send a draft now and delete it (needs message_send)

Parameters

NameTypeNotes
inboxstring (email)in path, required
draftstring (uuid)in path, required
Idempotency-Keystringin header, max length 256

Responses

  • 200 Accepted by the transport, or queued when the install has no transport: SendResult
  • 400 Error envelope: Error
  • 403 Error envelope: Error
  • 404 Error envelope: Error
  • 409 Error envelope: Error
  • 429 Error envelope: Error
  • 502 email_delivery_failed: the transport did not accept the message, with delivery_error_code and delivery_error: Error

GET/v0/inboxes/{inbox}/messages/{message}/timeline

The delivery states of one message, oldest first, with the source of each (requires message_read)

Parameters

NameTypeNotes
inboxstring (email)in path, required
messagestringin path, required, Message UUID or RFC Message-ID (URL encoded)

Responses

  • 200 OK: MessageTimeline
  • 403 Error envelope: Error
  • 404 Error envelope: Error

GET/v0/domains/{domain}/reputation

Bounce and complaint reputation of a domain, measured like the sending breaker (requires domain_read)

Parameters

NameTypeNotes
domainstringin path, required

Responses

  • 200 OK: DomainReputation
  • 403 Error envelope: Error
  • 404 Error envelope: Error

Send object

NameTypeNotes
tostring (email)[]required, at least 1 item
ccstring (email)[]
bccstring (email)[]
subjectstring
textstring
htmlstring
labelsstring[]
attachmentsAttachment[]

Reply object

NameTypeNotes
textstring
htmlstring
reply_allboolean
labelsstring[]
attachmentsAttachment[]

Forward object

NameTypeNotes
tostring (email)[]required, at least 1 item
ccstring (email)[]
bccstring (email)[]
commentstringOptional text above the quoted message, below the signature
labelsstring[]

SendResult object

NameTypeNotes
message_idstring
thread_idstring (uuid)
statusqueued | held | sent | suppressedState of the row the request created. held means the undo window is open: nothing has been attempted yet, and cancel may still withdraw it. suppressed means every recipient is on the suppression list: no message exists because there was nobody to write to.
cancel_window_ends_atstring (date-time)Deadline of POST .../cancel, present when status is held.
recipientsRecipientOutcome[]One entry per recipient of the request, in the order it named them. A suppressed recipient is reported here and not hidden: the message is never written for it.

Draft object

NameTypeNotes
draft_idstring (uuid)
inbox_idstring (email)
thread_idstring (uuid)Set for a reply draft
in_reply_tostringRFC Message-ID of the message replied to
referencesstring[]
tostring[]
ccstring[]
bccstring[]
subjectstring
textstring
htmlstring
labelsstring[]
attachmentsobject[]
send_atstring (date-time)
statusdraft | scheduled | failed
last_errorstringWhy a scheduled send was refused
created_atstring (date-time)
updated_atstring (date-time)

MessageTimeline object

NameTypeNotes
message_idstring (uuid)
inbox_idstringThe address that holds the message, never the list of recipients
statusreceived | queued | sending | sent | failed | held | cancelledThe state of the row now; the events carry how it got there
eventsMessageTimelineEvent[]

MessageTimelineEvent object

NameTypeNotes
typemessage.received | message.received.spam | message.sent | message.delivered | message.delivery_delayed | message.failed | message.rejected | message.bounced | message.complained | message.dropped | message.ruled
sourcesmtp | dsn | arf | breaker | rspamd | rulesmtp a reply, dsn a delivery status notification, arf a feedback report, breaker the sending health breaker, rspamd the spam filter, rule a deterministic rule
codestringThe SMTP status, the DSN status or the policy code; empty when the source has none. unknown always carries a reason in text.
textstringThe reply or the sentence, one line, at most 500 characters, never a body and never a recipient
atstring (date-time)

DomainReputation object

NameTypeNotes
domainstring
window_hoursintegerThe rolling window the counts cover, the same as sending_health.window_hours
sendsintegerMessages that reached a final outcome in the window
hard_bouncesintegerPermanent rejections at send time plus bounces reported by a DSN
complaintsintegerSpam complaints reported through the feedback loop
bounce_ratenumberhard_bounces divided by sends, 0 when sends is 0
complaint_ratenumbercomplaints divided by sends, 0 when sends is 0
thresholdsSendingThresholds
breach_stateok | breachedbreached when the window trips a limit or when the breaker has paused an inbox on the domain or the organization
breach_reasonstringThe sentence naming the window and the limit crossed; absent while breach_state is ok
paused_scopestringWhat paused sending: inbox, organization, or empty. A send is refused by both scopes, so this tells them apart.
paused_atstring (date-time)
paused_reasonstringThe sentence of the pause, the same one the 403 sending_paused carries