SMS Universal API
What is SMS Universal
SMS Universal is the modern and recommended format for sending SMS messages through the Qlara platform. Each HTTP request corresponds to a single message to a single recipient, making integration simple and straightforward.
Compared to the Legacy format, SMS Universal offers native support for placeholders, delivery notifications, simulation mode, and scheduled sending.
Endpoint
POST https://lora-api.agiletelecom.com/api/message-server/sms/send
Content-Type: application/json
Authentication: API Key (X-Api-Key) or Basic Auth (Authorization: Basic)
Request fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
destination | string | Yes | – | Recipient phone number in international format (e.g., +393401234567) |
sender | string | Yes | – | Message sender. Can be alphanumeric (max 11 characters, e.g., MyBrand) or numeric (e.g., +393401234567) |
body | string | Yes | – | SMS message text. Maximum length depends on the encoding (see Text encoding section) |
campaignId | string | No | – | Campaign identifier for grouping multiple messages. Maximum 255 characters |
messageId | string | No | (auto) | Custom message identifier for tracking. If not provided, it is automatically generated (UUID) |
udhData | string | No | – | User Data Header in hexadecimal format. Used for binary messages or manual concatenation. In most cases, this is not needed |
simulation | boolean | No | false | If true, the message is processed and validated but not physically sent. Useful for testing and debugging |
enableNotification | boolean | No | true | If true, enables delivery notifications to your callback URL |
placeholders | object | No | – | Key-value map to replace placeholders in the text. E.g., {"name": "Mario"} replaces {name} in the body field |
scheduledDate | string | No | – | Scheduled send date and time. Format: yyyy-MM-dd HH:mm:ss.SSSZ (e.g., 2025-10-01 09:00:00.000+0000) |
skipRcsOverride | boolean | No | false | If true, the SMS is sent directly without going through any RCS override configured at the account level |
Response
All send calls return a response with this structure:
{
"messageId": "e76614d1-4ac1-4d94-89f0-d07f1b5a190c",
"simulation": false,
"results": {
"sms": {
"accepted": true,
"unicode": false,
"parts": 1,
"reasons": []
}
}
}
| Field | Type | Description |
|---|---|---|
messageId | string | Unique message ID. Use it to correlate delivery notifications |
simulation | boolean | true if the message was in simulation mode |
results.sms.accepted | boolean | true if the message was accepted for sending |
results.sms.unicode | boolean | true if the message contains Unicode characters (UTF-16 encoding) |
results.sms.parts | integer | Number of SMS parts the message was split into |
results.sms.reasons | array | List of rejection reasons (empty if accepted) |
Important:
accepted: truemeans the message was accepted for sending, not that it was delivered. Delivery confirmation arrives via the Delivery Notification callback.
Text encoding
GSM-7 (standard)
:::tip Cost savings Use GSM-7 encoding when possible: it supports up to 160 characters per single message, compared to 70 for Unicode. Avoid emoji and non-Latin characters to keep costs at a minimum. :::
The default encoding for SMS. Supports Latin letters, numbers, common symbols, and some special characters.
- Single message: up to 160 characters
- Concatenated message: each part contains 153 characters (7 characters reserved for the concatenation header)
Unicode / UTF-16
Used automatically when the text contains characters not supported by GSM-7 (e.g., emoji, Chinese, Arabic, Cyrillic characters).
- Single message: up to 70 characters
- Concatenated message: each part contains 67 characters (3 characters reserved for the concatenation header)
Concatenation rules
:::warning Character limits and costs A single SMS supports 160 GSM-7 characters or 70 Unicode characters. Exceeding these limits causes the message to be split into multiple parts, and each part is billed separately. :::
When the text exceeds the maximum length of a single part, the message is automatically split into multiple parts (concatenation). The recipient's device reassembles the parts into a single message.
| Encoding | 1 part | 2 parts | 3 parts | N parts |
|---|---|---|---|---|
| GSM-7 | 160 chars | 306 chars | 459 chars | N x 153 chars |
| Unicode | 70 chars | 134 chars | 201 chars | N x 67 chars |
Note: Each SMS part is billed separately. The
results.sms.partsfield in the response indicates the number of parts.
Delivery notifications (Delivery Notification)
If enableNotification is true, the system sends an HTTP POST callback to your URL when the message status changes.
Callback format
{
"channel": "SMS",
"eventType": "DELIVERY",
"messageId": "e76614d1-4ac1-4d94-89f0-d07f1b5a190c",
"destination": "+393401234567",
"statusCode": 3,
"description": "delivered",
"price": 0.035,
"numPart": 1,
"totalParts": 1,
"eventDate": "2025-10-16T10:42:18Z"
}
| Field | Type | Description |
|---|---|---|
channel | string | Always "SMS" for this channel |
eventType | string | Always "DELIVERY" |
messageId | string | Message ID (the same one returned in the send response) |
destination | string | Recipient number |
statusCode | integer | Status code: 3 = delivered, 6 = undeliverable |
description | string | Textual description of the status |
price | number | Cost of the individual SMS part |
numPart | integer | Current part number (e.g., 1, 2, 3...) |
totalParts | integer | Total number of parts in the message |
eventDate | string | Event date and time in ISO 8601 format |
Status codes
| statusCode | Meaning |
|---|---|
3 | Message delivered to the recipient's device |
6 | Message undeliverable (non-existent number, phone off, etc.) |
Examples
1. Simple send
Basic SMS message send:
- cURL
- Node.js
- Python
- PHP
curl -X POST "https://lora-api.agiletelecom.com/api/message-server/sms/send" \
-H "X-Api-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"destination": "+393401234567",
"sender": "MyBrand",
"body": "Ciao! Il tuo ordine #12345 è stato spedito."
}'
const response = await fetch(
"https://lora-api.agiletelecom.com/api/message-server/sms/send",
{
method: "POST",
headers: {
"X-Api-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
destination: "+393401234567",
sender: "MyBrand",
body: "Ciao! Il tuo ordine #12345 è stato spedito.",
}),
}
);
const data = await response.json();
console.log(data);
import requests
response = requests.post(
"https://lora-api.agiletelecom.com/api/message-server/sms/send",
headers={
"X-Api-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
json={
"destination": "+393401234567",
"sender": "MyBrand",
"body": "Ciao! Il tuo ordine #12345 è stato spedito.",
},
)
print(response.json())
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => "https://lora-api.agiletelecom.com/api/message-server/sms/send",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"X-Api-Key: YOUR_API_KEY",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"destination" => "+393401234567",
"sender" => "MyBrand",
"body" => "Ciao! Il tuo ordine #12345 è stato spedito.",
]),
]);
$response = curl_exec($ch);
curl_close($ch);
echo $response;
Response:
{
"messageId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"simulation": false,
"results": {
"sms": {
"accepted": true,
"unicode": false,
"parts": 1,
"reasons": []
}
}
}
2. Send with placeholders
Placeholders in the body field are replaced with values from the placeholders map:
- cURL
- Node.js
- Python
- PHP
curl -X POST "https://lora-api.agiletelecom.com/api/message-server/sms/send" \
-H "X-Api-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"destination": "+393401234567",
"sender": "MyBrand",
"body": "Ciao {nome}, il tuo appuntamento è confermato per il {data} alle {ora}.",
"placeholders": {
"nome": "Mario",
"data": "15/03/2025",
"ora": "14:30"
},
"enableNotification": true
}'
const response = await fetch(
"https://lora-api.agiletelecom.com/api/message-server/sms/send",
{
method: "POST",
headers: {
"X-Api-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
destination: "+393401234567",
sender: "MyBrand",
body: "Ciao {nome}, il tuo appuntamento è confermato per il {data} alle {ora}.",
placeholders: {
nome: "Mario",
data: "15/03/2025",
ora: "14:30",
},
enableNotification: true,
}),
}
);
const data = await response.json();
console.log(data);
import requests
response = requests.post(
"https://lora-api.agiletelecom.com/api/message-server/sms/send",
headers={
"X-Api-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
json={
"destination": "+393401234567",
"sender": "MyBrand",
"body": "Ciao {nome}, il tuo appuntamento è confermato per il {data} alle {ora}.",
"placeholders": {
"nome": "Mario",
"data": "15/03/2025",
"ora": "14:30",
},
"enableNotification": True,
},
)
print(response.json())
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => "https://lora-api.agiletelecom.com/api/message-server/sms/send",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"X-Api-Key: YOUR_API_KEY",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"destination" => "+393401234567",
"sender" => "MyBrand",
"body" => "Ciao {nome}, il tuo appuntamento è confermato per il {data} alle {ora}.",
"placeholders" => [
"nome" => "Mario",
"data" => "15/03/2025",
"ora" => "14:30",
],
"enableNotification" => true,
]),
]);
$response = curl_exec($ch);
curl_close($ch);
echo $response;
The recipient will receive: "Ciao Mario, il tuo appuntamento è confermato per il 15/03/2025 alle 14:30."
3. Scheduled send
Schedule the message to be sent at a future date and time:
- cURL
- Node.js
- Python
- PHP
curl -X POST "https://lora-api.agiletelecom.com/api/message-server/sms/send" \
-H "X-Api-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"destination": "+393401234567",
"sender": "MyBrand",
"body": "Promemoria: la tua prenotazione è domani alle 10:00.",
"scheduledDate": "2025-10-01 09:00:00.000+0000",
"campaignId": "reminder-ottobre-2025"
}'
const response = await fetch(
"https://lora-api.agiletelecom.com/api/message-server/sms/send",
{
method: "POST",
headers: {
"X-Api-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
destination: "+393401234567",
sender: "MyBrand",
body: "Promemoria: la tua prenotazione è domani alle 10:00.",
scheduledDate: "2025-10-01 09:00:00.000+0000",
campaignId: "reminder-ottobre-2025",
}),
}
);
const data = await response.json();
console.log(data);
import requests
response = requests.post(
"https://lora-api.agiletelecom.com/api/message-server/sms/send",
headers={
"X-Api-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
json={
"destination": "+393401234567",
"sender": "MyBrand",
"body": "Promemoria: la tua prenotazione è domani alle 10:00.",
"scheduledDate": "2025-10-01 09:00:00.000+0000",
"campaignId": "reminder-ottobre-2025",
},
)
print(response.json())
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => "https://lora-api.agiletelecom.com/api/message-server/sms/send",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"X-Api-Key: YOUR_API_KEY",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"destination" => "+393401234567",
"sender" => "MyBrand",
"body" => "Promemoria: la tua prenotazione è domani alle 10:00.",
"scheduledDate" => "2025-10-01 09:00:00.000+0000",
"campaignId" => "reminder-ottobre-2025",
]),
]);
$response = curl_exec($ch);
curl_close($ch);
echo $response;
4. Simulation mode
The message is validated but not sent. Useful for verifying the correctness of the request before actual sending:
- cURL
- Node.js
- Python
- PHP
curl -X POST "https://lora-api.agiletelecom.com/api/message-server/sms/send" \
-H "X-Api-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"destination": "+393401234567",
"sender": "MyBrand",
"body": "Questo è un messaggio di test con emoji 🎉",
"simulation": true
}'
const response = await fetch(
"https://lora-api.agiletelecom.com/api/message-server/sms/send",
{
method: "POST",
headers: {
"X-Api-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
destination: "+393401234567",
sender: "MyBrand",
body: "Questo è un messaggio di test con emoji 🎉",
simulation: true,
}),
}
);
const data = await response.json();
console.log(data);
import requests
response = requests.post(
"https://lora-api.agiletelecom.com/api/message-server/sms/send",
headers={
"X-Api-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
json={
"destination": "+393401234567",
"sender": "MyBrand",
"body": "Questo è un messaggio di test con emoji 🎉",
"simulation": True,
},
)
print(response.json())
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => "https://lora-api.agiletelecom.com/api/message-server/sms/send",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"X-Api-Key: YOUR_API_KEY",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"destination" => "+393401234567",
"sender" => "MyBrand",
"body" => "Questo è un messaggio di test con emoji 🎉",
"simulation" => true,
]),
]);
$response = curl_exec($ch);
curl_close($ch);
echo $response;
Response:
{
"messageId": "f1e2d3c4-b5a6-7890-abcd-ef0987654321",
"simulation": true,
"results": {
"sms": {
"accepted": true,
"unicode": true,
"parts": 1,
"reasons": []
}
}
}
Note: The response shows
unicode: truebecause the message contains an emoji.
Error codes
| HTTP Code | Meaning | What to do |
|---|---|---|
200 | Request processed | Check results.sms.accepted to verify acceptance |
400 | Bad request | Verify required fields (destination, sender, body) and data format |
401 | Not authenticated | Verify your API Key or Basic Auth credentials |
403 | Access denied | IP not in whitelist or unauthorized resource |
404 | Not found | Incorrect endpoint |
422 | Unprocessable entity | Data is syntactically correct but semantically invalid (e.g., invalid recipient number) |
429 | Too many requests | You have exceeded the rate limit. Retry after the period indicated in the Retry-After header |
500 | Internal server error | Retry later. If the problem persists, contact support |