Skip to content

SMS Campaign API Documentation

Creating, costing, sending and controlling bulk SMS campaigns, plus the supporting endpoints for gateways, merge tags, saved templates and one-off messages.

The shape of a campaign

A campaign moves through a fixed set of states, and most endpoints here only accept it in some of them.

StatusMeaning
DraftBeing edited. The only status in which a campaign can be changed
ScheduledWaiting for its ScheduledAt to pass
QueueingIts audience is being resolved and queue rows written
SendingRows are being released to the gateway
PausedStopped by a user, or automatically on a cost drift
CancellingWinding down; workers are stopping cleanly
Sent, Cancelled, FailedFinal

The usual sequence is: smscampaign.create, then smscampaign.estimate and smscampaign.estimate.get to obtain a confirmation token, then smscampaign.send or smscampaign.schedule with that token. smscampaign.pause, .resume and .cancel control it afterwards.

A campaign cannot be sent without a cost estimate

smscampaign.send and smscampaign.schedule both require an EstimateID and a ConfirmationToken, and refuse without them. This is deliberate: it is what guarantees the cost a sender approved is the cost they are charged. Estimating is not an optional preview step, it is part of sending.

Campaign management

Create a Campaign

POST /api/v1/smscampaign.create

API Usage Notes

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

The campaign is created as a Draft. Its list must have a mobile phone number field configured, which list.sms.settings.update sets.

Request Body Parameters:

ParameterTypeRequiredDescription
CommandStringYesAPI command: smscampaign.create
SessionIDStringNoSession ID obtained from login
APIKeyStringNoAPI key for authentication
CampaignNameStringYesA name for the campaign
ListIDIntegerYesThe audience list. Must belong to the caller and have a phone field
MessageContentStringYesThe message body. May contain merge tags from sms.mergetags.get
GatewayIDIntegerNoThe sending gateway. Must be active and assigned to the account
SegmentIDIntegerNoNarrow the audience to a segment of that list
SenderIDStringNoThe sender number or alphanumeric id to send from
AppendOptOutFooterBooleanNoAppend the opt-out footer. Defaults to the account setting
OptOutFooterTextStringNoOverride the footer text for this campaign
TimezoneStringNoThe timezone quiet hours are evaluated in
bash
curl -X POST https://example.com/api/v1/smscampaign.create \
  -H "Content-Type: application/json" \
  -d '{
    "Command": "smscampaign.create",
    "SessionID": "your-session-id",
    "CampaignName": "October promotion",
    "ListID": 42,
    "MessageContent": "Hi {FirstName}, 20% off this week only.",
    "GatewayID": 3
  }'
json
{
  "Success": true,
  "ErrorCode": 0,
  "SMSCampaignID": 4821
}
json
{
  "Success": false,
  "Errors": [
    {
      "Code": 5,
      "Message": "This list has no mobile phone number field configured, so it cannot be an SMS campaign audience."
    }
  ],
  "ErrorCode": 5
}
txt
0: Success
4: Invalid ListID
5: The list has no mobile phone number field, so it cannot be an SMS audience
7: MessageContent is empty
8: Invalid GatewayID, or the gateway is not available to this account
9: The message is too long for the gateway's concatenation limit
10: The campaign could not be created
11: Invalid SegmentID, or the segment does not belong to this list

Update a Campaign

POST /api/v1/smscampaign.update

API Usage Notes

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

Only a Draft campaign can be updated. Every update bumps the campaign's modification time and changes its content fingerprint, which invalidates any estimate taken before it: after updating, run the estimate again before sending.

Request Body Parameters:

ParameterTypeRequiredDescription
CommandStringYesAPI command: smscampaign.update
SessionIDStringNoSession ID obtained from login
SMSCampaignIDIntegerYesThe campaign to update. Must be in Draft
CampaignNameStringNoA new name
MessageContentStringNoA new message body
GatewayIDIntegerNoA different gateway
SenderIDStringNoA different sender id
AppendOptOutFooterBooleanNoWhether to append the opt-out footer
OptOutFooterTextStringNoOverride the footer text
TimezoneStringNoThe quiet-hours timezone
bash
curl -X POST https://example.com/api/v1/smscampaign.update \
  -H "Content-Type: application/json" \
  -d '{
    "Command": "smscampaign.update",
    "SessionID": "your-session-id",
    "SMSCampaignID": 4821,
    "MessageContent": "Hi {FirstName}, 25% off this week only."
  }'
json
{
  "Success": true,
  "ErrorCode": 0
}
json
{
  "Success": false,
  "Errors": [{ "Code": 7, "Message": "The campaign could not be updated, so nothing was changed." }],
  "ErrorCode": 7
}
txt
0: Success
1: Missing or invalid SMSCampaignID parameter
7: The campaign could not be updated, so nothing was changed
10: The campaign could not be updated as a single transaction, so nothing was changed

