rootmail

Marketing

Campaigns

Send one email to a whole audience, and measure how it lands.

A campaign is a one-time send to an audience — a newsletter, an announcement. Create it, then send (or schedule) it; suppressed and unsubscribed contacts are skipped automatically.

Every recipient gets a personalized copy: the template's {{placeholders}} fill from each contact's own record (name, first_name, phone, custom metadata fields) at send time — one template, tailored per person.

GET/v1/campaigns
POST/v1/campaigns
GET/v1/campaigns/:id
PATCH/v1/campaigns/:id
POST/v1/campaigns/:id/send
GET/v1/campaigns/:id/analytics
DELETE/v1/campaigns/:id
campaign.ts
const c = await mail.campaigns.create({
  name: "March newsletter",
  listId: list.id,
  templateId: tmpl.id,
});
await mail.campaigns.send(c.id, { sendAt: "2026-03-01T15:00:00Z" });
const funnel = await mail.campaigns.analytics(c.id);

Segments & A/B by tags

Two optional fields target the send. segment_tag narrows the audience to list members carrying that tag. variants (up to 4) A/B-tests by tag: a contact carrying a variant's tag gets that variant's template (and optional subject) instead of the base message — first matching variant wins, everyone else gets the base. Distinct tags for a list come from GET /v1/lists/:id/tags.

campaign-ab.json
POST /v1/campaigns
{
  "name": "March newsletter",
  "list_id": "lst_…",
  "template_id": "tpl_base…",
  "segment_tag": "active",
  "variants": [
    { "tag": "vip", "template_id": "tpl_vip…", "subject": "A thank-you for being a VIP" }
  ]
}

Pre-flight: read (and change) each person's copy

A campaign reaches everyone at once, so there's no checking it afterwards. GET /v1/campaigns/:id/preview resolves each recipient's ACTUAL email — the variant their tags select, rendered with their contact fields — by the same rules the send applies. It's read-only.

If one person's copy isn't right, replace it. PUT /v1/campaigns/:id/overrides stores a subject and/or body against a single recipient; theirs wins over the template and over any A/B variant. Draft and scheduled campaigns only — an override on a campaign that already went out is refused rather than silently ignored.

GET/v1/campaigns/:id/preview
Response
{
  "object": "list",
  "total": 3,
  "data": [
    {
      "object": "campaign_recipient",
      "email": "grace@example.com",
      "name": "Grace Hopper",
      "tags": ["vip"],
      "variant_tag": "vip",
      "template_name": "Newsletter VIP",
      "edited": false,
      "subject": "VIP news, Grace",
      "html": "<p>Hi Grace — as an Enterprise customer…</p>",
      "text": "Hi Grace — as an Enterprise customer…"
    }
  ]
}
PUT/v1/campaigns/:id/overrides
Request body
{
  "email": "linus@example.com",
  "subject": "Just for you, {{first_name}}",
  "html": "<p>Hi {{first_name}} — a hand-written note only you get.</p>"
}
DELETE/v1/campaigns/:id/overrides?email=…
preflight.ts
// Check everyone, then fix the one that reads wrong.
const { data } = await mail.campaigns.preview(c.id);
const odd = data.find((r) => r.subject.includes("undefined"));
if (odd) {
  await mail.campaigns.setRecipientCopy(c.id, {
    email: odd.email,
    subject: "A quick note",
    html: "<p>Hi there — here's the update.</p>",
  });
}
noteAn edited copy still goes through the normal send path: {{variables}} you type are filled from the contact's record, and the compliance footer (postal address + unsubscribe) is still appended.
Campaigns · rootmail developers