Guides
Node SDK
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
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 yourmailConstruct
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.