Reading campaigns
The three read endpoints: one campaign by id, the project's filtered campaign list, and a digest of the whole project's campaign activity. This page also covers the fields a campaign carries and how to collect the result of an asynchronous estimate or delete.
Reading is not only how you look a campaign up. It is the step that closes the two asynchronous operations: an estimate answers 202 and a delete answers 204, and neither carries the outcome in its own response. Reading the campaign back is where you find it.
All three endpoints take the PROJECT_CAMPAIGN_READ scope and sit in the large rate-limit class.
Reading one campaign
View a campaign.
The campaign comes back under entity, in the same shape the creation endpoint returns.
{
"entity": {
"id": "68b0f2a4e5a6b7c8d9e0f1a2",
"title": "Summer sale announcement",
"status": "estimated",
"channelType": "sms",
"purpose": "standard",
"audienceContactsCount": 18400,
"channelContactsCount": 17250,
"contactsCount": 16980,
"ignoredContactsCount": 270,
"preparedUnits": 20376,
"priceUser": { "value": 611.28, "currency": "EUR" },
"campaignAt": "2026-06-15T09:30:00+02:00",
"scheduledAt": null,
"createdAt": "2026-06-01T11:04:22+00:00"
}
}The entity carries more than the example shows. The fields worth knowing by name group into four families:
| Family | Fields |
|---|---|
| What it is | id, title, emoji, description, favorite, and the channel as a pair: channelType is the string (sms or rcs) and channel is the channel's own object, whose shape follows from it. purpose marks what kind of campaign this is; one you created through the API is always standard. |
| Where it is | status, the raw status every operation in this section is gated by. See Statuses and phases. |
| Who it reaches | audienceContactsCount, channelContactsCount, contactsCount and ignoredContactsCount, explained below. The targeting itself reads back as includeQuerySegments and includeQueryFilters, with their exclude counterparts. |
| What it costs | preparedUnits is the billable message parts, with unitsMin and unitsMax bracketing it. priceUser is the cost applied to the account, with priceMin and priceMax around it. Every price is an object: { "value": 611.28, "currency": "EUR" }. |
| When it happens | campaignAt is the calendar anchor, scheduledAt the armed send time (null until a person confirms it), and preparedAt, confirmedAt, canceledAt, createdAt and updatedAt timestamp the rest. localTime and localScheduledAt give the same instants in the project's timezone. |
The four counts are easy to confuse, and they answer different questions. audienceContactsCount is who the targeting resolves to, ignoring the channel. channelContactsCount narrows that to the contacts the channel can actually reach. contactsCount is what the estimate or the preparation actually resolved, and ignoredContactsCount is what it dropped on the way. A gap between the first two is a reachability problem; a gap between the last two is a consent or data problem.
Once a campaign has been sent it also carries stats, channelBreakdown and statsAt. Those are the summary figures; the full reporting surface is campaign analytics.
Listing campaigns
List the project's campaigns.
The list takes the shared Query Filter syntax for filtering, sorting and pagination, so status_eq=sent or campaignAt_gte=2026-01-01T00:00:00+00:00 work here as they do on every other list endpoint. Campaigns come back under entities, with the window described under metadata:
{
"entities": [{ "id": "68b0f2a4e5a6b7c8d9e0f1a2", "title": "Summer sale announcement", "status": "sent" }],
"metadata": { "count": 47, "start": 0, "limit": 50 }
}Two behaviours are specific to this list:
- It is sorted newest first by calendar date. With no
_sort, the default iscampaignAt:desc. _limiton its own is ignored. Pagination needs both halves: send_start=0&_limit=50.
What the list leaves out
The list only returns campaigns a marketer would recognise as campaigns. One-off direct sends are recorded as campaigns internally, carry purpose: direct rather than standard, and are filtered out here unconditionally. There is no parameter that brings them back, and a filter of your own on purpose cannot widen it.
This matters if you are reconciling volumes: the campaign list is not the record of everything the project sent. It is the record of everything the project campaigned.
The project digest
A digest of the project's campaign activity.
One call that answers "what has this project been doing", without paging the list. It is built from aggregates over indexed queries and two point reads, so it stays cheap on projects with thousands of campaigns, and it is the natural thing to put behind a dashboard or hand to an assistant opening a conversation.
{
"entity": {
"total": 47,
"byPhase": { "draft": 3, "preparing": 1, "scheduled": 2, "sending": 0, "sent": 39, "stopped": 2 },
"byStatus": { "draft": 3, "prepared": 1, "scheduled": 2, "sent": 39, "canceled": 2 },
"lastSent": {
"id": "68b0f2a4e5a6b7c8d9e0f1a2",
"title": "Summer sale announcement",
"channel": "sms",
"sentAt": "2026-06-15T09:30:00+00:00",
"sentAgo": "6 days ago",
"recipients": 16980,
"sent": 16980,
"delivered": 16659,
"deliveryRate": 0.9812,
"clicks": 2143,
"cost": 611.28,
"statsAt": "2026-06-16T03:00:00+00:00"
},
"nextScheduled": {
"id": "68b0f2a4e5a6b7c8d9e0f1b7",
"title": "Restock notice",
"channel": "rcs",
"scheduledAt": "2026-06-22T10:00:00+00:00",
"inHours": 18.5,
"recipients": 4210
},
"cadence": { "sentLast30Days": 4, "sentLast90Days": 11 }
}
}| Field | What it holds |
|---|---|
total | Every campaign in the project. |
byPhase | Counts in the six phases: draft, preparing, scheduled, sending, sent, stopped. |
byStatus | The same campaigns counted by raw status, for when the six buckets are too coarse. |
lastSent | The most recent campaign that went out, with its headline figures. null if none ever did. |
nextScheduled | The next campaign due out, with inHours until it goes. null if nothing is armed. |
cadence | How many campaigns went out in the last 30 and 90 days. |
Two conveniences are worth noticing, because they save a round of arithmetic in a UI: deliveryRate arrives precomputed as a fraction between 0 and 1, and sentAgo arrives already in words ("6 days ago").
Collecting an asynchronous result
Both write operations on an existing campaign finish somewhere else. The pattern is the same for each: call, then re-read until the campaign tells you it is done.
After an estimate
The
202leaves the campaign inestimating. Re-read it untilstatuschanges, then takecontactsCount,ignoredContactsCount,preparedUnitsand the price fields. The worker typically takes about a minute, so poll on the order of seconds, not milliseconds.After a delete
The
204has no body and does not always mean the campaign is gone. Re-read it: a404means it is gone, and the campaign still there indeletedmeans a worker is removing its messages and will finish on its own. Neither is an error, and an integration that treats a post-delete404as a failure will report problems that did not happen.