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 →
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.
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.
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" }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" }
);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>",
});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"]}'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" }
} } }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"
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.
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.
Base URL https://api.usermails.com · authenticate with Authorization: Bearer um_live_… on every request.
| Method & path | Description | Notes |
|---|---|---|
| POST /emails | Send a single email (Resend-shaped payload). | Idempotency-Key scheduled_at |
| POST /emails/batch | Send multiple emails in one request. | Per-item results returned. |
| GET /emails/:id | Retrieve a message with its status and event timeline. | queued → sent → delivered / bounced … |
| GET /emails | List recent sends (the project log). | Filter by status, domain, tag, recipient, date. |
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.
Spin up a project, verify a domain, and put a live transactional email through in minutes.