Docs
Sending and replying
Send new messages, reply in thread and retry safely with Idempotency-Key.
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 -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:
Referencesaccumulates the chain plus the forwarded message, andIn-Reply-Tois 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.
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}/cancelinside the window withdraws the message: it becomescancelled,message.cancelledfires, andmessage.sentnever 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_statenaming the state the message is in. A cancel that quietly did nothing would tell you your mail did not leave when it did. message.sentfires 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 becomesfailedwith the reason of the pause inlast_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 thebounced,complainedorsuppressedlabel 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_errorcarries 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.deliveredis the transport accepting the message;message.sentreports the same moment as the outcome of the send.message.delivery_delayedis an attempt the remote server asked to repeat, or a delay report that came back. It is never a rejection.message.rejectedis a definitive 5xx refusal at send time, never a retry.message.droppedis a message discarded by policy without an attempt, such as the breaker pausing sending.message.bouncedcomes from a delivery status notification andmessage.complainedfrom a feedback loop report. Both count against the reputation of the domain.message.suppressedis a send to an address on a suppression list: the message was not attempted.message.delivered,message.bounced,message.complainedandmessage.suppressedwith sourcetestare 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,ccandbcc. - 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_atschedules the draft; the scheduler checks every minute.PATCHwithsend_at: nullcancels it.- If a scheduled send is refused (for example a plan limit), the draft gets
status: failedandlast_error. Edit it or send it by hand to retry. - Creating a draft needs
message_send; reading drafts needsmessage_read.draft.createdfires for every new draft.
Endpoints
POST/v0/inboxes/{inbox}/messages/send
Send a new message (new thread)
Parameters
| Name | Type | Notes |
|---|---|---|
inbox | string (email) | in path, required |
Idempotency-Key | string | in header, max length 256 |
Body (Send)
| Name | Type | Notes |
|---|---|---|
to | string (email)[] | required, at least 1 item |
cc | string (email)[] | |
bcc | string (email)[] | |
subject | string | |
text | string | |
html | string | |
labels | string[] | |
attachments | Attachment[] |
Responses
200Accepted by the transport, or queued when the install has no transport: SendResult403Error envelope: Error409Error envelope: Error422Error envelope: Error429Error envelope: Error502email_delivery_failed: the transport did not accept the message, with delivery_error_code and delivery_error: Error503Error envelope: Error
GET/v0/organization
The organization and the state of its outbound mail
Responses
200OK: Organization401Error envelope: Error403Error envelope: Error
POST/v0/inboxes/{inbox}/messages/{message}/reply
Reply in the same thread
Parameters
| Name | Type | Notes |
|---|---|---|
inbox | string (email) | in path, required |
message | string | in path, required, Message UUID or RFC Message-ID (URL encoded) |
Idempotency-Key | string | in header, max length 256 |
Body (Reply)
| Name | Type | Notes |
|---|---|---|
text | string | |
html | string | |
reply_all | boolean | |
labels | string[] | |
attachments | Attachment[] |
Responses
200Accepted by the transport, or queued when the install has no transport: SendResult403Error envelope: Error422Error envelope: Error429Error envelope: Error502email_delivery_failed: the transport did not accept the reply, with delivery_error_code and delivery_error: Error503Error envelope: Error
POST/v0/inboxes/{inbox}/messages/{message}/forward
Forward a message with its original attachments and an optional comment
Parameters
| Name | Type | Notes |
|---|---|---|
inbox | string (email) | in path, required |
message | string | in path, required, Message UUID or RFC Message-ID (URL encoded) |
Idempotency-Key | string | in header, max length 256 |
Body (Forward)
| Name | Type | Notes |
|---|---|---|
to | string (email)[] | required, at least 1 item |
cc | string (email)[] | |
bcc | string (email)[] | |
comment | string | Optional text above the quoted message, below the signature |
labels | string[] |
Responses
200Accepted by the transport, held by the undo window, or queued when the install has no transport: SendResult400Error envelope: Error403Error envelope: Error404Error envelope: Error429Error envelope: Error502email_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
| Name | Type | Notes |
|---|---|---|
inbox | string (email) | in path, required |
message | string | in path, required, Message UUID or RFC Message-ID (URL encoded) |
Responses
200Cancelled: Message403Error envelope: Error404Error envelope: Error409Error envelope: Error
PATCH/v0/inboxes/{inbox}
Update the display name, the signature, or the support intake of an inbox
Parameters
| Name | Type | Notes |
|---|---|---|
inbox | string (email) | in path, required |
Body
| Name | Type | Notes |
|---|---|---|
display_name | string | max length 200 |
ticket_intake | boolean | New threads on this inbox become tickets in the support queue. |
signature_html | string | HTML signature of this address. Empty string removes it., max length 20000 |
Responses
200OK: Inbox
POST/v0/inboxes/{inbox}/drafts
Create a draft (needs message_send)
Parameters
| Name | Type | Notes |
|---|---|---|
inbox | string (email) | in path, required |
Body (CreateDraft)
| Name | Type | Notes |
|---|---|---|
to | string (email)[] | |
cc | string (email)[] | |
bcc | string (email)[] | |
subject | string | |
text | string | |
html | string | |
labels | string[] | |
attachments | Attachment[] | |
in_reply_to | string | Message UUID or RFC Message-ID to reply to |
reply_all | boolean | With in_reply_to: copy the other recipients |
send_at | string (date-time) | Send automatically at this time (future, within 365 days) |
Responses
201Created: Draft400Error envelope: Error404Error envelope: Error
GET/v0/inboxes/{inbox}/drafts
List drafts, newest first (needs message_read)
Parameters
| Name | Type | Notes |
|---|---|---|
inbox | string (email) | in path, required |
limit | integer | in query, min 1, max 100, default 20 |
page_token | string | in query |
Responses
200OK: DraftList
GET/v0/inboxes/{inbox}/drafts/{draft}
Get a draft (needs message_read)
Parameters
| Name | Type | Notes |
|---|---|---|
inbox | string (email) | in path, required |
draft | string (uuid) | in path, required |
Responses
200OK: Draft404Error envelope: Error
PATCH/v0/inboxes/{inbox}/drafts/{draft}
Update a draft (needs message_send)
Parameters
| Name | Type | Notes |
|---|---|---|
inbox | string (email) | in path, required |
draft | string (uuid) | in path, required |
Body (UpdateDraft)
| Name | Type | Notes |
|---|---|---|
to | string (email)[] | |
cc | string (email)[] | |
bcc | string (email)[] | |
subject | string | |
text | string | |
html | string | |
labels | string[] | |
attachments | Attachment[] | |
send_at | string,null (date-time) | null cancels a scheduled send |
Responses
200OK: Draft400Error envelope: Error404Error envelope: Error
DELETE/v0/inboxes/{inbox}/drafts/{draft}
Delete a draft (needs message_send)
Parameters
| Name | Type | Notes |
|---|---|---|
inbox | string (email) | in path, required |
draft | string (uuid) | in path, required |
Responses
204Deleted
POST/v0/inboxes/{inbox}/drafts/{draft}/send
Send a draft now and delete it (needs message_send)
Parameters
| Name | Type | Notes |
|---|---|---|
inbox | string (email) | in path, required |
draft | string (uuid) | in path, required |
Idempotency-Key | string | in header, max length 256 |
Responses
200Accepted by the transport, or queued when the install has no transport: SendResult400Error envelope: Error403Error envelope: Error404Error envelope: Error409Error envelope: Error429Error envelope: Error502email_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
| Name | Type | Notes |
|---|---|---|
inbox | string (email) | in path, required |
message | string | in path, required, Message UUID or RFC Message-ID (URL encoded) |
Responses
200OK: MessageTimeline403Error envelope: Error404Error envelope: Error
GET/v0/domains/{domain}/reputation
Bounce and complaint reputation of a domain, measured like the sending breaker (requires domain_read)
Parameters
| Name | Type | Notes |
|---|---|---|
domain | string | in path, required |
Responses
200OK: DomainReputation403Error envelope: Error404Error envelope: Error
Send object
| Name | Type | Notes |
|---|---|---|
to | string (email)[] | required, at least 1 item |
cc | string (email)[] | |
bcc | string (email)[] | |
subject | string | |
text | string | |
html | string | |
labels | string[] | |
attachments | Attachment[] |
Reply object
| Name | Type | Notes |
|---|---|---|
text | string | |
html | string | |
reply_all | boolean | |
labels | string[] | |
attachments | Attachment[] |
Forward object
| Name | Type | Notes |
|---|---|---|
to | string (email)[] | required, at least 1 item |
cc | string (email)[] | |
bcc | string (email)[] | |
comment | string | Optional text above the quoted message, below the signature |
labels | string[] |
SendResult object
| Name | Type | Notes |
|---|---|---|
message_id | string | |
thread_id | string (uuid) | |
status | queued | held | sent | suppressed | State 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_at | string (date-time) | Deadline of POST .../cancel, present when status is held. |
recipients | RecipientOutcome[] | 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
| Name | Type | Notes |
|---|---|---|
draft_id | string (uuid) | |
inbox_id | string (email) | |
thread_id | string (uuid) | Set for a reply draft |
in_reply_to | string | RFC Message-ID of the message replied to |
references | string[] | |
to | string[] | |
cc | string[] | |
bcc | string[] | |
subject | string | |
text | string | |
html | string | |
labels | string[] | |
attachments | object[] | |
send_at | string (date-time) | |
status | draft | scheduled | failed | |
last_error | string | Why a scheduled send was refused |
created_at | string (date-time) | |
updated_at | string (date-time) |
MessageTimeline object
| Name | Type | Notes |
|---|---|---|
message_id | string (uuid) | |
inbox_id | string | The address that holds the message, never the list of recipients |
status | received | queued | sending | sent | failed | held | cancelled | The state of the row now; the events carry how it got there |
events | MessageTimelineEvent[] |
MessageTimelineEvent object
| Name | Type | Notes |
|---|---|---|
type | message.received | message.received.spam | message.sent | message.delivered | message.delivery_delayed | message.failed | message.rejected | message.bounced | message.complained | message.dropped | message.ruled | |
source | smtp | dsn | arf | breaker | rspamd | rule | smtp a reply, dsn a delivery status notification, arf a feedback report, breaker the sending health breaker, rspamd the spam filter, rule a deterministic rule |
code | string | The SMTP status, the DSN status or the policy code; empty when the source has none. unknown always carries a reason in text. |
text | string | The reply or the sentence, one line, at most 500 characters, never a body and never a recipient |
at | string (date-time) |
DomainReputation object
| Name | Type | Notes |
|---|---|---|
domain | string | |
window_hours | integer | The rolling window the counts cover, the same as sending_health.window_hours |
sends | integer | Messages that reached a final outcome in the window |
hard_bounces | integer | Permanent rejections at send time plus bounces reported by a DSN |
complaints | integer | Spam complaints reported through the feedback loop |
bounce_rate | number | hard_bounces divided by sends, 0 when sends is 0 |
complaint_rate | number | complaints divided by sends, 0 when sends is 0 |
thresholds | SendingThresholds | |
breach_state | ok | breached | breached when the window trips a limit or when the breaker has paused an inbox on the domain or the organization |
breach_reason | string | The sentence naming the window and the limit crossed; absent while breach_state is ok |
paused_scope | string | What paused sending: inbox, organization, or empty. A send is refused by both scopes, so this tells them apart. |
paused_at | string (date-time) | |
paused_reason | string | The sentence of the pause, the same one the 403 sending_paused carries |