The Wave campaign detail page is the per-event activation surface. It reuses the Lookout event detail components (hero, stats bar) and adds the campaign-state cards, the taste-cluster panel, and the entry point into the Campaigns lifecycle wizard.

What you’ll build

  • Hero (title, date, venue, status pills).
  • Event stats bar (Revenue / Purchases / Spent / ROAS).
  • Campaign cards (one per existing campaign, with pause/resume controls).
  • Taste clusters panel.
  • “Start setup” CTA → wizard.

Prerequisites

  • An eid (from the Wave list).
  • An authenticated user with Permissions.wave.

The call chain

On mount (parallel):
#MethodPathPurpose
1GET/events/{eid}Event header.
2GET/events/{eid}/attribution_modelCurrent attribution model.
3GET/events/{eid}/statisticsStats bar KPIs.
4GET/integrations/facebook/events/{eid}/campaign_resultsBranch “with stats” vs “no stats”.
5GET/integrations/facebook/events/{eid}/fb_insightsImpressions / clicks / ad spend.
6GET/events/{eid}/campaigns?page=1&limit=50Existing campaign list (paged).
7GET/events/{eid}/clustersTaste clusters.
8GET/setup_processes/{eid}Existing draft setup, or 404 if none.
9GET/meta/authorization/statusMeta (Facebook) auth state — used to gate the wizard.
10GET/setup_processes/event_types?...Setup wizard’s event-type metadata.
If the user enters setup mode, additional calls fire — see Campaigns lifecycle for the wizard call chain.

Reading the campaign list

{
  "total": 3,
  "items": [
    {
      "eid":         "evt_abc123",
      "tc":          "family_outings_munich",
      "tc_run_id":   42,
      "status":      "RUNNING",
      "creative":    { "headline": "...", "body": "...", "media_url": "..." },
      "targeting":   { "geo": [...], "demographics": [...] },
      "budget":      1500,
      "end_date":    "2026-06-12",
      "audience_id": "aud_777",
      "suggested_creative": {
        "headline": "Headline suggestion",
        "body":     "Body suggestion"
      }
    }
  ]
}
status rolls up multiple platform states. Common values: RUNNING, PAUSED, COMPLETED, IGNORED.

Per-campaign mutations

ActionEndpoint
Update creativePUT /campaigns/{eid}/{tc}/{tc_run_id}/creative (body: full creative)
Update target audiencePUT /campaigns/{partner_id}/{eid}/{tc}/{tc_run_id}/target_audience (body: full targeting)
Ignore / skipPUT /campaigns/{eid}/{tc}/{tc_run_id}/ignore (body: { reason })
Pause / resume event’s campaignsPUT /events/{eid}/client_campaign_status?client_campaign_status=PAUSED|RUNNING
Poll status-change taskGET /events/{eid}/client_campaign_status/task
Toggle automated optimisationPUT /events/{eid}/campaign_optimisation_status?campaign_optimisation_status=ON|OFF
Toggle ad-campaign optimisationPUT /events/{eid}/ad_campaign_optimisation_status?ad_campaign_optimisation_status=ON|OFF
Change attribution modelPUT /events/{eid}/attribution_model (body: { model })
Edit a single published clusterPOST /campaigns_setup (body: { eid, tc, tc_run_id, audience_id, creatives, targeting, saved_audience_id })
Several of these PUTs take their parameters as query string, not body: client_campaign_status, campaign_optimisation_status, ad_campaign_optimisation_status. The reference webapp uses this style inconsistently — copy the exact format per endpoint.

Reference implementation

export function WaveCampaignDetail({ eid }: { eid: string }) {
  const event      = useFd<Event>(`/events/${eid}`);
  const stats      = useFd<EventStats>(`/events/${eid}/statistics`);
  const campaigns  = useFd<CampaignList>(`/events/${eid}/campaigns?page=1&limit=50`);
  const clusters   = useFd<Clusters>(`/events/${eid}/clusters`);
  const setupProc  = useFd<SetupProcess | null>(`/setup_processes/${eid}`, {
    onError: e => (e.status === 404 ? null : Promise.reject(e)),  // 404 is fine
  });
  const metaAuth   = useFd<MetaAuth>("/meta/authorization/status");

  return (
    <>
      <Hero event={event.data} />
      <EventStatsBar stats={stats.data} />

      {campaigns.data?.items?.length ? (
        <CampaignCards
          campaigns={campaigns.data.items}
          onPause={()  => pauseEventCampaigns(eid)}
          onResume={() => resumeEventCampaigns(eid)}
        />
      ) : null}

      <TasteClustersPanel clusters={clusters.data} />

      {setupProc.data ? (
        <ResumeSetupCta setup={setupProc.data} />
      ) : (
        <StartSetupCta
          eid={eid}
          disabled={!metaAuth.data?.connected}
        />
      )}
    </>
  );
}

Pausing all campaigns for an event

This kicks off a long-running task; poll for status:
export async function pauseEventCampaigns(eid: string) {
  await fd.put(`/events/${eid}/client_campaign_status?client_campaign_status=PAUSED`);

  // Poll the task — same pattern as any long-running mutation
  let done = false;
  while (!done) {
    const { state } = await fd.get(`/events/${eid}/client_campaign_status/task`);
    done = state === "completed" || state === "failed";
    if (!done) await sleep(2000);
  }
}
See Polling long-running jobs for the generic pattern.

Gotchas

No draft yet ⇒ 404. Don’t treat it as an error; just show the “Start setup” CTA.
The wizard’s publish step refuses to call /boost if Meta auth is missing. The reference webapp checks /meta/authorization/status and surfaces a “Reconnect Meta” CTA on the page. Mirror that.
client_campaign_status, campaign_optimisation_status, ad_campaign_optimisation_status, and the event-series mutations all use query-string params, not a JSON body. Easy to send a body and get a confusing 400.
Don’t mix them up:
  • /setup_processes/{eid} PUT — edit a draft.
  • /campaigns_setup POST — edit one cluster of a published campaign.
/events/{eid}/client_campaign_status[/task], /events/{eid}/campaign_optimisation_status, /events/{eid}/ad_campaign_optimisation_status, /events/{eid}/attribution_model, and /events/{eid}/available_events_for_series are hit by the reference webapp but not present in the canonical OpenAPI spec. Confirm with Future Demand before relying on them.