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

# Preflight review of a campaign audience with the preview endpoint

> Run POST /api/v1/campaigns/audience/preview as the pre-launch gate for an audience — read the net projection (suppression, per-channel opt-outs, missing addresses, the advisory frequency-cap estimate) and remediate a zero-match audience with a cause-to-fix ladder.

# Preflight review of a campaign audience

An audience is approved for launch by looking at it, not by hoping the send resolves the way you drew it up. `POST /api/v1/campaigns/audience/preview` is the instrument for that look: feed it the same audience shape the campaign carries and it returns the gross match count, a 10-contact sample, and — when you pass a channel — the net projection the launch will actually attempt. This page walks one campaign through that review: a summer-sale SMS blast built on a static list.

The preview is a decide step, not a discover step. [Send a campaign end-to-end](/guides/campaign-end-to-end) covers the full lifecycle where this review sits; [Outbound compliance pre-flight checklist](/guides/send-gates-preflight-checklist) covers the gate chain (wallet, opt-out sync, quiet hours, frequency caps) that runs after the audience itself is sound. Consensus between those three passes is what clears launch.

***

## 1. Why preflight beats post-launch debugging

Post-launch, a bad audience surfaces as *absence*: rows that never appear in the send log, a stat total that visibly under-delivers, or a suppression cohort you learn about once the ledger has already counted them. At that point the campaign state is real — credits committed (or quietly *not* spent), and the debugging happens against launch artifacts whose zeroes are ambiguous (was the row suppressed, opted out, address-less, or capped?).

Preflight, the same defects are cheap and disambiguated. The preview is read-only: it mutates nothing, touches no wallet, enqueues nothing. Veto a bad list and the cost is an edited CSV and a re-check; veto three hours later and the cost is a launch artifact to explain. Run the review while the campaign is still a draft and the cost of a wrong audience stays at zero.

***

## 2. Run the preview and read the net projection

Post the same audience shape the campaign carries. For the summer-sale blast, the audience is a static list named `list_summerVIPs` and the channel is SMS:

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/campaigns/audience/preview" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "audience_type": "list",
    "audience_id": "list_summerVIPs",
    "channel": "sms"
  }'
