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.
/v1/campaigns/v1/campaigns/v1/campaigns/:id/v1/campaigns/:id/v1/campaigns/:id/send/v1/campaigns/:id/analytics/v1/campaigns/:idconst 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.
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.
/v1/campaigns/:id/preview{
"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…"
}
]
}/v1/campaigns/:id/overrides{
"email": "linus@example.com",
"subject": "Just for you, {{first_name}}",
"html": "<p>Hi {{first_name}} — a hand-written note only you get.</p>"
}/v1/campaigns/:id/overrides?email=…// 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>",
});
}