> ## 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.

# Smart-send timing playbook: per-contact schedules, send-time experiments, and next-best-channel

> Disambiguate preview-smart-send, the per-message smart router, and the send-time window experiment — then walk the full preview, opt-in, launch, and results sequence for campaign timing.

# 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.

| Surface | Input | Output | Answers |
| - | - | - | - |
| `POST /api/v1/campaigns/{id}/preview-smart-send` | A campaign's resolved audience | A per-contact optimal send time, plus a per-contact channel recommendation when the campaign declares two or more channels | "When does each contact get this campaign?" |
| `POST /api/v1/messages/smart-send` | One ad-hoc message | A dispatched send on the router's channel pick, optionally with an armed fallback chain | "Which channel should this one message ride?" |
| `POST /api/v1/campaigns/{id}/send-time-experiment/preview` | A campaign carrying `variables.send_time_experiment` | The deterministic cohort split across 2–4 fixed windows plus a hold-out control | "If I pit these fixed windows against each other, who lands where?" |
| `GET /api/v1/campaigns/{id}/send-time-experiment/results` | A launched experiment | Per-window lift over control, with a winner | "Which fixed window won?" |

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](/guides/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](/guides/smart-send-fallback-chains).

### 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](/guides/campaign-ab-testing#campaign-wide-control-holdout).
* `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](/guides/fallback-chains) for the chain surfaces.

## The preview-smart-send contract

```bash theme={null}
curl -s -X POST "https://api.orbit.devotel.io/api/v1/campaigns/{id}/preview-smart-send?limit=200" \
  -H "X-API-Key: $ORBIT_API_KEY" | jq '.data'
```

Response:

```json theme={null}
{
  "campaign_id": "cmp_01H...",
  "total_audience": 48213,
  "preview_count": 200,
  "profile_count": 176,
  "fallback_count": 24,
  "distribution_utc": [{ "hour": 0, "count": 0 }, { "hour": 8, "count": 31 }],
  "schedule": [
    { "contactId": "ct_01H...", "scheduledAt": "2026-10-17T18:00:00.000Z" }
  ]
}
```

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](/guides/quiet-hours-configuration) and [Frequency caps](/guides/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:

```json theme={null}
{
  "next_best_channel": {
    "candidate_channels": ["rcs", "sms"],
    "profile_count": 176,
    "fallback_count": 24,
    "recommendations": [
      { "contact_id": "ct_01H...", "channel": "rcs", "source": "profile" },
      { "contact_id": "ct_02H...", "channel": "sms", "source": "fallback" }
    ]
  }
}
```

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](/concepts/multi-channel-dlr-fallback).
* **It is not the static chain.** A static fallback chain ([Per-message cascade policy on smart-send](/guides/smart-send-fallback-chains), or the campaign ladder in [Fallback chains](/guides/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:

```json theme={null}
{
  "variables": {
    "send_time_experiment": {
      "windows": [
        { "label": "Morning", "hour": 9, "minute": 0 },
        { "label": "Lunch", "hour": 13, "minute": 0 },
        { "label": "Evening", "hour": 18, "minute": 30 }
      ],
      "holdout_pct": 10
    }
  }
}
```

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

```bash theme={null}
export ORBIT_API_KEY="dv_live_sk_your_key_here"
CAMPAIGN_ID="cmp_01H..."

# 1. Review the per-contact optimal-time spread on the draft campaign.
curl -s -X POST "https://api.orbit.devotel.io/api/v1/campaigns/$CAMPAIGN_ID/preview-smart-send?limit=200" \
  -H "X-API-Key: $ORBIT_API_KEY" | jq '.data | {total_audience, preview_count, profile_count, fallback_count}'

# 2. Decide fixed windows instead, and opt into the experiment.
curl -s -X PUT "https://api.orbit.devotel.io/api/v1/campaigns/$CAMPAIGN_ID" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "variables": {
      "send_time_experiment": {
        "windows": [
          { "label": "Morning", "hour": 9, "minute": 0 },
          { "label": "Lunch", "hour": 13, "minute": 0 },
          { "label": "Evening", "hour": 18, "minute": 30 }
        ],
        "holdout_pct": 10
      }
    }
  }' > /dev/null

# 3. Confirm the cohort plan: arm sizes and each window's next fire time.
curl -s -X POST "https://api.orbit.devotel.io/api/v1/campaigns/$CAMPAIGN_ID/send-time-experiment/preview" \
  -H "X-API-Key: $ORBIT_API_KEY" | jq '.data'

# Launch the campaign from the console, or with the launch call
# documented in the campaigns API reference. Control contacts receive
# no message for this send — that is the causal baseline.

# 4. After the windows fire and conversions accumulate, read the winner.
curl -s "https://api.orbit.devotel.io/api/v1/campaigns/$CAMPAIGN_ID/send-time-experiment/results" \
  -H "X-API-Key: $ORBIT_API_KEY" | jq '.data | {winning_window_index, winner_is_significant, control_count}'
```

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

* [A/B test a campaign](/guides/campaign-ab-testing) — variant splits, the campaign-wide control holdout, and the send-time experiment summary.
* [Per-message cascade policy on smart-send](/guides/smart-send-fallback-chains) — the ad-hoc message router and static fallback chains.
* [Best-time-to-dial](/guides/best-time-to-dial) — the same per-contact timing idea applied to dialer campaigns.
* [Holdout and uplift measurement](/guides/campaign-holdout-uplift-measurement) — reading lift reports and confidence intervals.
* [Quiet hours](/guides/quiet-hours-configuration) — the org-wide gate that still shapes every scheduled instant at dispatch.


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