Skip to main content

Campaign template + variable personalization walkthrough

The campaign end-to-end guide builds the flight: audience, dry-run, launch, measurement. This guide is the creative bake — everything that happens to the message between “draft text” and “ready to launch.” Walk it once and a generic campaign body becomes a polished, channel-aware template with a tested variant set and a pre-flight score you trust. The order is load-bearing: settle the creative per channel first, then resolve variable slots, then preview coverage and list hygiene, then split variants with a holdout, and only then hand the finished body to the deliverability lab. Each step below is read-only until the final launch call, so re-run every gate on every edit. For the wizard-driven version of the same flow, see Build a blast or drip campaign with the create wizard. Everything here also runs from the API — the wizard is a client of the same endpoints.

1. Framing: creative → channel → slots → locales → A/B → hygiene → pre-flight

Treat the creative side as a ladder with one rule: original beats generic, specific beats original. A recipient-specific {{first_name}} opener on the right channel outperforms a polished generic blast, and a channel-native body outperforms a reused one. The ladder this guide walks:
  1. Channel entries — write the body per channel instead of porting one SMS to everywhere (section 2).
  2. Variable slots — swap literals for merge-tags from the catalogue, and validate them (section 3).
  3. Locales — translate the finished template, preserving tokens and the opt-out line (section 4).
  4. A/B + holdout — split the polished body against alternatives with a randomized control (section 6).
  5. List hygiene — project what the send gates will drop and what it would have cost (section 5).
  6. Deliverability-lab pre-flight — score filtering risk per carrier and iterate until green (section 8).
Skip a rung and the payoff chain breaks: an A/B test over an unvalidated tag set compares two messages that both render blank, and a deliverability score computed before locale translation is a score for a body you are not sending.

2. Channel entries: write per channel, not once

A campaign’s channel (or its channels[] fallback chain) decides what the body can carry. Write each entry natively; the wizard’s Message step and the API’s message_template accept up to 100,000 chars, but each channel’s real budget is smaller.

SMS: one segment budget

One GSM segment is 160 chars; a U.S. carrier-grade target is under 160 so no handset sees a split. Put the hook and the call to action in the first 80 chars, and keep exactly one opt-out instruction.
Over budget options, in order: tighten the copy, shorten the link with short links, or move to MMS (below). Never drop the opt-out line to fit — that is a carrier and a legal problem, not a creative one.

MMS: attach the visual

MMS carries an image or video plus a longer caption. Use it when the creative asset does the selling — a product shot beats three extra sentences. Keep the caption in SMS shape anyway; handsets that fall back from MMS to SMS get the text alone.

Email: subject + visual body

Email adds a subject line and an HTML body built in the dashboard’s visual builder (or handed to the API as HTML). Run the same variable slots in the subject and body; the personalization preview validates both.
Prefer the visual builder’s saved blocks over hand-pasted HTML — the builder emits markup that survives the major inboxes’ rendering quirks, and the deliverability lab scores the subject alongside the body.

WhatsApp: approved template with numbered parameters

WhatsApp sends through pre-approved message templates whose variables are positional tokens — {{1}}, {{2}} — bound in template-parameter order, not contact-field merge-tags. Two consequences:
  • Composing is: pick the approved template, then map each positional slot to a contact field or campaign variable.
  • {{ first_name }}-style tags and the positional {{1}} form are validated by different layers — the merge-tag validator deliberately skips numeric tokens, so both can coexist in one body without false warnings.
Sample binding, conceptually: template body Hi {{1}}, your order {{2}} ships today. with slots {{1}} → first_name, {{2}} → variables.order_ref.

RCS: rich card with fallback covered

RCS carries rich cards — a title, body, media, and action buttons — so the creative unit is the card, not the line of text. Build the card, then write the SMS fallback text anyway: recipients whose handset or carrier cannot receive RCS get the SMS rendition on the same dispatch. The create wizard’s RCS sender panel reports your sender verification state; build now and launch when verified. The RCS campaign launch playbook covers verification and card shapes.

Editable samples, per send

Whatever the channel, the body stays editable until launch. The patterns above are starting points — edit them per campaign, and keep the opt-out line exactly once on every carrier-gated channel.

3. Variable slots: catalogue → insert → validate

Personalization tokens use {{token}} form and resolve per recipient at send time. Build them from the template-variable catalogue, not memory:
Insert the row’s key between double braces — {{first_name}}, never {{First name}}. The three contract layers, cheapest to richest:
  1. The five standard contact fields — {{first_name}}, {{last_name}}, {{phone}}, {{email}}, {{company}}. Resolver-backed; prefer them for reusable creative.
  2. {{coupon_code}} — per-recipient generated coupon, driven by the campaign’s coupon config.
  3. Campaign variables — every key on campaign.variables becomes a tag that resolves identically for every recipient: variables: { "promo_expiry": "Sunday" } renders {{promo_expiry}} as “Sunday” for everyone.
Anything else renders blank, silently, for every recipient — there is no send-time error. That is why the next gate exists.

Personalization preview: catch the blank before launch

The campaign dry-run validates every {{...}} tag in the bodies it would actually send against the full contract, and reports two things:
  • personalization.unresolved[] — tags outside the contract (a casing typo like {{firstName}}, a field the platform does not carry like {{loyalty_tier}}). Non-empty means the message ships a hole to everyone.
  • personalization.coverage[] — for per-recipient tags, how many sampled recipients actually carry a value. {{first_name}} with 8 empty contact records renders blank for those 8.
