Skip to content

Python quickstart

Use HTTP with requests, or SMTP with Python’s standard library.

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.

  1. A comail account with at least one enrolled sending domain. Sign up at /signup if you don’t have one.
  2. Your DID (did:plc:…) and API key. Regenerate the key at /account/api-keys if it’s lost.
  3. Python 3.8+ (python3 --version).
  4. A recipient address you can check. Your own works.

Install:

pip install requests

Save as send.py:

import os
import sys
import requests
DID = os.environ["COMAIL_DID"]
API_KEY = os.environ["COMAIL_API_KEY"]
resp = requests.post(
"https://smtp.atmos.email/v1/send",
headers={
"X-Atmos-DID": DID,
"Authorization": f"Bearer {API_KEY}",
},
json={
"from": "you@yourdomain.com",
"to": "alice@example.com",
"subject": "Hello from comail",
"text": "If you can read this, the API key works.",
},
timeout=15,
)
if not resp.ok:
error = resp.json()
print(f"send failed: {resp.status_code} {error['code']}: {error['error']}", file=sys.stderr)
sys.exit(1)
result = resp.json()
print(f"accepted: {len(result['accepted'])}")
print(f"rejected: {len(result['rejected'])}")
for r in result["accepted"]:
print(f" -> {r['recipient']} (messageId: {r['messageId']})")

Run:

export COMAIL_DID=did:plc:your-did-here
export COMAIL_API_KEY=atmos_your-api-key
python3 send.py

Expected:

accepted: 1
rejected: 0
-> alice@example.com (messageId: 12345)

No install needed; smtplib and email are in the stdlib.

import os
import smtplib
from email.message import EmailMessage
# IMPORTANT: nodemailer-style url.parse() URLs do not work for the
# username because DIDs contain colons, which url.parse() truncates.
# Pass username + password as separate arguments to login().
DID = os.environ["COMAIL_DID"]
API_KEY = os.environ["COMAIL_API_KEY"]
msg = EmailMessage()
msg["From"] = "you@yourdomain.com"
msg["To"] = "alice@example.com"
msg["Subject"] = "Hello from comail (SMTP)"
msg.set_content("If you can read this, SMTP submission works.")
with smtplib.SMTP("smtp.atmos.email", 587, timeout=15) as server:
server.starttls()
server.login(DID, API_KEY)
server.send_message(msg)
print("sent")

A couple of things to know about the SMTP path:

  • Port 587 with STARTTLS only. Port 465 (implicit TLS) is not exposed.
  • AUTH PLAIN. smtplib picks the right SASL mechanism automatically when you call login().
  • The username for login() is either your DID (did:plc:…) or your enrolled sending domain (yourdomain.com). Both work; pick whichever is easier to plumb through your config. Avoid passing the DID in a URL because some libraries truncate at the : between did and plc.

For a project where you’ll send from multiple places:

comail.py
import os
import requests
class ComailError(Exception):
def __init__(self, status, code, message):
super().__init__(f"{code}: {message}")
self.status = status
self.code = code
_DID = os.environ.get("COMAIL_DID")
_KEY = os.environ.get("COMAIL_API_KEY")
if not (_DID and _KEY):
raise RuntimeError("Set COMAIL_DID and COMAIL_API_KEY in the environment.")
_SESSION = requests.Session()
_SESSION.headers.update({
"X-Atmos-DID": _DID,
"Authorization": f"Bearer {_KEY}",
})
def send(*, sender, to, subject, text=None, html=None, reply_to=None,
category=None, headers=None, timeout=15):
"""Send via comail. Returns the parsed JSON response on success;
raises ComailError on a 4xx/5xx.
`sender` is the From: address (not `from` because Python keyword).
`to` may be a string or a list, up to 50 recipients.
"""
body = {
"from": sender,
"to": to,
"subject": subject,
}
if text is not None:
body["text"] = text
if html is not None:
body["html"] = html
if reply_to is not None:
body["replyTo"] = reply_to
if category is not None:
body["category"] = category
if headers:
body["headers"] = headers
resp = _SESSION.post(
"https://smtp.atmos.email/v1/send",
json=body,
timeout=timeout,
)
if not resp.ok:
err = resp.json()
raise ComailError(resp.status_code, err.get("code", "UNKNOWN"), err.get("error", ""))
return resp.json()

Usage:

from comail import send, ComailError
try:
result = send(
sender="auth@yourdomain.com",
to=["alice@example.com", "bob@example.com"],
subject="Sign-in link",
text="https://your-app.com/auth?token=abc123",
category="login-link",
)
print(f"sent to {len(result['accepted'])} recipients")
except ComailError as e:
if e.code == "RATE_LIMITED":
# back off and retry
pass
elif e.code in ("SUSPENDED", "PENDING"):
# account-level issue; check /account
pass
else:
raise

See HTTP Send API for the full error code table.

to accepts a list of up to 50. Each recipient gets its own accepted or rejected entry in the response. You can detect partial failures by inspecting both:

result = send(
sender="news@yourdomain.com",
to=["alice@example.com", "bob@example.com"],
subject="Acme weekly",
html="<p>Latest updates...</p>",
category="bulk",
)
for r in result["accepted"]:
print(f"OK {r['recipient']}: {r['messageId']}")
for r in result["rejected"]:
print(f"ERR {r['recipient']}: {r['error']}")

Both frameworks ship SMTP backends. Point them at comail:

# Django settings.py
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = "smtp.atmos.email"
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = "yourdomain.com" # your enrolled domain
EMAIL_HOST_PASSWORD = "atmos_your-api-key" # via env, not in source
DEFAULT_FROM_EMAIL = "you@yourdomain.com"
# Flask-Mail config
MAIL_SERVER = "smtp.atmos.email"
MAIL_PORT = 587
MAIL_USE_TLS = True
MAIL_USERNAME = "yourdomain.com"
MAIL_PASSWORD = "atmos_your-api-key" # via env

Both frameworks send bulk-shape mail by default (no X-Atmos-Category header). For transactional flows, add the header per-message via the framework’s “extra headers” mechanism.

Back to docs