eSMS AfricaeSMS Africa
SMS Platform

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.

FieldTypeDefaultWhat it does
flashbooleanfalseFlash SMS (message class 0): shown immediately on screen, not stored in the inbox by most handsets.
validity_periodinteger (minutes, 1-4320)network defaultHow long the network keeps retrying an undeliverable message before it expires.
priorityinteger (0-3)0Relative priority on the platform queue: 0 normal, 1 high, 2 higher, 3 highest/urgent.
binaryobject-Raw binary SMS (vCards, WAP Push/OTA, ringtones/logos). Advanced.
schedule_mode / scheduled_atstringnowDeliver 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" }'
send-sms.mjs
// 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;
}
send_sms.py
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
send_sms.php
<?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;
}
sendsms.go
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.

FieldTypeRequiredDescription
binary.datastring (hex)YesThe payload, hex encoded (e.g. 424547494E...).
binary.udhstring (hex)NoUser 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_codingintegerNo4 = 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).
  • binary cannot be combined with flash, and is not available on send-bulk.
  • Invalid hex, a UDH whose first byte does not match its length, or more than 140 octets returns 400 with code invalid_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:VCARD
curl -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); // 1
vcard = "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 imports

Scheduled 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 imports

The 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)" }
}
codeWhen
invalid_optionA bad flash, validity_period, priority or binary value, or a combination that is not allowed (e.g. flash with binary, binary on bulk).
invalid_binarybinary.data / binary.udh is not valid hex, the UDH length byte does not match, or UDH + data is over 140 octets.
feature_not_enabledflash, or binary with a udh, is not switched on for the platform yet - contact support.
destination_unavailableSingle send: the destination country is on request. Contact [email protected]. Not charged.
destinations_unavailableBulk 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.

On this page