The idea

One event → one call is the primitive. A campaign wraps that primitive in a goal: "collect 5 payment promises from the overdue list." callhook launches waves, tracks every outcome, requeues unanswered customers, and early-stops the instant the goal is achieved — everyone not yet called is skipped, and the call budget stays unspent.

Create a campaign

curl -X POST localhost:8080/api/campaigns -H 'Content-Type: application/json' -d '{
  "name": "September collections",
  "event_type": "invoice.due",
  "goal":   { "type": "count", "target": 5,
               "success_outcomes": ["payment_promised"] },
  "audience_source": "all_overdue",
  "waves":  { "size": 3, "delay": "15m", "max_waves": 5 },
  "budget": { "max_calls": 40 }
}'

Returns the campaign immediately; waves launch on the scheduler. Watch it live in the dashboard — progress bar, wave log, per-recipient states.

FieldTypeNotes
event_typestring · requiredAny blueprint type (invoice.due, promo.offer, ...) — the campaign fires these events.
goal.typecount | reach_allcount needs N successes; reach_all needs every audience member reached.
goal.success_outcomes[]stringWhich outcomes count as success. Defaults to the common success outcomes.
audiencelistExplicit audience: [{"customer_id": "cus_2001"}, ...] with optional phone/tz overrides.
audience_sourcestringall_overdue — auto-builds the audience from the business store.
waves.sizeintCalls per wave (default 3, capped by CALLHOOK_MAX_CONCURRENT).
waves.delaydurationWait between waves — "15m", "90s"...
waves.max_wavesintHard wave limit (0 = unlimited, bounded by audience + budget).
budget.max_callsintHard stop on placed calls. Default: one per audience member.

Lifecycle

Every terminal outcome updates the campaign. The rules:

Terminal statuses: goal_met · exhausted (audience/waves spent) · budget_exhausted · stopped (manual).

Verified end-to-end

E2E result: 32-person audience, target 5 payment promises, wave size 3, budget 15 → goal met in 4 waves / 12 calls. Five no-answers were requeued and recalled; 18 people were never called because the goal was already achieved.

Campaign API

Method & pathWhat
POST /api/campaignsCreate + start a campaign
GET /api/campaignsList campaigns with progress
GET /api/campaigns/{id}Full detail: goal, waves, audience states, log
POST /api/campaigns/{id}/stopStop a running campaign