Send email
POST /email/messages accepts raw content or a template, never both, and returns 202 with a message you can poll.
Send a message
POST /email/messages accepts either raw content or a template, never both. With raw content, subject is required alongside html or text. With template_id, the body comes from the active version of the template and variables fills the placeholders.
curl -X POST https://api.mailer.quathos.com/api/v1/email/messages \
-H "Authorization: Bearer $QUATHOS_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"from": { "email": "you@yourcompany.com", "name": "Your Company" },
"to": [{ "email": "user@example.com" }],
"subject": "Welcome aboard",
"html": "<h1>You are in.</h1>"
}'{
"from": { "email": "you@yourcompany.com" },
"to": [{ "email": "user@example.com" }],
"template_id": "019f70c2-2b41-7a5e-9c3e-2f1a8d55b310",
"variables": { "name": "Ana", "order_id": "A-1042" }
}Request fields
| Field | Notes |
|---|---|
| from | Object with email and optional name. Must be a verified domain or sender. |
| to | Array with exactly one recipient. More than one is refused with 400: send one request per recipient, or use cc/bcc for copies of the same message. |
| cc / bcc | Copies of the SAME message, up to 50 addresses combined. One id, one status, one charge. Cc becomes a visible header; Bcc only ever exists on the envelope. Every copy goes through the suppression list, and a suppressed copy refuses the whole message naming which address (role: cc or bcc). |
| headers | Custom headers, up to 10, by allowlist: X-* (except X-Quathos-*), In-Reply-To, References, List-Id, Precedence, Auto-Submitted. Envelope headers (From, To, Subject, Reply-To, List-Unsubscribe) are refused with 400 and the message names the proper field. No line breaks. |
| unsubscribe_group | Marketing only. The unsubscribe link and the one-click header of this message opt the recipient out of THIS group, not of everything. Create groups at POST /email/unsubscribe-groups. |
| reply_to | Object with email and optional name. Sets the Reply-To header: where answers go. |
| reply_to_list | Array of the same objects, up to 1000. Mutually exclusive with reply_to. |
| subject | Required unless template_id is present, where it overrides the template. |
| html / text | Raw content. Mutually exclusive with template_id. |
| template_id / variables | Template mode. Templates are sandboxed: they never execute code. |
| type | transactional (default) or marketing. Separate identities, separate reputation. |
| send_at | When to send it (ISO 8601, timezone REQUIRED: without an offset the moment is ambiguous and we refuse it). Up to 30 days ahead, or less when the message carries attachments, which expire. A time in the past sends immediately. The cost is reserved on acceptance and released if you cancel. |
| tags / metadata | Yours. Returned on events, useful for correlation. |
| tracking | Per-message opens and clicks toggles. Omitted (or null) inherits the default of the sending domain, which you set per domain; an explicit true/false always wins. |
| attachments | Array of files: content (base64), filename, optional type, disposition (attachment or inline) and content_id for inline parts, referenced from the HTML as cid:VALUE. Up to 10 files, 10 MB each and 15 MB combined, measured on the DECODED content, not on the base64 payload, which is a third larger and would not pass the receiving side. Extensions the large receivers discard on arrival (.exe, .js, .bat, …) are refused with 422. Files are discarded 7 days after the send. |
Where replies go
reply_to sets the Reply-To header of the delivered message: the address the recipient reaches when they hit Reply. It is independent of from: the message can be signed by no-reply and still be answerable. Send an object with email and an optional name, the same shape as from; the name travels into the header as the display name.
{
"from": { "email": "no-reply@yourcompany.com", "name": "Your Company" },
"to": [{ "email": "support@yourcompany.com" }],
"reply_to": { "email": "ana@customer.com", "name": "Ana Ribeiro" },
"subject": "New contact form submission",
"text": "Ana wrote: my invoice has the wrong address."
}This matters most for the two halves of a contact form. The copy that goes to your team should reply to whoever wrote in; the confirmation that goes back to them should reply to your monitored inbox. Without it, both ends answer into a no-reply address and the message is lost: people reply to "do not reply" messages anyway, to add a detail or attach a document.
Reply-To is an address list in RFC 5322, so more than one address is legal. Use reply_to_list for that, with up to 1000 entries of the same shape. The two fields are mutually exclusive: sending both is a 422, never a silent pick: choosing for you would send a reply somewhere you did not ask for.
{
"from": { "email": "no-reply@yourcompany.com", "name": "Your Company" },
"to": [{ "email": "ana@customer.com" }],
"reply_to_list": [
{ "email": "support@yourcompany.com", "name": "Support" },
{ "email": "billing@yourcompany.com" }
],
"subject": "We received your request",
"text": "Reply to this message and both teams will see it."
}Send in batches
POST /email/messages/batch takes up to 50 messages in one request and answers 207 with one outcome per item: accepted items carry the same object as a single send, rejected items carry the error code and message. A suppressed recipient in item 3 does not take items 1, 2 and 4 down. Idempotency-Key covers the whole batch: the same key returns the same 207 with the same ids.
POST https://api.mailer.quathos.com/api/v1/email/messages/batch
{
"messages": [
{ "from": { "email": "you@yourcompany.com" }, "to": [{ "email": "a@example.com" }],
"cc": [{ "email": "boss@example.com", "name": "Boss" }],
"headers": { "X-Ticket-Id": "4711" },
"subject": "Ticket 4711", "text": "..." },
{ "from": { "email": "you@yourcompany.com" }, "to": [{ "email": "b@example.com" }],
"subject": "Hello", "text": "..." },
{ "from": { "email": "you@yourcompany.com" }, "to": [{ "email": "unsubscribed@example.com" }],
"subject": "Hello", "text": "..." }
]
}
207 Multi-Status
{
"accepted_count": 2, "rejected_count": 1, "dry_run": false,
"results": [
{ "index": 0, "status": "accepted", "message": { "id": "01J...", "status": "QUEUED", ... } },
{ "index": 1, "status": "accepted", "message": { "id": "01J...", "status": "QUEUED", ... } },
{ "index": 2, "status": "rejected",
"error": { "code": "suppressed_recipient", "message": "This recipient (to) is on the suppression list.",
"details": { "reason": "UNSUBSCRIBE", "scope": "suppression", "role": "to" } } }
]
}Send before you verify a domain
While your own domain is not verified, you can send transactional email from the shared sending domain. Your address on it is derived from your account and frozen: the first request from another address on that domain is refused with 403 and the message tells you the right one. reply_to is required (the From is ours, replies need somewhere of yours to go), marketing is refused, attachments are capped at 1 MB and there is a daily cap per account. Authenticate your domain to remove all of that.
Response and status
{
"id": "019f7141-46ac-76c3-8276-042f38c68cd8",
"status": "queued",
"channel": "email",
"to": "user@example.com",
"from": "you@yourcompany.com",
"type": "transactional",
"created_at": "2026-07-17T18:05:31Z",
"estimated_cost_micros": 0,
"currency": "USD"
}Poll GET /email/messages/{id} for the current state, or, better, subscribe to webhooks and let the status come to you.
curl https://api.mailer.quathos.com/api/v1/email/messages/019f7141-46ac-76c3-8276-042f38c68cd8 \
-H "Authorization: Bearer $QUATHOS_TOKEN"