SMS Reporting API Documentation
Reporting endpoints for bulk SMS campaigns: counters and rates, time series, breakdowns, per-recipient status, raw events, replies, link clicks, and asynchronous CSV export.
How these numbers are produced
Campaign counters are recomputed from the deduplicated event store by a scheduled rollup that runs every minute, and are read from the campaign record rather than recalculated per request. CountersUpdatedAt on smscampaign.stats tells you how fresh they are, so you can tell a campaign whose counters were just refreshed and genuinely has no activity from one that has never been rolled up.
Everything else reads the event store directly and is deduplicated at read time. Bot clicks are excluded from every count unless you pass IncludeBots=1.
Get Campaign Statistics
GET/api/v1/smscampaign.statsAPI Usage Notes
- Authentication required: User API Key
- Required permissions:
SMSCampaigns.Get - Legacy endpoint access via
/api.phpis also supported
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: smscampaign.stats |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| SMSCampaignID | Integer | Yes | The campaign to report on |
Response fields worth knowing:
| Field | Meaning |
|---|---|
Counters.AwaitingReport | Messages sent that have no final delivery outcome yet. This is reported as its own number and is never folded into failures, so a campaign read moments after sending does not look like it failed |
Rates.* | null rather than 0 when the denominator is zero. A campaign that has delivered nothing has no click rate, and 0 would render as "0% clicked" next to a campaign that genuinely got none |
DetailExpired | true once per-recipient detail has passed its retention window. Counters remain correct and available; only the per-recipient list is gone |
Funnel.Skipped | Recipients removed before sending, broken down by reason |
curl -X GET https://example.com/api/v1/smscampaign.stats \
-H "Content-Type: application/json" \
-d '{
"Command": "smscampaign.stats",
"APIKey": "your-api-key",
"SMSCampaignID": 1234
}'{
"Success": true,
"SMSCampaignID": 1234,
"Status": "Sent",
"DetailExpired": false,
"DetailExpiredAt": null,
"Counters": {
"TotalAudience": 5,
"TotalQueued": 4,
"TotalSent": 4,
"TotalDelivered": 3,
"TotalUndelivered": 0,
"TotalExpired": 0,
"TotalClicks": 2,
"TotalUniqueClicks": 1,
"TotalReplies": 1,
"TotalOptOuts": 1,
"TotalParts": 8,
"AwaitingReport": 1,
"ActualCost": "0.08000",
"CostCurrency": "USD",
"CountersUpdatedAt": "2026-09-20 09:41:07"
},
"Rates": {
"DeliveryRate": 0.75,
"UndeliveredRate": 0,
"ClickRate": 0.333333,
"ReplyRate": 0.333333,
"OptOutRate": 0.333333
},
"Funnel": {
"Audience": 5,
"Skipped": [{"Reason": "suppressed", "Total": 1}],
"Queued": 4,
"Sent": 4,
"Delivered": 3,
"Clicked": 1,
"Replied": 1,
"OptedOut": 1
}
}{
"Success": false,
"ErrorCode": [2]
}1: Invalid or missing SMSCampaignID
2: Campaign not found, or it belongs to another account
5: The event store could not be readGet Campaign Statistics Over Time
GET/api/v1/smscampaign.stats.timeseriesAPI Usage Notes
- Authentication required: User API Key
- Required permissions:
SMSCampaigns.Get - Legacy endpoint access via
/api.phpis also supported
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: smscampaign.stats.timeseries |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| SMSCampaignID | Integer | Yes | The campaign to report on |
| Granularity | String | No | Possible values: hour (default), day |
| Events | String | No | Comma-separated event types to include. Possible values: queued, skipped, send_attempt, sent, send_failed, delivered, undelivered, expired, clicked, replied, opted_out. Defaults to all of them. An unrecognised value is an error, not an empty series |
| IncludeBots | Boolean | No | Include bot clicks. Defaults to excluding them |
Events and Messages count different things
Each point returns both Events and Messages.
Events counts distinct events and is meaningful for every event type. Messages counts distinct messages and is only meaningful for event types that carry a message identifier. queued events do not carry one, so read Events for those; reading Messages would report a single message for an entire campaign's queueing.
Granularity=day is served from a pre-aggregated table that carries no bot flag, so a daily click series always includes bot clicks whatever IncludeBots says. The ExcludesBots field in the response tells you which you got.
curl -X GET https://example.com/api/v1/smscampaign.stats.timeseries \
-H "Content-Type: application/json" \
-d '{
"Command": "smscampaign.stats.timeseries",
"APIKey": "your-api-key",
"SMSCampaignID": 1234,
"Granularity": "hour",
"Events": "sent,delivered,clicked"
}'{
"Success": true,
"SMSCampaignID": 1234,
"Granularity": "hour",
"Events": ["sent", "delivered", "clicked"],
"ExcludesBots": true,
"MessagesOnlyMeaningfulForOutcomes": true,
"Series": [
{"Bucket": "2026-09-19 14:00:00", "Event": "sent", "Events": 20, "Messages": 20}
]
}{
"Success": false,
"ErrorCode": [3]
}1: Invalid or missing SMSCampaignID
2: Campaign not found, or it belongs to another account
3: Invalid Granularity
4: Unknown event type
5: The event store could not be readGet a Campaign Breakdown
GET/api/v1/smscampaign.stats.breakdownAPI Usage Notes
- Authentication required: User API Key
- Required permissions:
SMSCampaigns.Get - Legacy endpoint access via
/api.phpis also supported
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: smscampaign.stats.breakdown |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| SMSCampaignID | Integer | Yes | The campaign to report on |
| Dimension | String | Yes | The single dimension to group by. Possible values: hour, day, gateway_status, error_code, carrier, country, link, sender_id |
| Event | String | No | Restrict to one event type. Possible values: queued, skipped, send_attempt, sent, send_failed, delivered, undelivered, expired, clicked, replied, opted_out |
| IncludeBots | Boolean | No | Include bot clicks. Defaults to excluding them |
| Limit | Integer | No | Rows to return, 1 to 500. Default 50 |
curl -X GET https://example.com/api/v1/smscampaign.stats.breakdown \
-H "Content-Type: application/json" \
-d '{
"Command": "smscampaign.stats.breakdown",
"APIKey": "your-api-key",
"SMSCampaignID": 1234,
"Dimension": "carrier"
}'{
"Success": true,
"SMSCampaignID": 1234,
"Dimension": "carrier",
"Event": null,
"ExcludesBots": true,
"Limit": 50,
"Breakdown": [
{"Dimension": "Turkcell", "Events": 120, "Messages": 118}
]
}{
"Success": false,
"ErrorCode": [3]
}1: Invalid or missing SMSCampaignID
2: Campaign not found, or it belongs to another account
3: Invalid Dimension
4: Unknown event type
5: The event store could not be readBrowse Campaign Recipients
GET/api/v1/smscampaign.recipientsAPI Usage Notes
- Authentication required: User API Key
- Required permissions:
SMSCampaigns.Get - Legacy endpoint access via
/api.phpis also supported
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: smscampaign.recipients.browse |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| SMSCampaignID | Integer | Yes | The campaign to read |
| Status | String | No | Filter by delivery status. Possible values: Queued, Released, Sending, Sent, Delivered, Failed, Expired, Rejected, Suppressed, Cancelled |
| Limit | Integer | No | Rows per page, 1 to 500. Default 50 |
| Cursor | Integer | No | NextCursor from the previous page. Ignored when RecordsFrom is sent |
| RecordsFrom | Integer | No | Offset into the result set. Sending it switches this call from cursor paging to offset paging and implies IncludeTotal |
| IncludeTotal | Boolean | No | Return TotalRecipients. Costs one extra COUNT over the same filters, so it is off unless asked for |
Two ways to page, and when each one is right
By default this endpoint pages by cursor, which steps forward cheaply however many rows a campaign has but cannot say "page 3 of 40" or jump to one.
Send RecordsFrom to page by offset instead, which is what a numbered pagination control needs. That is sound here because the result set is bounded by a single campaign's audience; it would not be on sms.replies.browse, whose table grows with every reply the account ever receives.
The two are mutually exclusive. A cursor is a position in the ordered set and an offset is a count into it, so honouring both at once would silently skip rows. RecordsFrom wins when both are sent.
TotalRecipients is null when it was not asked for, which is not the same as 0: one means "not counted", the other means "none".
Per-recipient detail expires
Once a campaign's queue rows pass their retention window, this endpoint returns DetailExpired: true and an empty Recipients array rather than pretending the campaign reached nobody. The campaign's counters remain correct and available through smscampaign.stats.
curl -X GET https://example.com/api/v1/smscampaign.recipients \
-H "Content-Type: application/json" \
-d '{
"Command": "smscampaign.recipients.browse",
"APIKey": "your-api-key",
"SMSCampaignID": 1234,
"Status": "Delivered",
"Limit": 100
}'{
"Success": true,
"SMSCampaignID": 1234,
"DetailExpired": false,
"Status": "Delivered",
"Limit": 100,
"Recipients": [
{
"QueueID": 994463,
"RelSubscriberID": 1005,
"RecipientNumber": "+905550000005",
"Status": "Delivered",
"SentTime": "2026-09-19 14:15:08",
"DeliveredTime": "2026-09-19 14:15:31",
"FirstClickedAt": null,
"ClickCount": 0
}
],
"NextCursor": 994463
}{
"Success": false,
"ErrorCode": [3]
}1: Invalid or missing SMSCampaignID
2: Campaign not found, or it belongs to another account
3: Invalid Status
5: The recipients could not be readBrowse Campaign Events
GET/api/v1/smscampaign.eventsAPI Usage Notes
- Authentication required: User API Key
- Required permissions:
SMSCampaigns.Get - Legacy endpoint access via
/api.phpis also supported
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: smscampaign.events.browse |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| SMSCampaignID | Integer | Yes | The campaign to read |
| Event | String | No | Restrict to one event type. Possible values: queued, skipped, send_attempt, sent, send_failed, delivered, undelivered, expired, clicked, replied, opted_out |
| From | String | No | Earliest event time, any parseable date |
| To | String | No | Latest event time |
| SubscriberID | Integer | No | Restrict to one contact |
| Limit | Integer | No | Rows per page, 1 to 500. Default 50 |
| CursorTime | String | No | NextCursor.CursorTime from the previous page. Must be sent together with CursorID |
| CursorID | String | No | NextCursor.CursorID from the previous page |
Why the cursor is a pair
Thousands of events can share the same millisecond at campaign volume, so a cursor on time alone would either repeat that millisecond on the next page or skip it. Paging is on (EventTime, EventID) together, which is unique. Send both values back exactly as received.
curl -X GET https://example.com/api/v1/smscampaign.events \
-H "Content-Type: application/json" \
-d '{
"Command": "smscampaign.events.browse",
"APIKey": "your-api-key",
"SMSCampaignID": 1234,
"Event": "clicked",
"Limit": 100
}'{
"Success": true,
"SMSCampaignID": 1234,
"Event": "clicked",
"Limit": 100,
"Events": [
{
"EventTime": "2026-09-19 14:15:08.000",
"EventID": "0cd4ad1c2deb6b2d7f10ffa11b5dea23e9bd4c03",
"Event": "clicked",
"Source": "campaign",
"SubscriberID": 1005,
"URL": "https://example.com/offer",
"IsBot": 0
}
],
"NextCursor": {
"CursorTime": "2026-09-19 14:15:08.000",
"CursorID": "0cd4ad1c2deb6b2d7f10ffa11b5dea23e9bd4c03"
}
}{
"Success": false,
"ErrorCode": [4]
}1: Invalid or missing SMSCampaignID
2: Campaign not found, or it belongs to another account
4: Unknown event type
5: The event store could not be read
6: Invalid From, To or CursorTime dateBrowse Campaign Replies
GET/api/v1/smscampaign.repliesAPI Usage Notes
- Authentication required: User API Key
- Required permissions:
SMSCampaigns.Get - Legacy endpoint access via
/api.phpis also supported
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: smscampaign.replies.browse |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| SMSCampaignID | Integer | Yes | The campaign to read |
| OptOutsOnly | Boolean | No | Return only replies that were treated as an opt-out |
| Limit | Integer | No | Rows per page, 1 to 500. Default 50 |
| Cursor | Integer | No | NextCursor from the previous page |
curl -X GET https://example.com/api/v1/smscampaign.replies \
-H "Content-Type: application/json" \
-d '{
"Command": "smscampaign.replies.browse",
"APIKey": "your-api-key",
"SMSCampaignID": 1234
}'{
"Success": true,
"SMSCampaignID": 1234,
"OptOutsOnly": false,
"Limit": 50,
"Replies": [
{
"InboundID": 8811,
"FromNumber": "+905550000005",
"MessageText": "STOP",
"ReceivedAt": "2026-09-19 14:22:10",
"IsOptOut": 1,
"OptOutScope": "user",
"UnsubscribeStatus": "done"
}
],
"NextCursor": null
}{
"Success": false,
"ErrorCode": [2]
}1: Invalid or missing SMSCampaignID
2: Campaign not found, or it belongs to another account
5: The replies could not be readGet Campaign Link Clicks
GET/api/v1/smscampaign.linkclicksAPI Usage Notes
- Authentication required: User API Key
- Required permissions:
SMSCampaigns.Get - Legacy endpoint access via
/api.phpis also supported
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: smscampaign.linkclicks.get |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| SMSCampaignID | Integer | Yes | The campaign to read |
| IncludeBots | Boolean | No | Include bot clicks. Defaults to excluding them |
Links that were sent and never clicked are returned with zero counts rather than omitted, so a report cannot silently hide the link nobody clicked.
curl -X GET https://example.com/api/v1/smscampaign.linkclicks \
-H "Content-Type: application/json" \
-d '{
"Command": "smscampaign.linkclicks.get",
"APIKey": "your-api-key",
"SMSCampaignID": 1234
}'{
"Success": true,
"SMSCampaignID": 1234,
"ExcludesBots": true,
"Links": [
{
"LinkOrdinal": 1,
"URL": "https://example.com/offer",
"TotalClicks": 2,
"UniqueClickers": 1,
"FirstClickedAt": "2026-09-19 14:20:01.000",
"LastClickedAt": "2026-09-19 14:41:55.000"
}
]
}{
"Success": false,
"ErrorCode": [2]
}1: Invalid or missing SMSCampaignID
2: Campaign not found, or it belongs to another account
4: The campaign's links could not be read
5: The event store could not be readStart a Campaign Event Export
POST/api/v1/smscampaign.events.exportAPI Usage Notes
- Authentication required: User API Key
- Required permissions:
SMSCampaigns.Get - Rate limit: 30 requests per 60 seconds
- Legacy endpoint access via
/api.phpis also supported
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: smscampaign.events.export.post |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| SMSCampaignID | Integer | Yes | The campaign to export |
| Event | String | No | Restrict to one event type |
| From | String | No | Earliest event time |
| To | String | No | Latest event time |
The export runs as a background job because a campaign's event set runs to millions of rows. Filters are validated when you submit, not when the job runs, so a mistake is reported immediately rather than becoming a failed job minutes later.
Only one export per campaign per account can be queued or running at a time, enforced by the database rather than by a check the request makes first, so two requests arriving together cannot both enqueue. Submitting again while one is in flight returns the existing ExportID with AlreadyQueued: true, so polling with POST cannot queue a job per poll. In that case Status reflects the existing job and is Pending or Running.
curl -X POST https://example.com/api/v1/smscampaign.events.export \
-H "Content-Type: application/json" \
-d '{
"Command": "smscampaign.events.export.post",
"APIKey": "your-api-key",
"SMSCampaignID": 1234,
"Event": "delivered"
}'{
"Success": true,
"ExportID": 91,
"Status": "Pending",
"AlreadyQueued": false,
"Filters": {"event": "delivered"}
}{
"Success": false,
"ErrorCode": [4]
}1: Invalid or missing SMSCampaignID
2: Campaign not found, or it belongs to another account
4: Unknown event type
5: The export could not be queued
6: Invalid From or To date
7: From is after ToGet a Campaign Event Export
GET/api/v1/smscampaign.events.exportAPI Usage Notes
- Authentication required: User API Key
- Required permissions:
SMSCampaigns.Get - Legacy endpoint access via
/api.phpis also supported
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: smscampaign.events.export.get |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| ExportID | Integer | Yes | The export job to read |
An export that does not finish is failed, not left running
If the worker handling an export dies partway through, a container restart is enough, the job would otherwise sit in Running forever and neither complete nor fail. Such a job is given up on after an hour and returned with Status: "Failed" and an Error saying so, which is your signal to submit it again.
It is failed rather than retried deliberately: retrying a job whose original worker might still be alive would put two workers on the same file.
The download link is signed and expires
DownloadURL carries a signature over the export and its owner and is valid for DownloadExpiresInSeconds. Request a fresh one when it expires rather than storing it.
A link is only offered when the file is actually on disk. Export files are removed after 7 days, so a Done export whose file has aged out returns FileAvailable: false and no URL rather than a link to a missing file.
curl -X GET https://example.com/api/v1/smscampaign.events.export \
-H "Content-Type: application/json" \
-d '{
"Command": "smscampaign.events.export.get",
"APIKey": "your-api-key",
"ExportID": 91
}'{
"Success": true,
"ExportID": 91,
"SMSCampaignID": 1234,
"Status": "Done",
"RowCount": 40,
"Error": "",
"Filters": {"event": "delivered"},
"CreatedAt": "2026-09-20 09:40:00",
"CompletedAt": "2026-09-20 09:41:12",
"DownloadURL": "https://example.com/sms_export_download.php?exportid=91&token=...",
"DownloadExpiresInSeconds": 900,
"FileAvailable": true
}{
"Success": false,
"ErrorCode": [2]
}1: Invalid or missing ExportID
2: Export not found, or it belongs to another account
5: The export could not be readGet Account SMS Statistics
GET/api/v1/sms.stats.accountAPI Usage Notes
- Authentication required: User API Key
- Required permissions:
SMSCampaigns.Get - Legacy endpoint access via
/api.phpis also supported
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: sms.stats.account |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| From | String | No | First day to include. Defaults to 30 days ago |
| To | String | No | Last day to include. Defaults to today |
Results are broken down by Source, because an account's SMS activity is not one number and "why did this month cost more" is usually answered by which source grew.
Cost and parts here are approximate
ApproxCost and ApproxParts are sums over a pre-aggregated table that cannot deduplicate a repeated event, so they are a trend rather than an invoice. For the exact figure read ActualCost on the campaign, which is derived from a deduplicated query. The response says so with CostAndPartsAreApproximate.
As with the campaign time series, Events counts distinct events and is meaningful everywhere, while Messages is only meaningful for event types that carry a message identifier.
curl -X GET https://example.com/api/v1/sms.stats.account \
-H "Content-Type: application/json" \
-d '{
"Command": "sms.stats.account",
"APIKey": "your-api-key",
"From": "2026-09-01",
"To": "2026-09-30"
}'{
"Success": true,
"From": "2026-09-01",
"To": "2026-09-30",
"CostAndPartsAreApproximate": true,
"MessagesOnlyMeaningfulForCampaignOutcomes": true,
"Series": [
{
"Day": "2026-09-19",
"Source": "campaign",
"Event": "sent",
"Events": 120205,
"Messages": 120205,
"ApproxCost": "0.16",
"ApproxParts": 120201
}
]
}{
"Success": false,
"ErrorCode": [6]
}5: The event store could not be read
6: Invalid From or To date
7: From is after ToBrowse Account SMS Replies
GET/api/v1/sms.repliesAPI Usage Notes
- Authentication required: User API Key
- Required permissions:
SMSCampaigns.Get - Legacy endpoint access via
/api.phpis also supported
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: sms.replies.browse |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| OptOutsOnly | Boolean | No | Return only replies that were treated as an opt-out. Superseded by Filter, and ignored when Filter is sent |
| Filter | String | No | One bucket of the feed. Possible values: optouts, failedoptouts, unattributed. Omit for everything |
| IncludeUnattributed | Boolean | No | Include replies that could not be matched to a contact. Defaults to true, so send it explicitly as false to exclude them |
| Order | String | No | Possible values: oldest (default), newest. See the note below before changing it |
| SearchNumber | String | No | Return only replies from numbers containing these digits. Non-digits are stripped; at least 3 digits are required |
| CreatedAfter | String | No | Only replies received at or after this point. YYYY-MM-DD or YYYY-MM-DD HH:MM:SS; a bare date means 00:00:00 |
| CreatedBefore | String | No | Only replies received at or before this point. A bare date means 23:59:59, so the whole day is included |
| Limit | Integer | No | Rows per page, 1 to 500. Default 50 |
| Cursor | Integer | No | NextCursor from the previous page. Ignored when RecordsFrom is sent |
| RecordsFrom | Integer | No | Offset into the result set. Sending it switches this call from cursor paging to offset paging, which is what a numbered pagination control needs |
Paging a feed that carries no total
This endpoint returns no row count, deliberately: the table grows with every reply the account ever receives, and a COUNT on every page turn is the cost cursor paging exists to avoid.
If you need page numbers, page by RecordsFrom and take the total from sms.replies.summary.get, which answers the same filters and breaks its counts out per bucket. That is what the interface does, so its page count costs no extra call. Take the count for the bucket you are showing, not Total, or a filtered feed gets the page count of the unfiltered one.
A cursor belongs to one order
Order changes the direction the cursor walks: ascending asks for InboundID > Cursor, descending asks for InboundID < Cursor. A cursor taken from one direction is meaningless in the other, so reset paging when you change Order rather than carrying a stored cursor across.
The default stays oldest so existing integrations paging through history are unaffected. A reply feed shown to a person usually wants newest.
Unattributed replies
A reply that could not be matched to a contact still belongs to somebody. Where the receiving gateway is assigned to your account alone, and is not a shared gateway, such replies are included here so they are not silently lost. On a shared gateway they are not, because they cannot be attributed unambiguously.
IncludesUnattributed in the response tells you whether any were eligible to be included.
curl -X GET https://example.com/api/v1/sms.replies \
-H "Content-Type: application/json" \
-d '{
"Command": "sms.replies.browse",
"APIKey": "your-api-key",
"OptOutsOnly": true
}'{
"Success": true,
"Limit": 50,
"OptOutsOnly": true,
"IncludesUnattributed": true,
"Replies": [
{
"InboundID": 8811,
"FromNumber": "+905550000005",
"MessageText": "STOP",
"ReceivedAt": "2026-09-19 14:22:10",
"ProcessingStatus": "processed",
"IsOptOut": 1
}
],
"NextCursor": null
}{
"Success": false,
"ErrorCode": [5]
}5: The replies could not be read
6: Invalid Filter value
7: Invalid Order value
8: Invalid CreatedAfter or CreatedBefore value
9: SearchNumber contains fewer than 3 digitsFilter and IncludeUnattributed can appear to contradict each other. Filter=unattributed wins: asking for the unattributed bucket while also excluding unattributed replies would otherwise answer an empty list rather than an error.
Get Account SMS Reply Counts
GET/api/v1/sms.replies.summary.getAPI Usage Notes
- Authentication required: User API Key
- Required permissions:
SMSCampaigns.Get - Legacy endpoint access via
/api.phpis also supported
How many replies match a set of filters, broken down by the states worth acting on. sms.replies.browse is cursor-paged and returns no total on purpose, because the inbound table grows with every reply the system ever receives and counting it on every page turn is the cost cursor paging exists to avoid. Ask this once instead.
Takes the same scope and the same filters as the feed, minus Filter itself: the response already breaks the buckets out separately, so narrowing to one would empty the others.
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: sms.replies.summary.get |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| IncludeUnattributed | Boolean | No | Count replies that could not be matched to a contact. Defaults to true |
| SearchNumber | String | No | Count only replies from numbers containing these digits. At least 3 digits |
| CreatedAfter | String | No | Only replies received at or after this point, parsed as in sms.replies.browse |
| CreatedBefore | String | No | Only replies received at or before this point |
Response Fields:
| Field | Type | Description |
|---|---|---|
| Total | Integer | Replies matching the filters |
| OptOuts | Integer | Of those, the ones treated as an opt-out |
| FailedOptOuts | Integer | Opt-outs the unsubscribe did not complete for. Any non-zero value needs attention: the person asked to be removed and was not |
| Unattributed | Integer | Replies that could not be matched to a contact |
| IncludesUnattributed | Boolean | Whether unattributed replies are actually inside these counts. Asking for them is not the same as getting them: they are only counted for gateways assigned to your account alone |
curl -X GET https://example.com/api/v1/sms.replies.summary.get \
-H "Content-Type: application/json" \
-d '{
"Command": "sms.replies.summary.get",
"APIKey": "your-api-key",
"CreatedAfter": "2026-09-01"
}'{
"Success": true,
"Total": 1284,
"OptOuts": 212,
"FailedOptOuts": 3,
"Unattributed": 41,
"IncludesUnattributed": true,
"SearchNumber": "",
"CreatedAfter": "2026-09-01 00:00:00",
"CreatedBefore": null
}{
"Success": false,
"Errors": [{ "Code": 8, "Message": "Invalid createdafter value. Expected YYYY-MM-DD or YYYY-MM-DD HH:MM:SS." }],
"ErrorCode": 8
}5: The reply summary could not be read
8: Invalid CreatedAfter or CreatedBefore value
9: SearchNumber contains fewer than 3 digitsGet a Subscriber's SMS History
GET/api/v1/subscriber.sms.eventsAPI Usage Notes
- Authentication required: User API Key
- Required permissions:
Subscribers.Get - Legacy endpoint access via
/api.phpis also supported
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: subscriber.sms.events.get |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| ListID | Integer | Yes | The list the contact belongs to. This is the ownership boundary and is checked before anything is read |
| SubscriberID | Integer | Yes | The contact to read |
| Event | String | No | Restrict to one event type |
| Limit | Integer | No | Rows per page, 1 to 500. Default 50 |
| CursorTime | String | No | NextCursor.CursorTime from the previous page. Must be sent together with CursorID |
| CursorID | String | No | NextCursor.CursorID from the previous page |
Newest first, unlike the campaign event browser, because a contact's history is read from the most recent thing that happened.
curl -X GET https://example.com/api/v1/subscriber.sms.events \
-H "Content-Type: application/json" \
-d '{
"Command": "subscriber.sms.events.get",
"APIKey": "your-api-key",
"ListID": 124,
"SubscriberID": 1005
}'{
"Success": true,
"ListID": 124,
"SubscriberID": 1005,
"Event": null,
"Limit": 50,
"Events": [
{
"EventTime": "2026-09-19 14:15:31.000",
"EventID": "1b2ba54fb97af29a5672af2295c1f06ee88c1969",
"Event": "delivered",
"Source": "campaign",
"SMSCampaignID": 1234,
"GatewayStatus": "Delivered"
}
],
"NextCursor": null
}{
"Success": false,
"ErrorCode": [2]
}1: Invalid or missing ListID or SubscriberID
2: List not found, or it belongs to another account
4: Unknown event type
5: The event store could not be read
6: Invalid CursorTime
