Client and call options
import { createClient } from "@mailroom/sdk";
const mailroom = createClient({
apiKey: process.env.MAILROOM_API_KEY!,
baseUrl: process.env.MAILROOM_URL!,
timeoutMs: 30_000, // per attempt
maxRetries: 2, // on 429 / 5xx / network errors
defaultBrand: "acme", // applied to send()/campaign() when the call omits one
headers: { "x-app": "storefront" }, // sent with every request
onRetry: ({ attempt, delayMs, reason }) => console.warn({ attempt, delayMs, reason }),
});
// Tighter budget for one call, and no retries.
await mailroom.send(
{ to: "ada@example.com", template: "welcome" },
{ timeoutMs: 5_000, maxRetries: 0, headers: { "x-trace-id": "req_123" } },
);
// Cancellation: an aborted request is never retried.
const ac = new AbortController();
const pending = mailroom.contacts.list({ status: "subscribed" }, { signal: ac.signal });
ac.abort();
await pending.catch((err) => console.warn("cancelled", err));
// Bring your own idempotency key so a client-side retry cannot double-send.
await mailroom.send(
{ to: "ada@example.com", template: "receipt", data: { orderId: "ord_123" } },
{ idempotencyKey: "receipt:ord_123" },
);
createClient(options)
| Option | Default | Notes |
|---|---|---|
apiKey | — | Required. pk_live_…, server-side only. |
baseUrl | — | Required. Your deployment's origin; a trailing slash is trimmed. |
timeoutMs | 30000 | Per attempt, not per call. |
maxRetries | 2 | Retries on 429 / 5xx / network errors. |
defaultBrand | — | Applied to send() / campaign() when the input omits brand. |
headers | — | Merged into every request (tracing, tenant tags). |
fetch | global fetch | Override for tests or a non-global-fetch runtime. |
onRequest | — | { method, url, attempt } before each attempt. |
onResponse | — | { method, url, status, attempt } after each response. |
onRetry | — | { method, url, attempt, delayMs, reason } before each backoff. |
Missing apiKey or baseUrl throws synchronously from createClient — a
configuration bug, not a request failure.
One client per process is enough: it holds no connections and no mutable state, so share it rather than constructing per request.
Per-call CallOptions
Every method takes an optional final argument:
| Option | Effect |
|---|---|
signal | AbortSignal for cancellation. An aborted request is never retried. |
timeoutMs | Override the per-attempt timeout for this call. |
maxRetries | Override the retry count (0 = fail fast). |
idempotencyKey | Your own key instead of the generated one. |
headers | Merged over the client and default headers. |
await mailroom.contacts.list({ status: "subscribed" }, { timeoutMs: 5_000, maxRetries: 0 });
Because the options object is always last, calls with an optional query still read
naturally: list({}, opts) when you have no filters.
Cancellation
const ac = new AbortController();
const pending = mailroom.contacts.list({}, { signal: ac.signal });
ac.abort(); // rejects with MailroomConnectionError, retryable: false
An already-aborted signal rejects without issuing a request. Cancellation
propagates into listAll iteration too, so a paginated walk stops at the current
page.
Observability
The hooks are the cheapest way to get Mailroom into your existing logging without a wrapper layer:
createClient({
apiKey,
baseUrl,
onResponse: ({ method, url, status, attempt }) => metrics.timing("mailroom", { method, url, status, attempt }),
onRetry: ({ reason, delayMs }) => log.warn(`mailroom retry: ${reason} in ${delayMs}ms`),
});
Every request also sends x-mailroom-sdk-version and a user-agent of
@mailroom/sdk/<version>, so server-side logs show which client version called.
Runtime support
Node 18+, Bun, Deno, Cloudflare Workers, and Vercel Functions. The SDK needs global
fetch, AbortController, crypto.randomUUID, and (for webhook verification)
Web Crypto — all standard in those runtimes. No dependencies; ESM and CJS builds
both ship.