Skip to content

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.stats

API Usage Notes

  • Authentication required: User API Key
  • Required permissions: SMSCampaigns.Get
  • Legacy endpoint access via /api.php is also supported

Request Body Parameters:

ParameterTypeRequiredDescription
CommandStringYesAPI command: smscampaign.stats
SessionIDStringNoSession ID obtained from login
APIKeyStringNoAPI key for authentication
SMSCampaignIDIntegerYesThe campaign to report on

Response fields worth knowing:

FieldMeaning
Counters.AwaitingReportMessages 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
DetailExpiredtrue once per-recipient detail has passed its retention window. Counters remain correct and available; only the per-recipient list is gone
Funnel.SkippedRecipients removed before sending, broken down by reason
bash
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
  }'
json
{
  "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
  }
}
json
{
  "Success": false,
  "ErrorCode": [2]
}
txt
1: Invalid or missing SMSCampaignID
2: Campaign not found, or it belongs to another account
5: The event store could not be read

Get Campaign Statistics Over Time ​

GET /api/v1/smscampaign.stats.timeseries

API Usage Notes

  • Authentication required: User API Key
  • Required permissions: SMSCampaigns.Get
  • Legacy endpoint access via /api.php is also supported

Request Body Parameters:

ParameterTypeRequiredDescription
CommandStringYesAPI command: smscampaign.stats.timeseries
SessionIDStringNoSession ID obtained from login
APIKeyStringNoAPI key for authentication
SMSCampaignIDIntegerYesThe campaign to report on
GranularityStringNoPossible values: hour (default), day
EventsStringNoComma-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
IncludeBotsBooleanNoInclude 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.

bash
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"
  }'
json
{
  "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}
  ]
}
json
{
  "Success": false,
  "ErrorCode": [3]
}
txt
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 read

Get a Campaign Breakdown ​

GET /api/v1/smscampaign.stats.breakdown

API Usage Notes

  • Authentication required: User API Key
  • Required permissions: SMSCampaigns.Get
  • Legacy endpoint access via /api.php is also supported

Request Body Parameters:

ParameterTypeRequiredDescription
CommandStringYesAPI command: smscampaign.stats.breakdown
SessionIDStringNoSession ID obtained from login
APIKeyStringNoAPI key for authentication
SMSCampaignIDIntegerYesThe campaign to report on
DimensionStringYesThe single dimension to group by. Possible values: hour, day, gateway_status, error_code, carrier, country, link, sender_id
EventStringNoRestrict to one event type. Possible values: queued, skipped, send_attempt, sent, send_failed, delivered, undelivered, expired, clicked, replied, opted_out
IncludeBotsBooleanNoInclude bot clicks. Defaults to excluding them
LimitIntegerNoRows to return, 1 to 500. Default 50
bash
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"
  }'
json
{
  "Success": true,
  "SMSCampaignID": 1234,
  "Dimension": "carrier",
  "Event": null,
  "ExcludesBots": true,
  "Limit": 50,
  "Breakdown": [
    {"Dimension": "Turkcell", "Events": 120, "Messages": 118}
  ]
}
json
{
  "Success": false,
  "ErrorCode": [3]
}
txt
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 read

Browse Campaign Recipients ​

GET /api/v1/smscampaign.recipients

API Usage Notes

  • Authentication required: User API Key
  • Required permissions: SMSCampaigns.Get
  • Legacy endpoint access via /api.php is also supported

Request Body Parameters:

ParameterTypeRequiredDescription
CommandStringYesAPI command: smscampaign.recipients.browse
SessionIDStringNoSession ID obtained from login
APIKeyStringNoAPI key for authentication
SMSCampaignIDIntegerYesThe campaign to read
StatusStringNoFilter by delivery status. Possible values: Queued, Released, Sending, Sent, Delivered, Failed, Expired, Rejected, Suppressed, Cancelled
LimitIntegerNoRows per page, 1 to 500. Default 50
CursorIntegerNoNextCursor from the previous page. Ignored when RecordsFrom is sent
RecordsFromIntegerNoOffset into the result set. Sending it switches this call from cursor paging to offset paging and implies IncludeTotal
IncludeTotalBooleanNoReturn 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.

bash
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
  }'
json
{
  "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
}
json
{
  "Success": false,
  "ErrorCode": [3]
}
txt
1: Invalid or missing SMSCampaignID
2: Campaign not found, or it belongs to another account
3: Invalid Status
5: The recipients could not be read

Browse Campaign Events ​

GET /api/v1/smscampaign.events

API Usage Notes

  • Authentication required: User API Key
  • Required permissions: SMSCampaigns.Get
  • Legacy endpoint access via /api.php is also supported

Request Body Parameters:

