Reference · v1
Send email from your cMailer domains over HTTPS.
One JSON request hands a message to the cMailer mail server, which signs it with your domain's DKIM key and delivers it exactly like mail sent from a mailbox. No SMTP client, no credentials for a real inbox, and every send is logged in the portal.
- Base URL
- https://api.cmailer.net
- Format
- JSON request and response bodies, UTF-8
- Auth
- Authorization: Bearer cm_…
Quick start
- Sign in to the portal, open API keys and create a key with sending access for your domain or a single inbox.
- Send your first message. The
fromaddress must belong to a domain the key covers. - Watch it arrive under API emails in the portal, or fetch it back with
GET /v1/emails/{id}.
curl https://api.cmailer.net/v1/emails \
-H "Authorization: Bearer cm_your_key" \
-H "Content-Type: application/json" \
-d '{
"from": "Acme <hello@acme.com>",
"to": ["jane@example.com"],
"subject": "Your Acme account is ready",
"html": "<p>Hi Jane, welcome aboard.</p>"
}'Authentication
Every request carries a bearer token. Keys start with cm_, are shown once when created, and are stored only as a hash. Rotate a key by creating a new one and deleting the old one; a deleted or disabled key fails immediately with 401.
Authorization: Bearer cm_0f3a…Two kinds of key
| Key | Can send as | Management API | Typical use |
|---|---|---|---|
| Domain key | Any existing address at its domains, e.g. a noreply@ mailbox or a billing@ forwarding address | Yes with full access, no with sending only | An application that sends on behalf of a whole domain |
| Inbox key | Only the exact mailbox addresses chosen when the key was made | Never | A tool or integration that must only ever speak as one inbox |
The mail server only accepts senders it knows, so the from address must already exist on the domain as a mailbox or forwarding address (add one under the domain in the portal). Sending from an address outside the key's scope, or one that does not exist, returns 403 sender_not_allowed and nothing is sent. Use GET /v1/domains to see what a key is allowed to do.
Send an email
POST/v1/emails
Builds the message, hands it to the mail server and returns as soon as the server has queued it. Delivery to the recipient's provider happens asynchronously; see Retrieve an email for the outcome.
Body parameters
| Field | Type | Notes |
|---|---|---|
from required | string | "Acme <hello@acme.com>" or a bare address. The domain must be one the key covers and the address must exist there as a mailbox or forwarding address (a catch-all also counts); inbox keys must use their exact address. |
to required | string · string[] · object[] | One or more recipients as "Name <user@example.com>", a bare address or { "name", "email" }. Up to 50 recipients across to, cc and bcc. |
cc, bcc | same as to | Bcc recipients receive the message but are never written into its headers. |
reply_to | same as to | Where replies should go when it differs from from. |
subject required | string | Up to 998 characters, no line breaks. |
html | string | HTML body. Provide html, text or both; with both, clients pick the part they can display. |
text | string | Plain-text body. |
headers | object | Up to 20 custom headers, e.g. { "X-Entity-Ref-ID": "order-1" }. Headers the mail system owns (From, To, Subject, Date, Message-ID, Content-Type, …) are rejected. |
attachments | object[] | Up to 20 files as { "filename", "content", "content_type"?, "content_id"? } with base64 content. See Attachments. |
tags | object[] | Up to 10 { "name", "value" } pairs (letters, numbers, _ and -) stored with the send log for your own bookkeeping. They are not added to the message. |
The complete encoded message, attachments included, must stay under 10 MB. Unknown fields are rejected so typos never silently drop content.
Examples
curl https://api.cmailer.net/v1/emails \
-H "Authorization: Bearer cm_your_key" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-2048-confirmation" \
-d '{
"from": "Acme Orders <orders@acme.com>",
"to": ["Jane Doe <jane@example.com>"],
"cc": ["accounts@acme.com"],
"reply_to": "support@acme.com",
"subject": "Order #2048 confirmed",
"html": "<h1>Thanks, Jane</h1><p>Your order ships tomorrow.</p>",
"text": "Thanks, Jane. Your order ships tomorrow.",
"headers": { "X-Entity-Ref-ID": "order-2048" },
"tags": [{ "name": "category", "value": "order_confirmation" }]
}'const response = await fetch("https://api.cmailer.net/v1/emails", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CMAILER_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "order-2048-confirmation",
},
body: JSON.stringify({
from: "Acme Orders <orders@acme.com>",
to: ["jane@example.com"],
subject: "Order #2048 confirmed",
html: "<h1>Thanks, Jane</h1><p>Your order ships tomorrow.</p>",
}),
});
const result = await response.json();
if (!response.ok) throw new Error(`${result.name}: ${result.message}`);
console.log(result.id); // "3f2b1c7e-…"import os, requests
response = requests.post(
"https://api.cmailer.net/v1/emails",
headers={
"Authorization": f"Bearer {os.environ['CMAILER_API_KEY']}",
"Idempotency-Key": "order-2048-confirmation",
},
json={
"from": "Acme Orders <orders@acme.com>",
"to": ["jane@example.com"],
"subject": "Order #2048 confirmed",
"html": "<h1>Thanks, Jane</h1><p>Your order ships tomorrow.</p>",
},
timeout=30,
)
result = response.json()
if not response.ok:
raise RuntimeError(f"{result['name']}: {result['message']}")
print(result["id"])Response
A 200 means the mail server accepted the message into its queue.
{
"id": "3f2b1c7e-6d4a-4f0b-9c1e-8a7d2b5e4c31",
"status": "queued",
"queue_id": "4Xk9Yz1ABC2",
"message_id": "<3f2b1c7e-6d4a-4f0b-9c1e-8a7d2b5e4c31@acme.com>",
"created_at": "2026-09-10T09:14:03.000Z"
}| Field | Meaning |
|---|---|
id | The email's id in this API. Use it with GET /v1/emails/{id}. |
status | queued: accepted by the mail server. A rejection returns an error instead of a body with status: failed. |
queue_id | The Postfix queue id, useful when correlating with server logs or the portal's delivery trace. |
message_id | The RFC 5322 Message-ID header written into the email, <{id}@yourdomain>. |
Idempotency
Retrying a send after a network failure can double-deliver. Add an Idempotency-Key header (up to 255 characters of letters, numbers, ., :, _ or -) and repeated requests with the same key return the original result with an Idempotent-Replayed: true header instead of sending again. Keys are scoped to the API key that used them and kept for as long as the send log (90 days by default).
Idempotency-Key: order-2048-confirmationAttachments and inline images
Send file content as base64 in content. Only the base name of filename is used. content_type is optional and is otherwise inferred from the file name.
{
"from": "Acme Billing <billing@acme.com>",
"to": ["jane@example.com"],
"subject": "Invoice 2048",
"text": "Your invoice is attached.",
"attachments": [
{
"filename": "invoice-2048.pdf",
"content": "JVBERi0xLjcKJc…",
"content_type": "application/pdf"
}
]
}To reference an image from the HTML body, give the attachment a content_id and use cid: in the src. Such attachments are marked inline.
{
"from": "Acme <hello@acme.com>",
"to": ["jane@example.com"],
"subject": "Welcome",
"html": "<p>Welcome!</p><img src=\"cid:logo@acme.com\" alt=\"Acme\">",
"attachments": [
{
"filename": "logo.png",
"content": "iVBORw0KGgo…",
"content_type": "image/png",
"content_id": "logo@acme.com"
}
]
}Remote URLs and server file paths are not fetched. Supply the bytes yourself.
Send a batch
POST/v1/emails/batch
Send up to 100 independent emails in one request. The body is a JSON array of the same objects POST /v1/emails accepts. The whole batch is validated and authorised first: one invalid entry fails the request with its index in the message and nothing is sent. Once sending starts, each email is handed to the mail server separately and reported on its own, so a rejection of one message does not undo the others. A batch counts as one request per email against the rate limit. Idempotency-Key is not applied to batches.
POST https://api.cmailer.net/v1/emails/batch
[
{ "from": "Acme <hello@acme.com>", "to": ["a@example.com"], "subject": "Hello A", "text": "Hi A" },
{ "from": "Acme <hello@acme.com>", "to": ["b@example.com"], "subject": "Hello B", "text": "Hi B" }
]{
"data": [
{ "id": "9c0d…", "status": "queued" },
{ "id": "1e7a…", "status": "failed", "error": { "name": "smtp_error", "message": "The mail server did not accept the message: 550 5.1.1 …" } }
]
}Retrieve an email
GET/v1/emails/{id}
Returns the send-log record: envelope addresses, subject, size, the mail server's acceptance and, where the server keeps a readable Postfix log, what happened next. Bodies and attachments are never stored, so they are not returned.
{
"object": "email",
"id": "3f2b1c7e-6d4a-4f0b-9c1e-8a7d2b5e4c31",
"from": "Acme Orders <orders@acme.com>",
"to": ["Jane Doe <jane@example.com>"],
"cc": [],
"bcc": [],
"reply_to": ["support@acme.com"],
"subject": "Order #2048 confirmed",
"status": "queued",
"queue_id": "4Xk9Yz1ABC2",
"message_id": "<3f2b1c7e-6d4a-4f0b-9c1e-8a7d2b5e4c31@acme.com>",
"error": null,
"size_bytes": 4821,
"attachments": 0,
"tags": [{ "name": "category", "value": "order_confirmation" }],
"idempotency_key": "order-2048-confirmation",
"created_at": "2026-09-10T09:14:03.000Z",
"delivery": {
"status": "delivered",
"detail": "250 2.0.0 OK 1757495650 x12si9876543 - gsmtp",
"checked_at": "2026-09-10T09:15:11.000Z",
"source": "postfix-log"
}
}Delivery status
delivery.status | Meaning |
|---|---|
delivered | The receiving mail server accepted the message for every recipient. |
partially_delivered | Accepted for some recipients; others are still pending or bounced. |
deferred | The receiving server asked cMailer to retry later; retries continue automatically for several days. |
bounced | The receiving server refused the message. detail carries its reason. |
queued | Accepted by cMailer; no delivery attempt recorded yet. |
unknown | Nothing found in the retained log. This is not proof the message was lost; logs are bounded and rotate. |
unavailable | This server does not expose its Postfix log to the API. Only the acceptance in status is known. |
not_sent | The mail server rejected the message at submission; see error. |
Delivery outcomes are read from the server's log on request and cached for a minute; terminal outcomes are never re-read. Domain keys can read any message sent for their domains; inbox keys only see their own sends.
List emails
GET/v1/emails
Newest first. Filter with status=queued|failed|pending, page with limit (1 to 200, default 50) and the next_cursor from the previous page. Listing does not include the delivery lookup.
curl "https://api.cmailer.net/v1/emails?limit=25&status=queued" \
-H "Authorization: Bearer cm_your_key"{
"object": "list",
"data": [ { "object": "email", "id": "…", "status": "queued", … } ],
"next_cursor": "MTc1NzQ5NTY0MzozZjJi…",
"has_more": true
}Domains
GET/v1/domains
Shows what the calling key may do: its scope, permissions and the domains or exact addresses it can send from. Domains are added and verified in the portal, not through this API.
{
"object": "list",
"key": { "name": "Website sender", "scope": "domain", "permissions": ["send"] },
"data": [
{ "object": "domain", "name": "acme.com", "senders": ["*@acme.com"] }
]
}Errors
Errors use HTTP status codes and a small JSON body. message is written for a developer and names the offending field where there is one.
{
"statusCode": 400,
"name": "validation_error",
"message": "to[0]: \"jane@example\" is not a valid email address"
}| Status | name | When |
|---|---|---|
| 400 | validation_error | A field is missing, malformed or unknown. Nothing was sent. |
| 401 | missing_api_key, invalid_api_key | No bearer token, or a token that is unknown, disabled or deleted. |
| 403 | forbidden | The key exists but lacks the permission (for example a management-only key calling /v1). |
| 403 | sender_not_allowed | The from address is outside the key's domains or inbox, does not exist on the domain, or the domain is disabled. |
| 404 | not_found | No such email id visible to this key, or no such route. |
| 413 | message_too_large | The encoded message exceeds 10 MB. |
| 429 | rate_limit_exceeded | Too many messages in the current minute. Honour Retry-After. |
| 502 | smtp_error | The mail server refused the message. message includes its reply; the attempt is logged as failed. |
| 503 | mail_server_unavailable | The mail server could not be reached. Nothing was sent; retry shortly. |
| 500 | internal_server_error | Something unexpected failed on cMailer's side. |
Rate limits
Each key may hand over 120 messages per minute, measured over a sliding window; a batch spends one unit per email. Every /v1 response includes X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (Unix seconds). When the limit is hit the response is 429 with a Retry-After header. Reads count against the same window.
Management API
Domain keys created with full access also work against the mailbox-management endpoints, with the same bearer header and base URL. Inbox keys and sending-only keys receive 403 here.
| Endpoint | Purpose |
|---|---|
GET /api/external/domains | Domains the key covers. |
GET /api/external/mailboxes?domain= | List mailboxes. |
POST /api/external/mailboxes | Create a mailbox: { email, password, name?, quota? }. |
POST /api/external/mailboxes/reset-password | Set a new mailbox password. |
GET · POST /api/external/forwardings | List or create forwards and catch-alls. |
PUT · DELETE /api/external/forwardings/{id} | Toggle, edit or remove a forward. |
These endpoints return { "data": … } on success and the portal's standard statusMessage errors.
Good to know
- Authentication and alignment. Mail is signed with your domain's DKIM key and sent from cMailer's outbound servers, so the SPF, DKIM and DMARC records the portal set up for the domain already cover API mail. Check them under Health & DMARC if a provider reports failures.
- Bounces go to the
fromaddress, exactly like mail sent from a mailbox. Use a real, monitored address or an inbox you can read from the portal. - Scheduling and templates are not part of this API; send when the message is due and render content in your application.
- Webhooks are not available. Poll
GET /v1/emails/{id}for outcomes where the server exposes its delivery log. - Privacy. The send log keeps addresses, subject, size and outcome. Bodies and attachments are never stored by the portal.