Get a Campaign

GET /api/v1/smscampaign.get

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.get
SessionIDStringNoSession ID obtained from login
SMSCampaignIDIntegerYesThe campaign to read
bash
curl -X GET https://example.com/api/v1/smscampaign.get \
  -H "Content-Type: application/json" \
  -d '{
    "Command": "smscampaign.get",
    "SessionID": "your-session-id",
    "SMSCampaignID": 4821
  }'
json
{
  "Success": true,
  "ErrorCode": 0,
  "Campaign": {
    "SMSCampaignID": 4821,
    "CampaignName": "October promotion",
    "Status": "Sending",
    "StatusReason": "",
    "RelListID": 42,
    "RelGatewayID": 3,
    "MessageContent": "Hi {FirstName}, 20% off this week only.",
    "TotalAudience": 400318,
    "ConfirmedCost": "4315.66000",
    "CostCurrency": "USD"
  }
}
json
{
  "Success": false,
  "Errors": [{ "Code": 2, "Message": "Campaign not found." }],
  "ErrorCode": 2
}
txt
0: Success
1: Missing SMSCampaignID parameter
2: Campaign not found
4: The campaign's links could not be read

List Campaigns

GET /api/v1/smscampaign.browse

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.browse
SessionIDStringNoSession ID obtained from login
APIKeyStringNoAPI key for authentication
StatusStringNoFilter by status. Possible values: Draft, Scheduled, Queueing, Sending, Paused, Cancelling, Cancelled, Sent, Failed
ListIDIntegerNoOnly campaigns targeting this list
RecordsPerRequestIntegerNoPage size, default 25, clamped to 200
RecordsFromIntegerNoOffset, default 0
CreatedAfterStringNoOnly campaigns created at or after this point. YYYY-MM-DD or YYYY-MM-DD HH:MM:SS; a bare date means 00:00:00
CreatedBeforeStringNoOnly campaigns created at or before this point. YYYY-MM-DD or YYYY-MM-DD HH:MM:SS; a bare date means 23:59:59, so the whole day is included
bash
curl -X GET https://example.com/api/v1/smscampaign.browse \
  -H "Content-Type: application/json" \
  -d '{
    "Command": "smscampaign.browse",
    "SessionID": "your-session-id",
    "Status": "Sending",
    "RecordsPerRequest": 25
  }'
json
{
  "Success": true,
  "ErrorCode": 0,
  "Campaigns": [
    { "SMSCampaignID": 4821, "CampaignName": "October promotion", "Status": "Sending" }
  ],
  "TotalCampaigns": 1
}
json
{
  "Success": false,
  "Errors": [{ "Code": 1, "Message": "Invalid Status value." }],
  "ErrorCode": 1
}
txt
0: Success
1: Invalid Status value
2: The campaign list could not be read
3: Invalid CreatedAfter or CreatedBefore value

Results are ordered newest first by SMSCampaignID and the order cannot be changed. A CreatedAfter or CreatedBefore value that cannot be parsed is refused with ErrorCode 3 rather than ignored, so a malformed date never silently widens the window.

Get a Campaign Summary

GET /api/v1/smscampaign.summary.get

API Usage Notes

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

Counts and totals across every campaign matching the filters, rather than across one page of smscampaign.browse. Intended for a dashboard header or a status rail: it answers how many campaigns sit in each status and what the whole selection sent, delivered, cost and lost to opt-outs, in two grouped queries rather than one call per status.

Takes the same ListID and CreatedAt filters as smscampaign.browse, so a summary and a list read under the same parameters always describe the same campaigns. Status is deliberately not accepted: the response already breaks every status out separately.

Request Body Parameters:

ParameterTypeRequiredDescription
CommandStringYesAPI command: smscampaign.summary.get
SessionIDStringNoSession ID obtained from login
APIKeyStringNoAPI key for authentication
ListIDIntegerNoOnly campaigns targeting this list
CreatedAfterStringNoOnly campaigns created at or after this point, parsed as in smscampaign.browse
CreatedBeforeStringNoOnly campaigns created at or before this point, parsed as in smscampaign.browse

Response Fields:

FieldTypeDescription
TotalCampaignsIntegerCampaigns matching the filters
StatusCountsObjectOne key per status, including the statuses at zero, so a caller rendering a fixed set of buckets never has to guess
TotalsObjectTotalAudience, TotalSent, TotalDelivered, TotalUndelivered, TotalOptOuts, TotalClicks and TotalParts, summed across the selection
CostsArrayOne entry per currency, each with Currency, Campaigns, ActualCost and ConfirmedCost
CreatedAfterStringThe window's start as it was parsed, or null when none was sent
CreatedBeforeStringThe window's end as it was parsed, or null when none was sent

