Campaigns
A broadcast to a list, priced before it goes and sent at a rate carriers will accept. Nothing sends until you ask it to.
| Method | Path | Does |
|---|---|---|
POST | /v1/campaigns | Create, as a draft or scheduled |
GET | /v1/campaigns/{id}/estimate | Reach and cost, before committing |
POST | /v1/campaigns/{id}/send | Send now |
PATCH | /v1/campaigns/{id} | Pause, resume or cancel |
GET | /v1/campaigns/{id}/performance | What it achieved |
Creating one
POST /v1/campaigns
{
"name": "Spring offer",
"body": "Half price cuts this week. Reply STOP to opt out.",
"contact_list_id": "01j8xk…",
"scheduled_at": "2026-09-23T09:00:00+01:00"
}Created as a draft, or scheduled if you gave a time. Either way it has not sent, and either way you can still look at it, price it and change your mind.
Price it before you send it
GET /v1/campaigns/{id}/estimate
{
"object": "estimate",
"recipients": 120,
"suppressed": 8,
"sendable": 112,
"segments_each": 1,
"estimated_cost": "3.92"
}recipients is everyone on the list; suppressed is how many of them have opted out and will be skipped; sendable is the difference and is the number that will actually be messaged. They are reported separately because "we are not messaging twelve of these people" is something you should know before you press send, not after.
A prepaid account is checked against this before a single message leaves. Finding out mid-broadcast that the balance ran dry leaves half a campaign sent and no way to tell which half.
Sending
POST /v1/campaigns/{id}/send → 202Fanned out in chunks at throttle_per_minute, default 600. The throttle is there to get your messages delivered, not to slow you down: carriers rate limit at their end, and a burst gets filtered rather than delivered.
Scheduled campaigns
A scheduled campaign is sent by a sweep that runs every minute. The balance is checked again at the moment it sends, not when it was scheduled — a balance that was fine on Friday can be empty by Tuesday.
If it cannot go, it is marked failed with the reason rather than retried. A campaign that quietly re-attempts every minute is indistinguishable from one that was never scheduled, right up until the balance is topped up and it sends at three in the morning.
Stopping one
PATCH /v1/campaigns/{id} { "status": "cancelled" }A campaign that is still sending can be stopped and the remainder will not go out. One that has completed cannot — those messages are already with the carriers, and there is no unsend.
What it achieved
GET /v1/campaigns/{id}/performance
{
"measured": {
"delivered": 118,
"clicks": 21,
"click_rate": 17.8,
"spend": "4.13"
},
"attributed": null,
"analytics_connected": false
}Two halves, deliberately never added together. Measured is ours and certain: what we sent, what was delivered, what was clicked, what it cost.Attributed comes from your Google Analytics and is session-based and lossy — somebody who clicks on a phone and converts on a laptop two days later does not attribute at all.
attributed is null, not zero, when nothing is connected."We do not know" and "it earned nothing" are opposite answers, and reporting the first as the second would cost you a channel that was working.
Opt-outs are not your problem
Suppressed contacts are removed from the send, counted in the estimate, and never messaged — you do not filter the list yourself. A recipient who replies STOP mid-campaign is suppressed before the next chunk reaches them.
Quiet hours
A campaign is marketing, so it respects quiet hours: held outside 08:00–21:00 in the recipient's own timezone. Messages held this way come back as QUIET_HOURSrather than a generic refusal, so you can tell "not now" from "not ever".