Developer docs

Ship your first email in five minutes

A drop-in compatible API on infrastructure you control. If you've used a modern transactional email SDK, you already know UserMails — change one base URL and you're sending. Not a developer? See the user guide →

Get an API key Quickstart
Quickstart

From zero to delivered

Every send goes through one endpoint — https://api.usermails.com/emails — backed by our own dedicated-IP sending infrastructure. No third-party relay in the path.

1 Get an API key

Create a project in the Dashboard and generate a key. Keys are project-scoped and shown once — store it as an environment variable, never in client-side code. Live keys are prefixed um_live_; sandbox keys um_test_.

# keep your key in the environment, not in source
export USERMAILS_API_KEY="um_live_xxxxxxxxxxxxxxxxxxxxxxxx"

Verify your sending domain first (DKIM/SPF/DMARC) so mail lands in the inbox. Adding a domain in the Dashboard generates the records and re-checks them for you.

2 Send your first email

A single authenticated POST with a Resend-shaped payload. The response returns the message id you'll use to track delivery.

curl -X POST https://api.usermails.com/emails \
  -H "Authorization: Bearer um_live_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Acme <onboarding@yourdomain.com>",
    "to": ["dev@example.com"],
    "subject": "Welcome to Acme",
    "html": "<p>Hello from <strong>usermails</strong>.</p>"
  }'
// 200 OK
{ "id": "4ef9a3b1-8c2d-4a77-9f0e-1b2c3d4e5f60" }

3 Install the UserMails SDK

Install our zero-dependency SDK, pass a UserMails key, and send — or point an existing transactional client at https://api.usermails.com with no rewrite.

import { Usermails } from "usermails";

const usermails = new Usermails("um_live_xxxxxxxx");

await usermails.emails.send(
  {
    from: "Acme <onboarding@yourdomain.com>",
    to: "dev@example.com",
    subject: "Welcome to Acme",
    html: "<p>Hello from usermails.</p>",
  },
  { baseUrl: "https://api.usermails.com" }
);

3b Or use the official UserMails SDK

Zero-dependency, typed, and adds verify(). Works in Node 18+, Next.js, and edge runtimes.

# npm install usermails
import { Usermails } from "usermails";
const um = new Usermails(process.env.USERMAILS_API_KEY);

await um.emails.send({
  from: "Acme <hi@yourdomain.com>", to: "dev@example.com",
  subject: "Welcome", html: "<p>Hello</p>",
});

3c Verify a list before you send

Catch invalid, dead-domain, disposable, role-based, blacklisted, and suppressed addresses up front — protect your sending reputation. Via the SDK or the API.

const { results, summary } = await um.verify([
  "real@gmail.com", "admin@acme.com", "user@mailinator.com",
]);
// summary -> { deliverable, risky, undeliverable, unknown }
curl -X POST https://api.usermails.com/verify \
  -H "Authorization: Bearer um_live_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"emails":["a@x.com","b@y.com"]}'

3d Use it from AI agents (MCP)

The UserMails MCP server gives Claude and other agents send_email, verify_emails, and get_email tools.

// claude_desktop_config.json / .mcp.json
{ "mcpServers": { "usermails": {
  "command": "npx", "args": ["-y", "@usermails/mcp"],
  "env": { "USERMAILS_API_KEY": "um_live_xxx" }
} } }

4Check status & the event timeline

Fetch a single message by id to see its current status and full lifecycle, or list recent sends for a project-wide log.

curl https://api.usermails.com/emails/4ef9a3b1-8c2d-4a77-9f0e-1b2c3d4e5f60 \
  -H "Authorization: Bearer um_live_xxxxxxxx"
{
  "id": "4ef9a3b1-8c2d-4a77-9f0e-1b2c3d4e5f60",
  "status": "delivered",
  "to": ["dev@example.com"],
  "subject": "Welcome to Acme",
  "events": [
    { "type": "queued",    "at": "2026-06-20T18:00:00Z" },
    { "type": "sent",      "at": "2026-06-20T18:00:00Z" },
    { "type": "delivered", "at": "2026-06-20T18:00:02Z" }
  ]
}

List the recent log — filter by status, domain, tag, recipient, or date:

curl "https://api.usermails.com/emails?status=bounced&limit=25" \
  -H "Authorization: Bearer um_live_xxxxxxxx"

5 Idempotency

Retrying a request after a timeout? Send an Idempotency-Key header. A repeated key within the retention window returns the original result instead of sending twice.

curl -X POST https://api.usermails.com/emails \
  -H "Authorization: Bearer um_live_xxxxxxxx" \
  -H "Idempotency-Key: order-1042-receipt" \
  -H "Content-Type: application/json" \
  -d '{ "from": "...", "to": "...", "subject": "...", "html": "..." }'

Use a key that's stable per logical action (an order id, a user-event id) — not a random value per attempt.

6 Webhooks

Register an endpoint in the Dashboard and subscribe to event types (email.sent, email.delivered, email.bounced, email.complained). UserMails POSTs the payload with the event name in the usermails-event header and an HMAC signature in usermails-signature (format sha256=<hex>), retrying with backoff on failure. Verify the signature against your endpoint's signing secret before trusting the body.

import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody, signature, secret) {
  const expected = "sha256=" + createHmac("sha256", secret)
    .update(rawBody).digest("hex");
  if (!signature || signature.length !== expected.length) return false;
  return timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

// in your handler — verify against the RAW request body
const sig = req.headers["usermails-signature"];
if (!verify(rawBody, sig, process.env.WEBHOOK_SECRET)) {
  return res.status(400).end();
}

Hash the raw bytes of the request body — parsing and re-serializing JSON changes the bytes and breaks the signature. Every event is delivered at least once; key off the event id to stay idempotent.

API reference

Core endpoints

Base URL https://api.usermails.com · authenticate with Authorization: Bearer um_live_… on every request.

Method & pathDescriptionNotes
POST /emailsSend a single email (Resend-shaped payload).Idempotency-Key scheduled_at
POST /emails/batchSend multiple emails in one request.Per-item results returned.
GET /emails/:idRetrieve a message with its status and event timeline.queued → sent → delivered / bounced …
GET /emailsList recent sends (the project log).Filter by status, domain, tag, recipient, date.
Migrating from Resend

Switch in one line

UserMails accepts Resend's payloads and responses, so your code barely moves. Swap the import — every emails.send() call, payload shape, and { data, error } response stays exactly as it is, now sending on infrastructure you control.

// before
import { Resend } from "resend";
// after — same methods, same payloads, same { data, error }
import { Resend } from "usermails/compat";

const resend = new Resend(process.env.USERMAILS_API_KEY);
await resend.emails.send({ from, to, subject, html });

Bring your domains, broadcast templates, and contacts across with one read-only command:

npx usermails-migrate resend --from re_xxxxxxxx

Run both in parallel during cutover: keep Resend live while you verify your domain and watch sends land in the UserMails log, then flip 100%. No payload migration, no data backfill.

Get your key and send

Spin up a project, verify a domain, and put a live transactional email through in minutes.

Open the Dashboard