Money is returned per currency and is never pre-summed. CostCurrency is a per-campaign column, so an account holding both USD and EUR campaigns has two totals and no single one; adding them would produce a figure in no currency at all. A caller showing a single spend figure should check that Costs holds exactly one entry.

bash
curl -X GET https://example.com/api/v1/smscampaign.summary.get \
  -H "Content-Type: application/json" \
  -d '{
    "Command": "smscampaign.summary.get",
    "SessionID": "your-session-id",
    "CreatedAfter": "2026-08-01"
  }'
json
{
  "Success": true,
  "ErrorCode": 0,
  "TotalCampaigns": 16,
  "StatusCounts": {
    "Draft": 1,
    "Scheduled": 0,
    "Queueing": 0,
    "Sending": 1,
    "Paused": 0,
    "Cancelling": 0,
    "Sent": 12,
    "Cancelled": 2,
    "Failed": 0
  },
  "Totals": {
    "TotalAudience": 143840,
    "TotalSent": 138211,
    "TotalDelivered": 133902,
    "TotalUndelivered": 4309,
    "TotalOptOuts": 512,
    "TotalClicks": 18420,
    "TotalParts": 148903
  },
  "Costs": [
    { "Currency": "USD", "Campaigns": 15, "ActualCost": 2764.22, "ConfirmedCost": 2801.00 },
    { "Currency": "EUR", "Campaigns": 1, "ActualCost": 41.60, "ConfirmedCost": 41.60 }
  ],
  "CreatedAfter": "2026-08-01 00:00:00",
  "CreatedBefore": null
}
json
{
  "Success": false,
  "Errors": [{ "Code": 3, "Message": "Invalid createdafter value. Expected YYYY-MM-DD or YYYY-MM-DD HH:MM:SS." }],
  "ErrorCode": 3
}
txt
0: Success
2: The campaign summary could not be read
3: Invalid CreatedAfter or CreatedBefore value

Delete a Campaign

POST /api/v1/smscampaign.delete

API Usage Notes

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

A campaign in progress cannot be deleted; cancel it first. Deleting removes its queue rows, links and estimates with it.

Request Body Parameters:

ParameterTypeRequiredDescription
CommandStringYesAPI command: smscampaign.delete
SessionIDStringNoSession ID obtained from login
SMSCampaignIDIntegerYesThe campaign to delete
bash
curl -X POST https://example.com/api/v1/smscampaign.delete \
  -H "Content-Type: application/json" \
  -d '{
    "Command": "smscampaign.delete",
    "SessionID": "your-session-id",
    "SMSCampaignID": 4821
  }'
json
{ "Success": true, "ErrorCode": 0 }
json
{
  "Success": false,
  "Errors": [{ "Code": 3, "Message": "A campaign in progress cannot be deleted. Cancel it first." }],
  "ErrorCode": 3
}
txt
0: Success
1: Missing SMSCampaignID parameter
2: Campaign not found
3: A campaign in progress cannot be deleted; cancel it first
4: The campaign could not be deleted, so nothing was deleted
5: The campaign could not be deleted as a single transaction, so nothing was deleted

Sending

Send a Campaign

POST /api/v1/smscampaign.send

API Usage Notes

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

Starts sending immediately. Requires the EstimateID and ConfirmationToken from smscampaign.estimate.get, and stores the confirmed cost, recipient count and projected completion on the campaign. Nothing is recomputed here: the figures the sender approved are the figures that are stored.

Request Body Parameters:

ParameterTypeRequiredDescription
CommandStringYesAPI command: smscampaign.send
SessionIDStringNoSession ID obtained from login
SMSCampaignIDIntegerYesThe campaign to send. Must be in Draft
EstimateIDIntegerYesFrom smscampaign.estimate
ConfirmationTokenStringYesFrom smscampaign.estimate.get
SendDeadlineAtStringNoYYYY-MM-DD HH:MM:SS. Stop sending after this moment even if recipients remain
bash
curl -X POST https://example.com/api/v1/smscampaign.send \
  -H "Content-Type: application/json" \
  -d '{
    "Command": "smscampaign.send",
    "SessionID": "your-session-id",
    "SMSCampaignID": 4821,
    "EstimateID": 173,
    "ConfirmationToken": "1758377028.8f2c..."
  }'
