Skip to content

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.

Send Content-Type: application/json (a charset=utf-8 parameter is allowed). A request with any other media type is refused with 415 UNSUPPORTED_MEDIA_TYPE when strict media-type checking is enabled; otherwise its body is still parsed as JSON.

Terminal window
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"
}
]
}'
Field Type Required Description
Idempotency-Key header string Yes (or legacy X-Idempotency-Key) Stable key for this logical batch. 1-128 ASCII letters, numbers, _, -, . or :. A completed 2xx response is kept for 24 hours: a retry with the same key and body returns the same HTTP status and JSON content (formatting can differ) with Idempotent-Replayed: true and is not a new send. Error responses are not kept. A 400 validation error is returned before the key is recorded, so fix the request and resend with the same key (an invalid key must itself be corrected). If the request failed after it was accepted for processing (for example a 5xx), retrying with the same key and the same body runs it again, or returns the stored response if the batch was saved after all (see 500 DB_ERROR); a key that an earlier failed attempt could not release returns 409 IDEMPOTENCY_REQUEST_IN_PROGRESS for at most 15 minutes. A key is scoped to the account, the workspace and this endpoint, and live and sk_test_ keys are separate. Requests are matched by a fingerprint of the normalized send: each recipient’s number, message text and sender ID after leading and trailing whitespace is trimmed, in request order. JSON formatting, field order and fields the endpoint does not read do not change the fingerprint; a different recipient order or a different trimmed value does. A different fingerprint with a key that is running or completed returns 409 IDEMPOTENCY_KEY_REUSED; with a live key this also applies for 24 hours after a failed attempt. A retry with the same fingerprint while the first request is still running returns 409 IDEMPOTENCY_REQUEST_IN_PROGRESS. X-Idempotency-Key is accepted as a legacy alias when Idempotency-Key is missing or blank (including whitespace-only); a nonblank Idempotency-Key wins if both are sent. X-CIO-Idempotency-Key is not accepted on this endpoint.
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; at most 4096 bytes and 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, messages over 4096 bytes or 10 SMS segments, and invalid senders return 400.

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 per-recipient tokens again, and includes Idempotent-Replayed: true.

