Skip to content
cMaileremail API

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

  1. Sign in to the portal, open API keys and create a key with sending access for your domain or a single inbox.
  2. Send your first message. The from address must belong to a domain the key covers.
  3. Watch it arrive under API emails in the portal, or fetch it back with GET /v1/emails/{id}.
curl
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.

Header
Authorization: Bearer cm_0f3a…

Two kinds of key

KeyCan send asManagement APITypical use
Domain keyAny existing address at its domains, e.g. a noreply@ mailbox or a billing@ forwarding addressYes with full access, no with sending onlyAn application that sends on behalf of a whole domain
Inbox keyOnly the exact mailbox addresses chosen when the key was madeNeverA 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

FieldTypeNotes
from requiredstring"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 requiredstring · 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, bccsame as toBcc recipients receive the message but are never written into its headers.
reply_tosame as toWhere replies should go when it differs from from.
subject requiredstringUp to 998 characters, no line breaks.
htmlstringHTML body. Provide html, text or both; with both, clients pick the part they can display.
textstringPlain-text body.
headersobjectUp 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.
attachmentsobject[]Up to 20 files as { "filename", "content", "content_type"?, "content_id"? } with base64 content. See Attachments.
tagsobject[]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
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" }]
  }'

Node.js
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-…"

Python
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.

200 OK
{
  "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"
}

FieldMeaning
idThe email's id in this API. Use it with GET /v1/emails/{id}.
statusqueued: accepted by the mail server. A rejection returns an error instead of a body with status: failed.
queue_idThe Postfix queue id, useful when correlating with server logs or the portal's delivery trace.
message_idThe 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).

Header
Idempotency-Key: order-2048-confirmation

Attachments 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.

Attachment
{
  "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.

Inline image
{
  "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.

Request
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" }
]

200 OK
{
  "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.

200 OK
{
  "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.statusMeaning
deliveredThe receiving mail server accepted the message for every recipient.
partially_deliveredAccepted for some recipients; others are still pending or bounced.
deferredThe receiving server asked cMailer to retry later; retries continue automatically for several days.
bouncedThe receiving server refused the message. detail carries its reason.
queuedAccepted by cMailer; no delivery attempt recorded yet.
unknownNothing found in the retained log. This is not proof the message was lost; logs are bounded and rotate.
unavailableThis server does not expose its Postfix log to the API. Only the acceptance in status is known.
not_sentThe 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
curl "https://api.cmailer.net/v1/emails?limit=25&status=queued" \
  -H "Authorization: Bearer cm_your_key"

200 OK
{
  "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.

200 OK
{
  "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.

400 Bad Request
{
  "statusCode": 400,
  "name": "validation_error",
  "message": "to[0]: \"jane@example\" is not a valid email address"
}

StatusnameWhen
400validation_errorA field is missing, malformed or unknown. Nothing was sent.
401missing_api_key, invalid_api_keyNo bearer token, or a token that is unknown, disabled or deleted.
403forbiddenThe key exists but lacks the permission (for example a management-only key calling /v1).
403sender_not_allowedThe from address is outside the key's domains or inbox, does not exist on the domain, or the domain is disabled.
404not_foundNo such email id visible to this key, or no such route.
413message_too_largeThe encoded message exceeds 10 MB.
429rate_limit_exceededToo many messages in the current minute. Honour Retry-After.
502smtp_errorThe mail server refused the message. message includes its reply; the attempt is logged as failed.
503mail_server_unavailableThe mail server could not be reached. Nothing was sent; retry shortly.
500internal_server_errorSomething 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.

EndpointPurpose
GET /api/external/domainsDomains the key covers.
GET /api/external/mailboxes?domain=List mailboxes.
POST /api/external/mailboxesCreate a mailbox: { email, password, name?, quota? }.
POST /api/external/mailboxes/reset-passwordSet a new mailbox password.
GET · POST /api/external/forwardingsList 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 from address, 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.