YourMail

Guides

Node SDK

npm install yourmail

yourmail is a zero-dependency TypeScript client for the YourMail API. It works in Node.js 18+, Cloudflare Workers, Vercel Edge, Deno, and any environment with a global fetch.

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 and the baseUrl https://api.yourmail.dev, also shown in your YourMail dashboard.

import { YourMail } from "yourmail";

const yourmail = new YourMail(process.env.YOURMAIL_API_KEY!, {
  baseUrl: process.env.YOURMAIL_BASE_URL!, // https://api.yourmail.dev
});

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);

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_exceeded429, 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 idempotencyKeywhen 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
Yes (global fetch)
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.