Skip to main content

Sending

One email at a time goes through mailroom.send(). Bulk goes through campaigns or sequences — never a loop over send().

send.ts
import { createClient } from "@mailroom/sdk";

const mailroom = createClient({
apiKey: process.env.MAILROOM_API_KEY!,
baseUrl: process.env.MAILROOM_URL!,
});

const { id } = await mailroom.send({
to: "ada@example.com",
template: "welcome",
data: { name: "Ada" },
});

console.log(`queued send ${id}`);

Address the recipient by to (an email) or contactId. One of the two is required; contactId wins when both are given, and the contact's own email is used.

Transactional vs marketing

type defaults to the template's declared type. The difference is not cosmetic:

transactionalmarketing
Requires a contactNo — an ad-hoc to address is fineYes; one is created if the address is new
Unsubscribe linkNot injectedunsubscribeUrl injected, List-Unsubscribe headers set
Suppression listEnforcedEnforced

Marketing sends anchor an unsubscribe link to a contact id, so Mailroom upserts the address into the contact list when it has to. Sending marketing mail to people who never opted in is a compliance problem, not a Mailroom feature — check consent before you call.

Subject and sender resolution

Subject: send({ subject }) → the project's defaultSubject for that template → the template's built-in default.

Sender: send({ from }) → the template-level from → the brand default. Any explicit from address must sit on a verified sending domain for the project, or the send fails.

Brands

Templates, content, and senders are brand-scoped. Resolution order: send({ brand }) → the contact's brand → the project's default brand. A project with no brand at all fails with no_brand.

Set defaultBrand once on the client instead of threading it through every call:

const mailroom = createClient({ apiKey, baseUrl, defaultBrand: "acme" });

Idempotency

The SDK attaches a generated Idempotency-Key to every mutation, so its own retries cannot double-send. To make your retries safe, pass a stable key derived from the thing you are emailing about:

await mailroom.send(
{ to: "ada@example.com", template: "receipt", data: { orderId: "ord_123" } },
{ idempotencyKey: "receipt:ord_123" },
);

The key is forwarded to the delivery provider, which drops the duplicate, and the send log's unique index keeps a second row from appearing. Without a key, two calls are two emails.

What can fail

All of these arrive as MailroomValidationError (422) — branch on err.code:

codeMeaning
invalid_requestPayload failed validation (bad email, missing template, neither to nor contactId)
unknown_templateNo such template key for this project/brand
template_not_enabledThe template exists but is not enabled — call templates.setEnabled
no_brandThe project has no brand to send under
invalid_contentThe published content does not satisfy the template's schema
send_failedThe provider rejected it, rendering failed, or the recipient is suppressed

404 not_found means the contactId does not exist in this project.

Suppressed recipients fail loudly

A suppressed address (bounce, complaint, or manual entry) never reaches the provider — the call fails with send_failed and a logged row explaining why. Check suppressions before assuming a delivery problem.

Confirming a send

send() resolves to { id }, the provider's message id. Engagement state lands later, via webhooks, on the send-log row:

const [latest] = await mailroom.sends.list({ template: "welcome", limit: 1 });
console.log(latest.status); // "sent" | "failed"
console.log(latest.delivery_status); // "queued" | "delivered" | "bounced" | "complained" | null
console.log(latest.open_count, latest.click_count);

Reading the log needs the sends:read scope. For a live feed instead of polling, subscribe to webhooks.

Reference