json
{ "Success": true, "ErrorCode": 0, "Status": "Queueing" }
json
{
  "Success": false,
  "Errors": [{ "Code": 34, "Message": "The campaign changed after it was costed. Run the estimate again." }],
  "ErrorCode": 34
}
txt
0: Success
1: Missing or invalid SMSCampaignID parameter
4: Missing or invalid EstimateID parameter; run smscampaign.estimate first
9: The campaign could not be sent, so nothing was sent
10: The campaign could not be sent as a single transaction, so nothing was sent
31: The cost estimate could not be read, so nothing was sent
32: Estimate not found for this campaign
33: The estimate has not finished; wait for it or run a new one
34: The campaign changed after it was costed; run the estimate again
35: The confirmation token is invalid or has expired
36: The estimate result is incomplete; run the estimate again

Schedule a Campaign

POST /api/v1/smscampaign.schedule

API Usage Notes

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

The same as sending, but at a future moment. The campaign sits in Scheduled until ScheduledAt passes, then moves to Queueing on its own. The confirmation token is required and expires on the same schedule, so schedule promptly after estimating.

Request Body Parameters:

ParameterTypeRequiredDescription
CommandStringYesAPI command: smscampaign.schedule
SessionIDStringNoSession ID obtained from login
SMSCampaignIDIntegerYesThe campaign to schedule. Must be in Draft
ScheduledAtStringYesYYYY-MM-DD HH:MM:SS, in the campaign's timezone. Must be in the future
EstimateIDIntegerYesFrom smscampaign.estimate
ConfirmationTokenStringYesFrom smscampaign.estimate.get
SendDeadlineAtStringNoStop sending after this moment. Must be after ScheduledAt
bash
curl -X POST https://example.com/api/v1/smscampaign.schedule \
  -H "Content-Type: application/json" \
  -d '{
    "Command": "smscampaign.schedule",
    "SessionID": "your-session-id",
    "SMSCampaignID": 4821,
    "ScheduledAt": "2026-10-01 09:00:00",
    "EstimateID": 173,
    "ConfirmationToken": "1758377028.8f2c..."
  }'
json
{ "Success": true, "ErrorCode": 0, "Status": "Scheduled" }
json
{
  "Success": false,
  "Errors": [{ "Code": 11, "Message": "Missing ScheduledAt parameter." }],
  "ErrorCode": 11
}
txt
0: Success
1: Missing or invalid SMSCampaignID parameter
4: Missing or invalid EstimateID parameter; run smscampaign.estimate first
9: The campaign could not be scheduled, so nothing was scheduled
10: The campaign could not be scheduled as a single transaction
11: Missing ScheduledAt parameter
31-36: The estimate and token errors listed under smscampaign.send

Lifecycle

Pause a Campaign

POST /api/v1/smscampaign.pause

API Usage Notes

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

Only a Queueing or Sending campaign can be paused. Pausing stops future releases; messages already handed to the gateway cannot be recalled. Pausing an already-paused campaign is a success rather than an error, because pause is the button people press twice.

Request Body Parameters:

ParameterTypeRequiredDescription
CommandStringYesAPI command: smscampaign.pause
SessionIDStringNoSession ID obtained from login
SMSCampaignIDIntegerYesThe campaign to pause
bash
curl -X POST https://example.com/api/v1/smscampaign.pause \
  -H "Content-Type: application/json" \
  -d '{ "Command": "smscampaign.pause", "SessionID": "your-session-id", "SMSCampaignID": 4821 }'
json
{ "Success": true, "ErrorCode": 0, "Status": "Paused" }
json
{
  "Success": false,
  "Errors": [{ "Code": 3, "Message": "Only a campaign that is queueing or sending can be paused. This one is Draft." }],
  "ErrorCode": 3
}
txt
0: Success
1: Missing or invalid SMSCampaignID parameter
2: Campaign not found
3: Only a queueing or sending campaign can be paused
4: The campaign changed status before it could be paused; read it again

Resume a Campaign

POST /api/v1/smscampaign.resume

API Usage Notes

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

Returns a paused campaign to the status it was paused from. A campaign paused automatically because its queued cost drifted from the confirmed cost needs AcknowledgeCost=1, which is how the sender says they accept the new figure; the response carries the confirmed and drifted costs so they can see what they are accepting.

Request Body Parameters:

ParameterTypeRequiredDescription
CommandStringYesAPI command: smscampaign.resume
SessionIDStringNoSession ID obtained from login
SMSCampaignIDIntegerYesThe campaign to resume. Must be Paused
AcknowledgeCostIntegerNoPass 1 to resume a campaign paused on cost drift
bash
curl -X POST https://example.com/api/v1/smscampaign.resume \
  -H "Content-Type: application/json" \
  -d '{
    "Command": "smscampaign.resume",
    "SessionID": "your-session-id",
    "SMSCampaignID": 4821,
    "AcknowledgeCost": 1
  }'
