Skip to main content

Smart-send timing playbook

“Smart send” names three different surfaces in Orbit, and they answer three different questions. This guide walks the campaign-side ones — when each contact should receive your campaign, and which channel each contact is likeliest to engage on — and keeps them straight from the per-message router and the fixed-window experiment. The first is a per-contact prediction read from each contact’s engagement history. The third and fourth are a controlled experiment between fixed windows you choose. They are complementary, not competing: preview-smart-send tells you what the model predicts per contact; the experiment tells you, causally, whether a window you chose beats doing nothing. For the dialer equivalent of per-contact timing (stamping each contact’s first dial attempt into their predicted answer window), see Best-time-to-dial.

Prerequisites

  • An API key with the campaigns:read scope (the preview and results endpoints are read-only; arming the experiment uses the standard campaign update call).
  • A campaign in draft or scheduled state. The previews resolve the campaign’s audience exactly as a launch would.
  • Engagement history makes the predictions sharper. A brand-new workspace with no history still gets a schedule: contacts fall back to a timezone-aware local window derived from their phone number.

The smart-send family, disambiguated

Per-contact campaign schedule: preview-smart-send

POST /api/v1/campaigns/{id}/preview-smart-send walks the campaign’s resolved audience — list, segment, CSV upload, manual picker, or all contacts — and returns, per contact, the instant the engagement model predicts that contact is most likely to engage. The predictions come from each contact’s own engagement profile (the same per-contact history every smart-send surface reads); nothing new is collected. The response is a bounded preview: by default the first 500 contacts, up to 5,000 with ?limit=. The full per-contact schedule for a 100k-contact audience is computed on the fly by the execution worker when the campaign runs — the preview exists so you can review the shape of the plan before launch, not to export it.

Per-message router: POST /messages/smart-send

POST /api/v1/messages/smart-send takes one ad-hoc message, picks a channel with the smart router, and dispatches. With a cascade_policy it also arms a fallback chain on that send. It never reads a campaign audience and produces no schedule. If your flow is “this one OTP must reach this one person on the best channel now,” that is the per-message router — covered in Per-message cascade policy on smart-send.

Fixed-window experiment: send-time-experiment/preview and /results

The send-time experiment does not predict a per-contact optimum. It splits the audience into randomized, mutually-exclusive cohorts — one per fixed UTC window you declare, plus an optional do-nothing control — sends each cohort at its window, and scores which window lifted conversion over control. Use it when you want a causal verdict between candidate windows (“9am vs 1pm vs 6pm”) rather than a per-contact prediction.

Two lift endpoints, two questions

  • GET /api/v1/campaigns/{id}/holdout-lift — did sending at all lift conversion? Treatment vs a do-nothing control. Covered in A/B test a campaign.
  • GET /api/v1/campaigns/{id}/smart-send-holdout-lift — did sending at each contact’s predicted best time lift conversion over their unoptimized schedule? Only meaningful on a recipient-optimal campaign that reserved a timing hold-out (smart_send_holdout_pct); a campaign that never opted in returns configured: false, not an error.
And a channel recommendation is not a fallback chain: next_best_channel (below) is advisory per-contact output, while a static fallback chain — the campaign channels[] ladder or a per-message cascade_policy — is what actually arms escalation on undelivered sends. See Fallback chains for the chain surfaces.

The preview-smart-send contract

Response:
Read it as a go/no-go check before launch:
  • total_audience vs preview_count. The schedule covers the preview slice; the whole audience is scheduled at send time. total_audience is the matched audience after suppression and opt-out netting, the same cohort a launch dry-run reports as deliverable.
  • profile_count vs fallback_count. Contacts whose time came from their own engagement history vs contacts who got the timezone-aware fallback. A high fallback share on a mature workspace usually means the audience skews to contacts you have never messaged.
  • distribution_utc. Sends per UTC hour across the preview. A healthy per-contact schedule spreads through each recipient’s daytime; a spike in one UTC hour on a single-timezone audience is what you want to see.
  • schedule. The per-contact map itself, ISO-8601 instants.
The preview never mutates the campaign and never sends. Quiet hours, frequency caps, and opt-outs still gate the actual send at dispatch time — a predicted instant outside your quiet-hours window is skipped then, exactly as on any campaign. See Quiet hours and Frequency caps.

Next-best-channel: advisory per-contact channel picks

