Skip to main content

Segments and campaigns

A campaign is one email to many people, right now or on a schedule. A segment is a saved filter you can point campaigns at.

campaign.ts
import { mailroom } from "./client";

// Segments are evaluated when the campaign runs, not when it is created.
const segment = await mailroom.segments.create({
label: "Engaged beta users",
filter: { status: "subscribed", tags: ["beta"] },
});

const campaign = await mailroom.campaigns.create({
name: "Beta feature launch",
template: "product-update",
subject: "New in beta this week",
audience: { mode: "segment", segmentId: segment.id },
scheduleAt: "2026-08-01T15:00:00Z", // omit to send immediately
variants: [
{ key: "a", subject: "New in beta this week", weight: 1 },
{ key: "b", subject: "You asked, we shipped", weight: 1 },
],
});

console.log(campaign.audienceSize, campaign.status);

// Progress: counters update as sends and engagement events land.
const record = await mailroom.campaigns.get(campaign.campaignId);
console.log(record.sent_count, record.opened_count, record.clicked_count);

// Stop a scheduled or in-flight campaign. Already-sent emails are unaffected.
await mailroom.campaigns.cancel(campaign.campaignId);

Audiences

audienceTargets
{ mode: "entire" }Every subscribed contact in the project
{ mode: "segment", segmentId }Contacts matching a saved segment
{ mode: "tag", tag }Contacts carrying that tag
{ mode: "people", emails: [...] }An explicit list (at least one address)

Unsubscribed and suppressed contacts are excluded no matter which mode you pick.

Segments

A segment is a label plus a filter over status, tags, and source:

const segment = await mailroom.segments.create({
label: "Engaged beta users",
filter: { status: "subscribed", tags: ["beta"] },
});

Filters are evaluated when the campaign runs, not when the segment is created — so a segment always reflects current membership. segments.list() returns each segment with a count; labels are unique per project (a duplicate is rejected 422).

Segment writes use the contacts:write scope, not a segment-specific one.

Scheduling

Omit scheduleAt to start immediately. Pass an ISO instant to queue it; add endsAt to stop a long-running send at a deadline instead of letting it finish.

await mailroom.campaigns.create({
template: "sale",
audience: { mode: "entire" },
scheduleAt: "2026-08-01T15:00:00Z",
endsAt: "2026-08-02T15:00:00Z",
});

Campaign sends are queued and drained by a dispatcher, so create() returns as soon as the audience is resolved — audienceSize and enrolled tell you how many recipients it found and enqueued.

A/B variants

Two to four arms, split by relative weight (not percentages):

variants: [
{ key: "a", subject: "New in beta this week", weight: 1 },
{ key: "b", subject: "You asked, we shipped", weight: 1 },
]

Each arm can override the subject. Per-arm counters land on the campaign record alongside the totals.

Progress and cancellation

const record = await mailroom.campaigns.get(campaignId);
// status: draft | scheduled | queued | sending | sent | cancelled | failed
// counters: total_recipients, sent_count, delivered_count, opened_count,
// clicked_count, bounced_count
const { cancelledJobs } = await mailroom.campaigns.cancel(campaignId);

Cancelling drops queued jobs; emails already handed to the provider are gone and cannot be recalled.

Scopes

campaigns:write to create and cancel; contacts:write for segment writes. Reads need only a valid key.

Reference