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.
| Status | Meaning |
|---|---|
Draft | Being edited. The only status in which a campaign can be changed |
Scheduled | Waiting for its ScheduledAt to pass |
Queueing | Its audience is being resolved and queue rows written |
Sending | Rows are being released to the gateway |
Paused | Stopped by a user, or automatically on a cost drift |
Cancelling | Winding down; workers are stopping cleanly |
Sent, Cancelled, Failed | Final |
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.createAPI Usage Notes
- Authentication required: User API Key
- Required permissions:
SMSCampaigns.Manage - Rate limit: 100 requests per 60 seconds
- Legacy endpoint access via
/api.phpis 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.
The audience is the whole list, a saved segment of it (SegmentID), or conditions from the segment rule builder (RulesJsonBundle). It is never both a segment and conditions. While the campaign is a Draft, smscampaign.update can change it, and smscampaign.audience.count counts an audience before anything is saved.
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: smscampaign.create |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| CampaignName | String | Yes | A name for the campaign |
| ListID | Integer | Yes | The audience list. Must belong to the caller and have a phone field |
| MessageContent | String | Yes | The message body. May contain merge tags from sms.mergetags.get, written as in email: {{ Subscriber:FirstName }}, or {{ Subscriber:FirstName | "there" }} with a fallback. A tag naming a field the list does not have is refused |
| GatewayID | Integer | No | The sending gateway. Must be active and assigned to the account |
| SegmentID | Integer | No | Narrow the audience to a saved segment of that list. Cannot be combined with RulesJsonBundle |
| RulesJsonBundle | String | No | Narrow the audience with conditions, as a JSON-encoded bundle holding exactly one criterion for ListID: {"operator":"or","criteria":[{"list_id":42,"operator":"and","rules":[...]}]}. rules takes the same shape as a saved segment's rules. The criterion's operator joins its rule groups, and the rules inside a group are joined the other way (see the example below). Possible values: and, or. Cannot be combined with SegmentID. Omit it to send to the whole list |
| SenderID | String | No | The sender number or alphanumeric id to send from |
| AppendOptOutFooter | Boolean | No | Append the opt-out footer. Defaults to the account setting |
| OptOutFooterText | String | No | Override the footer text for this campaign |
| Timezone | String | No | The timezone quiet hours and the schedule are evaluated in, as an IANA name such as Europe/Istanbul. Defaults to the account's own timezone. An unknown name is refused |
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
}'{
"Success": true,
"ErrorCode": 0,
"SMSCampaignID": 4821
}{
"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
}0: Success
1: Missing CampaignName parameter
2: Missing ListID parameter
3: Missing MessageContent parameter
4: Invalid ListID
5: The list has no mobile phone number field, so it cannot be an SMS audience
6: A link in MessageContent could not be used
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 (also returned when the campaign could not be created as a single transaction)
12: The audience conditions in RulesJsonBundle cannot be used; the message says why
13: SegmentID and RulesJsonBundle were both sent
14: MessageContent uses a merge tag for a field this list does not have; the message names it
15: The list's fields could not be read to check the merge tags, so nothing was created
16: MessageContent has a merge tag that cannot be read (a misspelt or email-only scope, or a space after the colon), which would be sent as typed; the message lists them
17: Timezone is not a known timezoneAn audience narrowed by conditions. Here, contacts whose phone number starts with a UK mobile prefix and who have the tag with id 7. The two rules are in separate groups because the criterion's operator joins groups, while the rules inside one group are joined the other way: under and, a group's rules are alternatives. Putting both rules in one group would select contacts matching either of them.
{
"operator": "or",
"criteria": [
{
"list_id": 42,
"operator": "and",
"rules": [
[ { "type": "fields", "field_id": "CustomField12", "operator": "begins with", "value": "447" } ],
[ { "type": "tags", "operator": "has this tag", "value": "7" } ]
]
}
]
}TIP
Conditions that could not be applied in full are refused rather than read as "everyone", so a bundle can never silently widen an audience. That covers empty rules, empty groups, and rules nested more than three levels deep (a top-level list of rules or groups, groups of rules or subgroups, subgroups of rules only), which the segment engine would otherwise ignore. To send to the whole list, omit RulesJsonBundle.
Update a Campaign
POST/api/v1/smscampaign.updateAPI Usage Notes
- Authentication required: User API Key
- Required permissions:
SMSCampaigns.Manage - Rate limit: 100 requests per 60 seconds
- Legacy endpoint access via
/api.phpis 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.
The audience can be changed too, while the campaign is a Draft. Only what you send changes, with two rules:
SegmentIDandRulesJsonBundlereplace each other. Sending one clears the other, soSegmentID=0orRulesJsonBundle=""sends to the whole list.- Changing
ListIDwithout sending either of them resets the audience to the whole new list, because a segment and a set of conditions each belong to the old list.
The audience is part of the content fingerprint, so an audience change invalidates an earlier estimate like any other edit.
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: smscampaign.update |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| SMSCampaignID | Integer | Yes | The campaign to update. Must be in Draft |
| CampaignName | String | No | A new name |
| ListID | Integer | No | Move the campaign to another list. Must belong to the caller and have a phone field |
| SegmentID | Integer | No | Narrow the audience to a saved segment of the list. 0 clears it. Cannot be combined with RulesJsonBundle |
| RulesJsonBundle | String | No | Narrow the audience with conditions, in the same format as smscampaign.create. An empty string clears them. Cannot be combined with SegmentID |
| MessageContent | String | No | A new message body |
| GatewayID | Integer | No | A different gateway |
| SenderID | String | No | A different sender id |
| AppendOptOutFooter | Boolean | No | Whether to append the opt-out footer |
| OptOutFooterText | String | No | Override the footer text |
| Timezone | String | No | The quiet-hours timezone, as an IANA name. An unknown name is refused |
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."
}'{
"Success": true,
"ErrorCode": 0
}{
"Success": false,
"Errors": [{ "Code": 7, "Message": "The campaign could not be updated, so nothing was changed." }],
"ErrorCode": 7
}0: Success
1: Missing or invalid SMSCampaignID parameter
2: Campaign not found
3: Only a Draft campaign can be edited
4: A link in MessageContent could not be used
5: MessageContent is empty
6: Nothing to update
7: The campaign could not be updated, so nothing was changed
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 updated as a single transaction, so nothing was changed
11: The campaign, or its links, could not be read, so nothing was changed
12: Invalid ListID
13: The list has no mobile phone number field, so it cannot be an SMS audience
14: Invalid SegmentID, or the segment does not belong to this list
15: The audience conditions in RulesJsonBundle cannot be used; the message says why
16: SegmentID and RulesJsonBundle were both sent
17: The message uses a merge tag for a field the campaign's list does not have, checked whenever the message or the list changes; the message names it
18: The list's fields could not be read to check the merge tags, so nothing was changed
19: The message has a merge tag that cannot be read, which would be sent as typed; the message lists them
20: Timezone is not a known timezoneCount an Audience
POST/api/v1/smscampaign.audience.countAPI Usage Notes
- Authentication required: User API Key
- Required permissions:
SMSCampaigns.Manage - Rate limit: 60 requests per 60 seconds
- Legacy endpoint access via
/api.phpis also supported
Counts an audience without saving anything, so it works before a campaign exists. It is the same count the estimate starts from: the list, narrowed by a saved segment or by conditions, limited to subscribed contacts.
It is not the number that will be sent. Invalid numbers, suppressed numbers and duplicates are removed afterwards, by smscampaign.estimate, which remains the only number a send is confirmed against.
The count is capped one above the per-campaign maximum. An audience above it is reported as the maximum with ExceedsMaximum set, and a campaign with that audience cannot be sent.
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: smscampaign.audience.count |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| ListID | Integer | Yes | The list to count. Must belong to the caller and have a phone field |
| SegmentID | Integer | No | Count a saved segment of the list. Cannot be combined with RulesJsonBundle |
| RulesJsonBundle | String | No | Count the contacts matching these conditions, in the same format as smscampaign.create. Cannot be combined with SegmentID |
curl -X POST https://example.com/api/v1/smscampaign.audience.count \
-H "Content-Type: application/json" \
-d '{
"Command": "smscampaign.audience.count",
"SessionID": "your-session-id",
"ListID": 42,
"SegmentID": 7
}'{
"Success": true,
"ErrorCode": 0,
"TotalAudience": 18240,
"ExceedsMaximum": false,
"MaxRecipients": 1000000
}{
"Success": false,
"Errors": [
{
"Code": 7,
"Message": "This audience uses a segment that picks contacts at random, which an SMS campaign cannot use: every page of the send would pick a different set of contacts."
}
],
"ErrorCode": 7
}0: Success
1: Missing ListID parameter
2: Invalid ListID
3: The list has no mobile phone number field, so it cannot be an SMS audience
4: Invalid SegmentID, or the segment does not belong to this list
5: The audience conditions in RulesJsonBundle cannot be used; the message says why
6: SegmentID and RulesJsonBundle were both sent
7: The audience uses a segment that picks contacts at random, which an SMS campaign cannot use
8: The audience uses a segment saved in an older rules format; open it in the segment editor and save it again
9: The audience could not be counted right nowGet a Campaign
GET/api/v1/smscampaign.getAPI Usage Notes
- Authentication required: User API Key
- Required permissions:
SMSCampaigns.Get - Legacy endpoint access via
/api.phpis also supported
QuietHours says whether the campaign's quiet hours are holding it right now. While Active is true a Sending campaign sends nothing, and ResumesAt (UTC) is when it starts again. Start and End are in the campaign's Timezone, and both are null when the campaign has no quiet hours.
The campaign's audience is RelListID, narrowed by at most one of RelSegmentID (a saved segment) and RulesJsonBundle (conditions, as the JSON string it was saved as). Both are null when the campaign goes to the whole list.
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: smscampaign.get |
| SessionID | String | No | Session ID obtained from login |
| SMSCampaignID | Integer | Yes | The campaign to read |
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
}'{
"Success": true,
"ErrorCode": 0,
"SMSCampaign": {
"SMSCampaignID": 4821,
"CampaignName": "October promotion",
"Status": "Sending",
"StatusReason": "",
"RelListID": 42,
"RelSegmentID": null,
"RulesJsonBundle": "{\"operator\":\"or\",\"criteria\":[{\"list_id\":42,\"operator\":\"and\",\"rules\":[[{\"type\":\"fields\",\"field_id\":\"CustomField12\",\"operator\":\"begins with\",\"value\":\"447\"}]]}]}",
"RelGatewayID": 3,
"MessageContent": "Hi {{ Subscriber:FirstName | \"there\" }}, 20% off this week only.",
"Timezone": "America/New_York",
"TotalAudience": 400318,
"ConfirmedCost": "4315.66000",
"CostCurrency": "USD"
},
"Links": [],
"IsEditable": false,
"QuietHours": {
"Active": true,
"Start": "21:00",
"End": "09:00",
"Timezone": "America/New_York",
"ResumesAt": "2026-09-23 13:00:00"
}
}{
"Success": false,
"Errors": [{ "Code": 2, "Message": "Campaign not found." }],
"ErrorCode": 2
}0: Success
1: Missing SMSCampaignID parameter
2: Campaign not found
4: The campaign's links could not be readList Campaigns
GET/api/v1/smscampaign.browseAPI 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.browse |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| Status | String | No | Filter by status. Possible values: Draft, Scheduled, Queueing, Sending, Paused, Cancelling, Cancelled, Sent, Failed |
| ListID | Integer | No | Only campaigns targeting this list |
| RecordsPerRequest | Integer | No | Page size, default 25, clamped to 200 |
| RecordsFrom | Integer | No | Offset, default 0 |
| CreatedAfter | String | No | Only campaigns created 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 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 |
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
}'{
"Success": true,
"ErrorCode": 0,
"Campaigns": [
{ "SMSCampaignID": 4821, "CampaignName": "October promotion", "Status": "Sending" }
],
"TotalCampaigns": 1
}{
"Success": false,
"Errors": [{ "Code": 1, "Message": "Invalid Status value." }],
"ErrorCode": 1
}0: Success
1: Invalid Status value
2: The campaign list could not be read
3: Invalid CreatedAfter or CreatedBefore valueResults 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.getAPI Usage Notes
- Authentication required: User API Key
- Required permissions:
SMSCampaigns.Get - Legacy endpoint access via
/api.phpis 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:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: smscampaign.summary.get |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| ListID | Integer | No | Only campaigns targeting this list |
| CreatedAfter | String | No | Only campaigns created at or after this point, parsed as in smscampaign.browse |
| CreatedBefore | String | No | Only campaigns created at or before this point, parsed as in smscampaign.browse |
Response Fields:
| Field | Type | Description |
|---|---|---|
| TotalCampaigns | Integer | Campaigns matching the filters |
| StatusCounts | Object | One key per status, including the statuses at zero, so a caller rendering a fixed set of buckets never has to guess |
| Totals | Object | TotalAudience, TotalSent, TotalDelivered, TotalUndelivered, TotalOptOuts, TotalClicks and TotalParts, summed across the selection |
| Costs | Array | One entry per currency, each with Currency, Campaigns, ActualCost and ConfirmedCost |
| CreatedAfter | String | The window's start as it was parsed, or null when none was sent |
| CreatedBefore | String | The 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.
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"
}'{
"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
}{
"Success": false,
"Errors": [{ "Code": 3, "Message": "Invalid createdafter value. Expected YYYY-MM-DD or YYYY-MM-DD HH:MM:SS." }],
"ErrorCode": 3
}0: Success
2: The campaign summary could not be read
3: Invalid CreatedAfter or CreatedBefore valueDelete a Campaign
POST/api/v1/smscampaign.deleteAPI Usage Notes
- Authentication required: User API Key
- Required permissions:
SMSCampaigns.Manage - Legacy endpoint access via
/api.phpis 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:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: smscampaign.delete |
| SessionID | String | No | Session ID obtained from login |
| SMSCampaignID | Integer | Yes | The campaign to delete |
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
}'{ "Success": true, "ErrorCode": 0 }{
"Success": false,
"Errors": [{ "Code": 3, "Message": "A campaign in progress cannot be deleted. Cancel it first." }],
"ErrorCode": 3
}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 deletedDuplicate a Campaign
POST/api/v1/smscampaign.copyAPI Usage Notes
- Authentication required: User API Key
- Required permissions:
SMSCampaigns.Manage - Rate limit: 100 requests per 60 seconds
- Legacy endpoint access via
/api.phpis also supported
Copies any campaign you own into a new Draft named "Copy of" followed by the original name, whatever the original's status. Resending a finished campaign is the usual reason.
The copy keeps the audience (list, saved segment or conditions), the message and its tracked links, the sender, the gateway, the opt-out footer, link settings, timezone, quiet hours and send rate. It starts without the original's schedule and deadline, estimate, confirmed cost and statistics, so estimate it before sending.
The audience is checked again the way smscampaign.create checks it, so a campaign whose list was deleted or no longer has a mobile phone field cannot be duplicated. A gateway that is no longer available to your account does not stop the copy: it is made with no gateway, GatewayCleared is true, and you choose one with smscampaign.update before sending.
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: smscampaign.copy |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| SMSCampaignID | Integer | Yes | The campaign to duplicate |
curl -X POST https://example.com/api/v1/smscampaign.copy \
-H "Content-Type: application/json" \
-d '{
"Command": "smscampaign.copy",
"SessionID": "your-session-id",
"SMSCampaignID": 4821
}'{
"Success": true,
"ErrorCode": 0,
"SMSCampaignID": 4907,
"SourceSMSCampaignID": 4821,
"GatewayCleared": false,
"Links": 1
}{
"Success": false,
"Errors": [{ "Code": 3, "Message": "The campaign cannot be duplicated because its audience is no longer valid: Invalid ListID." }],
"ErrorCode": 3
}0: Success
1: Missing or invalid SMSCampaignID parameter
2: Campaign not found
3: The campaign's audience is no longer valid (its list, segment or conditions)
4: The campaign's links could not be read, so nothing was duplicated
5: The campaign could not be duplicated, so nothing was createdSending
Send a Campaign
POST/api/v1/smscampaign.sendAPI Usage Notes
- Authentication required: User API Key
- Required permissions:
SMSCampaigns.Manage - Legacy endpoint access via
/api.phpis 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:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: smscampaign.send |
| SessionID | String | No | Session ID obtained from login |
| SMSCampaignID | Integer | Yes | The campaign to send. Must be in Draft |
| EstimateID | Integer | Yes | From smscampaign.estimate |
| ConfirmationToken | String | Yes | From smscampaign.estimate.get |
| SendDeadlineAt | String | No | YYYY-MM-DD HH:MM:SS. Stop sending after this moment even if recipients remain |
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..."
}'{ "Success": true, "ErrorCode": 0, "Status": "Queueing" }{
"Success": false,
"Errors": [{ "Code": 34, "Message": "The campaign changed after it was costed. Run the estimate again." }],
"ErrorCode": 34
}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 againSchedule a Campaign
POST/api/v1/smscampaign.scheduleAPI Usage Notes
- Authentication required: User API Key
- Required permissions:
SMSCampaigns.Manage - Legacy endpoint access via
/api.phpis 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:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: smscampaign.schedule |
| SessionID | String | No | Session ID obtained from login |
| SMSCampaignID | Integer | Yes | The campaign to schedule. Must be in Draft |
| ScheduledAt | String | Yes | YYYY-MM-DD HH:MM:SS, in the campaign's timezone. Must be in the future |
| EstimateID | Integer | Yes | From smscampaign.estimate |
| ConfirmationToken | String | Yes | From smscampaign.estimate.get |
| SendDeadlineAt | String | No | Stop sending after this moment. Must be after ScheduledAt |
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..."
}'{ "Success": true, "ErrorCode": 0, "Status": "Scheduled" }{
"Success": false,
"Errors": [{ "Code": 11, "Message": "Missing ScheduledAt parameter." }],
"ErrorCode": 11
}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.sendLifecycle
Pause a Campaign
POST/api/v1/smscampaign.pauseAPI Usage Notes
- Authentication required: User API Key
- Required permissions:
SMSCampaigns.Manage - Legacy endpoint access via
/api.phpis 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:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: smscampaign.pause |
| SessionID | String | No | Session ID obtained from login |
| SMSCampaignID | Integer | Yes | The campaign to pause |
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 }'{ "Success": true, "ErrorCode": 0, "Status": "Paused" }{
"Success": false,
"Errors": [{ "Code": 3, "Message": "Only a campaign that is queueing or sending can be paused. This one is Draft." }],
"ErrorCode": 3
}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 againResume a Campaign
POST/api/v1/smscampaign.resumeAPI Usage Notes
- Authentication required: User API Key
- Required permissions:
SMSCampaigns.Manage - Legacy endpoint access via
/api.phpis 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:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: smscampaign.resume |
| SessionID | String | No | Session ID obtained from login |
| SMSCampaignID | Integer | Yes | The campaign to resume. Must be Paused |
| AcknowledgeCost | Integer | No | Pass 1 to resume a campaign paused on cost drift |
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
}'{ "Success": true, "ErrorCode": 0, "Status": "Sending" }{
"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
}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 againCancel a Campaign
POST/api/v1/smscampaign.cancelAPI Usage Notes
- Authentication required: User API Key
- Required permissions:
SMSCampaigns.Manage - Legacy endpoint access via
/api.phpis 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:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: smscampaign.cancel |
| SessionID | String | No | Session ID obtained from login |
| SMSCampaignID | Integer | Yes | The campaign to cancel |
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 }'{ "Success": true, "ErrorCode": 0, "Status": "Cancelling" }{
"Success": false,
"Errors": [{ "Code": 3, "Message": "This campaign has already finished. It is Sent." }],
"ErrorCode": 3
}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 againCost 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.estimateAPI Usage Notes
- Authentication required: User API Key
- Required permissions:
SMSCampaigns.Manage - Legacy endpoint access via
/api.phpis 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:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: smscampaign.estimate |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| SMSCampaignID | Integer | Yes | The campaign to estimate. Must belong to the authenticated user and be in Draft |
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
}'{
"Success": true,
"ErrorCode": 0,
"EstimateID": 173,
"Status": "Pending",
"Reused": false
}{
"Success": false,
"Errors": [
{
"Code": 3,
"Message": "Only a draft campaign can be estimated. This one is Sending."
}
],
"ErrorCode": 3
}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; retryReused 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.estimateAPI 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 aSMSCampaigns.Getcapability - Legacy endpoint access via
/api.phpis also supported (commandsmscampaign.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:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: smscampaign.estimate.get |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| SMSCampaignID | Integer | Yes | The campaign the estimate belongs to |
| EstimateID | Integer | Yes | The id returned by smscampaign.estimate |
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
}'{
"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
}{
"Success": false,
"Errors": [
{
"Code": 5,
"Message": "Estimate not found for this campaign."
}
],
"ErrorCode": 5
}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 againResponse fields
| Field | Meaning |
|---|---|
Status | Pending, Running, Done or Failed |
Error | Present only when Status is Failed, explaining why |
Result | Present only when Status is Done |
Stale | true when the campaign changed after it was costed. No token is issued and the estimate has to be run again |
ConfirmationToken | Present only when Status is Done and Stale is false |
The funnel in Result
| Field | Meaning |
|---|---|
Audience | Subscribers matching the campaign's list and segment with a phone number present |
Invalid | Numbers that could not be normalized to a sendable form |
Duplicate | Numbers appearing more than once, counted once as sendable and the rest here |
Suppressed | Numbers suppressed at system, user, list or gateway scope |
TooLong | Messages needing more parts than the gateway will concatenate |
Sendable | What will actually be sent. This is the number the cost is based on |
TotalParts | Message parts across every sendable recipient |
Encodings | Sendable recipients by message encoding, GSM7 and UCS2 |
ProjectedCost | TotalParts multiplied by CostPerPart |
ProjectedCompletionAt | When 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 |
CampaignFingerprint | A 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.testAPI Usage Notes
- Authentication required: User API Key
- Required permissions:
SMSCampaigns.Manage - Rate limit: 60 requests per 60 seconds
- Legacy endpoint access via
/api.phpis 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:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: smscampaign.test |
| SessionID | String | No | Session ID obtained from login |
| SMSCampaignID | Integer | Yes | The campaign whose message to test |
| RecipientNumber | String | Yes | The destination number |
| SenderID | String | No | Override the campaign's sender id |
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"
}'{ "Success": true, "ErrorCode": 0 }{
"Success": false,
"Errors": [{ "Code": 4, "Message": "This campaign has no SMS gateway. Set GatewayID with smscampaign.update first." }],
"ErrorCode": 4
}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
12: The message's merge tags could not be rendered, so nothing was sent
13: The message has a merge tag that cannot be read, which would be sent as typedSend a Single Message
POST/api/v1/sms.sendAPI Usage Notes
- Authentication required: User API Key
- Required permissions:
SMSCampaigns.Manage - Rate limit: 300 requests per 60 seconds
- Legacy endpoint access via
/api.phpis 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:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: sms.send |
| SessionID | String | No | Session ID obtained from login |
| RecipientNumber | String | Yes | The destination number |
| MessageContent | String | Yes | The message body |
| GatewayID | Integer | Yes | The gateway to send through. sms.gateways.get lists the available ones |
| SenderID | String | No | The sender number or alphanumeric id |
| ListID | Integer | No | Attribute the message to a contact. Must be sent with SubscriberID |
| SubscriberID | Integer | No | The contact on that list. Must be sent with ListID |
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
}'{ "Success": true, "ErrorCode": 0 }{
"Success": false,
"Errors": [{ "Code": 6, "Message": "ListID and SubscriberID must be sent together, or not at all." }],
"ErrorCode": 6
}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 sentMessage 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.createAPI Usage Notes
- Authentication required: User API Key
- Required permissions:
SMSCampaigns.Manage - Legacy endpoint access via
/api.phpis also supported
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: smstemplate.create |
| SessionID | String | No | Session ID obtained from login |
| TemplateName | String | Yes | A name, up to 255 characters |
| MessageContent | String | No | The body. May be empty, so a name can be saved to fill in later |
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."
}'{ "Success": true, "ErrorCode": 0, "TemplateID": 17 }{
"Success": false,
"Errors": [{ "Code": 1, "Message": "Missing TemplateName parameter" }],
"ErrorCode": 1
}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 stringRead, 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.
/api/v1/smstemplate.get · GET /api/v1/smstemplate.browsePOST /api/v1/smstemplate.update · POST /api/v1/smstemplate.deleteAPI Usage Notes
- Authentication required: User API Key
- Required permissions:
SMSCampaigns.Getforgetandbrowse,SMSCampaigns.Manageforupdateanddelete - Legacy endpoint access via
/api.phpis also supported
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | One of smstemplate.get, smstemplate.browse, smstemplate.update or smstemplate.delete |
| SessionID | String | No | Session ID obtained from login |
| TemplateID | Integer | Yes | For get, update and delete. Must belong to the caller |
| TemplateName | String | No | For update |
| MessageContent | String | No | For update. Pass at least one of the two |
| Search | String | No | For browse. Matches the start of a template name |
| RecordsPerRequest | Integer | No | For browse. Default 25, clamped to 200 |
| RecordsFrom | Integer | No | For browse. Offset, default 0 |
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"
}'{
"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
}{
"Success": false,
"Errors": [{ "Code": 3, "Message": "Template not found." }],
"ErrorCode": 3
}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.getAPI Usage Notes
- Authentication required: User API Key
- Required permissions:
SMSCampaigns.Get - Legacy endpoint access via
/api.phpis 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:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: sms.gateways.get |
| SessionID | String | No | Session ID obtained from login |
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" }'{
"Success": true,
"ErrorCode": 0,
"Gateways": [
{
"GatewayID": 3,
"GatewayName": "Primary",
"MessageConcatenation": 5,
"SenderNumbers": ["+15550100000", "ACME"]
}
]
}{
"Success": false,
"Errors": [{ "Code": 1, "Message": "The gateway list could not be read." }],
"ErrorCode": 1
}0: Success
1: The gateway list could not be readGet Merge Tags for a List
GET/api/v1/sms.mergetags.getAPI Usage Notes
- Authentication required: User API Key
- Required permissions:
SMSCampaigns.Get - Legacy endpoint access via
/api.phpis also supported
The merge tags a message for this list may use: its custom fields by merge tag alias (or CustomField<ID> when a field has none), its global custom fields, then the standard subscriber fields. 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.
SMS merge tags use the email syntax and are rendered as plain text, never HTML-encoded:
{{ Subscriber:FirstName }}is the recipient's value, or nothing when it is empty.{{ Subscriber:FirstName | "there" }}falls back to the quoted text when the value is empty.- Email helpers work, for example
{{ uppercase Subscriber:FirstName }}or{{ truncate-10 Subscriber:City }}, and so do{{ List:... }}and{{ User:... }}. {{ Subscriber:EmailAddress }}is empty for a contact without an email address and for an SMS-only contact, whose placeholder address is never sent.
In a test send there is no recipient, so every subscriber tag renders its fallback.
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: sms.mergetags.get |
| SessionID | String | No | Session ID obtained from login |
| ListID | Integer | Yes | The list whose fields to return |
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 }'{
"Success": true,
"ErrorCode": 0,
"MergeTags": [
{ "Tag": "{{ Subscriber:FirstName }}", "Field": "FirstName", "FieldName": "First name", "Group": "Custom", "CustomFieldID": 7, "IsPhoneField": false },
{ "Tag": "{{ Subscriber:CustomField12 }}", "Field": "CustomField12", "FieldName": "Mobile number", "Group": "Custom", "CustomFieldID": 12, "IsPhoneField": true },
{ "Tag": "{{ Subscriber:Tier }}", "Field": "Tier", "FieldName": "Loyalty tier", "Group": "Global", "CustomFieldID": 30, "IsPhoneField": false },
{ "Tag": "{{ Subscriber:EmailAddress }}", "Field": "EmailAddress", "FieldName": "Email address", "Group": "Standard", "CustomFieldID": null, "IsPhoneField": false }
],
"PhoneFieldID": 12,
"SupportsDefaultValueSyntax": true,
"DefaultValueExample": "{{ Subscriber:FirstName | \"there\" }}"
}{
"Success": false,
"Errors": [{ "Code": 2, "Message": "Invalid ListID." }],
"ErrorCode": 2
}0: Success
1: Missing ListID parameter
2: Invalid ListID
3: The list's SMS settings could not be read
4: The list's fields could not be read
