Campaigns API
Auth: Clerk session.
CRUD
GET /v1/campaigns?cursor=&limit=50&status=draft
GET /v1/campaigns/:id
POST /v1/campaigns PATCH /v1/campaigns/:id DELETE /v1/campaigns/:id
// Create:
{
"name": "Welcome series",
"type": "broadcast",
"segment_id": "uuid",
"template_id": "uuid",
"provider_id": "uuid",
"channel": "email",
"schedule_at": null, // ISO if scheduled, else null = immediate
"goal_event": "deposit_confirmed", // optional
"goal_attribution_days": 7,
"goal_value_property": "amount_base"
}
Send
POST /v1/campaigns/:id/send
Triggers fan-out:
- Validates campaign is in
draft | scheduledstatus - Resolves segment + walks members in batches of 1000
- Filters self-excluded + frequency-capped + quiet-hours players
- Assigns variants (deterministic FNV-1a hash)
- Inserts
message_sendsrows + enqueues BullMQ jobs
{
"data": {
"campaign_id": "uuid",
"recipients_total": 3085,
"enqueued": 2914, // sent to BullMQ
"no_recipient": 33, // missing email/phone
"enqueued_at": "..."
}
}
Idempotent via Idempotency-Key header — replays return the same response without re-fanning out.
Cancel
POST /v1/campaigns/:id/cancel — drains in-flight; new fan-out blocks. Status → cancelled.
Stats (basic)
GET /v1/campaigns/:id/stats
{
"data": {
"campaign_id": "...",
"status": "sent",
"counters": {
"recipients_total": 3085,
"sent": 2914, "delivered": 2856, "opened": 1432, "clicked": 387,
"bounced": 58, "failed": 33, "capped": 0
},
"rates": {
"delivered_rate": 0.98,
"open_rate": 0.501,
"click_rate": 0.27,
"bounce_rate": 0.0199
}
}
}
Conversion stats (with variants)
GET /v1/campaigns/:id/conversion-stats — see A/B testing.
Recipients (drill-down)
GET /v1/campaigns/:id/recipients?cursor=&limit=50&status=failed
Lists message_sends rows. Filter by status to see bounces / failed / capped / SKIPPED_*.
Variants
GET /v1/campaigns/:id/variants POST /v1/campaigns/:id/variants PATCH /v1/campaigns/:id/variants/:variantId DELETE /v1/campaigns/:id/variants/:variantId
// Create variant:
{
"name": "Variant B",
"label": "B",
"template_id": "uuid",
"weight": 50,
"sort_order": 1
}
Operator clones the base template, tweaks copy, sets weight. Mid-campaign weight changes only affect new sends.