SMS
Send SMS
Sends one outbound SMS message to one or more recipients.
/api/v1/sms/messagesBefore you start
Choose IllyVoIP or an approved available sender ID, a recipient array and message text. Sending requires account access and sufficient balance.
Result
Save queued_message_ids. Acceptance means queued, not delivered; inspect invalid_numbers separately for rejected recipients.
Charges and safe retries
Only queued recipients are charged. Reuse the same Idempotency-Key for an identical retry; see delivery and encoding conditions below.
Authentication
Send your account API key in the X-Api-Key header. Keep it on your server; never embed it in browser code.
Request examples
Replace the example values with your own inputs. Examples do not run on this page. Production requests can change your account or incur charges.
cURL
curl -X POST "https://api.illyvoip.com/api/v1/sms/messages" \
-H "X-Api-Key: ${ILLYVOIP_API_KEY:?Set ILLYVOIP_API_KEY}" \
-H "Idempotency-Key: YOUR_UNIQUE_IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "IllyVoIP",
"to": [
"YOUR_DESTINATION_NUMBER"
],
"message": "Hello from API"
}'Node.js
const response = await fetch("https://api.illyvoip.com/api/v1/sms/messages", {
method: "POST",
headers: {
"X-Api-Key": process.env.ILLYVOIP_API_KEY,
"Idempotency-Key": "YOUR_UNIQUE_IDEMPOTENCY_KEY",
'Content-Type': 'application/json'
},
body: JSON.stringify({
"from": "IllyVoIP",
"to": [
"YOUR_DESTINATION_NUMBER"
],
"message": "Hello from API"
})
});
const data = await response.json();
console.log(response.status, data);PHP
<?php
$ch = curl_init('https://api.illyvoip.com/api/v1/sms/messages');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'X-Api-Key: ' . getenv('ILLYVOIP_API_KEY'),
'Idempotency-Key: YOUR_UNIQUE_IDEMPOTENCY_KEY',
'Content-Type: application/json'
],
CURLOPT_POSTFIELDS => '{
"from": "IllyVoIP",
"to": [
"YOUR_DESTINATION_NUMBER"
],
"message": "Hello from API"
}',
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo $status . PHP_EOL;
echo $response;Python
import os
import json
import requests
response = requests.request(
"POST",
"https://api.illyvoip.com/api/v1/sms/messages",
headers={
"X-Api-Key": os.environ["ILLYVOIP_API_KEY"],
"Idempotency-Key": "YOUR_UNIQUE_IDEMPOTENCY_KEY",
'Content-Type': 'application/json'
},
json=json.loads("{\n \"from\": \"IllyVoIP\",\n \"to\": [\n \"YOUR_DESTINATION_NUMBER\"\n ],\n \"message\": \"Hello from API\"\n}"),
)
print(response.status_code)
print(response.json())Node.js examples run on the server (Node.js 22.17+). Python examples require requests; PHP examples require cURL. Set the environment variables referenced in each example.
Request parameters
Header parameters
Idempotency-KeystringoptionalOptional stable retry key. Reuse the same value only when retrying the identical SMS request.
Request body
application/json
Body required.
fromstringrequiredUse IllyVoIP until a custom sender ID has been approved for this account and confirmed as available for SMS delivery. Custom sender IDs must contain 1-11 letters (A-Z); numeric phone numbers are not accepted.
toarray<string>requiredView nested fields
Array items
Type: string
Recipient number in E.164 format.
messagestringrequiredMessage body.
Additional properties: not allowed.
Full schema
{
"type": "object",
"additionalProperties": false,
"properties": {
"from": {
"type": "string",
"description": "Use IllyVoIP until a custom sender ID has been approved for this account and confirmed as available for SMS delivery. Custom sender IDs must contain 1-11 letters (A-Z); numeric phone numbers are not accepted.",
"minLength": 1,
"maxLength": 11
},
"to": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"description": "Recipient number in E.164 format."
}
},
"message": {
"type": "string",
"description": "Message body.",
"minLength": 1
}
},
"required": [
"from",
"to",
"message"
]
}Example body
{
"from": "IllyVoIP",
"to": [
"+12025550123"
],
"message": "Hello from API"
}Request behavior
The same endpoint supports single-recipient and bulk-recipient flows. If at least one unique recipient is accepted for delivery, the request succeeds and queues only the accepted recipients. queued_message_ids follows those accepted recipients in request order after normalization, duplicate removal, and destination validation. invalid_numbers is a separate list of rejected numbers and reasons; it is not positionally paired with queued_message_ids. duplicates_removed lists normalized duplicates that were not queued. total_cost includes only queued recipients. Recipient formatting (spaces, dots, brackets and hyphens) is normalized; 00 is treated as +.
The normalized destination must contain 7–15 digits with a recognised international calling code. Country-specific numbering lengths and ranges are advisory for outbound SMS, not proof of reachability. Unrecognised ranges can proceed only when existing routing, pricing and account checks allow them.
Digits are never silently added or removed. A provider may still reject the destination. An invalid recipient includes error_code invalid_destination.
If every number is invalid, HTTP 400 returns error_code invalid_destination with an explanation; validation precedes carrier lookup, queueing and charging. If numbers are correctly formatted but SMS service is unavailable for all of them, it returns HTTP 400 with the error No valid destinations provided. In either all-rejected case, no message is queued or charged.
Encoding is checked before queuing. If Unicode (UCS-2) is unsupported for a destination, its invalid_numbers entry includes error_code unsupported_encoding and a safe explanation. An all-rejected request involving this error returns HTTP 400 with error_code unsupported_encoding.
No message is queued or charged and no delivery webhook is generated for validation rejections. The JSON fields are from, to (an array), and message. An optional Idempotency-Key makes retries safe.
Conditions and examples
Recipient results
If at least one unique recipient is accepted, the response is successful: queued_message_ids contains one ID per queued recipient, invalid_numbers contains rejected numbers with their reasons, and duplicates_removed contains normalized duplicates. IDs follow accepted recipients in request order; the invalid-number list is separate. Only queued recipients are included in total_cost. When every recipient is rejected, the API returns 400, queues nothing, and charges nothing.
SMS delivery updates are available through the webhook system. Configure one webhook endpoint in Account Settings → Integrations / CRM & webhooks and subscribe it to sms_sent, sms_delivered, and/or sms_failed. When a send may be retried, include an Idempotency-Key header and reuse the same stable value for every retry. The key is case-sensitive, must be 8-128 letters, numbers, dots, colons, underscores, or dashes, and is not included in webhook deliveries. Store the returned queued_message_ids and correlate them with webhook data.message.message_id. Reusing a key for a changed recipient list, order, message, or sender returns 400.
Content screening can return HTTP 400 with error_code: sms_use_case_information_required. Contact support with fields.review_reference to explain your SMS use case. sms_terms_blocked means the message was stopped by screening. Neither response queues or charges a message. These are not transient retry errors. Staff approval does not resend old messages; review and submit again after approval. Temporary screening unavailability returns HTTP 503, not a content accusation.
Encoding availability
Messages are checked as GSM-7 or Unicode (UCS-2) before quoting and queuing. GSM-7 allows 160 units in one SMS, or 153 per part in a multipart message; extended characters such as € and ^ use two units. Unicode allows 70 UTF-16 units in one SMS, or 67 per part; some characters use two units. Destination limits can be stricter and not every destination supports multipart or Unicode. A length-limit rejection asks you to shorten the text; it is different from an unsupported-encoding rejection. A rejected recipient can include error_code: unsupported_encoding and a reason asking you to use GSM-7 characters. If the whole request is rejected for encoding, HTTP 400 returns that code and explanation. No SMS is queued or charged for validation rejections, so no delivery webhook is generated for them. Some destinations offer automatic GSM-7 character conversion. The review shows the converted text before sending. In the API, text_conversion describes the frozen options per recipient; status and delivery webhooks include text_conversion_applied, submitted_message, submitted_encoding and submitted_segments after confirmed submission. Original message text is preserved. Unmapped emoji or scripts are never discarded. The quote reserves the maximum sending cost; unused segments are credited during settlement.
Number validation
Formatting such as +383.49.100602 is normalized to +38349100602. Malformed destinations, unknown calling codes and numbers outside 7–15 digits are rejected before carrier lookup, queueing or charging. Country-specific length and range disagreements do not by themselves block outbound SMS. Check the exact destination carefully; routing availability, provider acceptance and delivery remain separate checks. Mixed batches return rejected recipients with error_code: invalid_destination; an all-invalid request returns HTTP 400. Number validation does not establish reachability. Correct a number explicitly; missing digits are never invented.
Error responses
Expected failures return status: error and a message. Save any error_code when contacting support.
400 · Input or review required
Request validation failed, including sender, recipient, message, or idempotency conflicts.
-
Sender ID must be 1–11 letters (A–Z).
-
fromis required. -
Sender ID is not approved. Request approval in the portal before using it.
-
tomust be a non-empty array. -
Invalid destination number. Check the country code and full phone number.
-
No valid destinations provided.
-
messageis required. -
idempotency_keymust be 8–128 letters, numbers, dots, colons, underscores, or dashes. -
Idempotency key is already associated with a different SMS submission.
-
Idempotency key is already associated with an incomplete SMS submission.
-
Additional information about your SMS use case is required. Contact support with the reference.
-
This SMS was stopped by content screening. Contact support with the reference.
-
This message requires Unicode (UCS-2), which is not supported for this destination. Use GSM-7 (GSM 03.38) characters and try again.
401 · Authentication
The API key is missing or invalid.
-
Invalid API key.
-
Missing API key.
402 · Available balance
The account does not have enough available credit.
- Insufficient credit.
403 · Account access
API or SMS access, account role, or KYC policy does not permit the request.
-
SMS is disabled for this account.
-
API access is disabled.
-
API access is disabled for this account.
-
Only portal administrators can use the SMS API.
413 · Request size
The JSON request body exceeds the accepted size.
- JSON request body is too large.
429 · Rate limit
The API request rate was exceeded.
- Too many API requests. Please retry in a minute.
500 · Unexpected error
An unexpected server error occurred. Quote error_code when contacting support.
- There is a problem with our backend services right now. Please try again later.
503 · Temporarily unavailable
SMS idempotency state or the telephony dependency is temporarily unavailable.
-
Unable to verify SMS idempotency state right now.
-
Telephony service is unavailable. Try again later.
Response fields
status: Request outcome. Read the resource state separately; success does not always mean delivery or completion.queued_message_ids: One ID per accepted recipient, in accepted-recipient order after normalization and deduplication.total_cost: Charge for the recipients accepted by this request.invalid_numbers: Rejected recipients and reasons. This list is separate from queued_message_ids.duplicates_removed: Normalized duplicate recipients that were not queued again.encoding: Message encoding used for the documented stage of processing.segments_per_recipient: SMS segments counted for each accepted recipient.replayed: Whether this response reuses an earlier request with the same idempotency key.queued_count: Number of messages accepted into the sending process.
HTTP responses
Expand a status to inspect its documented response format and examples. Example prices, IDs and timestamps are illustrative values, not quotes or account records.
200 Successful customer-facing response.
application/json
{
"status": "success",
"queued_message_ids": [
"msg_api_example_0"
],
"total_cost": 0.05,
"invalid_numbers": [],
"duplicates_removed": [],
"encoding": "GSM-7",
"segments_per_recipient": 1,
"replayed": false,
"queued_count": 1
}statusstringrequiredqueued_message_idsarray<string>requiredOne message ID per accepted unique recipient, in accepted request order after normalization, duplicate removal, and destination validation.
View nested fields
Array items
Type: string
total_costnumberrequiredAmount reserved for accepted recipients. Unused segments are credited when the submitted sending charge is settled.
invalid_numbersarray<object>requiredRejected numbers and reasons. This list is separate from and not positionally paired with queued_message_ids.
View nested fields
Array items
numberstringrequiredreasonstringrequirederror_codestringoptionalOptional recipient or encoding validation reason; this recipient was not queued or charged and has no delivery webhook.
Additional properties: not allowed.
duplicates_removedarray<string>requiredNormalized duplicate recipients that were not queued or charged.
View nested fields
Array items
Type: string
encodingstringrequiredEncoding of the original input text. See text_conversion for destination-specific sending variants.
segments_per_recipientintegerrequiredSegment count of the original input. The destination-specific maximum is included in text_conversion; total_cost reserves the quoted maximum.
replayedbooleanrequiredqueued_countintegerrequiredNumber of accepted recipients queued; equal to the number of queued_message_ids.
text_conversionarray<object>optionalDestination-specific text and segment options, frozen when the message is queued. Delivery/status events describe the actual submitted variant.
View nested fields
Array items
tostringoptionalconversion_possiblebooleanoptionalconversion_requiredbooleanoptionalconverted_messagestringoptionalencoding_optionsarray<string>optionalView nested fields
Array items
Type: string
{
"enum": [
"GSM-7",
"UCS-2"
]
}min_segmentsintegeroptionalmax_segmentsintegeroptionalAdditional properties: not allowed.
Additional properties: not allowed.
Full schema
{
"type": "object",
"additionalProperties": false,
"properties": {
"status": {
"type": "string",
"enum": [
"success"
]
},
"queued_message_ids": {
"type": "array",
"items": {
"type": "string"
},
"description": "One message ID per accepted unique recipient, in accepted request order after normalization, duplicate removal, and destination validation."
},
"total_cost": {
"type": "number",
"description": "Amount reserved for accepted recipients. Unused segments are credited when the submitted sending charge is settled."
},
"invalid_numbers": {
"type": "array",
"description": "Rejected numbers and reasons. This list is separate from and not positionally paired with queued_message_ids.",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"number",
"reason"
],
"properties": {
"number": {
"type": "string"
},
"reason": {
"type": "string"
},
"error_code": {
"type": "string",
"enum": [
"unsupported_encoding",
"invalid_destination"
],
"description": "Optional recipient or encoding validation reason; this recipient was not queued or charged and has no delivery webhook."
}
}
}
},
"duplicates_removed": {
"type": "array",
"description": "Normalized duplicate recipients that were not queued or charged.",
"items": {
"type": "string"
}
},
"encoding": {
"type": "string",
"description": "Encoding of the original input text. See text_conversion for destination-specific sending variants."
},
"segments_per_recipient": {
"type": "integer",
"description": "Segment count of the original input. The destination-specific maximum is included in text_conversion; total_cost reserves the quoted maximum."
},
"replayed": {
"type": "boolean"
},
"queued_count": {
"type": "integer",
"description": "Number of accepted recipients queued; equal to the number of queued_message_ids."
},
"text_conversion": {
"type": "array",
"description": "Destination-specific text and segment options, frozen when the message is queued. Delivery/status events describe the actual submitted variant.",
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"to": {
"type": "string"
},
"conversion_possible": {
"type": "boolean"
},
"conversion_required": {
"type": "boolean"
},
"converted_message": {
"type": "string",
"nullable": true
},
"encoding_options": {
"type": "array",
"items": {
"type": "string",
"enum": [
"GSM-7",
"UCS-2"
]
}
},
"min_segments": {
"type": "integer",
"minimum": 1
},
"max_segments": {
"type": "integer",
"minimum": 1
}
}
}
}
},
"required": [
"status",
"queued_message_ids",
"total_cost",
"invalid_numbers",
"duplicates_removed",
"encoding",
"segments_per_recipient",
"replayed",
"queued_count"
]
}400 Request validation failed, including sender, recipient, message, or idempotency conflicts.
application/json
{
"status": "error",
"message": "Sender ID must be 1–11 letters (A–Z)."
}{
"status": "error",
"message": "`from` is required."
}{
"status": "error",
"message": "Sender ID is not approved. Request approval in the portal before using it."
}{
"status": "error",
"message": "`to` must be a non-empty array."
}{
"status": "error",
"message": "Invalid destination number. Check the country code and full phone number."
}{
"status": "error",
"message": "No valid destinations provided."
}{
"status": "error",
"message": "`message` is required."
}{
"status": "error",
"message": "`idempotency_key` must be 8–128 letters, numbers, dots, colons, underscores, or dashes."
}{
"status": "error",
"message": "Idempotency key is already associated with a different SMS submission."
}{
"status": "error",
"message": "Idempotency key is already associated with an incomplete SMS submission."
}{
"status": "error",
"message": "Additional information about your SMS use case is required. Contact support with the reference."
}{
"status": "error",
"message": "This SMS was stopped by content screening. Contact support with the reference."
}{
"status": "error",
"message": "Additional information about your SMS use case is required. Contact support with the reference.",
"error_code": "sms_use_case_information_required",
"fields": {
"review_reference": "0123456789abcdef0123456789abcdef"
}
}{
"status": "error",
"error_code": "unsupported_encoding",
"message": "This message requires Unicode (UCS-2), which is not supported for this destination. Use GSM-7 (GSM 03.38) characters and try again."
}{
"status": "error",
"error_code": "invalid_destination",
"message": "Invalid destination number. Check the country code and full phone number."
}statusstringrequiredmessagestringrequirederror_codestringoptionalfieldsobjectoptionalView nested fields
review_referencestringoptionalAdditional properties: not allowed.
Additional properties: not allowed.
Full schema
{
"type": "object",
"additionalProperties": false,
"required": [
"status",
"message"
],
"properties": {
"status": {
"type": "string",
"enum": [
"error"
]
},
"message": {
"type": "string"
},
"error_code": {
"type": "string"
},
"fields": {
"type": "object",
"additionalProperties": false,
"properties": {
"review_reference": {
"type": "string",
"pattern": "^[a-f0-9]{32}$"
}
}
}
}
}401 The API key is missing or invalid.
application/json
{
"status": "error",
"message": "Invalid API key."
}{
"status": "error",
"message": "Missing API key."
}statusstringrequiredmessagestringrequirederror_codestringoptionalPublic incident or validation code when the endpoint provides one.
fieldsobjectoptionalPublic validation details when supplied by the endpoint.
Additional properties: not allowed.
Full schema
{
"type": "object",
"additionalProperties": false,
"required": [
"status",
"message"
],
"properties": {
"status": {
"type": "string",
"enum": [
"error"
]
},
"message": {
"type": "string"
},
"error_code": {
"type": "string",
"nullable": true,
"description": "Public incident or validation code when the endpoint provides one."
},
"fields": {
"type": "object",
"additionalProperties": true,
"description": "Public validation details when supplied by the endpoint."
}
}
}402 The account does not have enough available credit.
application/json
{
"status": "error",
"message": "Insufficient credit."
}statusstringrequiredmessagestringrequirederror_codestringoptionalPublic incident or validation code when the endpoint provides one.
fieldsobjectoptionalPublic validation details when supplied by the endpoint.
Additional properties: not allowed.
Full schema
{
"type": "object",
"additionalProperties": false,
"required": [
"status",
"message"
],
"properties": {
"status": {
"type": "string",
"enum": [
"error"
]
},
"message": {
"type": "string"
},
"error_code": {
"type": "string",
"nullable": true,
"description": "Public incident or validation code when the endpoint provides one."
},
"fields": {
"type": "object",
"additionalProperties": true,
"description": "Public validation details when supplied by the endpoint."
}
}
}403 API or SMS access, account role, or KYC policy does not permit the request.
application/json
{
"status": "error",
"message": "SMS is disabled for this account."
}{
"status": "error",
"message": "API access is disabled."
}{
"status": "error",
"message": "API access is disabled for this account."
}{
"status": "error",
"message": "Only portal administrators can use the SMS API."
}statusstringrequiredmessagestringrequirederror_codestringoptionalPublic incident or validation code when the endpoint provides one.
fieldsobjectoptionalPublic validation details when supplied by the endpoint.
Additional properties: not allowed.
Full schema
{
"type": "object",
"additionalProperties": false,
"required": [
"status",
"message"
],
"properties": {
"status": {
"type": "string",
"enum": [
"error"
]
},
"message": {
"type": "string"
},
"error_code": {
"type": "string",
"nullable": true,
"description": "Public incident or validation code when the endpoint provides one."
},
"fields": {
"type": "object",
"additionalProperties": true,
"description": "Public validation details when supplied by the endpoint."
}
}
}413 The JSON request body exceeds the accepted size.
application/json
{
"status": "error",
"message": "JSON request body is too large."
}statusstringrequiredmessagestringrequirederror_codestringoptionalPublic incident or validation code when the endpoint provides one.
fieldsobjectoptionalPublic validation details when supplied by the endpoint.
Additional properties: not allowed.
Full schema
{
"type": "object",
"additionalProperties": false,
"required": [
"status",
"message"
],
"properties": {
"status": {
"type": "string",
"enum": [
"error"
]
},
"message": {
"type": "string"
},
"error_code": {
"type": "string",
"nullable": true,
"description": "Public incident or validation code when the endpoint provides one."
},
"fields": {
"type": "object",
"additionalProperties": true,
"description": "Public validation details when supplied by the endpoint."
}
}
}429 The API request rate was exceeded.
application/json
{
"status": "error",
"message": "Too many API requests. Please retry in a minute."
}statusstringrequiredmessagestringrequirederror_codestringoptionalPublic incident or validation code when the endpoint provides one.
fieldsobjectoptionalPublic validation details when supplied by the endpoint.
Additional properties: not allowed.
Full schema
{
"type": "object",
"additionalProperties": false,
"required": [
"status",
"message"
],
"properties": {
"status": {
"type": "string",
"enum": [
"error"
]
},
"message": {
"type": "string"
},
"error_code": {
"type": "string",
"nullable": true,
"description": "Public incident or validation code when the endpoint provides one."
},
"fields": {
"type": "object",
"additionalProperties": true,
"description": "Public validation details when supplied by the endpoint."
}
}
}500 An unexpected server error occurred. Quote error_code when contacting support.
application/json
{
"status": "error",
"message": "There is a problem with our backend services right now. Please try again later.",
"error_code": "IVERR-20260814101501-example"
}statusstringrequiredmessagestringrequirederror_codestringoptionalPublic incident or validation code when the endpoint provides one.
fieldsobjectoptionalPublic validation details when supplied by the endpoint.
Additional properties: not allowed.
Full schema
{
"type": "object",
"additionalProperties": false,
"required": [
"status",
"message"
],
"properties": {
"status": {
"type": "string",
"enum": [
"error"
]
},
"message": {
"type": "string"
},
"error_code": {
"type": "string",
"nullable": true,
"description": "Public incident or validation code when the endpoint provides one."
},
"fields": {
"type": "object",
"additionalProperties": true,
"description": "Public validation details when supplied by the endpoint."
}
}
}503 SMS idempotency state or the telephony dependency is temporarily unavailable.
application/json
{
"status": "error",
"message": "Unable to verify SMS idempotency state right now."
}{
"status": "error",
"message": "Telephony service is unavailable. Try again later."
}statusstringrequiredmessagestringrequirederror_codestringoptionalPublic incident or validation code when the endpoint provides one.
fieldsobjectoptionalPublic validation details when supplied by the endpoint.
Additional properties: not allowed.
Full schema
{
"type": "object",
"additionalProperties": false,
"required": [
"status",
"message"
],
"properties": {
"status": {
"type": "string",
"enum": [
"error"
]
},
"message": {
"type": "string"
},
"error_code": {
"type": "string",
"nullable": true,
"description": "Public incident or validation code when the endpoint provides one."
},
"fields": {
"type": "object",
"additionalProperties": true,
"description": "Public validation details when supplied by the endpoint."
}
}
}Sandbox scenarios
Use https://sandbox-api.illyvoip.com with Sandbox credentials and the X-Illyvoip-Sandbox-Scenario header. Omit the header for the documented success default. This header belongs to sandbox requests.
success, not-found, rate-limited, forbidden, service-unavailable, insufficient-credit, idempotency-conflict
Try this operation in Sandbox · Environment setup and limitations