Campaigns
Goal-driven calling: declare a goal and an audience, and callhook orchestrates waves of calls that stop the moment the goal is met — never wasting a call.
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.
| Field | Type | Notes |
|---|---|---|
event_type | string · required | Any blueprint type (invoice.due, promo.offer, ...) — the campaign fires these events. |
goal.type | count | reach_all | count needs N successes; reach_all needs every audience member reached. |
goal.success_outcomes | []string | Which outcomes count as success. Defaults to the common success outcomes. |
audience | list | Explicit audience: [{"customer_id": "cus_2001"}, ...] with optional phone/tz overrides. |
audience_source | string | all_overdue — auto-builds the audience from the business store. |
waves.size | int | Calls per wave (default 3, capped by CALLHOOK_MAX_CONCURRENT). |
waves.delay | duration | Wait between waves — "15m", "90s"... |
waves.max_waves | int | Hard wave limit (0 = unlimited, bounded by audience + budget). |
budget.max_calls | int | Hard stop on placed calls. Default: one per audience member. |
Lifecycle
Every terminal outcome updates the campaign. The rules:
- Early-stop — the moment the goal is met, pending audience members are marked skipped and no further calls go out.
- Smart requeue —
no_answerentries return to the pending pool for the next wave (max 2 rounds per person). - Budget hard-stop —
budget_exhaustedstatus, never more calls than allowed. - Crash-safe — campaigns are journal-persisted; waves resume after a restart.
Terminal statuses: goal_met · exhausted (audience/waves spent) · budget_exhausted · stopped (manual).
Verified end-to-end
Campaign API
| Method & path | What |
|---|---|
POST /api/campaigns | Create + start a campaign |
GET /api/campaigns | List campaigns with progress |
GET /api/campaigns/{id} | Full detail: goal, waves, audience states, log |
POST /api/campaigns/{id}/stop | Stop a running campaign |