Skip to content

HTTP Send API

Send email over HTTP from any language or runtime.

Replace example addresses with your enrolled sending address and a real recipient address you control. The relay rejects IANA example domains such as example.com as recipients.

POST https://smtp.atmos.email/v1/send

Pass your DID and API key in the request headers:

X-Atmos-DID: did:plc:your-did-here
Authorization: Bearer atmos_your-api-key

You can also pass the DID as a ?did= query parameter.

The key must be allowed to send from the domain in the message’s from address. See creating and limiting API keys.

JSON with Content-Type: application/json. Maximum 1 MiB.

{
"from": "you@yourdomain.com",
"to": "recipient@example.com",
"subject": "Hello from comail",
"text": "Plain text body",
"html": "<p>HTML body (optional)</p>",
"replyTo": "replies@yourdomain.com",
"headers": { "X-Custom": "value" },
"category": "login-link"
}
Field Type Required Description
from string yes Sender address. Must use an enrolled domain.
to string or array yes One recipient or an array of recipients.
subject string yes Email subject line.
text string yes* Plain text body. Required unless html is set.
html string no HTML body. The relay sends it as a multipart alternative alongside text.
replyTo string no Reply-To address.
headers object no Additional headers. The relay reserves From, To, Subject, and DKIM-related headers.
category string no One of login-link, password-reset, mfa-otp, verification, bulk, broadcast. The first four categories identify user-initiated transactional messages. They omit List-Unsubscribe and bypass suppressions caused only by an unsubscribe. Other suppression sources still block delivery. bulk and broadcast both add List-Unsubscribe and enforce the suppression list. An untagged message gets no List-Unsubscribe header, but the suppression list still applies. If you include your own List-Unsubscribe or List-Unsubscribe-Post header (via the headers field) on a bulk or broadcast message, comail detects it and does not inject its own. You own the pair and are responsible for a valid RFC 8058 combination. CR/LF characters in any header-bound field (from, subject, replyTo, category, custom header names or values, recipient addresses) are rejected with INVALID_REQUEST.

Response

200 on success:

{
"accepted": [
{ "recipient": "user@example.com", "messageId": 448 }
],
"rejected": []
}

Errors return 4xx/5xx with:

{
"error": "description of what went wrong",
"code": "ERROR_CODE"
}
Code HTTP Meaning
AUTH_REQUIRED 400 / 401 Missing DID (X-Atmos-DID header or ?did= query) or missing Bearer API key.
INVALID_REQUEST 400 / 413 Malformed JSON, invalid DID format, missing required fields, header injection (CR/LF in from/subject/etc.), body over 1 MiB, more than 50 recipients.
AUTH_FAILED 401 API key does not match any enrolled domain on this DID.
SUSPENDED 403 Member is suspended.
PENDING 403 Member is pending approval.
DOMAIN_MISMATCH 403 From address domain is not enrolled under this DID.
LABEL_DENIED 403 Member’s reputation label blocks sending.
INVALID_RECIPIENT_DOMAIN 422 A recipient uses an IANA example domain that cannot receive mail. Remove or correct the address; do not retry unchanged.
RATE_LIMITED 429 Warming tier or per-member rate limit exceeded. Retry later.
QUEUE_FULL 503 Submission queue is at capacity. Retryable.
TEMPORARILY_UNAVAILABLE 503 Internal dependency unavailable. Retryable.
INTERNAL_ERROR 500 Server-side error. Retryable; check status.
curl -X POST https://smtp.atmos.email/v1/send \
-H "Content-Type: application/json" \
-H "X-Atmos-DID: did:plc:your-did" \
-H "Authorization: Bearer atmos_your-api-key" \
-d '{
"from": "you@yourdomain.com",
"to": "friend@example.com",
"subject": "Hello",
"text": "Sent via comail"
}'
const res = await fetch("https://smtp.atmos.email/v1/send", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Atmos-DID": "did:plc:your-did",
"Authorization": "Bearer atmos_your-api-key",
},
body: JSON.stringify({
from: "you@yourdomain.com",
to: "friend@example.com",
subject: "Hello",
text: "Sent via comail",
}),
});
const data = await res.json();
import requests
resp = requests.post(
"https://smtp.atmos.email/v1/send",
headers={
"X-Atmos-DID": "did:plc:your-did",
"Authorization": "Bearer atmos_your-api-key",
},
json={
"from": "you@yourdomain.com",
"to": "friend@example.com",
"subject": "Hello",
"text": "Sent via comail",
},
)
print(resp.json())
  • The relay applies DKIM signatures for your domain and d=atmos.email. See DNS records for what gets signed.
  • Messages tagged bulk or broadcast get List-Unsubscribe and List-Unsubscribe-Post headers per RFC 8058 (the Gmail/Yahoo one-click format). One-click unsubscribes add the recipient to your suppression list. If you already include your own List-Unsubscribe or List-Unsubscribe-Post header via the headers field, comail detects it and does not inject its own. You own the pair and are responsible for a valid RFC 8058 combination: List-Unsubscribe must contain an HTTPS URI (a mailto: URI may additionally be present) and List-Unsubscribe-Post must be exactly List-Unsubscribe=One-Click. Suppression-list enforcement stays active either way.
  • The four user-initiated categories (login-link, password-reset, mfa-otp, verification) omit the List-Unsubscribe header and bypass suppressions caused only by an unsubscribe. Suppressions from bounces, complaints, operator actions, and unknown sources still apply. Use these only for messages the recipient just triggered.
  • Untagged messages get no List-Unsubscribe header, but suppression-list checks still apply. The strict policy in the AUP treats untagged as bulk for compliance purposes; tag explicitly to make intent legible.
  • The relay returns int64 messageId values. If your integration assumes string IDs, convert.
  • New domains hit warming tier rate limits during the first 14 days.
  • The from address must use a domain enrolled under the authenticating DID. See Identity and sending domains.
  • Accepted messages are fsynced before the response. The relay retries failed deliveries. See Reliability for the retry schedule and status vocabulary.
  • See Troubleshooting for common errors.

Back to docs