Node.js quickstart
Send a message from Node.js with the built-in fetch function. Node 18 or newer includes it.
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.
What you need
Section titled “What you need”- A comail account with at least one enrolled sending domain. If you don’t have one, sign up at /signup.
- Your DID (
did:plc:…) and the API key shown once on the credentials page after enrollment. If you lost the key, regenerate it at /account/api-keys. - Node 18 or newer (
node --version). - A recipient address you can check. Use one of your own.
The minimum send
Section titled “The minimum send”Save as send.mjs:
const res = await fetch("https://smtp.atmos.email/v1/send", { method: "POST", headers: { "Content-Type": "application/json", "X-Atmos-DID": "did:plc:your-did-here", "Authorization": "Bearer atmos_your-api-key", }, body: JSON.stringify({ from: "you@yourdomain.com", to: "alice@example.com", subject: "Hello from comail", text: "If you can read this, the API key works.", }),});
if (!res.ok) { const error = await res.json(); console.error(`send failed: ${res.status} ${error.code}: ${error.error}`); process.exit(1);}
const result = await res.json();console.log(`accepted: ${result.accepted.length}`);console.log(`rejected: ${result.rejected.length}`);for (const r of result.accepted) { console.log(` → ${r.recipient} (messageId: ${r.messageId})`);}Replace four values:
did:plc:your-did-herewith your actual DIDatmos_your-api-keywith your actual keyyou@yourdomain.comwith an address on your enrolled domainalice@example.comwith a recipient you can check
Run it:
node send.mjsExpected output on success:
accepted: 1rejected: 0 → alice@example.com (messageId: 12345)Then check the recipient’s inbox. Check the spam folder too. See Warming tiers for why the first sends can land in spam during the first 14 days.
A reusable helper
Section titled “A reusable helper”For an example suitable for a project:
const COMAIL_DID = process.env.COMAIL_DID;const COMAIL_API_KEY = process.env.COMAIL_API_KEY;
if (!COMAIL_DID || !COMAIL_API_KEY) { throw new Error("Set COMAIL_DID and COMAIL_API_KEY in the environment.");}
export async function sendMail({ from, to, subject, text, html, replyTo, category, headers }) { const res = await fetch("https://smtp.atmos.email/v1/send", { method: "POST", headers: { "Content-Type": "application/json", "X-Atmos-DID": COMAIL_DID, "Authorization": `Bearer ${COMAIL_API_KEY}`, }, body: JSON.stringify({ from, to, subject, text, html, replyTo, category, headers }), });
const body = await res.json(); if (!res.ok) { const err = new Error(`comail send failed: ${body.code} ${body.error}`); err.status = res.status; err.code = body.code; throw err; } return body;}Usage:
import { sendMail } from "./comail.mjs";
await sendMail({ from: "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",});Set credentials before running:
export COMAIL_DID=did:plc:your-did-hereexport COMAIL_API_KEY=atmos_your-api-keynode your-script.mjsHandling errors
Section titled “Handling errors”fetch doesn’t throw on HTTP errors, so you check res.ok yourself. The error body always has error (human-readable) and code (machine-readable). The codes you’ll commonly see:
try { await sendMail({ /* ... */ });} catch (err) { switch (err.code) { case "RATE_LIMITED": // Warming tier or per-member cap. Retry later (respect the // retry hint in err.error if present). break; case "DOMAIN_MISMATCH": // from: address domain isn't enrolled under this DID. break; case "SUSPENDED": case "PENDING": // Account-level issue. Check /account. break; case "INVALID_REQUEST": // Bad input: missing field, CRLF in headers, body > 1 MiB, // > 50 recipients, etc. break; case "QUEUE_FULL": case "TEMPORARILY_UNAVAILABLE": // Server-side. Safe to retry with backoff. break; default: throw err; }}See HTTP Send API for the full code table.
Many recipients
Section titled “Many recipients”to accepts either a string or an array. Pass an array for a single API call to multiple recipients:
await sendMail({ from: "news@yourdomain.com", to: ["alice@example.com", "bob@example.com", "carol@example.com"], subject: "Acme weekly", html: "<p>This week's updates...</p>", category: "bulk",});Maximum 50 recipients per request. Each recipient gets its own accepted or rejected entry in the response, so you can detect partial failures.
What’s messageId?
Section titled “What’s messageId?”A 64-bit integer the relay assigns when it accepts the message into its queue. Comes back as a JSON number (not a string), so if you stash it in a typed system that assumes string IDs, convert.
Next steps
Section titled “Next steps”- Message categories: pick the right
categoryfor your send. - Bounces + suppression: what happens when sends fail and how to read DSNs.
- Reply forwarding: point your MX at the relay so replies come back to you.
- SMTP submission: if you’d rather use SMTP from a framework that already speaks it.