> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Campaign template + variable personalization walkthrough: creative → channel → slots → A/B → pre-flight

> The creative bake for a campaign: turn a generic message into a polished, channel-aware template — per-channel samples, variable slots, locales, personalization preview, list hygiene, voice fallback variants, A/B split with holdout, template analytics, and the deliverability-lab ladder before launch.

# Campaign template + variable personalization walkthrough

The [campaign end-to-end guide](/guides/campaign-end-to-end) 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](/guides/campaign-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.

```text theme={null}
Hi {{first_name}}, early access to the fall sale: 25% off through Sunday.
Shop: {{short_link}} Reply STOP to opt out.
```

Over budget options, in order: tighten the copy, shorten the link with [short links](/guides/short-links-and-click-tracking), 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.

```text theme={null}
{{first_name}}, the fall coat you asked about is back — 25% off through Sunday.
See it: {{short_link}} Reply STOP to opt out.
```

### 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](/guides/campaign-personalization-preview) validates both.

```text theme={null}
Subject: {{first_name}}, your early-access code is inside
Body:    Hi {{first_name}} — the fall sale opens for members today …
```

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](/guides/campaign-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](/guides/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](/guides/campaigns-template-variable-catalogue), not memory:

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/campaigns/template-variables" \
  -H "X-API-Key: $ORBIT_API_KEY"
```

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](/guides/campaign-personalization-preview). 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](/guides/campaign-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.

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/campaigns/cmp_abc123/dry-run" \
  -H "X-API-Key: $ORBIT_API_KEY"
```

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](/guides/dnc-preflight-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](/guides/campaign-ab-testing) 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](/guides/campaign-ab-testing#generate-variants-with-ai) rewrite the base message and keep the same tokens and single opt-out line, so suggestions stay launch-ready.

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/campaigns/cmp_abc123/variants" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "variants": [
      { "name": "Control", "message_body": "Hi {{first_name}}, early access: 25% off through Sunday. {{short_link}} Reply STOP to opt out.", "variant_index": 0, "sample_percent": 50 },
      { "name": "Benefit-led", "message_body": "{{first_name}}, members save 25% before everyone else — ends Sunday. {{short_link}} Reply STOP to opt out.", "variant_index": 1, "sample_percent": 30 },
      { "name": "Short", "message_body": "Member early access: 25% off. {{short_link}} Reply STOP to opt out.", "variant_index": 2, "sample_percent": 20 }
    ],
    "ab_winner_metric": "clicked",
    "ab_holdout_percent": 10,
    "ab_confidence_threshold": 0.95
  }'
```

**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](/guides/campaign-holdout-uplift-measurement) 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](/guides/voice-broadcast-with-sms-fallback), and the broadcast-only variant is [voice broadcasts](/guides/voice-broadcasts).

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

The final gate before launch is the [deliverability lab](/guides/campaign-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:

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/campaigns/deliverability-lab" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "sms",
    "body": "Hi {{first_name}}, early access to the fall sale: 25% off through Sunday. https://shop.acme.example/fall Reply STOP to opt out.",
    "window": "30d"
  }'
```

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](/guides/campaign-deliverability-lab#pair-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](/guides/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

| Symptom | Where it shows | Fix |
| - | - | - |
| Unresolved tag renders blank | Dry-run `personalization.unresolved[]`, `warnings[]` | Fix the tag per the [merge-tag guide](/guides/campaign-personalization-preview); re-run dry-run |
| Bounce-backs / failed deliveries climb post-launch | `GET /campaigns/:id/stats`, campaign exports | Net-projection cohorts in the dry-run (`suppressed` / `opted_out` / `unreachable`) told you; clean the list, re-run [list hygiene](/guides/campaign-list-hygiene-projection) |
| AI-generated variant refused or thin | Variants generation response | The generator stays grounded in your base copy — give it a substantive base message; see [ai-suggest-failed](/troubleshooting/ai-suggest-failed) |
| Content blocked pre-launch | Deliverability lab `blocking_warnings`, `ready_to_send: false` | Fix the flagged finding (shortener, vocabulary, link shape) and re-score — section 8 ladder |
| Slow delivery / thin spread | Stats accumulating slowly, `skipped_estimate` high | Quiet-hours window defers recipients ([send windows & quiet hours](/concepts/send-gating-and-quiet-hours)); narrow the audience timezone spread or adjust the window |
| Voice leg never reaches, SMS fires anyway | Per-hop waterfall receipt | Expected on terminal voice failure inside the window; shorten `fallback_window_seconds` if stale SMS is worse than no SMS — [voice broadcast fallback](/guides/voice-broadcast-with-sms-fallback) treats it |
| Email creative filtered, legal posture clean | Lab `content_risk` vs. policy scanner pass | The scanner is legal, the lab is carrier filtering — they answer different questions; fix the lab findings |

## Related guides

* [Send a campaign end-to-end](/guides/campaign-end-to-end) — the flight this bake feeds.
* [Build a campaign with the create wizard](/guides/campaign-create-wizard) — the dashboard path over the same endpoints.
* [Template-variable catalogue](/guides/campaigns-template-variable-catalogue), [personalization preview](/guides/campaign-personalization-preview), [A/B testing](/guides/campaign-ab-testing), [holdout uplift](/guides/campaign-holdout-uplift-measurement), [list-hygiene projection](/guides/campaign-list-hygiene-projection), [deliverability lab](/guides/campaign-deliverability-lab), [template analytics](/guides/template-analytics) — the per-step deep dives this walkthrough stitches together.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.