Skip to content
YourMail

Guides

Node SDK

npm install yourmail

yourmail is a zero-dependency TypeScript client for the YourMail API. It works in any server-side environment with a global fetch — Node.js 18+, Cloudflare Workers, Vercel Edge and Deno. It is not for browsers: the key it carries is a secret.

Why use this

The SDK is an optional helper that wraps the API for you. Rather than writing raw HTTP requests and parsing responses yourself, you call neat typed methods — your editor autocompletes the options and flags mistakes before you run anything. It does the same thing under the hood; it just saves you the boilerplate and the typos.

For example

Write yourmail.send({ to, subject, html }) with full autocomplete, instead of hand-building a fetch call with the right URL, headers, and JSON body.

Install

npm install yourmail

Construct

Pass your API key. The client already points at https://api.yourmail.dev, so you only need the optional baseUrl option if you are targeting a different deployment.

import { YourMail } from "yourmail";

const yourmail = new YourMail(process.env.YOURMAIL_API_KEY!);

Send an email

All fields match the POST /v1/emails request schema. Returns { id: string }.

const { id } = await yourmail.send({
  from: "hello@mail.acme.com",           // verified domain required for live sends
  to: ["alice@example.com"],             // up to 50 recipients across to/cc/bcc
  subject: "Welcome to Acme",
  html: "<h1>Hello Alice</h1>",
  text: "Hello Alice",                   // plain-text fallback
  cc: ["bob@example.com"],
  bcc: ["archive@acme.com"],
  replyTo: "support@acme.com",
  attachments: [
    {
      filename: "invoice.pdf",
      content: "<base64-encoded content>",  // base64 string
      contentType: "application/pdf",       // defaults to application/octet-stream
    },
  ],
  tags: [{ name: "category", value: "welcome" }],  // max 10
  idempotencyKey: "welcome-usr_123",
  headers: {
    "List-Unsubscribe": "<mailto:unsub@acme.com>",  // max 10 headers
  },
});

console.log(id);

react-email

Install the optional peer dependency @react-email/render to pass a React component directly. The SDK renders it to HTML locally before sending — the server only ever receives a plain HTML string. The react and html fields are mutually exclusive.

// Install the peer dependency first:
// npm install @react-email/render

import { WelcomeEmail } from "./emails/WelcomeEmail"; // your react-email component

await yourmail.send({
  from: "hello@mail.acme.com",
  to: "alice@example.com",
  subject: "Welcome",
  react: <WelcomeEmail name="Alice" />, // mutually exclusive with html
});
// The SDK renders to HTML locally; the server only sees a plain HTML string.

Batch send

Send up to 100 emails in one request. The response is an array with one entry per input — either { id } (success) or { error } (failure). The whole call always resolves (unless a request-level error occurs); iterate and check each item.

const { data } = await yourmail.batch([
  {
    from: "hello@mail.acme.com",
    to: "alice@example.com",
    subject: "Your receipt",
    html: "<p>Receipt for Alice</p>",
  },
  {
    from: "hello@mail.acme.com",
    to: "bob@example.com",
    subject: "Your receipt",
    html: "<p>Receipt for Bob</p>",
  },
]);

for (const result of data) {
  if ("id" in result) {
    console.log("queued:", result.id);
  } else {
    console.error("failed:", result.error.type, result.error.message);
  }
}

Get an email

const email = await yourmail.get("jn7abc123def456");

// EmailStatus shape:
// {
//   id: string
//   status: "queued"|"sent"|"delivered"|"bounced"|"complained"|"failed"
//   from: string
//   to: string[]
//   subject: string
//   sesMessageId: string | null
//   error: string | null
//   createdAt: number        // Unix ms
//   sentAt: number | null    // Unix ms
// }
console.log(email.status);

Usage, domains, keys and webhooks

The account side of the API, for scripting onboarding. Every call here needs a full-access key.

const usage = await yourmail.usage();
// usage.month.limit is null on an unlimited plan — check before subtracting.

const domain = await yourmail.domains.create("mail.acme.com");
domain.dnsRecords; // publish these; domains.get(id).status flips to "verified"

const { key } = await yourmail.apiKeys.create({ name: "ci", scope: "send" });

const hook = await yourmail.webhooks.create({
  url: "https://acme.com/hooks",
  events: ["*"],
});
hook.secret; // on the create result only

// After publishing the DNS records, ask for a re-check instead of waiting for
// the background poll. Safe in a loop: it never moves a domain backwards, and a
// verified one is still re-checked so a repaired DNS record can be confirmed.
const checked = await yourmail.domains.verify(domain.id);

Schedule and cancel

A time in the past — or within a few seconds — resolves to “send now” rather than being refused: clock skew between your server and ours is real and small, and an outage caused by a four-second difference is a worse failure than an early send.