json
{ "Success": true, "ErrorCode": 0, "Status": "Sending" }
json
{
  "Success": false,
  "Errors": [{ "Code": 4, "Message": "This campaign was paused because its queued cost drifted from the confirmed cost. Pass AcknowledgeCost=1 to resume it anyway." }],
  "ErrorCode": 4,
  "ConfirmedCost": "12.50000",
  "ConfirmedRecipients": 250,
  "CostCurrency": "USD",
  "CostDriftTolerance": 0.05
}
txt
0: Success
1: Missing or invalid SMSCampaignID parameter
2: Campaign not found
3: Only a Paused campaign can be resumed
4: Paused on cost drift; pass AcknowledgeCost=1 to resume anyway
5: The campaign changed status before it could be resumed; read it again

Cancel a Campaign

POST /api/v1/smscampaign.cancel

API Usage Notes

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

Cancelling returns immediately and does not wait for the send to wind down. A Draft campaign goes straight to Cancelled; anything already in flight passes through Cancelling so the workers can stop cleanly, and its queue rows are still present for a short time afterwards. Cancellation is final: a cancelled campaign cannot be resumed.

Request Body Parameters:

ParameterTypeRequiredDescription
CommandStringYesAPI command: smscampaign.cancel
SessionIDStringNoSession ID obtained from login
SMSCampaignIDIntegerYesThe campaign to cancel
bash
curl -X POST https://example.com/api/v1/smscampaign.cancel \
  -H "Content-Type: application/json" \
  -d '{ "Command": "smscampaign.cancel", "SessionID": "your-session-id", "SMSCampaignID": 4821 }'
json
{ "Success": true, "ErrorCode": 0, "Status": "Cancelling" }
json
{
  "Success": false,
  "Errors": [{ "Code": 3, "Message": "This campaign has already finished. It is Sent." }],
  "ErrorCode": 3
}
txt
0: Success
1: Missing or invalid SMSCampaignID parameter
2: Campaign not found
3: The campaign has already finished
4: The campaign changed status before it could be cancelled; read it again

Cost estimation

Why the estimate is a job rather than an answer

Measuring a campaign means resolving its audience, removing suppressed, invalid and duplicate numbers, personalizing every message and counting the parts each one will take. For a one-million recipient audience that cannot finish inside a single request, so smscampaign.estimate queues the work and returns an id. Poll smscampaign.estimate.get until it reports Done, then pass the EstimateID and ConfirmationToken it returns to smscampaign.send or smscampaign.schedule.

The token is what proves the cost was seen before the campaign was sent. It is signed over the estimate, the campaign and the campaign's content, so editing the campaign after estimating it invalidates the token and the estimate has to be run again. It also expires, after SMS_CAMPAIGN_ESTIMATE_TOKEN_TTL seconds (900 by default).

Cost is message parts multiplied by the configured cost per part. No SMS gateway exposes its pricing to the platform, so this is an estimate for planning, not a billing figure.

Start a Campaign Cost Estimate

POST /api/v1/smscampaign.estimate

API Usage Notes

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

Only a campaign still in Draft can be estimated. Requesting an estimate for a campaign that already has an unfinished job for the same content returns that job instead of queueing a second one, so polling clients and double-clicked buttons do not cause the same audience to be measured twice.

Request Body Parameters:

ParameterTypeRequiredDescription
CommandStringYesAPI command: smscampaign.estimate
SessionIDStringNoSession ID obtained from login
APIKeyStringNoAPI key for authentication
SMSCampaignIDIntegerYesThe campaign to estimate. Must belong to the authenticated user and be in Draft
bash
curl -X POST https://example.com/api/v1/smscampaign.estimate \
  -H "Content-Type: application/json" \
  -d '{
    "Command": "smscampaign.estimate",
    "SessionID": "your-session-id",
    "SMSCampaignID": 4821
  }'
json
{
  "Success": true,
  "ErrorCode": 0,
  "EstimateID": 173,
  "Status": "Pending",
  "Reused": false
}
json
{
  "Success": false,
  "Errors": [
    {
      "Code": 3,
      "Message": "Only a draft campaign can be estimated. This one is Sending."
    }
  ],
  "ErrorCode": 3
}
txt
0: Success
1: Missing or invalid SMSCampaignID parameter
2: Campaign not found
3: The campaign is not a draft, so it cannot be estimated
4: The list could not be read; retry
5: The list has no mobile phone number field, so it cannot receive SMS
6: The estimate could not be started; retry

Reused is true when an unfinished job for this campaign and this exact content already existed and was returned instead of a new one.

Read a Campaign Cost Estimate

GET /api/v1/smscampaign.estimate

