Send Options
Flash SMS, validity period, priority, binary (smart messaging) and scheduled delivery on the SMS HTTP API.
On top of to, text and sender_id, POST /api/messages/send
accepts optional fields that control how a message is delivered. The same
endpoint is also reachable as POST /v1/messages/send.
| Field | Type | Default | What it does |
|---|---|---|---|
flash | boolean | false | Flash SMS (message class 0): shown immediately on screen, not stored in the inbox by most handsets. |
validity_period | integer (minutes, 1-4320) | network default | How long the network keeps retrying an undeliverable message before it expires. |
priority | integer (0-3) | 0 | Relative priority on the platform queue: 0 normal, 1 high, 2 higher, 3 highest/urgent. |
binary | object | - | Raw binary SMS (vCards, WAP Push/OTA, ringtones/logos). Advanced. |
schedule_mode / scheduled_at | string | now | Deliver later (5 minutes to 7 days ahead). |
None of these change the price: a flash, prioritised or time-limited message is billed exactly like a normal SMS with the same segments. A binary message is always billed as 1 segment.
POST /api/messages/send-bulk also
accepts flash, validity_period and priority (but not binary).
Rolling out
flash, and binary with a udh, are being rolled out on the platform. Until
they are switched on, the API returns 400 with code feature_not_enabled -
contact support for availability. Plain binary
(no udh), validity_period and priority work for every account. Flash and
binary are also not yet available for every destination country (400
invalid_option names the country).
Calling the API
The official SDKs do not have typed fields for these options yet
(their send methods only forward the standard fields), so the examples below
call the HTTP API directly. Each language tab defines a small sendSms helper
once; the examples further down reuse it. Keep using the SDKs for everything
else (status, balance, routes, Verify).
export ESMS_API_KEY="esms_live_your_api_key"
curl -X POST https://sms.esmsafrica.io/api/messages/send \
-H "Authorization: Bearer $ESMS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{ "to": "+254712345678", "text": "Hello", "sender_id": "MyApp" }'// Node.js 18+ (global fetch and crypto.randomUUID)
export async function sendSms(body) {
const res = await fetch('https://sms.esmsafrica.io/api/messages/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.ESMS_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify(body),
});
const data = await res.json();
if (!res.ok) {
// e.g. { code: "invalid_option", message: "..." }
throw new Error(`${res.status} ${JSON.stringify(data.detail ?? data)}`);
}
return data;
}import os
import uuid
import requests
def send_sms(body: dict) -> dict:
res = requests.post(
"https://sms.esmsafrica.io/api/messages/send",
headers={
"Authorization": f"Bearer {os.environ['ESMS_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json=body,
timeout=30,
)
data = res.json()
if not res.ok:
# e.g. {"code": "invalid_option", "message": "..."}
raise RuntimeError(f"{res.status_code} {data.get('detail', data)}")
return data<?php
function sendSms(array $body): array
{
$ch = curl_init('https://sms.esmsafrica.io/api/messages/send');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('ESMS_API_KEY'),
'Content-Type: application/json',
'Idempotency-Key: ' . bin2hex(random_bytes(16)),
],
CURLOPT_POSTFIELDS => json_encode($body),
]);
$raw = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$data = json_decode($raw, true);
if ($status >= 400) {
// e.g. ['code' => 'invalid_option', 'message' => '...']
throw new RuntimeException($status . ' ' . json_encode($data['detail'] ?? $data));
}
return $data;
}package main
import (
"bytes"
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"os"
)
func sendSms(body map[string]any) (map[string]any, error) {
payload, _ := json.Marshal(body)
req, err := http.NewRequest("POST", "https://sms.esmsafrica.io/api/messages/send", bytes.NewReader(payload))
if err != nil {
return nil, err
}
key := make([]byte, 16)
rand.Read(key)
req.Header.Set("Authorization", "Bearer "+os.Getenv("ESMS_API_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", hex.EncodeToString(key))
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var data map[string]any
if err := json.NewDecoder(res.Body).Decode(&data); err != nil {
return nil, err
}
if res.StatusCode >= 400 {
// e.g. {"code": "invalid_option", "message": "..."}
return nil, fmt.Errorf("%d %v", res.StatusCode, data["detail"])
}
return data, nil
}Flash SMS
Set flash: true to send a class 0 message that pops up on the recipient's
screen without being saved to the inbox (on most handsets). It works with both
GSM-7 and Unicode text and is billed like a normal SMS with the same segments.
Handset support varies, and some networks deliver flash messages as a normal
SMS. Do not rely on flash for anything the recipient must be able to read
again later. flash cannot be combined with binary.
curl -X POST https://sms.esmsafrica.io/api/messages/send \
-H "Authorization: Bearer $ESMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+254712345678",
"text": "Your table is ready. Please come to the front desk.",
"sender_id": "MyApp",
"flash": true
}'const res = await sendSms({
to: '+254712345678',
text: 'Your table is ready. Please come to the front desk.',
sender_id: 'MyApp',
flash: true,
});
console.log(res.id, res.options); // { flash: true, ... }res = send_sms({
"to": "+254712345678",
"text": "Your table is ready. Please come to the front desk.",
"sender_id": "MyApp",
"flash": True,
})
print(res["id"], res["options"])$res = sendSms([
'to' => '+254712345678',
'text' => 'Your table is ready. Please come to the front desk.',
'sender_id' => 'MyApp',
'flash' => true,
]);
echo $res['id'];res, err := sendSms(map[string]any{
"to": "+254712345678",
"text": "Your table is ready. Please come to the front desk.",
"sender_id": "MyApp",
"flash": true,
})
if err != nil {
panic(err)
}
fmt.Println(res["id"], res["options"])Validity period and priority (OTP)
validity_period is the number of minutes (1-4320, i.e. up to 3 days) the
network keeps trying to deliver a message to a phone that is off or out of
coverage. After that the message expires instead of arriving late. Omit it to
use the network default. For one-time passwords, match it to your code expiry
(for example 10).
priority moves a message ahead on the platform queue: 0 normal (default),
1 high, 2 higher, 3 highest/urgent. Use it for OTPs and time-critical
alerts. The price is the same at every priority.
curl -X POST https://sms.esmsafrica.io/api/messages/send \
-H "Authorization: Bearer $ESMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+447700900123",
"text": "Your login code is 482913. It expires in 10 minutes.",
"sender_id": "MyApp",
"validity_period": 10,
"priority": 3
}'const res = await sendSms({
to: '+447700900123',
text: 'Your login code is 482913. It expires in 10 minutes.',
sender_id: 'MyApp',
validity_period: 10, // minutes
priority: 3, // highest
});res = send_sms({
"to": "+447700900123",
"text": "Your login code is 482913. It expires in 10 minutes.",
"sender_id": "MyApp",
"validity_period": 10, # minutes
"priority": 3, # highest
})$res = sendSms([
'to' => '+447700900123',
'text' => 'Your login code is 482913. It expires in 10 minutes.',
'sender_id' => 'MyApp',
'validity_period' => 10, // minutes
'priority' => 3, // highest
]);res, err := sendSms(map[string]any{
"to": "+447700900123",
"text": "Your login code is 482913. It expires in 10 minutes.",
"sender_id": "MyApp",
"validity_period": 10, // minutes
"priority": 3, // highest
})Sending OTPs? eSMS Verify generates, sends and checks the code for you, including expiry and retry limits.
Binary SMS (advanced)
binary sends a raw 8-bit message for smart-messaging content such as vCards,
WAP Push/OTA configuration, ringtones or operator logos. Leave text out (or
send it empty) when you use binary.
| Field | Type | Required | Description |
|---|---|---|---|
binary.data | string (hex) | Yes | The payload, hex encoded (e.g. 424547494E...). |
binary.udh | string (hex) | No | User Data Header. The first byte is the UDH length, e.g. 0605040B8423F0 (port addressing to port 0B84, used for WAP Push) or 06050423F40000 (port 23F4, vCard). |
binary.data_coding | integer | No | 4 = 8-bit binary (default), 8 = UCS-2. |
Rules:
- UDH + data must fit in one SMS: at most 140 octets in total. Each request is billed as exactly 1 segment.
- For longer payloads, split the data yourself into several requests and add a
concatenation element to each UDH (for example IEI
00: reference, total parts, part number). binarycannot be combined withflash, and is not available onsend-bulk.- Invalid hex, a UDH whose first byte does not match its length, or more than
140 octets returns
400with codeinvalid_binary.
The example sends a contact card (vCard) to the handset's vCard port. The payload is the hex encoding of:
BEGIN:VCARD
VERSION:2.1
N:Doe;Jane
TEL:+254712345678
END:VCARDcurl -X POST https://sms.esmsafrica.io/api/messages/send \
-H "Authorization: Bearer $ESMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+254712345678",
"sender_id": "MyApp",
"binary": {
"udh": "06050423F40000",
"data": "424547494E3A56434152440D0A56455253494F4E3A322E310D0A4E3A446F653B4A616E650D0A54454C3A2B3235343731323334353637380D0A454E443A56434152440D0A",
"data_coding": 4
}
}'const vcard = 'BEGIN:VCARD\r\nVERSION:2.1\r\nN:Doe;Jane\r\nTEL:+254712345678\r\nEND:VCARD\r\n';
const res = await sendSms({
to: '+254712345678',
sender_id: 'MyApp',
binary: {
udh: '06050423F40000', // port addressing, destination port 0x23F4 (vCard)
data: Buffer.from(vcard, 'utf8').toString('hex').toUpperCase(),
data_coding: 4, // 8-bit binary
},
});
console.log(res.segments); // 1vcard = "BEGIN:VCARD\r\nVERSION:2.1\r\nN:Doe;Jane\r\nTEL:+254712345678\r\nEND:VCARD\r\n"
res = send_sms({
"to": "+254712345678",
"sender_id": "MyApp",
"binary": {
"udh": "06050423F40000", # port addressing, destination port 0x23F4 (vCard)
"data": vcard.encode("utf-8").hex().upper(),
"data_coding": 4, # 8-bit binary
},
})
print(res["segments"]) # 1$vcard = "BEGIN:VCARD\r\nVERSION:2.1\r\nN:Doe;Jane\r\nTEL:+254712345678\r\nEND:VCARD\r\n";
$res = sendSms([
'to' => '+254712345678',
'sender_id' => 'MyApp',
'binary' => [
'udh' => '06050423F40000', // port addressing, destination port 0x23F4 (vCard)
'data' => strtoupper(bin2hex($vcard)),
'data_coding' => 4, // 8-bit binary
],
]);vcard := "BEGIN:VCARD\r\nVERSION:2.1\r\nN:Doe;Jane\r\nTEL:+254712345678\r\nEND:VCARD\r\n"
res, err := sendSms(map[string]any{
"to": "+254712345678",
"sender_id": "MyApp",
"binary": map[string]any{
"udh": "06050423F40000", // port addressing, destination port 0x23F4 (vCard)
"data": strings.ToUpper(hex.EncodeToString([]byte(vcard))),
"data_coding": 4, // 8-bit binary
},
})
// add "strings" to the importsScheduled delivery
Set schedule_mode to scheduled and pass scheduled_at as an ISO 8601 time
with a timezone, between 5 minutes and 7 days ahead. The wallet is charged
when you schedule; the response has status: "scheduled". Scheduling combines
with the other options (for example priority). Changed your mind? Cancel it
before the send time with POST /api/messages/{id}/cancel and the charge is
refunded.
curl -X POST https://sms.esmsafrica.io/api/messages/send \
-H "Authorization: Bearer $ESMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+14155550123",
"text": "Reminder: your appointment is tomorrow at 10:00.",
"schedule_mode": "scheduled",
"scheduled_at": "2026-10-12T09:00:00-07:00"
}'const inOneHour = new Date(Date.now() + 60 * 60 * 1000).toISOString(); // UTC, ends in "Z"
const res = await sendSms({
to: '+14155550123',
text: 'Reminder: your appointment is tomorrow at 10:00.',
schedule_mode: 'scheduled',
scheduled_at: inOneHour,
});
console.log(res.status, res.scheduled_at); // "scheduled", "..."from datetime import datetime, timedelta, timezone
in_one_hour = (datetime.now(timezone.utc) + timedelta(hours=1)).isoformat()
res = send_sms({
"to": "+14155550123",
"text": "Reminder: your appointment is tomorrow at 10:00.",
"schedule_mode": "scheduled",
"scheduled_at": in_one_hour,
})
print(res["status"], res["scheduled_at"])$inOneHour = (new DateTimeImmutable('+1 hour', new DateTimeZone('UTC')))->format(DATE_ATOM);
$res = sendSms([
'to' => '+14155550123',
'text' => 'Reminder: your appointment is tomorrow at 10:00.',
'schedule_mode' => 'scheduled',
'scheduled_at' => $inOneHour,
]);inOneHour := time.Now().UTC().Add(time.Hour).Format(time.RFC3339)
res, err := sendSms(map[string]any{
"to": "+14155550123",
"text": "Reminder: your appointment is tomorrow at 10:00.",
"schedule_mode": "scheduled",
"scheduled_at": inOneHour,
})
// add "time" to the importsThe SDKs also support scheduling natively (messages.schedule(...)), see the
SDK pages.
Bulk sends
send-bulk accepts flash, validity_period and priority and applies them
to every recipient. binary is not available on bulk. Multi-country batches
price each recipient at its own country's rate. If any recipient is in a
country that is still on request (or has an invalid number), the whole request
is rejected with destinations_unavailable (listing those countries and an
invalid_count) and nothing is charged. Pass "skip_unavailable": true to drop
those recipients instead - they are not charged - and send to everyone else.
curl -X POST https://sms.esmsafrica.io/api/messages/send-bulk \
-H "Authorization: Bearer $ESMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"recipients": [
{ "to": "+254712345678" },
{ "to": "+447700900123" }
],
"text": "Service alert: scheduled maintenance tonight 22:00-23:00 UTC.",
"sender_id": "MyApp",
"priority": 2,
"validity_period": 120
}'const res = await fetch('https://sms.esmsafrica.io/api/messages/send-bulk', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.ESMS_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
recipients: [{ to: '+254712345678' }, { to: '+447700900123' }],
text: 'Service alert: scheduled maintenance tonight 22:00-23:00 UTC.',
sender_id: 'MyApp',
priority: 2,
validity_period: 120,
}),
});
console.log(await res.json()); // { batch_id, total_recipients, estimated_cost, status }res = requests.post(
"https://sms.esmsafrica.io/api/messages/send-bulk",
headers={"Authorization": f"Bearer {os.environ['ESMS_API_KEY']}"},
json={
"recipients": [{"to": "+254712345678"}, {"to": "+447700900123"}],
"text": "Service alert: scheduled maintenance tonight 22:00-23:00 UTC.",
"sender_id": "MyApp",
"priority": 2,
"validity_period": 120,
},
timeout=30,
)
print(res.json())$ch = curl_init('https://sms.esmsafrica.io/api/messages/send-bulk');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('ESMS_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'recipients' => [['to' => '+254712345678'], ['to' => '+447700900123']],
'text' => 'Service alert: scheduled maintenance tonight 22:00-23:00 UTC.',
'sender_id' => 'MyApp',
'priority' => 2,
'validity_period' => 120,
]),
]);
$res = json_decode(curl_exec($ch), true);
curl_close($ch);payload, _ := json.Marshal(map[string]any{
"recipients": []map[string]string{{"to": "+254712345678"}, {"to": "+447700900123"}},
"text": "Service alert: scheduled maintenance tonight 22:00-23:00 UTC.",
"sender_id": "MyApp",
"priority": 2,
"validity_period": 120,
})
req, _ := http.NewRequest("POST", "https://sms.esmsafrica.io/api/messages/send-bulk", bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer "+os.Getenv("ESMS_API_KEY"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)The options echo
A successful send response is unchanged, plus an options object that echoes
what was applied:
{
"id": "msg_abc123",
"status": "submitted",
"segments": 1,
"encoding": "GSM7",
"price_per_sms": 1.2,
"cost": 0.0096,
"cost_currency": "USD",
"route": "ESMS_KE",
"balance_after": 12.34,
"scheduled_at": null,
"options": {
"flash": false,
"validity_period": 10,
"priority": 3,
"binary": false
}
}Errors
Validation errors return HTTP 400 with the standard error envelope (error plus the original detail object):
{
"error": {
"code": "invalid_option",
"message": "priority must be 0 (normal), 1, 2 or 3 (highest)",
"request_id": "3f9a1c2b4d5e6f708192a3b4c5d6e7f8"
},
"detail": { "code": "invalid_option", "message": "priority must be 0 (normal), 1, 2 or 3 (highest)" }
}code | When |
|---|---|
invalid_option | A bad flash, validity_period, priority or binary value, or a combination that is not allowed (e.g. flash with binary, binary on bulk). |
invalid_binary | binary.data / binary.udh is not valid hex, the UDH length byte does not match, or UDH + data is over 140 octets. |
feature_not_enabled | flash, or binary with a udh, is not switched on for the platform yet - contact support. |
destination_unavailable | Single send: the destination country is on request. Contact [email protected]. Not charged. |
destinations_unavailable | Bulk send: one or more destination countries are on request (listed in countries). Nothing is charged. |
See Global Coverage for which countries are available and Errors for the full error envelope.