Skip to main content

Campaign lifecycle

A campaign in Orbit is one object with a real state machine: it is drafted, gated, scheduled, executed — possibly paused along the way — and lands in a terminal outcome that feeds your analytics. Guides walk you through sending a campaign end-to-end and A/B variants; this page names the model those guides operate on. Read it before you branch an integration on campaign.status or design an automation around campaign events.

The campaign object

Every campaign is one row identified by a cmp_-prefixed id, created via POST /campaigns and read back via GET /campaigns/:id. Behind that one id sit four choices that define the campaign:
  • One channel set — the primary channel plus an optional channel chain (channels[]) executed as fallback attempts. In a drip or journey, each step picks its own channel.
  • One audience — a list, a segment, a filter, or audience_type: "all" resolved at execution time. Audiences are re-resolved when a step executes, not frozen at draft time.
  • One template (or several) — the body rendered for each recipient, with per-recipient variables. A/B variants become multiple template arms under the same campaign.
  • One modeblast, drip, or journey (plus ad for paid-audience sync campaigns). The mode decides how the audience is treated at runtime; see Blast vs drip vs journey.
Everything else — throttle, quiet hours, send-time optimization, credit cap, holdout — is configuration layered on top of those four choices.

The status machine

A campaign’s status field is the branch point for every integration. The list endpoint (GET /campaigns) validates ?status= and ?statuses= filters against this set: The happy path is draft → scheduled → sending → running → completed. sending and running are both live: sending marks active fan-out, running is the steady state of a campaign between batches (a drip holding for the next step delay, a journey waiting on events). The dashboard’s “Active Now” rollup counts running, aborting, and sending together, so filter with ?statuses=running,aborting,sending — a bare ?status=running misses the others and disagrees with the tile. Each transition has a distinct owner: Sequence for a scheduled blast that completes cleanly:

Blast vs drip vs journey

The mode you set at create time picks one of three runtime models: Blast — one-shot fan-out. The audience resolves once, the template renders per recipient, and the execution service pushes batches at the campaign’s throttle until the audience is exhausted. One status arc, one send per recipient, done. A blast with send_time_optimization: "recipient-optimal" is still a blast — each recipient gets their own scheduled_at, but still a single send. Drip — an ordered step sequence (steps[], up to 50). Each step has its own channel, template, delay, and optional send condition. A contact advances through the steps in order; each step evaluates its condition against the previous step’s outcome for that contact — e.g. send step 2 only to contacts whose step-1 message is not_delivered. Because conditions gate on delivery statuses, they are channel-dependent:
  • Read gates (read / not_read) are valid only after a read-capable channel — WhatsApp, RCS, email, Viber, or Telegram. A read gate behind an SMS, MMS, voice, fax, or push step can never match (those channels top out at sent/delivered/failed and never report read), so the API rejects the combination with a 422 instead of silently dropping every contact at that step.
  • delivered / not_delivered conditions work after any channel.
Drips also support exit goals (exit_goals): when a contact hits a goal event (a purchase, a reply, a conversion), they leave the sequence early — converted contacts stop receiving reminder steps. Journey — event-driven. Instead of a fixed step list, a journey is a graph whose routers move each contact based on what they do: entry routers decide who enters (enrollment events, segment membership, single-contact enrollment), condition routers branch on contact behavior, and exit routers remove contacts on goal or terminal events. A journey sits in running for as long as entries keep arriving — unlike a blast or drip, the audience is a stream, not a fixed set. Journeys back the contact-360 “enroll” action and the single-contact enrollment endpoint, so one individual contact can enter a journey mid-flight. Mode is not an afterthought — pick it against the shape of your audience (fixed set vs. arriving stream) and whether your follow-up logic is time-based (drip) or behavior-based (journey).

What pauses a campaign

Three forces can move a live campaign to paused, and each resumes differently:
  1. Spend governor — credit_cap_reached. If you set credit_cap_usd_cents at create or update time, the execution service refuses any batch that would push credit_spent_usd_cents past the cap and auto-pauses the campaign, writing metadata.auto_paused_reason: "credit_cap_reached". Raising the cap (or clearing it with null) and resuming puts the campaign back to running. Read auto_paused_reason before treating a paused campaign as an operator action — it tells you the governor fired, not a human.
  2. Quiet-hours deferral. When quiet_hours_enabled is set on the campaign, the dispatch loops defer sends inside the recipient-local quiet window (cross-midnight windows like 21:00 → 07:00 are supported). The campaign stays live and resumes automatically when the window closes — you do not resume anything.
  3. Manual pause. You pause it from the dashboard or API. Only you resume it.
Distinguish paused (live, resumable) from aborting/cancelled (the exit path). A cancelled campaign cannot be resumed; a paused one can.

Send-time optimization

send_time_optimization picks how each recipient’s send time is computed:
  • fixed (default) — every recipient is sent at the campaign’s single batch time. One scheduled_at for the whole audience.
  • recipient-optimal — the execution worker computes a per-contact scheduled_at from that contact’s engagement profile (the smoothed best hour-of-day / day-of-week signal the platform has for them, per channel) and spreads the audience across the next 24 hours. Recipients with insufficient engagement signal fall back to 10:00 local time.
The legacy boolean smart_send maps onto the same behavior (truerecipient-optimal); if you send both, the explicit enum wins. Whether you send fixed or recipient-optimal, quiet hours still apply — an optimal time inside a recipient’s quiet window defers. Add smart_send_holdout_pct (0–50) and a slice of the audience is held out at the fixed batch time as a control group. After the campaign completes, GET /campaigns/:id/smart-send-holdout-lift measures the incremental lift of optimal timing against that holdout.

Terminal outcomes and analytics

When a campaign leaves the live set, its outcome lands in one of four terminal statuses — completed, partially_failed, cancelled, failed — and the analytics layer treats them differently:
  • completed / partially_failed roll into campaign stats: delivered/read rates, per-step drip breakdowns, and per-variant A/B results. Revenue attribution joins those outcomes against conversion events to produce spend, attributed-revenue, ROAS, and ROI per campaign. With a holdout configured, lift endpoints compare treated vs. control cohorts.
  • cancelled reports what was sent before the abort; nothing after the cancel counts.
  • failed reports the blocking error; partial sends are visible in stats but the campaign never reached its audience end-to-end.
For the endpoint payloads that carry these numbers, see the campaigns API reference. For the message-level states a campaign’s sends move through (the delivered/read/submitted_no_receipt vocabulary drip conditions gate on), see the delivery lifecycle. To put the model to use, send a campaign end-to-end.