API Usage Notes

  • Authentication required: User API Key
  • Required permissions: SMSCampaigns.Manage, including for this read: the confirmation token it returns is what authorizes a spend, so it is not a SMSCampaigns.Get capability
  • Legacy endpoint access via /api.php is also supported (command smscampaign.estimate.get)

Poll this until Status is Done or Failed. A ConfirmationToken is returned only when the estimate is Done and the campaign still matches the one that was costed.

"Still matches" is decided on the campaign's content, not only on its modification time. The estimate records a fingerprint of the audience, message, footer and gateway it measured, and that is compared with the campaign as it stands now. A modification time alone would not be enough: it has one second of resolution, so an edit landing in the same second as the measurement would leave the timestamp unchanged while the content it describes had moved on. When the two disagree the response carries Stale: true and a StaleReason, and no token, so run the estimate again.

Request Body Parameters:

ParameterTypeRequiredDescription
CommandStringYesAPI command: smscampaign.estimate.get
SessionIDStringNoSession ID obtained from login
APIKeyStringNoAPI key for authentication
SMSCampaignIDIntegerYesThe campaign the estimate belongs to
EstimateIDIntegerYesThe id returned by smscampaign.estimate
bash
curl -X GET https://example.com/api/v1/smscampaign.estimate \
  -H "Content-Type: application/json" \
  -d '{
    "Command": "smscampaign.estimate.get",
    "SessionID": "your-session-id",
    "SMSCampaignID": 4821,
    "EstimateID": 173
  }'
json
{
  "Success": true,
  "ErrorCode": 0,
  "EstimateID": 173,
  "SMSCampaignID": 4821,
  "Status": "Done",
  "CreatedAt": "2026-09-20 14:02:11",
  "CompletedAt": "2026-09-20 14:03:48",
  "Result": {
    "Audience": 412903,
    "Invalid": 1184,
    "Duplicate": 2071,
    "Suppressed": 9330,
    "TooLong": 0,
    "Sendable": 400318,
    "TotalParts": 431566,
    "Encodings": {
      "GSM7": 388201,
      "UCS2": 12117
    },
    "ProjectedCost": 4315.66,
    "CostCurrency": "USD",
    "CostPerPart": 0.01,
    "ProjectedCompletionAt": "2026-09-22 09:15:00",
    "MeasuredAt": "2026-09-20 14:03:48",
    "CampaignFingerprint": "6b1e...c04a"
  },
  "Stale": false,
  "ConfirmationToken": "1758377028.8f2c...",
  "ConfirmationTokenExpiresInSeconds": 900
}
json
{
  "Success": false,
  "Errors": [
    {
      "Code": 5,
      "Message": "Estimate not found for this campaign."
    }
  ],
  "ErrorCode": 5
}
txt
0: Success
1: Missing or invalid SMSCampaignID parameter
2: Missing or invalid EstimateID parameter
3: Campaign not found
4: The estimate could not be read; retry
5: Estimate not found for this campaign
6: The estimate finished but its result cannot be read; run it again

Response fields

FieldMeaning
StatusPending, Running, Done or Failed
ErrorPresent only when Status is Failed, explaining why
ResultPresent only when Status is Done
Staletrue when the campaign changed after it was costed. No token is issued and the estimate has to be run again
ConfirmationTokenPresent only when Status is Done and Stale is false

The funnel in Result

FieldMeaning
AudienceSubscribers matching the campaign's list and segment with a phone number present
InvalidNumbers that could not be normalized to a sendable form
DuplicateNumbers appearing more than once, counted once as sendable and the rest here
SuppressedNumbers suppressed at system, user, list or gateway scope
TooLongMessages needing more parts than the gateway will concatenate
SendableWhat will actually be sent. This is the number the cost is based on
TotalPartsMessage parts across every sendable recipient
EncodingsSendable recipients by message encoding, GSM7 and UCS2
ProjectedCostTotalParts multiplied by CostPerPart
ProjectedCompletionAtWhen the campaign would finish at the account's current send-rate limits. null when there is nothing to project from, which includes an account with no configured limit and a send-rate counter that could not be read: an unknown schedule is reported as unknown rather than as immediate
CampaignFingerprintA fingerprint of the campaign content that was measured. Used to decide staleness; see below

Test and direct sends

Send a Test Message

POST /api/v1/smscampaign.test

API Usage Notes

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

Sends one copy of a campaign's message to one number, using the campaign's gateway and links. It counts against the account's SMS send limits like any other message.

Request Body Parameters:

ParameterTypeRequiredDescription
CommandStringYesAPI command: smscampaign.test
SessionIDStringNoSession ID obtained from login
SMSCampaignIDIntegerYesThe campaign whose message to test
RecipientNumberStringYesThe destination number
SenderIDStringNoOverride the campaign's sender id
bash
curl -X POST https://example.com/api/v1/smscampaign.test \
  -H "Content-Type: application/json" \
  -d '{
    "Command": "smscampaign.test",
    "SessionID": "your-session-id",
    "SMSCampaignID": 4821,
    "RecipientNumber": "+15550100001"
  }'
json
{ "Success": true, "ErrorCode": 0 }
json
{
  "Success": false,
  "Errors": [{ "Code": 4, "Message": "This campaign has no SMS gateway. Set GatewayID with smscampaign.update first." }],
  "ErrorCode": 4
}
txt
0: Success
1: Missing or invalid SMSCampaignID parameter
2: Missing RecipientNumber parameter
3: Campaign not found
4: The campaign has no SMS gateway
5: The campaign's gateway is no longer active or assigned to this account
7: The campaign's links could not be read, so nothing was sent
8: The campaign has no message content to test
9: The test message is too long for the gateway's concatenation limit
10: SMS rate limit exceeded for an interval
11: The test message could not be queued, so nothing was sent

Send a Single Message

POST /api/v1/sms.send

API Usage Notes

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

One message to one number, with no campaign involved. ListID and SubscriberID are optional and must be given together: supplying them attributes the message to that contact, so it appears in their SMS history.

Request Body Parameters:

ParameterTypeRequiredDescription
CommandStringYesAPI command: sms.send
SessionIDStringNoSession ID obtained from login
RecipientNumberStringYesThe destination number
MessageContentStringYesThe message body
GatewayIDIntegerYesThe gateway to send through. sms.gateways.get lists the available ones
SenderIDStringNoThe sender number or alphanumeric id
ListIDIntegerNoAttribute the message to a contact. Must be sent with SubscriberID
SubscriberIDIntegerNoThe contact on that list. Must be sent with ListID
bash
curl -X POST https://example.com/api/v1/sms.send \
  -H "Content-Type: application/json" \
  -d '{
    "Command": "sms.send",
    "SessionID": "your-session-id",
    "RecipientNumber": "+15550100001",
    "MessageContent": "Your code is 4821.",
    "GatewayID": 3
  }'
json
{ "Success": true, "ErrorCode": 0 }
json
{
  "Success": false,
  "Errors": [{ "Code": 6, "Message": "ListID and SubscriberID must be sent together, or not at all." }],
  "ErrorCode": 6
}
txt
0: Success
1: Missing RecipientNumber parameter
2: Missing MessageContent parameter
3: Missing or invalid GatewayID parameter
4: Invalid GatewayID, or the gateway is not available to this account
6: ListID and SubscriberID must be sent together, or not at all
7: Invalid ListID
8: The subscriber could not be read, so nothing was sent
9: That subscriber is not on that list
10: The message is too long for the gateway's concatenation limit
11: SMS rate limit exceeded for an interval
12: The message could not be queued, so nothing was sent

Message templates

A saved message body that can be reused when composing a campaign. Templates are per account: one account can never read, change or delete another's.

Create a Template

POST /api/v1/smstemplate.create

API Usage Notes

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

Request Body Parameters:

ParameterTypeRequiredDescription
CommandStringYesAPI command: smstemplate.create
SessionIDStringNoSession ID obtained from login
TemplateNameStringYesA name, up to 255 characters
MessageContentStringNoThe body. May be empty, so a name can be saved to fill in later
bash
curl -X POST https://example.com/api/v1/smstemplate.create \
  -H "Content-Type: application/json" \
  -d '{
    "Command": "smstemplate.create",
    "SessionID": "your-session-id",
    "TemplateName": "Weekly promo",
    "MessageContent": "Hi {FirstName}, this week only."
  }'
json
{ "Success": true, "ErrorCode": 0, "TemplateID": 17 }
json
{
  "Success": false,
  "Errors": [{ "Code": 1, "Message": "Missing TemplateName parameter" }],
  "ErrorCode": 1
}
txt
0: Success
1: Missing or invalid TemplateName parameter, including a non-string value
2: TemplateName is longer than 255 characters
3: The template could not be created
4: MessageContent must be a string

Read, List, Update and Delete Templates

smstemplate.get returns one template, smstemplate.browse lists them, smstemplate.update changes a name or body, and smstemplate.delete removes one. They share their parameters and error codes, so they are documented together.

GET /api/v1/smstemplate.get · GET /api/v1/smstemplate.browsePOST /api/v1/smstemplate.update · POST /api/v1/smstemplate.delete

API Usage Notes

  • Authentication required: User API Key
  • Required permissions: SMSCampaigns.Get for get and browse, SMSCampaigns.Manage for update and delete
  • Legacy endpoint access via /api.php is also supported

