curl -X POST https://api.tracklysms.com/api/v2/send \
-H "X-Api-Key: trk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"to": "+14155551234",
"list_number": "+18005551000",
"body": "Hello! Your order is confirmed. Ref: {{messageId}}",
"wrap_links": true
}'
import requests
response = requests.post(
"https://api.tracklysms.com/api/v2/send",
headers={
"X-Api-Key": "trk_your_api_key_here",
"Content-Type": "application/json",
},
json={
"to": "+14155551234",
"list_number": "+18005551000",
"body": "Hello! Your order is confirmed. Ref: {{messageId}}",
"wrap_links": True,
},
)
data = response.json()
print(data["message_id"])
const response = await fetch("https://api.tracklysms.com/api/v2/send", {
method: "POST",
headers: {
"X-Api-Key": "trk_your_api_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
to: "+14155551234",
list_number: "+18005551000",
body: "Hello! Your order is confirmed. Ref: {{messageId}}",
wrap_links: true,
}),
});
const data = await response.json();
console.log(data.message_id);
{
"success": true,
"message_id": "a1b2c3d4",
"status": "queued"
}
{
"error": "Invalid recipient phone number format",
"code": "invalid_phone"
}
Messages (v2)
Send Single Message
Send a single SMS message to a recipient phone number.
POST
/
v2
/
send
curl -X POST https://api.tracklysms.com/api/v2/send \
-H "X-Api-Key: trk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"to": "+14155551234",
"list_number": "+18005551000",
"body": "Hello! Your order is confirmed. Ref: {{messageId}}",
"wrap_links": true
}'
import requests
response = requests.post(
"https://api.tracklysms.com/api/v2/send",
headers={
"X-Api-Key": "trk_your_api_key_here",
"Content-Type": "application/json",
},
json={
"to": "+14155551234",
"list_number": "+18005551000",
"body": "Hello! Your order is confirmed. Ref: {{messageId}}",
"wrap_links": True,
},
)
data = response.json()
print(data["message_id"])
const response = await fetch("https://api.tracklysms.com/api/v2/send", {
method: "POST",
headers: {
"X-Api-Key": "trk_your_api_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
to: "+14155551234",
list_number: "+18005551000",
body: "Hello! Your order is confirmed. Ref: {{messageId}}",
wrap_links: true,
}),
});
const data = await response.json();
console.log(data.message_id);
{
"success": true,
"message_id": "a1b2c3d4",
"status": "queued"
}
{
"error": "Invalid recipient phone number format",
"code": "invalid_phone"
}
Keys requiring send approval receive
Authenticated requests can also fail with
403 confirmation_required for direct sends. Use pending sends for approved single or bulk SMS; raw sends have no approval workflow.
Send a single SMS message through your sending list. The message is queued for delivery and assigned an opaque, variable-length message identifier for tracking.
Body Parameters
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 (an active account-owned SMS offer), partnershipOfferId (an active, account-owned offer with offer management enabled; requires the Partnerships product), or customUrl (an absolute HTTPS URL of at most 2,048 characters on a public hostname, not an IP literal). A customUrl authority must not contain macros. Each referenced {{linkN}} is replaced with a freshly minted short URL; body placeholders without a matching override remain unchanged, and unreferenced entries create no affiliate or offer-access state. Requires wrap_links: true. productId is not supported.object
Optional per-contact data for macro substitution. Supports
first_name (string), last_name (string), custom_fields (object), and timezone (string). These values are used to populate template variables like {{first_name}} in the message body.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 to correlate messages with your own systems.
Response Fields
boolean
Whether the message was successfully queued.
string
An opaque, variable-length message identifier for tracking delivery and clicks.
string
Current message status. Will be
"queued" on successful submission.Examples
curl -X POST https://api.tracklysms.com/api/v2/send \
-H "X-Api-Key: trk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"to": "+14155551234",
"list_number": "+18005551000",
"body": "Hello! Your order is confirmed. Ref: {{messageId}}",
"wrap_links": true
}'
import requests
response = requests.post(
"https://api.tracklysms.com/api/v2/send",
headers={
"X-Api-Key": "trk_your_api_key_here",
"Content-Type": "application/json",
},
json={
"to": "+14155551234",
"list_number": "+18005551000",
"body": "Hello! Your order is confirmed. Ref: {{messageId}}",
"wrap_links": True,
},
)
data = response.json()
print(data["message_id"])
const response = await fetch("https://api.tracklysms.com/api/v2/send", {
method: "POST",
headers: {
"X-Api-Key": "trk_your_api_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
to: "+14155551234",
list_number: "+18005551000",
body: "Hello! Your order is confirmed. Ref: {{messageId}}",
wrap_links: true,
}),
});
const data = await response.json();
console.log(data.message_id);
{
"success": true,
"message_id": "a1b2c3d4",
"status": "queued"
}
{
"error": "Invalid recipient phone number format",
"code": "invalid_phone"
}
Error Codes
| HTTP Status | Error Code | Description |
|---|---|---|
| 400 | missing_to | The to field is required but was not provided. |
| 400 | missing_list_number | The list_number field is required but was not provided. |
| 400 | missing_body | The body field is required but was not provided. |
| 400 | invalid_phone | The to field is not a valid E.164 phone number. |
| 400 | invalid_list_number | The list_number field is not a valid E.164 phone number. |
| 400 | invalid_contact_data | contact_data or one of its supported macro fields has the wrong JSON type. |
| 400 | invalid_wrap_links | Non-empty linkOverrides used a wrap_links value that was not a JSON boolean. |
| 400 | list_not_found | The sending list was not found or does not belong to your account. |
| 400 | webhook_not_configured | Webhook verification required before sending (BYOC lists). |
| 400 | pending_confirmation | The recipient has not confirmed their double opt-in yet, so they cannot be messaged. |
| 400 | doi_expired | The recipient never confirmed their double opt-in and the confirmation window has expired. |
| 400 | warmup_limit | The recipient is not yet eligible to be messaged under the list’s warm-up schedule. |
| 400 | contact_suppressed | The recipient is opted out or blocked and cannot receive the message. |
| 400 | link_overrides_require_wrap_links | A non-empty linkOverrides object was supplied without the literal wrap_links: true. |
| 400 | 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. |
| 400 | 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. |
| 403 | partnerships_not_enabled | partnershipOfferId requires the Partnerships product on the account. |
| 404 | offer_not_found | A referenced offerId is missing, inactive, or belongs to another account. |
| 404 | partnership_offer_not_found | A referenced partnershipOfferId is missing, inactive, does not have offer management enabled, or belongs to another account. |
| 409 | offer_access_not_active | Your access to the referenced partnershipOfferId is paused or has not been approved. |
| 409 | no_domain_configured | A referenced override needs a short URL, but neither the sending list nor the account has a link-shortener domain configured. |
| 403 | list_not_allowed_for_key | The API key’s send_policy.list_numbers does not include this list_number. Applies to sandbox keys too. |
| 429 | daily_cap_exceeded | The API key’s daily_send_cap is reached for today (UTC). Sandbox sends do not count. |
| 503 | send_cap_unavailable | The daily quota check or reservation could not be confirmed; the send was refused. An uncertain reservation may still count toward today’s cap. Retry with backoff. |
| 502 | kafka_producer_failure | Publication was not confirmed; delivery may still occur and daily quota remains charged. The body includes errorCode: "KAFKA_PRODUCER_FAILURE". Reconcile the message ID when available before considering another send. |
401 invalid_credentials, 403 account_suspended, or 429 rate_limited — see Error Codes.
Next Steps
Campaign Execution
How messages flow from queue to delivery
Create Contact
Add contacts before sending