// Book a send instead of running it now. Charged against your quota when it
// dispatches, not when you book it — so a cancelled send costs nothing.
const { id } = await yourmail.send({
  from: "hello@mail.acme.com",
  to: ["alice@example.com"],
  subject: "Your trial ends tomorrow",
  text: "…",
  scheduledAt: "2026-09-20T09:00:00Z", // ISO 8601 or Unix ms
});

// Cancellable right up to the moment it goes. `canceled: false` means it had
// already left — the row's status decides, never the scheduler.
const { canceled } = await yourmail.cancel(id);

Listing and suppressions

Both lists are cursor-paginated, and both have an async-iterator form that pages for you. Prefer the iterator: the list endpoint may return an empty page while more rows exist, so a loop that stops on a short page under-reports.

// One page at a time, newest first.
const page = await yourmail.list({ status: "bounced", limit: 50 });
page.data; page.hasMore; page.nextCursor;

// Or let the client page for you. Prefer this: the list endpoint may legally
// return an EMPTY page with hasMore: true, so a hand-rolled loop that stops on
// a short page reports "nothing" for an account that has rows further back.
for await (const email of yourmail.listAll({ status: "bounced" })) {
  console.log(email.id, email.to);
}

// Suppressions: who we will not send to, and why.
for await (const row of yourmail.listAllSuppressions()) {
  console.log(row.address, row.reason); // hard_bounce | complaint | unsubscribe | manual
}
await yourmail.createSuppression("alice@example.com");
// Moving a list off another provider? Up to 100 per request, rather than one
// call per address.
await yourmail.createSuppressions(["a@x.com", "b@y.com"]);
await yourmail.deleteSuppression("alice@example.com");

Error handling

All API errors throw a YourMailError, or one of its subclasses — ValidationError, AuthenticationError, NotFoundError, RateLimitError and ServerError — so you can branch with instanceof instead of comparing type strings. Every one extends YourMailError, so an existing catch keeps working. The full error type reference is on the Errors page. The retryAfter field is only set for rate_limited errors.

import { YourMail, YourMailError } from "yourmail";

try {
  await yourmail.send({ ... });
} catch (err) {
  if (err instanceof YourMailError) {
    console.error(err.status);    // HTTP status, e.g. 429
    console.error(err.type);      // e.g. "rate_limited"
    console.error(err.message);   // human-readable description

    if (err.retryAfter !== undefined) {
      // Seconds to wait — only present on rate_limited
      console.log(`Retry after ${err.retryAfter}s`);
    }
  }
}

Automatic retries

Transient failures are retried for you — up to maxRetries times (default 2, so three attempts) with exponential backoff and full jitter, honouring a server-sent Retry-After up to a 20-second ceiling. Retried: rate_limited 429s, 5xx responses, network errors and timeouts. Not retried: any other 4xx, a quota_exceeded 429, and any 429 asking for longer than the ceiling — a monthly quota or a daily cap can't clear inside a retry loop, so you get the error immediately and can schedule off retryAfter.

Retrying a send can't deliver twice: send() and batch() attach an idempotencyKey when you don't supply one and reuse it across attempts, so if a response is lost after the API accepted the message, the replay is recognised and returns the original id.

// Retries are on by default — nothing to wire up.
const yourmail = new YourMail(process.env.YOURMAIL_API_KEY!);

// Turn them off for this client...
const noRetry = new YourMail(process.env.YOURMAIL_API_KEY!, { maxRetries: 0 });

// ...or just for one call.
await yourmail.send(email, { maxRetries: 0 });

// Supply your own key to keep the same guarantee across separate calls
// (e.g. a webhook handler your provider may deliver more than once).
await yourmail.send({ ...email, idempotencyKey: `welcome-${orderId}` });

AbortSignal & timeout

Every method accepts an optional second argument with a signal property for cancellation or timeout. timeoutMs bounds the whole call including retries, not each attempt, so turning retries on never multiplies the worst case.

// Timeout using AbortSignal.timeout (Node 17.3+)
await yourmail.send(
  { from: "...", to: "...", subject: "...", html: "..." },
  { signal: AbortSignal.timeout(5000) },
);

// Manual abort
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000);
await yourmail.get("msg_abc123", { signal: controller.signal });

Runtime support

Node.js 18+
Yes (global fetch built in)
Cloudflare Workers
Yes
Vercel Edge Runtime
Yes
Deno
Yes
Browsers
No. Your API key is a secret, and anything running in a browser ships it to every visitor. The API also sends no CORS headers, so the request fails at the preflight even if you try. Call YourMail from your server — a Route Handler, Server Action, or backend endpoint — and have the browser call that.
Node.js < 18
No (no global fetch)

Zero runtime dependencies. TypeScript types ship with the package (dist/index.d.ts). Ships both ESM and CommonJS builds, so import and require both work.