Skip to main content

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:

  1. Validates campaign is in draft | scheduled status
  2. Resolves segment + walks members in batches of 1000
  3. Filters self-excluded + frequency-capped + quiet-hours players
  4. Assigns variants (deterministic FNV-1a hash)
  5. Inserts message_sends rows + 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.