Skip to content

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.

  1. A comail account with at least one enrolled sending domain. If you don’t have one, sign up at /signup.
  2. 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.
  3. Node 18 or newer (node --version).
  4. A recipient address you can check. Use one of your own.

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-here with your actual DID
  • atmos_your-api-key with your actual key
  • you@yourdomain.com with an address on your enrolled domain
  • alice@example.com with a recipient you can check

Run it:

node send.mjs

Expected output on success:

accepted: 1
rejected: 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.

For an example suitable for a project:

comail.mjs
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-here
export COMAIL_API_KEY=atmos_your-api-key
node your-script.mjs

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.

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.

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.

Back to docs