Per-contact channel optimization
A campaign sends every recipient on one declared channel. Per-contact channel optimization relaxes that: each recipient goes out on the channel their own engagement history says they respond to — picked from the channels you already declared on the campaign. A recipient who always opens RCS gets RCS; a recipient who answers SMS gets SMS; one campaign, the right channel per person. This page defines how the optimization decides, what bounds it, and how to turn it on. The engagement data itself (delivery, open, reply events per contact) is covered under the CDP event model.Section 1 — The primitives that already existed
Two engagement-driven primitives shipped before this one. Both read the same per-contact engagement profile, and neither changed which channel a campaign dispatched on:-
Smart send (per-contact timing). With
send_time_optimization: "recipient-optimal"on a campaign, each recipient is scheduled at their next best-time window instead of a single send-at. The ranking behind it also computes a per-recipient channel hint — exposed as thenext_best_channelblock onPOST /campaigns/:id/preview-smart-send— but the hint was advisory only. The blast executor still dispatched everyone on the campaign’s declared channel. -
Delivery-based fallback. The
cross-channel fallback chain walks a
recipient down your channel list on a delivery failure signal (
failed/no_delivery/no_engagement). That reacts to a failed send attempt; it never picks a better channel up front.
Section 2 — Where the ranking comes from
The ranking reads each contact’s existing engagement profile: the per-channel event counters the platform already maintains from delivery and engagement traffic (the same counters smart send reads for send-time prediction). There is no new schema, no new data collection, and nothing to enable upstream — campaigns that have been sending already carry the history this feature ranks on. Per candidate channel, the ranking compares how much that contact has engaged on that channel, with older events counting for less than recent ones. A contact with no usable history gets a fallback recommendation, and the campaign keeps them on the base channel. Because the profile is per-contact and per-channel, the ranking can’t invent a channel you didn’t offer it — it only ranks the candidate set the campaign supplies, which the next sections bound.Section 3 — Opt-in contract
Channel optimization is opt-in, default off. A campaign turns it on with one key in itsvariables object — a JSONB field carried on the campaign
resource, settable through the update-campaign API:
adaptive_pacing, trust_tier_governor, and ab_bandit all live under
campaign.variables — so one PATCH sets it and no migration or separate
resource is involved. Absent, malformed, or "enabled": false means every
campaign keeps its existing static-channel behaviour.
The candidates the feature ranks are derived from the campaign itself:
- The campaign’s base channel is always a candidate.
- Every channel in the campaign’s fallback chain that sits inside the safe-swap envelope (next section) joins the candidate set.
- If the base channel is outside the envelope, or fewer than two distinct safe candidates exist, there is nothing to optimize and the campaign sends exactly as before.
Section 4 — The safe-swap envelope
Per-contact channel overrides are restricted to the same channel set the budget-aware fallback already uses for mid-campaign downgrades: SMS, MMS, RCS, and Viber. Four properties make that set safe to switch per recipient:- All four are phone-addressed. The recipient is the contact’s phone number on each of them — no identifier remapping when the channel changes.
- No per-loop setup is skipped. Email needs a subject and one-click unsubscribe headers; WhatsApp needs an approved template resolved before the send loop; voice needs consent, caller-ID, and TTS configuration. Those steps run once, keyed off the campaign’s declared base channel — a per-recipient switch into one of those channels would silently bypass them. The envelope excludes those channels outright, so an override can never route a recipient somewhere the campaign is not set up to send.
- Push and social channels (messenger, instagram, line, telegram, kakao, zalo, wechat) are excluded for the same reason: identifier-based addressing, not E.164.
Section 5 — Credit caps stay an upper bound
The ranking is cost-blind: an engagement preference for MMS over SMS is legitimate, but MMS costs more per message. A campaign with a credit cap estimates its spend per batch as base-channel price × batch size — if an override routed recipients onto a pricier channel, real spend could exceed the estimate and breach the cap. So when the campaign has a credit cap configured, the candidate set is narrowed to channels whose per-message price is no more than the base channel’s, priced through the same source the cap estimate uses. The cap estimate then stays a strict upper bound on real spend. No cap configured — the full engagement-ranked candidate set is used.Section 6 — Where the advisory preview ends
POST /api/v1/ai/optimize-channel still answers “which channel should a
single message go out on?” for one-off sends — it ranks and recommends, and
the returned ranking remains what your own sending logic does with it. That
endpoint never touched campaign dispatch; channel optimization is what wires
the ranking into the campaign executor. Use the endpoint for ad-hoc single
sends, the opt-in for campaigns.
Safety and invariant notes
- Tenant-owned controls only. The opt-in lives on the campaign resource you own; the ranking reads your tenant’s own engagement profiles, not any cross-tenant signal.
- No new providers or channels. An override only re-labels which of your campaign’s already-declared channels a message goes out on. Outbound SMS, MMS, and RCS keep exiting through the Devotel softswitch exactly as before — the feature introduces no channel or provider the campaign didn’t already declare.
- Fail-closed. Malformed opt-in config, an unpriced channel under a credit cap, or a failing profile read all resolve to “keep the base channel” — never to a guessed override.
See also
- Cross-channel fallback — the delivery-signal-driven chain this feature complements.
- CDP event model — where the engagement counters come from.
- Campaign lifecycle — where dispatch scheduling sits in the campaign state machine.
- AI endpoints reference — the single-send
/ai/optimize-channeladvisory endpoint.