curl -X POST https://api.tracklysms.com/api/v2/send/bulk \
-H "X-Api-Key: trk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{
"to": "+14155551234",
"list_number": "+18005551000",
"body": "Your promo code is SAVE20. Ref: {{messageId}}"
},
{
"to": "+14155559876",
"list_number": "+18005551000",
"body": "Your promo code is SAVE20. Ref: {{messageId}}"
}
]
}'
import requests
response = requests.post(
"https://api.tracklysms.com/api/v2/send/bulk",
headers={
"X-Api-Key": "trk_your_api_key_here",
"Content-Type": "application/json",
},
json={
"messages": [
{
"to": "+14155551234",
"list_number": "+18005551000",
"body": "Your promo code is SAVE20. Ref: {{messageId}}",
},
{
"to": "+14155559876",
"list_number": "+18005551000",
"body": "Your promo code is SAVE20. Ref: {{messageId}}",
},
]
},
)
data = response.json()
print(f"Queued: {data['queued_count']}, Errors: {data['error_count']}")
const response = await fetch("https://api.tracklysms.com/api/v2/send/bulk", {
method: "POST",
headers: {
"X-Api-Key": "trk_your_api_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
messages: [
{
to: "+14155551234",
list_number: "+18005551000",
body: "Your promo code is SAVE20. Ref: {{messageId}}",
},
{
to: "+14155559876",
list_number: "+18005551000",
body: "Your promo code is SAVE20. Ref: {{messageId}}",
},
],
}),
});
const data = await response.json();
console.log(`Queued: ${data.queued_count}, Errors: ${data.error_count}`);
{
"queued_count": 2,
"error_count": 0,
"errors": []
}
{
"queued_count": 1,
"error_count": 1,
"errors": [
{
"index": 1,
"to": "not-a-number",
"code": "invalid_phone",
"error": "Invalid recipient phone number format"
}
]
}
{
"error": "messages array is required",
"code": "missing_messages"
}
{
"error": "Maximum 1000 messages per request",
"code": "too_many_messages"
}
Messages (v2)
Send Bulk Messages
Send multiple SMS messages in a single bulk request.
POST
/
v2
/
send
/
bulk
curl -X POST https://api.tracklysms.com/api/v2/send/bulk \
-H "X-Api-Key: trk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{
"to": "+14155551234",
"list_number": "+18005551000",
"body": "Your promo code is SAVE20. Ref: {{messageId}}"
},
{
"to": "+14155559876",
"list_number": "+18005551000",
"body": "Your promo code is SAVE20. Ref: {{messageId}}"
}
]
}'
import requests
response = requests.post(
"https://api.tracklysms.com/api/v2/send/bulk",
headers={
"X-Api-Key": "trk_your_api_key_here",
"Content-Type": "application/json",
},
json={
"messages": [
{
"to": "+14155551234",
"list_number": "+18005551000",
"body": "Your promo code is SAVE20. Ref: {{messageId}}",
},
{
"to": "+14155559876",
"list_number": "+18005551000",
"body": "Your promo code is SAVE20. Ref: {{messageId}}",
},
]
},
)
data = response.json()
print(f"Queued: {data['queued_count']}, Errors: {data['error_count']}")
const response = await fetch("https://api.tracklysms.com/api/v2/send/bulk", {
method: "POST",
headers: {
"X-Api-Key": "trk_your_api_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
messages: [
{
to: "+14155551234",
list_number: "+18005551000",
body: "Your promo code is SAVE20. Ref: {{messageId}}",
},
{
to: "+14155559876",
list_number: "+18005551000",
body: "Your promo code is SAVE20. Ref: {{messageId}}",
},
],
}),
});
const data = await response.json();
console.log(`Queued: ${data.queued_count}, Errors: ${data.error_count}`);
{
"queued_count": 2,
"error_count": 0,
"errors": []
}
{
"queued_count": 1,
"error_count": 1,
"errors": [
{
"index": 1,
"to": "not-a-number",
"code": "invalid_phone",
"error": "Invalid recipient phone number format"
}
]
}
{
"error": "messages array is required",
"code": "missing_messages"
}
{
"error": "Maximum 1000 messages per request",
"code": "too_many_messages"
}
Keys requiring send approval receive
Individual messages that fail are reported per-message (below) and the request still returns
403 confirmation_required for direct sends. Use pending sends for approved single or bulk SMS; raw sends have no approval workflow.
Send up to 1,000 SMS messages in a single API call. Each message in the batch is validated independently — successfully validated messages are queued even if others in the batch fail validation.
Body Parameters
array
required
Array of message objects (maximum 1,000 per request). Each object supports the following fields:
Show Message object fields
Show Message object fields
string
required
Recipient phone number in E.164 format (e.g.
+14155551234).string
required
Sending list phone number in E.164 format. Must belong to your account.
string
required
Message body text. Use the
{{messageId}} placeholder to insert the unique message ID into the body at send time.boolean
default:"false"
Enables link processing for this message. Send the JSON value
true or false. A non-empty linkOverrides object requires literal true; other types return invalid_wrap_links. Raw URLs typed into body are still not auto-shortened — only {{linkN}} placeholders resolved through linkOverrides are. For a click-tracked link without placeholders, create it via the Shorten Link endpoint and put the returned short URL in your body.object
Per-placeholder link targets, keyed
link1 … link99 (max 10 entries). Each entry sets exactly one of offerId (active account-owned SMS offer), partnershipOfferId (an active, account-owned offer with offer management enabled), or customUrl (absolute HTTPS URL of at most 2,048 characters on a public hostname, not an IP literal; its authority must not contain macros). Requires wrap_links: true; productId is unsupported. Body placeholders without a matching override remain unchanged. Only referenced placeholders can create affiliate or offer-access state. A message-specific failure is reported in errors; missing Partnerships entitlement rejects the whole request with 403 partnerships_not_enabled.object
Optional per-contact data for macro substitution. Supports
first_name (string), last_name (string), custom_fields (object), and timezone (string).object
Optional key-value metadata dictionary. Currently accepted but not stored or returned — the endpoint reads this field and then discards it, so it cannot yet be used for correlation.
Response Fields
integer
Number of messages successfully queued for delivery.
integer
Number of messages that failed validation or queueing.
array
Array of error objects for messages that failed validation or queueing. Each object contains:
Show Error object fields
Show Error object fields
integer
Zero-based index of the failed message in the original
messages array.string
The recipient phone number from the failed message, if provided.
string
Machine-readable error code identifying the failure.
string
Human-readable error description.
string
Present only when
code is kafka_producer_failure. Constant value KAFKA_PRODUCER_FAILURE, indicating the message passed validation but publication was not confirmed.string
Present only when
code is kafka_producer_failure. The message ID that was assigned before the publish failure.Examples
curl -X POST https://api.tracklysms.com/api/v2/send/bulk \
-H "X-Api-Key: trk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{
"to": "+14155551234",
"list_number": "+18005551000",
"body": "Your promo code is SAVE20. Ref: {{messageId}}"
},
{
"to": "+14155559876",
"list_number": "+18005551000",
"body": "Your promo code is SAVE20. Ref: {{messageId}}"
}
]
}'
import requests
response = requests.post(
"https://api.tracklysms.com/api/v2/send/bulk",
headers={
"X-Api-Key": "trk_your_api_key_here",
"Content-Type": "application/json",
},
json={
"messages": [
{
"to": "+14155551234",
"list_number": "+18005551000",
"body": "Your promo code is SAVE20. Ref: {{messageId}}",
},
{
"to": "+14155559876",
"list_number": "+18005551000",
"body": "Your promo code is SAVE20. Ref: {{messageId}}",
},
]
},
)
data = response.json()
print(f"Queued: {data['queued_count']}, Errors: {data['error_count']}")
const response = await fetch("https://api.tracklysms.com/api/v2/send/bulk", {
method: "POST",
headers: {
"X-Api-Key": "trk_your_api_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
messages: [
{
to: "+14155551234",
list_number: "+18005551000",
body: "Your promo code is SAVE20. Ref: {{messageId}}",
},
{
to: "+14155559876",
list_number: "+18005551000",
body: "Your promo code is SAVE20. Ref: {{messageId}}",
},
],
}),
});
const data = await response.json();
console.log(`Queued: ${data.queued_count}, Errors: ${data.error_count}`);
{
"queued_count": 2,
"error_count": 0,
"errors": []
}
{
"queued_count": 1,
"error_count": 1,
"errors": [
{
"index": 1,
"to": "not-a-number",
"code": "invalid_phone",
"error": "Invalid recipient phone number format"
}
]
}
{
"error": "messages array is required",
"code": "missing_messages"
}
{
"error": "Maximum 1000 messages per request",
"code": "too_many_messages"
}
Error Codes
Request-Level Errors
| HTTP Status | Error Code | Description |
|---|---|---|
| 400 | missing_messages | The messages array is required but was not provided. |
| 403 | partnerships_not_enabled | At least one message uses partnershipOfferId, but the account does not have the Partnerships product. The entire request is rejected. |
| 413 | too_many_messages | The messages array exceeds the 1,000 message limit. |
| 429 | daily_cap_exceeded | The API key’s daily_send_cap cannot fit every eligible message in this batch for today (UTC). The whole batch is rejected and nothing is queued; send fewer messages or wait for the next UTC day. |
| 503 | send_cap_unavailable | The daily quota check or reservation could not be confirmed; nothing was queued. An uncertain reservation may still count toward today’s cap. Retry with backoff. |
201. The request returns 502 only when every message fails to queue for delivery. Authenticated requests can also fail with 401 invalid_credentials, 403 account_suspended, or 429 rate_limited — see Error Codes.
Per-Message Errors
Returned inside each failed entry of the response’serrors[] array (as code), with the request status 201.
| Code | Description |
|---|---|
missing_to | The to field is required but was not provided. |
missing_list_number | The list_number field is required but was not provided. |
missing_body | The body field is required but was not provided. |
invalid_phone | The to field is not a valid E.164 phone number. |
invalid_list_number | The list_number field is not a valid E.164 phone number. |
invalid_contact_data | contact_data or one of its supported macro fields has the wrong JSON type. |
invalid_wrap_links | Non-empty linkOverrides used a wrap_links value that was not a JSON boolean. |
list_not_found | The sending list was not found or does not belong to your account. |
list_not_allowed_for_key | The API key’s send_policy.list_numbers does not include this message’s list_number. |
webhook_not_configured | Webhook verification required before sending (BYOC lists). |
pending_confirmation | The recipient has not confirmed their double opt-in yet, so they cannot be messaged. |
doi_expired | The recipient never confirmed their double opt-in and the confirmation window has expired. |
warmup_limit | The recipient is not yet eligible to be messaged under the list’s warm-up schedule. |
contact_suppressed | The recipient is opted out or blocked and cannot receive the message. |
link_overrides_require_wrap_links | A non-empty linkOverrides object was supplied without the literal wrap_links: true. |
link_override_invalid | linkOverrides has the wrong type, a key or entry is malformed, more than 10 entries were supplied, an entry does not set exactly one supported arm, or a URL exceeds 2,048 characters before or after macro expansion. |
link_override_unsafe_url | A customUrl is malformed, is not HTTPS, names an IP address or restricted hostname, contains userinfo, or contains a macro in its authority. |
offer_not_found | A referenced offerId is missing, inactive, or belongs to another account. |
partnership_offer_not_found | A referenced partnershipOfferId is missing, inactive, does not have offer management enabled, or belongs to another account. |
offer_access_not_active | Your access to the referenced partnershipOfferId is paused or has not been approved. |
no_domain_configured | A referenced override needs a short URL, but neither that message’s sending list nor the account has a link-shortener domain configured. |
kafka_producer_failure | Publication was not confirmed; delivery may still occur and daily quota remains charged. Reconcile the message ID when available before considering another send. |
Next Steps
Campaign Execution
How messages flow from queue to delivery
Create Contact
Add contacts before sending