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.
Prerequisites
Section titled “Prerequisites”- A comail account with at least one enrolled sending domain. Sign up at /signup if you don’t have one.
- Your DID (
did:plc:…) and API key. Regenerate the key at /account/api-keys if it’s lost. - Python 3.8+ (
python3 --version). - A recipient address you can check. Your own works.
HTTP variant (requests)
Section titled “HTTP variant (requests)”Install:
pip install requestsSave as send.py:
import osimport sysimport 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-hereexport COMAIL_API_KEY=atmos_your-api-keypython3 send.pyExpected:
accepted: 1rejected: 0 -> alice@example.com (messageId: 12345)SMTP variant (smtplib)
Section titled “SMTP variant (smtplib)”No install needed; smtplib and email are in the stdlib.
import osimport smtplibfrom 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.
smtplibpicks the right SASL mechanism automatically when you calllogin(). - 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:betweendidandplc.
Reusable helper
Section titled “Reusable helper”For a project where you’ll send from multiple places:
import osimport 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: raiseSee HTTP Send API for the full error code table.
Many recipients
Section titled “Many recipients”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']}")Django / Flask integration
Section titled “Django / Flask integration”Both frameworks ship SMTP backends. Point them at comail:
# Django settings.pyEMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"EMAIL_HOST = "smtp.atmos.email"EMAIL_PORT = 587EMAIL_USE_TLS = TrueEMAIL_HOST_USER = "yourdomain.com" # your enrolled domainEMAIL_HOST_PASSWORD = "atmos_your-api-key" # via env, not in sourceDEFAULT_FROM_EMAIL = "you@yourdomain.com"# Flask-Mail configMAIL_SERVER = "smtp.atmos.email"MAIL_PORT = 587MAIL_USE_TLS = TrueMAIL_USERNAME = "yourdomain.com"MAIL_PASSWORD = "atmos_your-api-key" # via envBoth 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.
Next steps
Section titled “Next steps”- Message categories: pick the right tag for each send.
- Bounces + suppression: handle DSNs and the suppression list.
- Reply forwarding: point your MX at the relay to get replies back.