When — and only when — the campaign declares more than one channel (the primary channel plus at least one entry in the channels fallback ladder, e.g. RCS with an SMS fallback), the preview response also carries a next_best_channel block:
Each recommendation ranks the declared candidate channels for that contact from the contact’s own per-channel engagement history. source: "profile" means the contact has history on at least one candidate; source: "fallback" means the contact has none and the recommendation is the neutral default. Two things the block deliberately does not do:
  • It is advisory. It informs how you order the campaign’s channel ladder before launch. It does not change which channel the execution worker dispatches on first — dispatch follows the chain you configured, and escalation follows terminal-DLR fallback.
  • It is not the static chain. A static fallback chain (Per-message cascade policy on smart-send, or the campaign ladder in Fallback chains) is an ordering you set and the platform executes on failure. next_best_channel is a per-contact readout you may use to choose that ordering. If you want the first-hop decision made per contact at send time, that is the per-message smart router, not this preview.
A single-channel campaign gets no next_best_channel block — there is nothing to recommend between.

The send-time experiment variables

Opt in by setting send_time_experiment in the campaign’s variables with the standard campaign update call:
Rules the parser applies:
  • Two to four windows. Fewer than two usable windows means no experiment — the campaign sends as a single, un-experimented blast. Windows beyond four are dropped.
  • hour 0–23 and minute 0–59, integers. A malformed window is skipped, not coerced to midnight.
  • Distinct clock times. Two windows at the same instant are deduped; a “winner” between identical times would be meaningless.
  • holdout_pct 0–50. The randomized do-nothing control. Values above 50 clamp to 50; omit it (or send 0) for no control — but then windows are scored on raw conversion rate alone, and you lose the causal baseline.
  • Windows are UTC hour-of-day. Convert from your audience’s timezone before you choose.
label is optional and defaults to HH:MM. Each contact’s arm (a window, or control) is assigned deterministically per contact, so pause/resume and audience re-resolution never move a contact between arms, and the assignment is uncorrelated with your A/B variant split, the campaign-wide control holdout, and any journey holdout.

Preview the cohort plan before launch

POST /api/v1/campaigns/{id}/send-time-experiment/preview resolves the eligible audience (suppressed, opted-out, and address-less contacts netted out, matching the launch dry-run) and returns each window’s cohort with its next fire instant, plus the control size. It is read-only: calling it reports the assigned-arm mapping without arming or mutating anything. Candidate sends are anchored to the campaign’s configured start: a campaign scheduled for next week shows each window’s first occurrence on or after that start, never before it. A campaign that launches immediately rolls each window to its next occurrence from now.

Reading the experiment results

GET /api/v1/campaigns/{id}/send-time-experiment/results re-partitions the audience into the same arms (deterministic assignment reconstructs them exactly), joins each arm’s real conversions, and scores every window against the control:
  • Per window: treatment and control conversion rates, the absolute lift in percentage points with its confidence interval, and an is_significant flag.
  • winning_window_index: the window with the highest statistically significant lift over control; when nothing is significant yet, the highest raw conversion rate; ties break to the earlier-declared window. winner_is_significant tells you which rule applied.
  • The control row itself: how many contacts were held out and how many converted.
Before conversions join, the endpoint returns the window table with empty rates rather than an error — poll it as the send progresses. Promote the winner by scheduling the remaining audience at that window with the same update call you used to set the variables.

Worked example: preview, opt in, launch, read results

To measure the per-contact prediction instead of fixed windows, skip steps 2–3, launch with the recipient-optimal schedule, and reserve smart_send_holdout_pct so GET /campaigns/{id}/smart-send-holdout-lift can score optimized vs unoptimized timing afterwards.

Troubleshooting

  • 404 on the preview. The campaign id must exist in the workspace; the preview resolves the campaign before anything else.
  • High fallback_count. New workspace, or an audience of contacts you have never messaged. The timezone-aware fallback is the designed behavior, not a failure — history accumulates as campaigns send.
  • No next_best_channel block. The campaign declares fewer than two channels. Add a fallback ladder entry (e.g. RCS primary with an SMS chain entry) and the block appears on the next preview.
  • Experiment not taking effect. Check the variables: the key is send_time_experiment under variables; two or more windows must survive validation (integer hours 0–23, minutes 0–59, distinct clock times). A payload with one usable window parses to no experiment and the campaign sends once.
  • Window times look a day late. Windows are UTC and roll to their next occurrence; a window whose time has already passed today fires tomorrow, and never before the campaign’s configured start.

See also