cURL
curl --request POST \
--url https://api.belio.co.ke/message/{serviceId} \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"type": "SendToEach",
"messages": [
{
"text": "<string>",
"phone": "<string>"
}
]
}
'import requests
url = "https://api.belio.co.ke/message/{serviceId}"
payload = {
"type": "SendToEach",
"messages": [
{
"text": "<string>",
"phone": "<string>"
}
]
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({type: 'SendToEach', messages: [{text: '<string>', phone: '<string>'}]})
};
fetch('https://api.belio.co.ke/message/{serviceId}', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.belio.co.ke/message/{serviceId}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'type' => 'SendToEach',
'messages' => [
[
'text' => '<string>',
'phone' => '<string>'
]
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.belio.co.ke/message/{serviceId}"
payload := strings.NewReader("{\n \"type\": \"SendToEach\",\n \"messages\": [\n {\n \"text\": \"<string>\",\n \"phone\": \"<string>\"\n }\n ]\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.belio.co.ke/message/{serviceId}")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"type\": \"SendToEach\",\n \"messages\": [\n {\n \"text\": \"<string>\",\n \"phone\": \"<string>\"\n }\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.belio.co.ke/message/{serviceId}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"type\": \"SendToEach\",\n \"messages\": [\n {\n \"text\": \"<string>\",\n \"phone\": \"<string>\"\n }\n ]\n}"
response = http.request(request)
puts response.read_body{
"desc": {
"requestId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"units": {
"Safaricom": 34,
"Airtel": 16
},
"addresses": {
"Safaricom": 34,
"Airtel": 45
},
"skipped": [
"254700000000",
"254100000000"
]
}
}{
"desc": "<string>",
"result": "<unknown>"
}{
"desc": "<string>",
"result": "<unknown>"
}{
"desc": "<string>",
"result": "<unknown>"
}{
"desc": "<string>",
"result": "<unknown>"
}{
"error": "<string>",
"error_description": "<string>"
}{
"desc": "<string>",
"result": "<unknown>"
}SMS
Send Message
Send a message to one or multiple recipients
POST
/
message
/
{serviceId}
cURL
curl --request POST \
--url https://api.belio.co.ke/message/{serviceId} \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"type": "SendToEach",
"messages": [
{
"text": "<string>",
"phone": "<string>"
}
]
}
'import requests
url = "https://api.belio.co.ke/message/{serviceId}"
payload = {
"type": "SendToEach",
"messages": [
{
"text": "<string>",
"phone": "<string>"
}
]
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({type: 'SendToEach', messages: [{text: '<string>', phone: '<string>'}]})
};
fetch('https://api.belio.co.ke/message/{serviceId}', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.belio.co.ke/message/{serviceId}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'type' => 'SendToEach',
'messages' => [
[
'text' => '<string>',
'phone' => '<string>'
]
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.belio.co.ke/message/{serviceId}"
payload := strings.NewReader("{\n \"type\": \"SendToEach\",\n \"messages\": [\n {\n \"text\": \"<string>\",\n \"phone\": \"<string>\"\n }\n ]\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.belio.co.ke/message/{serviceId}")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"type\": \"SendToEach\",\n \"messages\": [\n {\n \"text\": \"<string>\",\n \"phone\": \"<string>\"\n }\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.belio.co.ke/message/{serviceId}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"type\": \"SendToEach\",\n \"messages\": [\n {\n \"text\": \"<string>\",\n \"phone\": \"<string>\"\n }\n ]\n}"
response = http.request(request)
puts response.read_body{
"desc": {
"requestId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"units": {
"Safaricom": 34,
"Airtel": 16
},
"addresses": {
"Safaricom": 34,
"Airtel": 45
},
"skipped": [
"254700000000",
"254100000000"
]
}
}{
"desc": "<string>",
"result": "<unknown>"
}{
"desc": "<string>",
"result": "<unknown>"
}{
"desc": "<string>",
"result": "<unknown>"
}{
"desc": "<string>",
"result": "<unknown>"
}{
"error": "<string>",
"error_description": "<string>"
}{
"desc": "<string>",
"result": "<unknown>"
}Refer to Authentication for information on how to obtain a
bearer token. This endpoint requires the
message.sms.send.oneway API client
authorization scope to send one way messages. You can set up scopes on the
API Clients page.Rate Limit
This endpoint is rate limited to 400 messages per second.Request Modes
The requiredtype field selects one of four request modes. Free-form modes
compose raw message text; template modes send a pre-approved template by
referencing its templateId (see SMS Templates).
type | Kind | Description |
|---|---|---|
SendToEach | Free-form | Message tailored per recipient |
SendToMany | Free-form | Same message sent to multiple recipients |
TemplateSendToEach | Template | Each recipient receives their own parameter values |
TemplateSendToMany | Template | One shared parameter set sent to multiple recipients |
Limits
| Rule | Free-form (SendToEach / SendToMany) | Template (TemplateSendToEach / TemplateSendToMany) |
|---|---|---|
| Maximum recipients per request | 100 | 10 |
| Maximum message length | 960 characters | 960 characters (after parameters are resolved) |
| SMS message unit size | 160 characters | 160 characters |
Request Schemas
- SendToEach
- SendToMany
- TemplateSendToEach
- TemplateSendToMany
Send individually tailored free-form messages to multiple recipients.
Each item in
| Field | Type | Required | Description |
|---|---|---|---|
type | string | yes | Must be "SendToEach" |
messages | object[] | yes | Per-recipient entries (1–100) |
receiptRequest | object | no | Optional delivery receipt callback |
messages:| Field | Type | Required | Description |
|---|---|---|---|
phone | string | yes | Recipient phone number |
message | string | yes | Message text (max 960 characters) |
curl --request POST \
--url https://api.belio.co.ke/message/{serviceId} \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{
"type": "SendToEach",
"messages": [
{
"phone": "254700111213",
"message": "Hi Alice, your order #1042 has shipped."
},
{
"phone": "254700111214",
"message": "Hi Bob, your order #1043 has shipped."
}
]
}'
Send the same free-form message to multiple phone numbers.
| Field | Type | Required | Description |
|---|---|---|---|
type | string | yes | Must be "SendToMany" |
addresses | string[] | yes | Recipient phone numbers (1–100) |
message | string | yes | Message text (max 960 characters) |
receiptRequest | object | no | Optional delivery receipt callback |
curl --request POST \
--url https://api.belio.co.ke/message/{serviceId} \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{
"type": "SendToMany",
"addresses": [
"+25470012345",
"254100000000"
],
"message": "Your appointment is confirmed for tomorrow at 10am.",
"receiptRequest": {
"correlator": "reminder-batch-001",
"callbackUrl": "https://example.com/dlr"
}
}'
Send the same template to multiple recipients, each with their own parameter values.
Each item in
| Field | Type | Required | Description |
|---|---|---|---|
type | string | yes | Must be "TemplateSendToEach" |
templateId | string | yes | ID of the SMS template |
messages | object[] | yes | Per-recipient entries (1–10) |
receiptRequest | object | no | Optional delivery receipt callback |
messages:| Field | Type | Required | Description |
|---|---|---|---|
phone | string | yes | Recipient phone number |
params | object[] | yes | Template parameters for this recipient |
curl --request POST \
--url https://api.belio.co.ke/message/{serviceId} \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{
"type": "TemplateSendToEach",
"templateId": "tpl-sms-001",
"messages": [
{
"phone": "254700111213",
"params": [
{ "name": "sample", "value": "Alice" },
{ "name": "code", "value": "111111" }
]
},
{
"phone": "254700111214",
"params": [
{ "name": "sample", "value": "Bob" },
{ "name": "code", "value": "222222" }
]
}
]
}'
Send the same resolved template message to multiple phone numbers.
| Field | Type | Required | Description |
|---|---|---|---|
type | string | yes | Must be "TemplateSendToMany" |
templateId | string | yes | ID of the SMS template |
addresses | string[] | yes | Recipient phone numbers (1–10) |
params | object[] | yes | Template parameter values |
receiptRequest | object | no | Optional delivery receipt callback |
curl --request POST \
--url https://api.belio.co.ke/message/{serviceId} \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{
"type": "TemplateSendToMany",
"templateId": "tpl-sms-001",
"addresses": [
"+254712345678",
"254100000000"
],
"params": [
{ "name": "sample", "value": "Alice" },
{ "name": "code", "value": "654321" }
],
"receiptRequest": {
"correlator": "otp-batch-001",
"callbackUrl": "https://example.com/dlr"
}
}'
Important: for
TemplateSendToEach and TemplateSendToMany, after all
parameters are substituted, the final message must not exceed 960
characters. A longer resolved message is rejected with Resolved content cannot be longer than 960 characters. There is no truncation or fallback.Errors
General errors
| HTTP | Condition | Example desc |
|---|---|---|
| 400 | Invalid phone numbers | Invalid phone numbers: ... |
| 400 | Empty addresses / messages | No valid recipients found... or No valid messages found... |
| 400 | Too many recipients | Maximum number of recipients exceeded. (100 for free-form, 10 for template modes) |
| 400 | Template not found | Message template tpl-sms-001 not found. |
| 400 | Template not sendable | Message template tpl-sms-001 is not sendable. Current status: Active and approval status: PENDING |
| 400 | Managed service not found | Managed service descriptor not found for key: otp-service. See Belio-managed Senders and Approval. |
| 400 | Category mismatch | Template category AUTHENTICATION does not match managed service descriptor category MARKETING. See Belio-managed Senders and Approval. |
| 400 | Insufficient message units | Team balance error message. Raised when consumption fails entirely for the routable units |
| 400 | Invalid receiptRequest | Correlator or callback URL validation message |
| 401 | Authentication failure | — |
Recipients with no matching active service channel, or on a channel with an
insufficient balance, do not cause an error. The request succeeds with
HTTP 200 and those numbers are returned in
skipped — even when every recipient is skipped.
Malformed phone numbers still return HTTP 400.Template parameter validation errors
When parameter validation fails, the response isHTTP 400 with desc set to Template parameter validation failed.
For TemplateSendToEach, errors are keyed by recipient phone number:
{
"desc": "Template parameter validation failed",
"result": {
"254700111213": [
"Missing required parameter 'code'"
],
"254700111214": [
"Missing required parameter 'code'",
"Parameter 'sample': Value for parameter 'sample' must be a valid name"
]
}
}
errors key:
{
"desc": "Template parameter validation failed",
"result": {
"errors": [
"Missing required parameter 'code'",
"Parameter 'code': Value for parameter 'code' must not exceed 6 characters"
]
}
}
Common parameter validation messages
| Message | Cause |
|---|---|
Missing required parameter '{name}' | Required placeholder with no default; not supplied in params |
Unknown parameter '{name}' | Supplied parameter not defined on the template |
Duplicate parameter '{name}' | Same name appears more than once in params |
Parameter '{name}': Value for parameter '{name}' must not exceed N characters | Value longer than maxLength |
Parameter '{name}': Value for parameter '{name}' must be alphanumeric | format is Alphanumeric |
Parameter '{name}': Value for parameter '{name}' must be numeric | format is Numeric |
Parameter '{name}': Value for parameter '{name}' must be a valid name | format is Name |
Value for parameter '{name}' must be non-empty | Empty value |
Value for parameter '{name}' contains forbidden characters | Value contains {, }, or control characters |
Name must be a non-empty identifier using letters, digits, and underscores | Invalid parameter name |
Resolved content cannot be longer than 960 characters | Substituted message too long |
Success Response
On success the endpoint returnsHTTP 200:
{
"result": {
"type": "SmsResponse",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"units": {
"Safaricom": 1,
"Airtel": 1
},
"addresses": {
"Safaricom": 2,
"Airtel": 2
},
"skipped": [
"254712345678",
"254100000000"
]
}
}
| Field | Type | Description |
|---|---|---|
requestId | UUID | Correlation ID for this send request, used to match delivery receipts |
units | object | Message units consumed per mobile network channel |
addresses | object | Number of recipients accepted and dispatched per channel |
skipped | string[] | Recipients that were not debited and not dispatched |
skipped is always present. It is an empty array ([]) when every recipient was
accepted.
Skipped recipients
A request is no longer rejected when some — or all — of its recipients cannot be processed. Only recipients that can be routed on an active SMS service channel are debited and dispatched; the rest are returned inskipped.
A phone number is added to skipped when:
| Reason | Behaviour |
|---|---|
| No matching active SMS service channel for that recipient’s network | Not debited, not dispatched |
| The channel returned zero consumed units (insufficient balance on that channel) | Excluded from dispatch, included in skipped |
addresses and units for their channel.
Partial success
When a request mixes routable and non-routable recipients, the routable ones are processed normally (units deducted, message dispatched), the rest are listed inskipped, and the API still returns HTTP 200.
Request against a service configured for Safaricom only:
{
"type": "SendToMany",
"message": "Hello World",
"addresses": [
"254712345678",
"254710000000",
]
}
{
"result": {
"type": "SmsResponse",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"units": { "Safaricom": 1 },
"addresses": { "Safaricom": 1 },
"skipped": ["254700111213"]
}
}
All recipients skipped
If none of the provided phone numbers has a matching service channel:- The API returns
HTTP 200— notHTTP 400. unitsandaddressesare empty.skippedcontains every submitted phone number, exactly as provided in the request.- No units are consumed and no messages are dispatched.
- For template sends, template lookup and validation are not performed, since nothing is routable.
{
"result": {
"type": "SmsResponse",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"units": {},
"addresses": {},
"skipped": ["+254700000000", "123456789"]
}
}
Integration guidance
Treat
HTTP 200 as acceptance of the request, not as confirmation that every
recipient was sent a message. Always inspect skipped.- Retry skipped numbers through another service or channel where appropriate, or surface them to operators when delivery was expected.
- Use
addressesandunitsfor billing and delivery expectations for this request. - Use
requestIdto correlate delivery receipts for the recipients that were dispatched. - Do not treat a non-empty
skippedlist as a failure of the whole request.
Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your auth token.
Path Parameters
Unique identifier for the messaging service
Body
application/json
Message details for sending to one or multiple recipients
Response
Message(s) sent successfully
The response
Show child attributes
Show child attributes