Wave has two activation surfaces with different shapes and use cases.
SurfaceAtomLifecycle holderUse when
CampaignsOne per (eid, tc, tc_run_id)setup_processes/{eid} rowYou want to run paid media against one event.
PackagesAudience CSV/CRM segmentpackage_builder/packages/{id} rowYou want to export an audience (CSV/CRM) without running media yourself.
Most partner integrations need both. They share underlying Affinity output but the API shapes are different — don’t try to unify them prematurely.

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).
POST /setup_processes/{eid}          # start draft
PUT  /setup_processes/{eid}          # autosave every ~1s of edits
GET  /setup_processes/{eid}          # read back
DELETE /setup_processes/{eid}?cancel=true  # cancel
POST /setup_processes/{eid}/boost    # publish → creates campaigns
POST /campaigns_setup                # edit one published cluster (no /{eid} in URL!)
See Wave Campaigns Lifecycle for the full state machine and payloads.

Where the actual “campaign” rows come from

POST /setup_processes/{eid}/boost is what produces the campaigns. Once published, you list them with:
GET /events/{eid}/campaigns
GET /campaigns/by_partner_id?partner_id=...&event_id=...
Each campaign carries its composite identifier (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.
POST /package_builder/packages                              # create (async)
GET  /package_builder/packages/{id}                         # status poll + result
POST /package_builder/packages/{id}/regenerate              # re-run an expired package
GET  /package_builder/packages/{id}/{tc}/customer_list      # download CSV for one cluster
POST /package_builder/packages/merge?packages_ids=...       # merge several into one
Create payload:
{
  "num_packages":   3,
  "n_top_events":   3,
  "event_ids":      [12345, 23456, 34567],
  "package_title":  "Munich shows Q3",
  "targeting_mode": "lookalike" | "intent",
  "filters": {
    "included_customers":            [],
    "excluded_customers":            [],
    "exclude_buying_behavior_above": 100,
    "exclude_old_purchases_before":  "2024-01-01",
    "exclude_recent_purchases_after": null,
    "exclude_ticket_categories":     ["VIP"]
  }
}
See Package Builder guide for the full lifecycle and polling logic.

Lifecycle parallels

Both surfaces share an async “draft → submitted → ready” arc, but the state names differ. Here’s the rough mapping:
StepCampaignsPackages
Draft creationPOST /setup_processes/{eid}POST /package_builder/packages
Autosave / editPUT /setup_processes/{eid}(no edit — re-create or regenerate)
Trigger productionPOST /setup_processes/{eid}/boost(implicit on create)
Pending staten/a (synchronous publish)INITIALIZINGINPROGRESS
Ready statecampaigns visible in /events/{eid}/campaignsstatus: DONE
Stale statestatus: CLOSEDstatus: EXPIRED (regenerable)
Failuresets Sentry: INVALID_SETUPstatus: FAILED

Attribution

Once campaigns or packages are out in the world, attribution runs:
  • Campaign attributionGET /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_evaluation returns customer / item / purchase counts per cluster, with /backhaul_evaluation_details controlling the model and window.
See Attribution.

Common gotchas

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.
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.
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.
Unlike most POSTs, POST /campaigns/ takes everything as query string, not a body. Documented but unusual.