Campaigns API
A campaign is a first-class send container: it holds an audience, one or more ordered messages, and a lifecycle. Single-message campaigns cover one-off sends; multi-message campaigns let you author a sequence of messages, release them together, and read a per-message performance report.
These endpoints are authenticated with a bearer token:
Authorization: Bearer YOUR_ACCESS_TOKEN
The campaign object
| Field | Type | Description |
|---|---|---|
id | string | Campaign UUID. |
name | string | Display name. |
status | string | One of draft, scheduled, live, completed, paused, archived. |
category | string | Messaging category: transactional or marketing. Defaults to marketing. |
target | object | Audience target. Defaults to {"type": "all"}. |
content | object | Inline notification content. Omitted when template_id supplies the content. |
template_id | string | Template UUID, when content comes from a template. |
channel_ids | array of strings | Channel UUIDs the campaign sends through. |
holdout_percentage | integer | Percentage of the audience held out as a control group. |
scheduled_at | string | RFC 3339 send time. Omitted for immediate sends. |
messages | array | The campaign's ordered messages (see below). The top-level content / template_id / channel_ids / scheduled_at mirror the first message for single-message consumers. |
created_at | string | Creation timestamp. |
updated_at | string | Last-update timestamp. |
Each entry in messages:
| Field | Type | Description |
|---|---|---|
id | string | Message UUID. |
content | object | Inline notification content. Omitted when template_id is set. |
template_id | string | Template UUID. |
channel_ids | array of strings | Channel UUIDs for this message. |
scheduled_at | string | RFC 3339 send time for this message. Omitted = sends with the campaign. |
position | integer | Zero-based order within the campaign. |
job_id | string | The notification job this message produced on release. Empty until released. |
POST /api/v1/campaigns
Creates a campaign as a draft. Supply either messages (a multi-message
campaign) or the top-level content + channel_ids (a single message). Send
the draft later with POST /api/v1/campaigns/:id/release.
Request Body
{
"name": "Spring onboarding",
"category": "marketing",
"target": { "type": "all" },
"holdout_percentage": 5,
"messages": [
{
"content": { "title": "Welcome!", "body": "Thanks for joining." },
"channel_ids": ["ch_11111111-2222-3333-4444-555555555555"]
},
{
"template_id": "tpl_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"channel_ids": ["ch_11111111-2222-3333-4444-555555555555"],
"scheduled_at": "2026-05-03T09:00:00Z"
}
]
}
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Campaign name. |
category | string | No | transactional or marketing. Defaults to marketing. |
target | object | No | Audience target. Defaults to {"type": "all"}. |
content | object | Conditional | Inline content for a single-message campaign. Required (with channel_ids) when messages is absent. |
template_id | string | No | Template UUID supplying the content. Must belong to your tenant. |
channel_ids | array of strings | Conditional | Channel UUIDs. Required with content when messages is absent. |
holdout_percentage | integer | No | Percentage of the audience to hold out. |
scheduled_at | string | No | RFC 3339 send time. |
messages | array | Conditional | Ordered message list for a multi-message campaign. Each message takes content, template_id, channel_ids, and an optional per-message scheduled_at. |
Response — 201 Created
{
"id": "cmp_9f8e7d6c-5b4a-3210-fedc-ba9876543210",
"name": "Spring onboarding",
"status": "draft",
"category": "marketing",
"target": { "type": "all" },
"content": { "title": "Welcome!", "body": "Thanks for joining." },
"channel_ids": ["ch_11111111-2222-3333-4444-555555555555"],
"holdout_percentage": 5,
"messages": [
{
"id": "msg_00000000-0000-0000-0000-000000000001",
"content": { "title": "Welcome!", "body": "Thanks for joining." },
"channel_ids": ["ch_11111111-2222-3333-4444-555555555555"],
"position": 0
},
{
"id": "msg_00000000-0000-0000-0000-000000000002",
"template_id": "tpl_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"channel_ids": ["ch_11111111-2222-3333-4444-555555555555"],
"scheduled_at": "2026-05-03T09:00:00Z",
"position": 1
}
],
"created_at": "2026-05-01T12:00:00Z",
"updated_at": "2026-05-01T12:00:00Z"
}
Error Codes
| Status | Reason |
|---|---|
| 401 | Missing or invalid credentials. |
| 422 | name missing, neither messages nor content + channel_ids supplied, invalid scheduled_at, unknown category, or template_id does not belong to your tenant. |
GET /api/v1/campaigns
Returns a paginated list of campaigns for the authenticated tenant.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number (1-based). |
page_size | integer | 20 | Number of results per page. |
status | string | — | Optional. Filter by lifecycle status (e.g. draft, live). |
Response — 200 OK
{
"items": [
{
"id": "cmp_9f8e7d6c-5b4a-3210-fedc-ba9876543210",
"name": "Spring onboarding",
"status": "live",
"category": "marketing",
"target": { "type": "all" },
"channel_ids": ["ch_11111111-2222-3333-4444-555555555555"],
"holdout_percentage": 5,
"messages": [],
"created_at": "2026-05-01T12:00:00Z",
"updated_at": "2026-05-02T09:00:00Z"
}
],
"total": 12,
"page": 1,
"page_size": 20
}
GET /api/v1/campaigns/:id
Retrieves a single campaign, including its full messages list.
Response — 200 OK
Returns the full campaign object (same shape as the create response).
Error Codes
| Status | Reason |
|---|---|
| 401 | Missing or invalid credentials. |
| 404 | No campaign found with that ID, or it belongs to a different tenant. |
PUT /api/v1/campaigns/:id
Replaces a draft campaign's audience and messages. The entire message list is replaced, not merged. Campaigns that have left the draft state cannot be edited.
Request Body
{
"name": "Spring onboarding v2",
"category": "marketing",
"target": { "type": "all" },
"holdout_percentage": 10,
"messages": [
{
"content": { "title": "Welcome aboard", "body": "Glad you are here." },
"channel_ids": ["ch_11111111-2222-3333-4444-555555555555"]
}
]
}
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Campaign name. |
category | string | No | transactional or marketing. |
target | object | No | Replacement audience target. |
holdout_percentage | integer | No | Replacement holdout percentage. |
messages | array | Yes | Replacement message list. At least one message is required. |
Response — 200 OK
Returns the full updated campaign object.
Error Codes
| Status | Reason |
|---|---|
| 401 | Missing or invalid credentials. |
| 404 | Campaign not found. |
| 409 | The campaign is no longer a draft. |
| 422 | Empty messages, or a message has an invalid scheduled_at. |
PATCH /api/v1/campaigns/:id
Applies a lifecycle action to the campaign. Pausing or archiving also deactivates any recurring schedule owned by the campaign; resuming reactivates it.
Request Body
{ "action": "pause" }
| Action | Effect | Allowed from |
|---|---|---|
pause | Stops sending; status becomes paused. | scheduled, live |
resume | Resumes sending; status becomes scheduled. | paused |
archive | Retires the campaign; status becomes archived. | Any non-archived status |
unarchive | Restores the exact pre-archive status. | archived |
Response — 200 OK
Returns the full updated campaign object.
Error Codes
| Status | Reason |
|---|---|
| 401 | Missing or invalid credentials. |
| 404 | Campaign not found. |
| 409 | The action is not allowed from the campaign's current status. |
| 422 | Unknown action. |
POST /api/v1/campaigns/:id/release
Sends a draft campaign: one notification job is created per message, each
scheduled at its message's own scheduled_at (or immediately when absent).
The campaign transitions out of draft and each message's job_id is
populated.
Release is idempotent: a message already linked to a job is skipped, so retrying after a partial failure resumes where it left off.
Response — 200 OK
Returns the full updated campaign object, with status advanced and job_id
set on each released message.
Error Codes
| Status | Reason |
|---|---|
| 401 | Missing or invalid credentials. |
| 404 | Campaign not found. |
| 409 | The campaign is not a draft (already released). |
GET /api/v1/campaigns/:id/report
Returns the campaign's funnel KPIs plus per-channel and per-message breakdowns over a time window.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
start | string | 30 days ago | RFC 3339 start of the reporting window. |
end | string | now | RFC 3339 end of the reporting window. |
Response — 200 OK
{
"sent": 4200,
"delivered": 4012,
"opened": 1830,
"clicked": 402,
"converted": 96,
"revenue_cents": 481500,
"by_channel": [
{ "channel": "ios", "delivered": 2400, "opened": 1100, "clicked": 240 },
{ "channel": "android", "delivered": 1612, "opened": 730, "clicked": 162 }
],
"by_message": [
{ "position": 0, "label": "Welcome!", "delivered": 2100, "opened": 990, "clicked": 220 },
{ "position": 1, "label": "Message 2", "delivered": 1912, "opened": 840, "clicked": 182 }
]
}
| Field | Type | Description |
|---|---|---|
sent | integer | Notifications sent in the window. |
delivered | integer | Notifications delivered. |
opened | integer | Opens recorded. |
clicked | integer | Clicks recorded. |
converted | integer | Attributed conversions. |
revenue_cents | integer | Attributed revenue, in cents. |
by_channel | array | Per-channel delivered/opened/clicked counts. |
by_message | array | Per-message breakdown, labeled by each message's content title (or Message N). Populated only for multi-message campaigns; empty otherwise. |
Error Codes
| Status | Reason |
|---|---|
| 401 | Missing or invalid credentials. |
| 404 | Campaign not found, or it belongs to a different tenant. |