curl -X POST https://api.tracklysms.com/api/v1/phone/validate/batch \
-H "X-Api-Key: your_api_key" \
-H "Content-Type: application/json" \
-d '{
"phones": [
"+14155551234",
"+13103059808",
"not-a-phone",
"+15551234567"
]
}'
import requests
response = requests.post(
"https://api.tracklysms.com/api/v1/phone/validate/batch",
headers={
"X-Api-Key": "your_api_key",
"Content-Type": "application/json",
},
json={"phones": phone_list},
)
payload = response.json()
clean = [
r["phone"]
for r in payload["results"]
if r.get("disposition") == "ok"
]
print(f"Keeping {len(clean)} of {payload['summary']['total']}; "
f"cost ${payload['summary']['total_cost']}")
const axios = require('axios');
const { data } = await axios.post(
'https://api.tracklysms.com/api/v1/phone/validate/batch',
{ phones: phoneList },
{
headers: {
'X-Api-Key': 'your_api_key',
'Content-Type': 'application/json',
},
}
);
const clean = data.results
.filter((r) => r.disposition === 'ok')
.map((r) => r.phone);
console.log(`Keeping ${clean.length} of ${data.summary.total}; ` +
`cost $${data.summary.total_cost}`);
{
"results": [
{
"phone": "+14155551234",
"valid": true,
"line_type": "mobile",
"carrier": "Verizon",
"carrier_raw": "CELLCO PARTNERSHIP DBA VERIZON",
"ported": false,
"country": "US",
"state": "California",
"city": "San Francisco",
"on_dnc": false,
"disposition": "ok",
"cost": 0.003,
"cached": false
},
{
"phone": "+13103059808",
"valid": true,
"line_type": "landline",
"carrier": "Frontier",
"carrier_raw": "FRONTIER CALIFORNIA, INC.",
"ported": true,
"country": "US",
"state": "California",
"city": "Santa Monica",
"on_dnc": false,
"disposition": "unreachable",
"cost": 0.003,
"cached": false
},
{
"phone": "not-a-phone",
"error": "validation_error",
"code": "invalid_phone",
"message": "phone must be E.164 format for list cleaning"
},
{
"phone": "+15551234567",
"valid": true,
"line_type": "mobile",
"carrier": "T-Mobile",
"carrier_raw": "T-MOBILE USA, INC.",
"ported": false,
"country": "US",
"state": "",
"city": "",
"on_dnc": false,
"disposition": "ok",
"cost": 0,
"cached": true
}
],
"summary": {
"total": 4,
"ok": 2,
"unreachable": 1,
"invalid": 0,
"risky": 0,
"unknown": 0,
"errors": 1,
"cached": 1,
"total_cost": 0.006
}
}
{
"error": "validation_error",
"code": "missing_phones",
"message": "phones array is required and must be non-empty"
}
{
"error": "billing_not_enabled",
"code": "no_payment_method",
"message": "An active payment method is required to use phone validation."
}
{
"error": "validation_error",
"code": "too_many_phones",
"message": "Maximum 500 phones per request"
}
Phone Validation (v1)
Validate Phone Numbers (Batch)
Validate up to 500 phone numbers in a single request for list cleaning.
POST
/
v1
/
phone
/
validate
/
batch
curl -X POST https://api.tracklysms.com/api/v1/phone/validate/batch \
-H "X-Api-Key: your_api_key" \
-H "Content-Type: application/json" \
-d '{
"phones": [
"+14155551234",
"+13103059808",
"not-a-phone",
"+15551234567"
]
}'
import requests
response = requests.post(
"https://api.tracklysms.com/api/v1/phone/validate/batch",
headers={
"X-Api-Key": "your_api_key",
"Content-Type": "application/json",
},
json={"phones": phone_list},
)
payload = response.json()
clean = [
r["phone"]
for r in payload["results"]
if r.get("disposition") == "ok"
]
print(f"Keeping {len(clean)} of {payload['summary']['total']}; "
f"cost ${payload['summary']['total_cost']}")
const axios = require('axios');
const { data } = await axios.post(
'https://api.tracklysms.com/api/v1/phone/validate/batch',
{ phones: phoneList },
{
headers: {
'X-Api-Key': 'your_api_key',
'Content-Type': 'application/json',
},
}
);
const clean = data.results
.filter((r) => r.disposition === 'ok')
.map((r) => r.phone);
console.log(`Keeping ${clean.length} of ${data.summary.total}; ` +
`cost $${data.summary.total_cost}`);
{
"results": [
{
"phone": "+14155551234",
"valid": true,
"line_type": "mobile",
"carrier": "Verizon",
"carrier_raw": "CELLCO PARTNERSHIP DBA VERIZON",
"ported": false,
"country": "US",
"state": "California",
"city": "San Francisco",
"on_dnc": false,
"disposition": "ok",
"cost": 0.003,
"cached": false
},
{
"phone": "+13103059808",
"valid": true,
"line_type": "landline",
"carrier": "Frontier",
"carrier_raw": "FRONTIER CALIFORNIA, INC.",
"ported": true,
"country": "US",
"state": "California",
"city": "Santa Monica",
"on_dnc": false,
"disposition": "unreachable",
"cost": 0.003,
"cached": false
},
{
"phone": "not-a-phone",
"error": "validation_error",
"code": "invalid_phone",
"message": "phone must be E.164 format for list cleaning"
},
{
"phone": "+15551234567",
"valid": true,
"line_type": "mobile",
"carrier": "T-Mobile",
"carrier_raw": "T-MOBILE USA, INC.",
"ported": false,
"country": "US",
"state": "",
"city": "",
"on_dnc": false,
"disposition": "ok",
"cost": 0,
"cached": true
}
],
"summary": {
"total": 4,
"ok": 2,
"unreachable": 1,
"invalid": 0,
"risky": 0,
"unknown": 0,
"errors": 1,
"cached": 1,
"total_cost": 0.006
}
}
{
"error": "validation_error",
"code": "missing_phones",
"message": "phones array is required and must be non-empty"
}
{
"error": "billing_not_enabled",
"code": "no_payment_method",
"message": "An active payment method is required to use phone validation."
}
{
"error": "validation_error",
"code": "too_many_phones",
"message": "Maximum 500 phones per request"
}
Validate a batch of phone numbers in one request. Per-number errors (bad format, provider failure, rate-limit timeout) are returned in-band — a single bad phone never fails the whole batch. Results are returned in submission order so you can zip them back into your original list.
Same data source, pricing, and caching as
POST /v1/phone/validate: LRN + DNC, $0.003 per non-cached lookup, permanent per-account cache.
Pricing & Billing
Each non-cached number in the batch is billed at $0.003. Once a phone has been validated for your account, repeat lookups are free indefinitely (cost: 0) — pass force=true at the request level (single top-level boolean, applies to every phone in the batch) to bypass the cache and re-validate at $0.003 per number (rare; LRN data only changes on carrier porting). Charges accrue through the month and are billed to your payment method on file on the 1st of the following month.
Your account must have an active payment method. Requests from accounts with payment_failed or suspended billing status return 402 Payment Required.
Size Limit
Up to 500 phones per request — 500 cache-miss numbers take roughly 10 seconds worst-case. For larger lists, chunk client-side or fire multiple requests in parallel. The permanent per-account cache makes re-validation of the same numbers effectively free after the first pass.Request
string[]
required
Array of phone numbers to validate. E.164 format recommended (e.g.,
+14155551234). Common US formats are normalized automatically. Duplicate numbers within a batch are not deduplicated — each occurrence is validated independently and may be billed. Passing force=true bypasses both the permanent per-account record and the 30-day cache for every phone in the batch, so each one is billed again.boolean
default:"false"
Request-level flag — applies to every phone in the batch. Bypasses both the permanent per-account record and the 30-day cache, running a fresh paid lookup for each number. Defaults to
false. Must be a strict boolean — string "true" is rejected with invalid_force.Response
array
Per-number result in submission order. Each entry is either a successful validation (same schema as the single-number endpoint) or an inline error object with
phone, error, code, and message fields.object
Aggregated counts and total cost for the batch.
Show summary fields
Show summary fields
integer
Total phones submitted.
integer
Valid mobile, not on DNC — safe to send.
integer
Landline or VoIP — cannot receive SMS.
integer
Not a valid number per the carrier network.
integer
Valid mobile but on DNC list.
integer
Line type could not be determined.
integer
Per-number errors (invalid format, provider failure, rate-limited).
integer
Served from the per-account cache ($0 billed). Cache is permanent per (account, phone) once validated.
number
Sum of USD billed for this batch.
curl -X POST https://api.tracklysms.com/api/v1/phone/validate/batch \
-H "X-Api-Key: your_api_key" \
-H "Content-Type: application/json" \
-d '{
"phones": [
"+14155551234",
"+13103059808",
"not-a-phone",
"+15551234567"
]
}'
import requests
response = requests.post(
"https://api.tracklysms.com/api/v1/phone/validate/batch",
headers={
"X-Api-Key": "your_api_key",
"Content-Type": "application/json",
},
json={"phones": phone_list},
)
payload = response.json()
clean = [
r["phone"]
for r in payload["results"]
if r.get("disposition") == "ok"
]
print(f"Keeping {len(clean)} of {payload['summary']['total']}; "
f"cost ${payload['summary']['total_cost']}")
const axios = require('axios');
const { data } = await axios.post(
'https://api.tracklysms.com/api/v1/phone/validate/batch',
{ phones: phoneList },
{
headers: {
'X-Api-Key': 'your_api_key',
'Content-Type': 'application/json',
},
}
);
const clean = data.results
.filter((r) => r.disposition === 'ok')
.map((r) => r.phone);
console.log(`Keeping ${clean.length} of ${data.summary.total}; ` +
`cost $${data.summary.total_cost}`);
{
"results": [
{
"phone": "+14155551234",
"valid": true,
"line_type": "mobile",
"carrier": "Verizon",
"carrier_raw": "CELLCO PARTNERSHIP DBA VERIZON",
"ported": false,
"country": "US",
"state": "California",
"city": "San Francisco",
"on_dnc": false,
"disposition": "ok",
"cost": 0.003,
"cached": false
},
{
"phone": "+13103059808",
"valid": true,
"line_type": "landline",
"carrier": "Frontier",
"carrier_raw": "FRONTIER CALIFORNIA, INC.",
"ported": true,
"country": "US",
"state": "California",
"city": "Santa Monica",
"on_dnc": false,
"disposition": "unreachable",
"cost": 0.003,
"cached": false
},
{
"phone": "not-a-phone",
"error": "validation_error",
"code": "invalid_phone",
"message": "phone must be E.164 format for list cleaning"
},
{
"phone": "+15551234567",
"valid": true,
"line_type": "mobile",
"carrier": "T-Mobile",
"carrier_raw": "T-MOBILE USA, INC.",
"ported": false,
"country": "US",
"state": "",
"city": "",
"on_dnc": false,
"disposition": "ok",
"cost": 0,
"cached": true
}
],
"summary": {
"total": 4,
"ok": 2,
"unreachable": 1,
"invalid": 0,
"risky": 0,
"unknown": 0,
"errors": 1,
"cached": 1,
"total_cost": 0.006
}
}
{
"error": "validation_error",
"code": "missing_phones",
"message": "phones array is required and must be non-empty"
}
{
"error": "billing_not_enabled",
"code": "no_payment_method",
"message": "An active payment method is required to use phone validation."
}
{
"error": "validation_error",
"code": "too_many_phones",
"message": "Maximum 500 phones per request"
}
Error Codes
| Code | HTTP | Description |
|---|---|---|
missing_phones | 400 | phones array missing or empty |
invalid_force | 400 | force is not a strict boolean (string "true" / int 1 are rejected) |
too_many_phones | 413 | More than 500 phones in one request |
invalid_credentials | 401 | Missing or invalid X-Api-Key |
account_suspended | 403 | Your account is suspended. Resolve outstanding billing or contact support. |
no_billing_config | 402 | Account has no billing configuration |
no_payment_method | 402 | No Stripe customer / card on file |
payment_failed | 402 | Most recent charge failed |
suspended | 402 | Account is suspended |
Per-number inline error codes
These appear insideresults[], not at the top level:
| Code | Description |
|---|---|
invalid_phone | The number could not be parsed as E.164 |
provider_error | Upstream lookup failed for this specific number |
rate_limited | Batch hit our LRN provider safety ceiling; retry this subset later |
Behavior Notes
Rate limiting. We self-cap provider calls at 50/s per account. Beyond that, we honor the provider’s own
x-ratelimit-* headers and back off on 429. Very large cleans may be throttled — stagger parallel requests and rely on the per-account cache to absorb load after the first pass.Cost control. Cache hits cost nothing. Re-validating the same list any time later is effectively free — the cache is permanent per (account, phone) once a number has been validated. If you process the same customer base daily, your cost tends toward “new numbers today × $0.003” rather than “total list × $0.003”.