ParameterTypeRequiredDescription
CommandStringYesAPI command: smscampaign.events.browse
SessionIDStringNoSession ID obtained from login
APIKeyStringNoAPI key for authentication
SMSCampaignIDIntegerYesThe campaign to read
EventStringNoRestrict to one event type. Possible values: queued, skipped, send_attempt, sent, send_failed, delivered, undelivered, expired, clicked, replied, opted_out
FromStringNoEarliest event time, any parseable date
ToStringNoLatest event time
SubscriberIDIntegerNoRestrict to one contact
LimitIntegerNoRows per page, 1 to 500. Default 50
CursorTimeStringNoNextCursor.CursorTime from the previous page. Must be sent together with CursorID
CursorIDStringNoNextCursor.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.

bash
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
  }'
json
{
  "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"
  }
}
json
{
  "Success": false,
  "ErrorCode": [4]
}
txt
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 date

Browse Campaign Replies ​

GET /api/v1/smscampaign.replies

API Usage Notes

  • Authentication required: User API Key
  • Required permissions: SMSCampaigns.Get
  • Legacy endpoint access via /api.php is also supported

Request Body Parameters:

ParameterTypeRequiredDescription
CommandStringYesAPI command: smscampaign.replies.browse
SessionIDStringNoSession ID obtained from login
APIKeyStringNoAPI key for authentication
SMSCampaignIDIntegerYesThe campaign to read
OptOutsOnlyBooleanNoReturn only replies that were treated as an opt-out
LimitIntegerNoRows per page, 1 to 500. Default 50
CursorIntegerNoNextCursor from the previous page
bash
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
  }'
json
{
  "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
}
json
{
  "Success": false,
  "ErrorCode": [2]
}
txt
1: Invalid or missing SMSCampaignID
2: Campaign not found, or it belongs to another account
5: The replies could not be read
GET /api/v1/smscampaign.linkclicks

API Usage Notes

  • Authentication required: User API Key
  • Required permissions: SMSCampaigns.Get
  • Legacy endpoint access via /api.php is also supported

Request Body Parameters:

ParameterTypeRequiredDescription
CommandStringYesAPI command: smscampaign.linkclicks.get
SessionIDStringNoSession ID obtained from login
APIKeyStringNoAPI key for authentication
SMSCampaignIDIntegerYesThe campaign to read
IncludeBotsBooleanNoInclude 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.

bash
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
  }'
json
{
  "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"
    }
  ]
}
json
{
  "Success": false,
  "ErrorCode": [2]
}
txt
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 read

Start a Campaign Event Export ​

POST /api/v1/smscampaign.events.export

API Usage Notes

  • Authentication required: User API Key
  • Required permissions: SMSCampaigns.Get
  • Rate limit: 30 requests per 60 seconds
  • Legacy endpoint access via /api.php is also supported

Request Body Parameters:

ParameterTypeRequiredDescription
CommandStringYesAPI command: smscampaign.events.export.post
SessionIDStringNoSession ID obtained from login
APIKeyStringNoAPI key for authentication
SMSCampaignIDIntegerYesThe campaign to export
EventStringNoRestrict to one event type
FromStringNoEarliest event time
ToStringNoLatest 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.

bash
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"
  }'
json
{
  "Success": true,
  "ExportID": 91,
  "Status": "Pending",
  "AlreadyQueued": false,
  "Filters": {"event": "delivered"}
}
json
{
  "Success": false,
  "ErrorCode": [4]
}
txt
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 To

Get a Campaign Event Export ​

GET /api/v1/smscampaign.events.export

API Usage Notes

  • Authentication required: User API Key
  • Required permissions: SMSCampaigns.Get
  • Legacy endpoint access via /api.php is also supported

Request Body Parameters:

ParameterTypeRequiredDescription
CommandStringYesAPI command: smscampaign.events.export.get
SessionIDStringNoSession ID obtained from login
APIKeyStringNoAPI key for authentication
ExportIDIntegerYesThe 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.

bash
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
  }'
json
{
  "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
}
json
{
  "Success": false,
  "ErrorCode": [2]
}
txt
1: Invalid or missing ExportID
2: Export not found, or it belongs to another account
5: The export could not be read

Get Account SMS Statistics ​

GET /api/v1/sms.stats.account

API Usage Notes

  • Authentication required: User API Key
  • Required permissions: SMSCampaigns.Get
  • Legacy endpoint access via /api.php is also supported

Request Body Parameters:

ParameterTypeRequiredDescription
CommandStringYesAPI command: sms.stats.account
SessionIDStringNoSession ID obtained from login
APIKeyStringNoAPI key for authentication
FromStringNoFirst day to include. Defaults to 30 days ago
ToStringNoLast 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.

