Send individual (personalized bulk)
Copy page
Send a personalized bulk of SMS in a single request: every recipient gets its
own message text and its own sender ID. This is the difference from
/sms/send, which fans one message out to many recipients.
POST
/api/v1/sms/send-individual
· Permission: sms.send
Each accepted recipient is queued, billed and tracked exactly like a normal send,
and gets its own message_id in the response — so you can correlate delivery
by message_id or by phone number via GET /sms/messages.
A blocked_country recipient is returned in results[] without a message_id
and is not queued. Missing local pricing does not reject a valid live recipient:
it is accepted and queued with a provisional local cost.
Request
Section titled “Request”curl -X POST https://restlink23telecom.com/api/v1/sms/send-individual \ -H "X-API-Key: $API_KEY" \ -H "Idempotency-Key: send-20260627-batch-001" \ -H "Content-Type: application/json" \ -d '{ "messages": [ { "to": "+14155551234", "message": "Hi John, your code is 847291", "sender_id": "MyApp" }, { "to": "+447911123456", "message": "Hi Anna, your code is 113355", "sender_id": "MyApp" } ]}'const res = await fetch('https://restlink23telecom.com/api/v1/sms/send-individual', { method: 'POST', headers: { 'X-API-Key': process.env.API_KEY, 'Idempotency-Key': 'send-20260627-batch-001', 'Content-Type': 'application/json' }, body: JSON.stringify({ "messages": [ { "to": "+14155551234", "message": "Hi John, your code is 847291", "sender_id": "MyApp" }, { "to": "+447911123456", "message": "Hi Anna, your code is 113355", "sender_id": "MyApp" } ] }),});
if (!res.ok) { const err = await res.json(); throw new Error(`${err.error_code}: ${err.description}`);}const data = await res.json();console.log(data);import osimport requests
res = requests.post( "https://restlink23telecom.com/api/v1/sms/send-individual", headers={"X-API-Key": os.environ["API_KEY"], "Idempotency-Key": "send-20260627-batch-001"}, json={ "messages": [{ "to": "+14155551234", "message": "Hi John, your code is 847291", "sender_id": "MyApp" }, { "to": "+447911123456", "message": "Hi Anna, your code is 113355", "sender_id": "MyApp" }] },)res.raise_for_status()print(res.json())<?php$ch = curl_init('https://restlink23telecom.com/api/v1/sms/send-individual');curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => 'POST', CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['X-API-Key: ' . getenv('API_KEY'), 'Idempotency-Key: send-20260627-batch-001', 'Content-Type: application/json'], CURLOPT_POSTFIELDS => json_encode([ 'messages' => [[ 'to' => '+14155551234', 'message' => 'Hi John, your code is 847291', 'sender_id' => 'MyApp' ], [ 'to' => '+447911123456', 'message' => 'Hi Anna, your code is 113355', 'sender_id' => 'MyApp' ]] ]),]);
$response = curl_exec($ch);$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);curl_close($ch);
if ($status !== 200) { throw new Exception("HTTP $status: $response");}$data = json_decode($response, true);print_r($data);require "net/http"require "json"
uri = URI("https://restlink23telecom.com/api/v1/sms/send-individual")req = Net::HTTP::Post.new(uri)req["X-API-Key"] = ENV.fetch("API_KEY")req["Idempotency-Key"] = "send-20260627-batch-001"req["Content-Type"] = "application/json"req.body = { messages: [{ to: "+14155551234", message: "Hi John, your code is 847291", sender_id: "MyApp" }, { to: "+447911123456", message: "Hi Anna, your code is 113355", sender_id: "MyApp" }]}.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }raise "HTTP #{res.code}: #{res.body}" unless res.is_a?(Net::HTTPSuccess)
puts JSON.parse(res.body)import java.net.URI;import java.net.http.HttpClient;import java.net.http.HttpRequest;import java.net.http.HttpResponse;
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://restlink23telecom.com/api/v1/sms/send-individual")) .header("X-API-Key", System.getenv("API_KEY")) .header("Idempotency-Key", "send-20260627-batch-001") .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(""" { "messages": [ { "to": "+14155551234", "message": "Hi John, your code is 847291", "sender_id": "MyApp" }, { "to": "+447911123456", "message": "Hi Anna, your code is 113355", "sender_id": "MyApp" } ] }""")) .build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());System.out.println(response.body());package main
import ( "fmt" "io" "net/http" "os" "strings")
func main() { payload := strings.NewReader(`{ "messages": [ { "to": "+14155551234", "message": "Hi John, your code is 847291", "sender_id": "MyApp" }, { "to": "+447911123456", "message": "Hi Anna, your code is 113355", "sender_id": "MyApp" } ]}`)
req, err := http.NewRequest("POST", "https://restlink23telecom.com/api/v1/sms/send-individual", payload) if err != nil { panic(err) } req.Header.Set("X-API-Key", os.Getenv("API_KEY")) req.Header.Set("Idempotency-Key", "send-20260627-batch-001") req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close()
data, _ := io.ReadAll(res.Body) fmt.Println(string(data))}using System.Text;
using var client = new HttpClient();client.DefaultRequestHeaders.Add("X-API-Key", Environment.GetEnvironmentVariable("API_KEY"));client.DefaultRequestHeaders.Add("Idempotency-Key", "send-20260627-batch-001");
var json = """{ "messages": [ { "to": "+14155551234", "message": "Hi John, your code is 847291", "sender_id": "MyApp" }, { "to": "+447911123456", "message": "Hi Anna, your code is 113355", "sender_id": "MyApp" } ]}""";var content = new StringContent(json, Encoding.UTF8, "application/json");var response = await client.PostAsync("https://restlink23telecom.com/api/v1/sms/send-individual", content);
response.EnsureSuccessStatusCode();Console.WriteLine(await response.Content.ReadAsStringAsync());| Field | Type | Required | Description |
|---|---|---|---|
Idempotency-Key header | string | Yes | Stable key for this logical batch. Reuse it after a timeout or 5xx. Completed responses are retained for 24 hours and replayed byte-for-byte with Idempotent-Replayed: true; a replay is not a new send. A key used with a different body returns 409 IDEMPOTENCY_KEY_REUSED |
messages | object[] | Yes | 1 to 1000 messages |
messages[].to | string | Yes | Recipient phone number in E.164 format (+ optional, 6-15 digits) |
messages[].message | string | Yes | Message text for this recipient — encoding is detected automatically; max 10 SMS segments |
messages[].sender_id | string | Yes | Sender ID for this recipient: 3-11 ASCII alphanumeric characters, first character cannot be a number |
The API trims leading and trailing whitespace from each to, message and
sender_id before validation. Values that are blank after trimming, malformed
phone numbers, over-10-segment messages, and invalid senders return 400.
Response
Section titled “Response”The response shape is identical to /sms/send: each accepted
recipient appears in both messages and results with its own message_id.
When a completed request is retried with the same Idempotency-Key, the API
returns the stored response, does not consume batch tokens again, and includes
Idempotent-Replayed: true.
{ "status": true, "messages": [ {"dnis": "+14155551234", "message_id": "api_42_1743667200123456789_a3f8b2c1d9e45f67", "segment_num": 1}, {"dnis": "+447911123456", "message_id": "api_42_1743667200123456789_b7c4e8f1a2d3690b", "segment_num": 1} ], "results": [ {"dnis": "+14155551234", "message_id": "api_42_1743667200123456789_a3f8b2c1d9e45f67", "segments": 1, "status": "accepted"}, {"dnis": "+447911123456", "message_id": "api_42_1743667200123456789_b7c4e8f1a2d3690b", "segments": 1, "status": "accepted"} ], "summary": { "total_recipients": 2, "total_segments": 2, "total_cost": 0.02, "encoding": "GSM-7", "accepted_count": 2, "blocked_count": 0, "unpriced_count": 0, "queue_error_count": 0, "config_error_count": 0, "db_error_count": 0 }}| Field | Description |
|---|---|
messages | Accepted messages only (one entry per recipient) |
results | All recipients with their individual status |
summary | Totals: accepted, blocked, errors, cost and batch encoding. encoding is GSM-7 or UCS-2 when at least one recipient is accepted, and can be empty when no recipient is accepted |
Per-recipient statuses
Section titled “Per-recipient statuses”results[].status | Meaning |
|---|---|
accepted | Queued for delivery |
blocked_country | Recipient’s country is in your blocked list |
unpriced | Legacy/sandbox non-acceptance status retained for compatibility. Current live pricing misses are accepted and queued with a provisional local cost |
queue_error | Sandbox simulator storage failure (sk_test_ only); live Redis enqueue failures stay accepted and are recovered from the durable outbox |
config_error | SMS sending not configured on the account |
db_error | The recipient’s outbox row could not be written |
Errors
Section titled “Errors”| HTTP | Code | Description |
|---|---|---|
| 400 | INVALID_BODY | Cannot parse request body |
| 400 | INVALID_MESSAGES | Missing or empty messages array |
| 400 | IDEMPOTENCY_KEY_REQUIRED / INVALID_IDEMPOTENCY_KEY | Missing or invalid Idempotency-Key header |
| 400 | TOO_MANY_MESSAGES | Over 1000 messages |
| 400 | INVALID_TO | A messages[i].to is missing or not E.164-style |
| 400 | INVALID_MESSAGE | A messages[i].message is missing or exceeds 10 SMS segments |
| 400 | INVALID_SENDER | A messages[i].sender_id is missing or not 3-11 ASCII alphanumeric characters |
| 403 | NO_SMS_ACCESS | SMS not enabled on your account |
| 403 | CONFIG_ERROR | SMS credentials incomplete — contact support |
| 403 | WORKSPACE_NOT_AVAILABLE | Target workspace was deleted — use a live workspace |
| 409 | IDEMPOTENCY_KEY_REUSED | Same key was already used with a different request body |
| 409 | IDEMPOTENCY_REQUEST_IN_PROGRESS | Same key is still processing; retry later with the same key |
| 429 | RATE_LIMIT_EXCEEDED | The batch exceeds your per-second message budget — see the note below |
| 500 | DB_ERROR | Could not queue (atomic rollback) — retry the whole request |
| 503 | SANDBOX_UNAVAILABLE / RATE_LIMIT_UNAVAILABLE / QUEUE_UNAVAILABLE | Sandbox simulator not enabled (sk_test_), rate limiter unavailable, or SMS queue unavailable — retry shortly with the same Idempotency-Key |
Delivery tracking
Section titled “Delivery tracking”The send response confirms acceptance, not delivery. Because every recipient
gets a distinct message_id, you can track each one precisely:
- Webhooks (recommended): receive a delivery report per recipient the moment the carrier reports it.
- Per message: call GET /sms/status/:message_id for any
message_idreturned above. - By phone: call GET /sms/messages with
phone=,from=andto=to pull every message to a number over a date range and match results by recipient.