Each finding lands in warnings[] and holds ready_to_launch at false until it is cleared. The full contract, warning shapes, and fixes are in Validate campaign personalization merge-tags before launch. Gate your pipeline on it: any warning beginning with Personalization blocks launch.

4. Locales: translate the finished template

Translate only after the body is final — a re-translated message re-needs every gate downstream. POST /api/v1/campaigns/templates/translate rewrites a template body for a target language while preserving two invariants:
  • Tokens survive verbatim — every {{token}} stays in place, unchanged.
  • The opt-out instruction stays exactly once — a carrier requirement the translator enforces.
Store the localized bodies as variant templates on the campaign, or split the audience by language and run one campaign per locale. Either way, re-run the dry-run and the deliverability lab per locale: the same claim translated can score differently against a language-specific spam classifier.

5. List hygiene: project what the gates will drop

Before you split variants or score deliverability, the dry-run’s list-hygiene projection tells you how much of the deliverable cohort the send-time gates will actually drop — suppression, opt-outs, unreachable addresses, and the Do-Not-Call sample over phone-shaped channels.
Read list_hygiene.dnc.blocked_estimate and formatted_savings_usd alongside audience.deliverable: the first says “roughly N recipients vanish at the send gate,” the second “$X of spend avoided.” When dnc.checked is false — email-only channel, or a sampling failure — treat DNC exposure as unknown, never as clean. A high sampled-hit ratio is the signal to run the batch pre-flight scrub for per-number verdicts and clean the list before you buy variant data on recipients who will never receive anything. These are tenant-owned controls: the projection reads your own suppression, consent, and registry-sync data and writes nothing. It never messages a carrier.

6. A/B split with a holdout for uplift

With the creative settled, run the A/B test against it. Two disciplines separate a real answer from noise: Split variants, not audiences. Two to four variants, each with a distinct angle — a different hook, value framing, or call to action — assigned evenly or with a weighted sample_percent split that sums to 100. Each recipient sees exactly one variant. AI-generated variants rewrite the base message and keep the same tokens and single opt-out line, so suggestions stay launch-ready.
Hold out a control cohort. ab_holdout_percent (1–50) carves a slice that receives nothing during exploration. Without it, you learn which message beat the others; with it, GET /campaigns/:id/holdout-lift tells you whether the winner beat doing nothing — the holdout uplift guide covers both the campaign-wide and the smart-send timing holdouts. Assignment is deterministic per contact, so a paused-and-resumed campaign keeps the same control set, and no experiment surface biases another. Pick ab_winner_metric to match the campaign’s goal. A click-through promo judged on delivered locks in the variant that arrived, not the one that converted.

7. Voice variants for fallback

When the campaign’s fallback chain ends on voice — or a voice-first design falls back to SMS — the creative work shifts from text to script. The voice leg is a spoken script with an optional guided flow, and the SMS fallback restates the same purpose in one segment with the opt-out line:
  • Voice script: who is calling, why, and the one action you want (press 1 to confirm, call back a number). Keep it under ~30 seconds spoken.
  • SMS fallback: the same purpose compressed, with Reply STOP to opt out — it may be the recipient’s only touch with the message.
cascade_policy never carries a voice hop — route voice-first chains through the /notify waterfall instead. The full recipe — payload shape, fallback window, TCPA/DNC gates for the voice leg, and the per-hop receipt — is Voice broadcast with SMS fallback, and the broadcast-only variant is voice broadcasts.

8. Deliverability-lab ladder: score, fix, re-score

The final gate before launch is the deliverability lab. It is distinct from the regulatory policy scanner: the scanner answers “is this send legal,” the lab answers “will carriers let it through.” Both run in the wizard’s Review step; from the API the lab is read-only and re-runnable on every edit:
The ladder to climb, quoted from the lab’s own loop:
  1. Pre-send score. content_risk.score (0–100) blends spam-trigger vocabulary with URL/link heuristics — shared shorteners, bare IP links, http:// links, link-stuffing. The verdict lands as pass / warn / block with per-finding remediation, and predicted_delivery discounts your own per-carrier history by that score.
  2. Retry. blocking_warnings non-empty means the body would be filtered or the predicted rate sits at or below 50%. Fix where the penalties came from — branded link domain instead of a shared shortener, https://, pull the urgency vocabulary.
  3. Adjust and re-score. Re-run the lab on the edited body. Anchor on ready_to_send: true and an empty blocking_warnings — gate on the verdict, not a numeric threshold, since scores shift as the keyword table evolves.
Per-carrier predicted rates are sample-weighted, so a low-volume carrier cannot swing the headline; no history returns “has_history: false” instead of an invented number. Measure the prediction against your own test handsets with the seed lab when you want terminal-delivery truth instead of a forecast.

9. Template analytics: read what the creative did

After launch, a reused template accumulates its history across every campaign that sent it. Template analytics rolls that into one view — GET /api/v1/templates/{id}/analytics returns cross-campaign totals, delivery/open/click rates, and a per-campaign funnel — so the creative question “which message actually converts” gets answered across campaigns instead of one send at a time. A delivery drop after a template edit is the signal to re-run sections 3–8 on the new copy.

10. Trouble matrix