Request Body Parameters:

ParameterTypeRequiredDescription
CommandStringYesOne of smstemplate.get, smstemplate.browse, smstemplate.update or smstemplate.delete
SessionIDStringNoSession ID obtained from login
TemplateIDIntegerYesFor get, update and delete. Must belong to the caller
TemplateNameStringNoFor update
MessageContentStringNoFor update. Pass at least one of the two
SearchStringNoFor browse. Matches the start of a template name
RecordsPerRequestIntegerNoFor browse. Default 25, clamped to 200
RecordsFromIntegerNoFor browse. Offset, default 0
bash
curl -X GET https://example.com/api/v1/smstemplate.browse \
  -H "Content-Type: application/json" \
  -d '{
    "Command": "smstemplate.browse",
    "SessionID": "your-session-id",
    "Search": "Weekly"
  }'
json
{
  "Success": true,
  "ErrorCode": 0,
  "Templates": [
    {
      "TemplateID": 17,
      "TemplateName": "Weekly promo",
      "MessageContent": "Hi {FirstName}, this week only.",
      "CreatedAt": "2026-09-20 14:02:11",
      "UpdatedAt": "2026-09-20 14:02:11"
    }
  ],
  "TotalTemplates": 1
}
json
{
  "Success": false,
  "Errors": [{ "Code": 3, "Message": "Template not found." }],
  "ErrorCode": 3
}
txt
0: Success
1: Missing or invalid TemplateID parameter (get, update, delete); read failure (browse)
2: The template could not be read (get, update, delete); Search must be a string (browse)
3: Template not found, including a template belonging to another account
4: TemplateName cannot be empty (update)
5: TemplateName is longer than 255 characters (update)
6: Nothing to update; pass TemplateName, MessageContent or both
7: The template could not be updated
8: MessageContent must be a string (update)
9: TemplateName must be a string (update)

Asking for a template that belongs to another account answers Template not found rather than a permission error, and deleting one reports the same. There is no response that distinguishes "exists but is not yours" from "does not exist", so template ids cannot be probed.

Every parameter that is expected to be a string is rejected when it is not one, rather than being coerced. api.php accepts nested structures, so a value can arrive as an array, and casting one to a string yields the literal Array: an endpoint that cast before validating would store a template named Array. "Empty" and "not a string" are separate codes on update, because they call for different corrections.

Supporting endpoints

Get Available SMS Gateways

GET /api/v1/sms.gateways.get

API Usage Notes

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

The gateways this account may send through, with the capabilities a composer needs: how many parts each will concatenate, and which sender ids are available.

Request Body Parameters:

ParameterTypeRequiredDescription
CommandStringYesAPI command: sms.gateways.get
SessionIDStringNoSession ID obtained from login
bash
curl -X GET https://example.com/api/v1/sms.gateways.get \
  -H "Content-Type: application/json" \
  -d '{ "Command": "sms.gateways.get", "SessionID": "your-session-id" }'
json
{
  "Success": true,
  "ErrorCode": 0,
  "Gateways": [
    {
      "GatewayID": 3,
      "GatewayName": "Primary",
      "MessageConcatenation": 5,
      "SenderNumbers": ["+15550100000", "ACME"]
    }
  ]
}
json
{
  "Success": false,
  "Errors": [{ "Code": 1, "Message": "The gateway list could not be read." }],
  "ErrorCode": 1
}
txt
0: Success
1: The gateway list could not be read

Get Merge Tags for a List

GET /api/v1/sms.mergetags.get

API Usage Notes

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

The merge tags a message for this list may use. Every tag is measured at its rendered length when the message is costed, so the parts reported by the estimate are the parts that will be sent.

Request Body Parameters:

ParameterTypeRequiredDescription
CommandStringYesAPI command: sms.mergetags.get
SessionIDStringNoSession ID obtained from login
ListIDIntegerYesThe list whose fields to return
bash
curl -X GET https://example.com/api/v1/sms.mergetags.get \
  -H "Content-Type: application/json" \
  -d '{ "Command": "sms.mergetags.get", "SessionID": "your-session-id", "ListID": 42 }'
json
{
  "Success": true,
  "ErrorCode": 0,
  "MergeTags": [
    { "Tag": "{FirstName}", "FieldName": "First name" },
    { "Tag": "{CustomField12}", "FieldName": "Mobile number" }
  ]
}
json
{
  "Success": false,
  "Errors": [{ "Code": 2, "Message": "Invalid ListID." }],
  "ErrorCode": 2
}
txt
0: Success
1: Missing ListID parameter
2: Invalid ListID
3: The list's SMS settings could not be read

Any questions? Contact us.