```

A healthy response for this audience looks like:

```json theme={null}
{
  "data": {
    "matching_count": 1480,
    "sample_contacts": [
      { "id": "cnt_01HQ…", "phone": "+13125550142", "first_name": "Maya" }
    ],
    "evaluated_at": "2026-09-11T14:02:11.000Z",
    "cached": false,
    "cohort": {
      "suppressed": 41,
      "opted_out": 63,
      "unreachable": 9,
      "net": 1367,
      "frequency_capped": 122
    }
  }
}
```

`matching_count` is the gross match — the audience shape on its own, before any channel exclusions. Pass `channel` and the response adds the `cohort` block, the net projection that partitions the gross into the cohorts the launch will see:

| Cohort             | What it counts                                                                                | Tenant-owned control that clears it                                                                 |
| ------------------ | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `suppressed`       | Matches appearing on the suppression list                                                     | [Opt-out & suppression lists](/guides/opt-out-lists) — your imported block list, under your control |
| `opted_out`        | Matches with a per-channel opt-out flag for this channel                                      | Per-channel preference state; cleared only by the recipient re-opting in                            |
| `unreachable`      | Matches with no deliverable address for this channel (no phone on file for an SMS blast)      | Contact data completeness on your contact records                                                   |
| `net`              | The deliverability-eligible count after those exclusions — the cohort the launch will attempt | —                                                                                                   |
| `frequency_capped` | Advisory estimate of how many sends a frequency cap will hold back                            | [Frequency caps](/guides/frequency-caps) — your per-channel ceilings                                |

Four reading rules:

* **Read `net`, not `matching_count`, as the headcount.** A healthy summer-sale audience yields most of its gross in `net` and a small `suppressed` / `opted_out` cohort. If `net` is materially below gross, the difference is assigned to specific cohorts in the response — that assignment is what remediates.
* **`unreachable` should trend to zero** on a phone channel for a well-kept list. Non-zero `unreachable` means missing contact data, not a compliance block.
* **Expect `suppressed` + `opted_out` to be small but non-zero.** A list that has ever been sent will carry some excludes; a perfectly clean pair is the anomaly to double-check, not the goal.
* **`frequency_capped` is advisory, not a gate.** It is the estimate of sends your own configured caps will hold back — it never blocks launch, and a large value means "your cap policy is about to shave this many," nothing more.

The same `suppressed` / `opted_out` / `unreachable` / `deliverable` math reappears in the dry-run (`audience.*` buckets), so the preview's reading carries forward verbatim into the end-to-end gate — the reconciliation stays exact.

***

## 3. The five-minute cache in the render loop

Results are cached for five minutes on a hash of the criteria — `cached: true` on a hit. Two consequences for the wizard render loop:

* **Re-rendering the same drawer is free.** Repeated preview calls with the same audience shape replay the cached projection instead of re-evaluating, so the preview card can refresh on every keystroke without paying for a resolution pass.
* **Edits break the cache.** Change the criteria — a different `audience_id`, a regenerated `csv_recipients`, an added `contact_ids` member — and the hash changes, so the next call re-evaluates live. If you're troubleshooting and a fix you just made doesn't move the numbers, confirm the criteria actually changed before assuming the cache is stale; identical criteria within the five-minute window is a cache hit by design.

***

## 4. The `matching_count = 0` fix ladder

A zero gross match means the audience shape itself resolved to nothing — before any suppression, opt-out, or address math ran. Work the ladder top to bottom; each rung names one cause and its one fix.

| Cause                                | Diagnosis                                                                                                                                                 | Fix                                                                                                                                                      |
| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Wrong audience id                    | `audience_id` points at a list/segment that doesn't exist or was renamed — the resolver matches zero                                                      | Correct the `audience_id` to the existing list or segment ([static lists](/guides/contact-lists-static-targeting), [CDP segments](/guides/cdp-segments)) |
| Empty source                         | The source resolves honestly to zero rows — a segment whose filter matches no one, an empty CSV, an emptied list                                          | Rebuild the source upstream of the campaign (loosen the segment filter, re-import the CSV)                                                               |
| Wrong audience shape for the payload | `audience_type: "list"` with an empty `audience_id`, or `audience_type: "csv"` sent with no `csv_recipients` array — the required partner field is absent | Send the shape's required partner field (`audience_id` for `list`/`segment`, `csv_recipients` for `csv`, `contact_ids` for `manual`)                     |
| Over-narrow eligibility upstream     | The upstream producer (CRM filter, segment rule) already filtered the roster to zero before the preview ever ran                                          | Widen the producing filter at its source, then re-preview                                                                                                |
| Import that never landed             | The CSV/list was authored but the import that populates the members hasn't been run or failed                                                             | Complete the [list import](/guides/import-contacts) so the members exist, then re-preview                                                                |

Rung membership matters: a zero `matching_count` is never a compliance condition. Suppression and opt-outs reduce `net` from a healthy gross, they never zero `matching_count` — so a zero is a roster problem with one of the five fixes above, not a remediation of the block list.

***

## 5. Where this sits in the launch path

The audience review is the first gate, not the only one. Two hand-offs close the path:

* **Back into [Send a campaign end-to-end](/guides/campaign-end-to-end) §3** — that guide runs this preview as step 3 of the lifecycle and immediately consumes the cohort counts in the dry-run; return there once the audience reads clean.
* **Across to [Outbound compliance pre-flight checklist](/guides/send-gates-preflight-checklist)** — a healthy audience still walks the admission chain; the checklist then owns the wallet posture, out-of-band opt-out sync, quiet hours, and frequency-cap posture this page deliberately left aside.

Run both and the campaign launches against evidence, not assumption.
