Send SMS
Copy page
Send an SMS to one or more recipients.
POST /api/v1/sms/send · Permission: sms.send
Request
Section titled “Request”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.
curl -X POST https://restlink23telecom.com/api/v1/sms/send \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "to": [ "+14155551234", "+447911123456" ], "message": "Your verification code is 847291", "sender_id": "MyApp"}'const res = await fetch('https://restlink23telecom.com/api/v1/sms/send', { method: 'POST', headers: { 'X-API-Key': process.env.API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "to": [ "+14155551234", "+447911123456" ], "message": "Your verification code is 847291", "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", headers={"X-API-Key": os.environ["API_KEY"]}, json={ "to": ["+14155551234", "+447911123456"], "message": "Your verification code is 847291", "sender_id": "MyApp" },)res.raise_for_status()print(res.json())<?php$ch = curl_init('https://restlink23telecom.com/api/v1/sms/send');curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => 'POST', CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['X-API-Key: ' . getenv('API_KEY'), 'Content-Type: application/json'], CURLOPT_POSTFIELDS => json_encode([ 'to' => ['+14155551234', '+447911123456'], 'message' => 'Your verification code is 847291', '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")req = Net::HTTP::Post.new(uri)req["X-API-Key"] = ENV.fetch("API_KEY")req["Content-Type"] = "application/json"req.body = { to: ["+14155551234", "+447911123456"], message: "Your verification code is 847291", 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")) .header("X-API-Key", System.getenv("API_KEY")) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(""" { "to": [ "+14155551234", "+447911123456" ], "message": "Your verification code is 847291", "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(`{ "to": [ "+14155551234", "+447911123456" ], "message": "Your verification code is 847291", "sender_id": "MyApp"}`)
req, err := http.NewRequest("POST", "https://restlink23telecom.com/api/v1/sms/send", payload) if err != nil { panic(err) } req.Header.Set("X-API-Key", os.Getenv("API_KEY")) 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"));
var json = """{ "to": [ "+14155551234", "+447911123456" ], "message": "Your verification code is 847291", "sender_id": "MyApp"}""";var content = new StringContent(json, Encoding.UTF8, "application/json");var response = await client.PostAsync("https://restlink23telecom.com/api/v1/sms/send", content);
response.EnsureSuccessStatusCode();Console.WriteLine(await response.Content.ReadAsStringAsync());| Field | Type | Required | Description |
|---|---|---|---|
to |
string[] | Yes | 1 to 100 phone numbers in E.164 format (+ optional, 6-15 digits) |
message |
string | Yes | Message text — encoding is detected automatically; at most 4096 bytes and 10 SMS segments |
sender_id |
string | Yes | Sender ID for the message: 3-11 ASCII alphanumeric characters, first character cannot be a number |
Idempotency-Key header |
string | No | 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. |
The API trims leading and trailing whitespace from to, message and
sender_id before validation. Blank values, malformed phone numbers,
messages over 4096 bytes or 10 SMS segments, and invalid senders return 400.
Response
Section titled “Response”{ "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 (backward-compatible array) |
results |
All recipients with their individual status |
summary |
Totals: accepted, blocked, errors, cost, detected encoding |
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 |
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) |
Errors
Section titled “Errors”| HTTP | Code | Description |
|---|---|---|
| 400 | INVALID_BODY |
Cannot parse request body |
| 400 | INVALID_TO |
Recipient must be 6–15 digits, optional leading +, digits only |
| 400 | INVALID_MESSAGE |
Message is at most 4096 bytes and 10 SMS segments |
| 400 | INVALID_SENDER |
sender_id is 3–11 ASCII alphanumeric characters and cannot start with a number |
| 400 | TOO_MANY_RECIPIENTS |
Over 100 recipients |
| 400 | INVALID_IDEMPOTENCY_KEY |
The Idempotency-Key (or legacy X-Idempotency-Key) value 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 send: a trimmed to number, message or sender_id, or the order of to, differs (see the Idempotency-Key row). Use a new key for a new send |
| 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 |
Temporarily exhausted: retry after 1 second; over burst: split into requests of at most the burst; suspended: contact support (Retry-After: 60) |
| 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. If you sent an Idempotency-Key, retry with the same 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. Without a key, check GET /api/v1/sms/messages before resending (an API key needs the sms.read permission; otherwise contact support), or the batch may be sent twice |
| 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 |
Temporary errors (503)
Section titled “Temporary errors (503)”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 | — |
Delivery tracking
Section titled “Delivery tracking”The send response confirms acceptance, not delivery. To learn whether the message reached the handset:
- Webhooks (recommended): receive a delivery report on your server the moment the carrier reports it.
- Polling: call GET /sms/status/:message_id until the status is final.