200 OK
{
"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
results[].status Meaning
accepted Queued for delivery
blocked_country Recipient’s country is in your blocked list
message_too_long Live keys only; link shortening pushed the message past 10 SMS segments; no message_id; not queued; not counted in any summary field
queue_error sk_test_ simulator only
unpriced Not returned by current sends (legacy value)
config_error Not returned by current sends (legacy value)
db_error Not returned by current sends (legacy value)
HTTP Code Description
400 INVALID_BODY Cannot parse request body
400 INVALID_MESSAGES Missing or empty messages array
400 TOO_MANY_MESSAGES Over 1000 messages
400 INVALID_TO A messages[i].to is missing or is not 6–15 digits with an optional leading +
400 INVALID_MESSAGE A messages[i].message is missing, over 4096 bytes or over 10 SMS segments
400 INVALID_SENDER A messages[i].sender_id is missing, is not 3–11 ASCII alphanumeric characters, or starts with a number
400 IDEMPOTENCY_KEY_REQUIRED Neither Idempotency-Key nor the legacy X-Idempotency-Key header is set; blank values count as missing
400 INVALID_IDEMPOTENCY_KEY The key is over 128 characters or has unsupported characters
400 AMBIGUOUS_AUTHENTICATION An X-API-Key was sent together with a portal session cookie (auth_token or admin_token). Send the API key without cookies. Returned before the request body is read; nothing is sent
400 INVALID_WORKSPACE_HEADER Portal sessions only (cookie or bearer token): X-Workspace-ID is not a positive integer. API-key requests ignore this header. Returned before the request body is read; nothing is sent
401 UNAUTHORIZED Valid API authentication is required
403 NO_SMS_ACCESS SMS is not set up for the workspace in use, or its SMS settings could not be loaded. That is the API key’s workspace; for a portal session, the X-Workspace-ID workspace or, without that header, the default described on the Workspaces page. No message is created. Check the workspace and retry; if it persists, contact support. sk_test_ keys never get this error
403 CONFIG_ERROR The workspace’s SMS settings are incomplete. No message is created; contact support. sk_test_ keys never get this error
403 WORKSPACE_NOT_AVAILABLE The workspace was deleted while the request was being processed. No message is created; send from a live workspace. sk_test_ keys never get this error
403 FORBIDDEN An API key lacks sms.send; the body carries required_permission ("sms.send"). Enable it in Portal → API Keys. An Integration SMS key also gets FORBIDDEN here, without required_permission
403 SECTION_ACCESS_DENIED A team member (portal session, or a key they created) without sms.send or whose role hides SMS; the body carries section. Nothing is sent
403 PERMISSION_DENIED Team-member portal sessions only: the send route’s own sms.send re-check refused the request (access changed during the request). API keys never get this code
405 METHOD_NOT_ALLOWED GET is not allowed; Allow: POST names the supported method
409 IDEMPOTENCY_KEY_REUSED The key was already used for a different batch: a trimmed to, message or sender_id, or the order of messages, differs (see the Idempotency-Key row). Use a new key for a new batch
409 IDEMPOTENCY_REQUEST_IN_PROGRESS The same key is still processing, or an earlier attempt failed and could not release it. Wait and retry with the same key and body; the key is released at the latest 15 minutes after the first attempt started
415 UNSUPPORTED_MEDIA_TYPE Content-Type is not application/json (a charset=utf-8 parameter is allowed) while strict media-type checking is enabled. Nothing is sent; resend as JSON
422 URL_TRACKING_INVALID URL tracking was requested but a URL cannot be safely shortened; fix the URL and retry
429 RATE_LIMIT_EXCEEDED The batch exceeds your per-second message budget — see the note below
500 DB_ERROR The batch could not be saved. Usually it was rolled back and no message was created, but if the error came at the final commit the batch may have been saved and will still be sent. Retry with the same Idempotency-Key and body: you get the stored response if the batch was saved, otherwise it runs again; if the retry returns 409 IDEMPOTENCY_REQUEST_IN_PROGRESS, wait, because the key is released 15 minutes after the first attempt
500 IDEMPOTENCY_ERROR Could not reserve the idempotency key; nothing was sent. Retry with the same key and body
500 INTERNAL_ERROR Portal could not prepare the request (its fingerprint, a message ID or the response). With a live key no message is created; retry, with the same Idempotency-Key and body if you sent one. With an sk_test_ key, test messages for earlier recipients may already exist and a retry creates new ones
500 PERMISSION_CHECK_FAILED Team-member portal sessions only: your permission to send could not be checked. Nothing is sent; retry

A 503 means no SMS was sent. Many dependency failures are temporary: retry a few times after a short pause (when Retry-After is present it is 1). A 503 is never stored for idempotency, so if you sent an Idempotency-Key, retry with the same key and body. Authentication, access, sandbox and request-level rate-limit errors happen before the key is reserved. QUEUE_UNAVAILABLE, URL_TRACKING_UNAVAILABLE and a RATE_LIMIT_UNAVAILABLE while a multi-recipient batch is counted can happen after an Idempotency-Key was reserved; Portal attempts to mark the key retryable. If that update fails, the key may remain in progress: follow the existing IDEMPOTENCY_REQUEST_IN_PROGRESS guidance. URL_TRACKING_UNAVAILABLE can also mean that no link-shortening domain is set up; that does not clear on retry, so if it persists set up a domain or contact support. SANDBOX_UNAVAILABLE can mean the simulator is disabled or absent; if it persists, request sandbox enablement or contact support. Every 5xx body carries the generic description An internal error occurred. Reference: req-…; quote the X-Request-ID header to support.

error_code When Retry-After
API_KEY_VALIDATION_UNAVAILABLE The API key could not be checked 1
AUTH_PROTECTION_UNAVAILABLE Failed-attempt protection is unavailable (unrecognised API keys) 1
REPLAY_PROTECTION_UNAVAILABLE Replay protection is unavailable (signed API-key requests) 1
RATE_LIMIT_UNAVAILABLE The rate limiter is unavailable — when the request arrives, or while a multi-recipient batch is counted —
SECTION_ACCESS_UNAVAILABLE Section access could not be checked (team-member sessions and member-created API keys) —
WORKSPACE_ACCESS_UNAVAILABLE Workspace access could not be checked (portal sessions only) 1
SANDBOX_UNAVAILABLE The sk_test_ simulator is not available on this server —
QUEUE_UNAVAILABLE The SMS queue is unavailable; no message was created —
URL_TRACKING_UNAVAILABLE Link tracking could not be prepared — temporarily, or because no link-shortening domain is set up —

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_id returned above.
  • By phone: call GET /sms/messages with phone=, from= and to= to pull every message to a number over a date range and match results by recipient.