| Surface | Atom | Lifecycle holder | Use when |
|---|---|---|---|
| Campaigns | One per (eid, tc, tc_run_id) | setup_processes/{eid} row | You want to run paid media against one event. |
| Packages | Audience CSV/CRM segment | package_builder/packages/{id} row | You want to export an audience (CSV/CRM) without running media yourself. |
Campaigns (the wizard side)
Wave’s campaign wizard creates and edits a setup process — one row per event that holds:- Goal, total budget, start/end dates.
- Selected taste clusters at a fixed
tc_run_id. - Creative per cluster (headline, body, media, CTA).
- Targeting per cluster (geo, demographics, custom audiences).
- Integration details (which ad account, pixel, lead form, …).
- DSA payor / beneficiary (EU compliance).
Where the actual “campaign” rows come from
POST /setup_processes/{eid}/boost is what produces the campaigns. Once
published, you list them with:
(eid, tc, tc_run_id).
Per-campaign updates target /campaigns/{eid}/{tc}/{tc_run_id}/....
Packages (the audience-export side)
The Package Builder produces standalone audience packages — taste- cluster-derived customer lists for an event or set of events. Use them when you want to:- Hand a CSV to an external media buyer.
- Push a segment into your CRM (Hubspot, Salesforce, …).
- Generate addressable lists for email marketing.
Lifecycle parallels
Both surfaces share an async “draft → submitted → ready” arc, but the state names differ. Here’s the rough mapping:| Step | Campaigns | Packages |
|---|---|---|
| Draft creation | POST /setup_processes/{eid} | POST /package_builder/packages |
| Autosave / edit | PUT /setup_processes/{eid} | (no edit — re-create or regenerate) |
| Trigger production | POST /setup_processes/{eid}/boost | (implicit on create) |
| Pending state | n/a (synchronous publish) | INITIALIZING → INPROGRESS |
| Ready state | campaigns visible in /events/{eid}/campaigns | status: DONE |
| Stale state | status: CLOSED | status: EXPIRED (regenerable) |
| Failure | sets Sentry: INVALID_SETUP | status: FAILED |
Attribution
Once campaigns or packages are out in the world, attribution runs:- Campaign attribution —
GET /campaign_results/?eid=&partner_id=returns the per-event sales-adjustment rows. The reference webapp surfaces these in the Event detail page. - Package attribution (Backhaul) —
GET /package_builder/packages/{id}/backhaul_evaluationreturns customer / item / purchase counts per cluster, with/backhaul_evaluation_detailscontrolling the model and window.
Common gotchas
`/campaigns_setup` is not /setup_processes/...
`/campaigns_setup` is not /setup_processes/...
The “edit one published cluster” endpoint is
POST /campaigns_setup —
note: no {eid} in the URL, the eid goes in the body. It is not
the same as PUT /setup_processes/{eid}. They are two different
lifecycle stages.One setup_process per event
One setup_process per event
There is no
setupId in the URL. Concurrent edits across tabs race on
the same row. The reference webapp uses a module-level AbortController
to drop stale PUTs — replicate or you’ll see flicker.Package status polling has no cancel
Package status polling has no cancel
The reference webapp polls every 5s in a
setTimeout chain with no
unmount cancellation. If your UI follows that pattern, gate the poll
on a mounted flag — or you’ll be polling against a dead component.POST /campaigns/ uses query-string parameters
POST /campaigns/ uses query-string parameters
Unlike most POSTs,
POST /campaigns/ takes everything as query string,
not a body. Documented but unusual.