curl -X POST https://api.tracklysms.com/api/v2/send-raw/bulk \
-H "X-Api-Key: trk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{
"to": "+14155551234",
"list_number": "+18005551000",
"body": "Verification code: 482901. Ref: {{messageId}}",
"skip_duplicate_check": true,
"skip_rate_limit": true
},
{
"to": "+14155559876",
"list_number": "+18005551000",
"body": "Verification code: 739204. Ref: {{messageId}}",
"skip_duplicate_check": true,
"skip_rate_limit": true,
"skip_contact_validation": true
}
]
}'
import requests
response = requests.post(
"https://api.tracklysms.com/api/v2/send-raw/bulk",
headers={
"X-Api-Key": "trk_your_api_key_here",
"Content-Type": "application/json",
},
json={
"messages": [
{
"to": "+14155551234",
"list_number": "+18005551000",
"body": "Verification code: 482901. Ref: {{messageId}}",
"skip_duplicate_check": True,
"skip_rate_limit": True,
},
{
"to": "+14155559876",
"list_number": "+18005551000",
"body": "Verification code: 739204. Ref: {{messageId}}",
"skip_duplicate_check": True,
"skip_rate_limit": True,
"skip_contact_validation": True,
},
]
},
)
data = response.json()
print(f"Queued: {data['queued_count']}, Errors: {data['error_count']}")
const response = await fetch("https://api.tracklysms.com/api/v2/send-raw/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: "Verification code: 482901. Ref: {{messageId}}",
skip_duplicate_check: true,
skip_rate_limit: true,
},
{
to: "+14155559876",
list_number: "+18005551000",
body: "Verification code: 739204. Ref: {{messageId}}",
skip_duplicate_check: true,
skip_rate_limit: true,
skip_contact_validation: true,
},
],
}),
});
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": "+14155559876",
"code": "list_not_found",
"error": "Sending list not found for this list_number"
}
]
}
{
"error": "messages array is required",
"code": "missing_messages"
}
{
"error": "Maximum 1000 messages per request",
"code": "too_many_messages"
}
Messages (v2)
Send Raw Bulk Messages
Send multiple raw SMS messages in bulk with optional skip flags for advanced control.
POST
/
v2
/
send-raw
/
bulk
curl -X POST https://api.tracklysms.com/api/v2/send-raw/bulk \
-H "X-Api-Key: trk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{
"to": "+14155551234",
"list_number": "+18005551000",
"body": "Verification code: 482901. Ref: {{messageId}}",
"skip_duplicate_check": true,
"skip_rate_limit": true
},
{
"to": "+14155559876",
"list_number": "+18005551000",
"body": "Verification code: 739204. Ref: {{messageId}}",
"skip_duplicate_check": true,
"skip_rate_limit": true,
"skip_contact_validation": true
}
]
}'
import requests
response = requests.post(
"https://api.tracklysms.com/api/v2/send-raw/bulk",
headers={
"X-Api-Key": "trk_your_api_key_here",
"Content-Type": "application/json",
},
json={
"messages": [
{
"to": "+14155551234",
"list_number": "+18005551000",
"body": "Verification code: 482901. Ref: {{messageId}}",
"skip_duplicate_check": True,
"skip_rate_limit": True,
},
{
"to": "+14155559876",
"list_number": "+18005551000",
"body": "Verification code: 739204. Ref: {{messageId}}",
"skip_duplicate_check": True,
"skip_rate_limit": True,
"skip_contact_validation": True,
},
]
},
)
data = response.json()
print(f"Queued: {data['queued_count']}, Errors: {data['error_count']}")
const response = await fetch("https://api.tracklysms.com/api/v2/send-raw/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: "Verification code: 482901. Ref: {{messageId}}",
skip_duplicate_check: true,
skip_rate_limit: true,
},
{
to: "+14155559876",
list_number: "+18005551000",
body: "Verification code: 739204. Ref: {{messageId}}",
skip_duplicate_check: true,
skip_rate_limit: true,
skip_contact_validation: true,
},
],
}),
});
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": "+14155559876",
"code": "list_not_found",
"error": "Sending list not found for this list_number"
}
]
}
{
"error": "messages array is required",
"code": "missing_messages"
}
{
"error": "Maximum 1000 messages per request",
"code": "too_many_messages"
}
Keys requiring send approval receive
Partial successes return
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 raw 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. Each message object accepts a set of skip_* flags intended for granular control over validation and rate-limiting behavior.
The per-message
skip_* flags are currently accepted but have no effect. The endpoint does not read them, so every check it performs runs regardless of what you pass. This endpoint skips the recipient consent checks that POST /v2/send/bulk runs before queuing — its only consent checks are for a pending, unconfirmed double opt-in (pending_confirmation) and an expired confirmation window (doi_expired). A recipient who has opted out of the list or is blocked on it is accepted here instead of rejected; that message is still dropped before delivery with status contact_blocked, so a 201 is not proof of delivery — read each message’s status. Every other consent decision is yours: use this endpoint only for recipients whose consent you manage yourself. Other checks, including sending-list ownership and the list’s warm-up schedule, still apply. It differs from POST /v2/send/bulk in more than link wrapping and contact-data macro substitution.The following checks are always enforced:
- Your account must own the sending list
- Phone numbers must be in E.164 format
- Your account must be in active status
Body Parameters
array
required
Array of raw 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"
Intended to skip the duplicate message check. Currently accepted but ignored — this flag has no effect.
boolean
default:"false"
Intended to skip per-list rate limiting. Currently accepted but ignored — this flag has no effect.
boolean
default:"false"
Intended to skip contact validation such as opt-out and block list checks. Currently accepted but ignored — this flag has no effect, and this endpoint does not run those opt-out or block-list checks in the first place.
boolean
default:"false"
Intended to skip the journey enrollment check. Currently accepted but ignored — this flag has no effect.
object
Optional key-value metadata dictionary. Currently accepted but not stored or returned — the endpoint discards this field, 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 could not be published to the delivery queue.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-raw/bulk \
-H "X-Api-Key: trk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{
"to": "+14155551234",
"list_number": "+18005551000",
"body": "Verification code: 482901. Ref: {{messageId}}",
"skip_duplicate_check": true,
"skip_rate_limit": true
},
{
"to": "+14155559876",
"list_number": "+18005551000",
"body": "Verification code: 739204. Ref: {{messageId}}",
"skip_duplicate_check": true,
"skip_rate_limit": true,
"skip_contact_validation": true
}
]
}'
import requests
response = requests.post(
"https://api.tracklysms.com/api/v2/send-raw/bulk",
headers={
"X-Api-Key": "trk_your_api_key_here",
"Content-Type": "application/json",
},
json={
"messages": [
{
"to": "+14155551234",
"list_number": "+18005551000",
"body": "Verification code: 482901. Ref: {{messageId}}",
"skip_duplicate_check": True,
"skip_rate_limit": True,
},
{
"to": "+14155559876",
"list_number": "+18005551000",
"body": "Verification code: 739204. Ref: {{messageId}}",
"skip_duplicate_check": True,
"skip_rate_limit": True,
"skip_contact_validation": True,
},
]
},
)
data = response.json()
print(f"Queued: {data['queued_count']}, Errors: {data['error_count']}")
const response = await fetch("https://api.tracklysms.com/api/v2/send-raw/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: "Verification code: 482901. Ref: {{messageId}}",
skip_duplicate_check: true,
skip_rate_limit: true,
},
{
to: "+14155559876",
list_number: "+18005551000",
body: "Verification code: 739204. Ref: {{messageId}}",
skip_duplicate_check: true,
skip_rate_limit: true,
skip_contact_validation: true,
},
],
}),
});
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": "+14155559876",
"code": "list_not_found",
"error": "Sending list not found for this list_number"
}
]
}
{
"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. |
| 413 | too_many_messages | The messages array exceeds the 1,000 message limit. |
| 503 | send_cap_unavailable | Stored send-policy metadata is invalid before message processing. The response has error and code fields. |
| 503 | number_authority_unavailable | Sending-number authority cannot be initialized before message processing. The response has error and code fields. |
201 with per-message errors. When no messages were confirmed queued, any publication failure makes the request return 502; otherwise, an unavailable cap or number-authority check makes it return 503, even if other items failed validation. Authenticated requests can also fail with 401 invalid_credentials, 403 account_suspended, or 429 rate_limited — see Error Codes.
After the first unavailable cap check, the remaining eligible messages in that request receive send_cap_unavailable without retrying the counter store. Earlier successful sends remain queued, and other validation failures retain their own codes. A later request checks the counter again.
Per-Message Errors
Returned inside each failed entry of the response’serrors[] array as code.
| 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. |
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. |
daily_cap_exceeded | The API key’s daily_send_cap was reached before this message; each message in a raw batch is counted individually, so earlier messages in the same batch may have been queued. |
send_cap_unavailable | The daily quota check or reservation could not be confirmed for this message, or an earlier item encountered this error. The message was refused, but an uncertain reservation may still count toward today’s cap. Retry the failed records with backoff. |
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