Skip to main content

Client and call options

client.ts
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)

OptionDefaultNotes
apiKeyRequired. pk_live_…, server-side only.
baseUrlRequired. Your deployment's origin; a trailing slash is trimmed.
timeoutMs30000Per attempt, not per call.
maxRetries2Retries on 429 / 5xx / network errors.
defaultBrandApplied to send() / campaign() when the input omits brand.
headersMerged into every request (tracing, tenant tags).
fetchglobal fetchOverride 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:

OptionEffect
signalAbortSignal for cancellation. An aborted request is never retried.
timeoutMsOverride the per-attempt timeout for this call.
maxRetriesOverride the retry count (0 = fail fast).
idempotencyKeyYour own key instead of the generated one.
headersMerged 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.

Reference