Docs & quickstart

Ship your first email in five minutes

A Resend-compatible API on infrastructure you control. If you've used the resend SDK, you already know usermails — change one base URL and you're sending.

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 Drop in the Resend Node SDK

Already on Resend? Keep the SDK you have. Pass a usermails key and override baseUrl — no rewrite, no new dependency.

import { Resend } from "resend";

const resend = new Resend("um_live_xxxxxxxx");

await resend.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" }
);

4 Check 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 (delivered, bounced, complained, opened, clicked). usermails POSTs an HMAC-signed payload and retries with backoff on failure. Verify the um-signature header against your endpoint's signing secret before trusting the body.

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

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

// in your handler — verify against the RAW request body
if (!verify(rawBody, req.headers["um-signature"], 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 & 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.
Migrating from Resend

It's a base-URL change

Because the API accepts Resend's payloads and responses, migration is one line. Point the SDK at usermails and swap your key — your send() calls, payload shapes, and response handling stay exactly as they are.

// before
new Resend("re_xxxxxxxx");

// after — same SDK, same payloads
new Resend("um_live_xxxxxxxx");
// pass { baseUrl: "https://api.usermails.com" } per call,
// or set RESEND_BASE_URL in your environment.

Run both in parallel during cutover: keep Resend for production while you verify your domain and watch sends land in the usermails log, then flip the base URL. 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