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 oncampaign.status or design an automation around campaign events.
The campaign object
Every campaign is one row identified by acmp_-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
channelplus 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 mode —
blast,drip, orjourney(plusadfor paid-audience sync campaigns). The mode decides how the audience is treated at runtime; see Blast vs drip vs journey.
The status machine
A campaign’sstatus 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
Themode 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 atsent/delivered/failedand never reportread), so the API rejects the combination with a 422 instead of silently dropping every contact at that step. delivered/not_deliveredconditions work after any channel.
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 topaused, and each resumes differently:
- Spend governor —
credit_cap_reached. If you setcredit_cap_usd_centsat create or update time, the execution service refuses any batch that would pushcredit_spent_usd_centspast the cap and auto-pauses the campaign, writingmetadata.auto_paused_reason: "credit_cap_reached". Raising the cap (or clearing it withnull) and resuming puts the campaign back torunning. Readauto_paused_reasonbefore treating a paused campaign as an operator action — it tells you the governor fired, not a human. - Quiet-hours deferral. When
quiet_hours_enabledis 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. - Manual pause. You pause it from the dashboard or API. Only you resume it.
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. Onescheduled_atfor the whole audience.recipient-optimal— the execution worker computes a per-contactscheduled_atfrom 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.
smart_send maps onto the same behavior (true ≡ recipient-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_failedroll 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.cancelledreports what was sent before the abort; nothing after the cancel counts.failedreports the blocking error; partial sends are visible in stats but the campaign never reached its audience end-to-end.
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.