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.
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" }
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" } );
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 (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.
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. |
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.
Spin up a project, verify a domain, and put a live transactional email through in minutes.