bash
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"
  }'
json
{
  "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
    }
  ]
}
json
{
  "Success": false,
  "ErrorCode": [6]
}
txt
5: The event store could not be read
6: Invalid From or To date
7: From is after To

Browse Account SMS Replies ​

GET /api/v1/sms.replies

API Usage Notes

  • Authentication required: User API Key
  • Required permissions: SMSCampaigns.Get
  • Legacy endpoint access via /api.php is also supported

Request Body Parameters:

ParameterTypeRequiredDescription
CommandStringYesAPI command: sms.replies.browse
SessionIDStringNoSession ID obtained from login
APIKeyStringNoAPI key for authentication
OptOutsOnlyBooleanNoReturn only replies that were treated as an opt-out. Superseded by Filter, and ignored when Filter is sent
FilterStringNoOne bucket of the feed. Possible values: optouts, failedoptouts, unattributed. Omit for everything
IncludeUnattributedBooleanNoInclude replies that could not be matched to a contact. Defaults to true, so send it explicitly as false to exclude them
OrderStringNoPossible values: oldest (default), newest. See the note below before changing it
SearchNumberStringNoReturn only replies from numbers containing these digits. Non-digits are stripped; at least 3 digits are required
CreatedAfterStringNoOnly replies received at or after this point. YYYY-MM-DD or YYYY-MM-DD HH:MM:SS; a bare date means 00:00:00
CreatedBeforeStringNoOnly replies received at or before this point. A bare date means 23:59:59, so the whole day is included
LimitIntegerNoRows per page, 1 to 500. Default 50
CursorIntegerNoNextCursor from the previous page. Ignored when RecordsFrom is sent
RecordsFromIntegerNoOffset 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.

bash
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
  }'
json
{
  "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
}
json
{
  "Success": false,
  "ErrorCode": [5]
}
txt
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 digits

Filter 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.get

API Usage Notes

  • Authentication required: User API Key
  • Required permissions: SMSCampaigns.Get
  • Legacy endpoint access via /api.php is 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:

ParameterTypeRequiredDescription
CommandStringYesAPI command: sms.replies.summary.get
SessionIDStringNoSession ID obtained from login
APIKeyStringNoAPI key for authentication
IncludeUnattributedBooleanNoCount replies that could not be matched to a contact. Defaults to true
SearchNumberStringNoCount only replies from numbers containing these digits. At least 3 digits
CreatedAfterStringNoOnly replies received at or after this point, parsed as in sms.replies.browse
CreatedBeforeStringNoOnly replies received at or before this point

Response Fields:

FieldTypeDescription
TotalIntegerReplies matching the filters
OptOutsIntegerOf those, the ones treated as an opt-out
FailedOptOutsIntegerOpt-outs the unsubscribe did not complete for. Any non-zero value needs attention: the person asked to be removed and was not
UnattributedIntegerReplies that could not be matched to a contact
IncludesUnattributedBooleanWhether 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
bash
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"
  }'
json
{
  "Success": true,
  "Total": 1284,
  "OptOuts": 212,
  "FailedOptOuts": 3,
  "Unattributed": 41,
  "IncludesUnattributed": true,
  "SearchNumber": "",
  "CreatedAfter": "2026-09-01 00:00:00",
  "CreatedBefore": null
}
json
{
  "Success": false,
  "Errors": [{ "Code": 8, "Message": "Invalid createdafter value. Expected YYYY-MM-DD or YYYY-MM-DD HH:MM:SS." }],
  "ErrorCode": 8
}
txt
5: The reply summary could not be read
8: Invalid CreatedAfter or CreatedBefore value
9: SearchNumber contains fewer than 3 digits

Get a Subscriber's SMS History ​

GET /api/v1/subscriber.sms.events

API Usage Notes

  • Authentication required: User API Key
  • Required permissions: Subscribers.Get
  • Legacy endpoint access via /api.php is also supported

Request Body Parameters:

ParameterTypeRequiredDescription
CommandStringYesAPI command: subscriber.sms.events.get
SessionIDStringNoSession ID obtained from login
APIKeyStringNoAPI key for authentication
ListIDIntegerYesThe list the contact belongs to. This is the ownership boundary and is checked before anything is read
SubscriberIDIntegerYesThe contact to read
EventStringNoRestrict to one event type
LimitIntegerNoRows per page, 1 to 500. Default 50
CursorTimeStringNoNextCursor.CursorTime from the previous page. Must be sent together with CursorID
CursorIDStringNoNextCursor.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.

bash
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
  }'
json
{
  "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
}
json
{
  "Success": false,
  "ErrorCode": [2]
}
txt
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

Any questions? Contact us.