Send Bulk SMS
Reach any mobile number across Safaricom, Airtel and Telkom with a single API call. Send to one recipient or thousands; Sozuri queues, routes and delivers each message and pushes a status callback as it completes.
- Request parameters
- Response parameters
- Sample request
- Delivery callback
- Error responses
- Retries & backoff
- Submission rate (TPS)
Request parameters
The request body is JSON (or form-encoded). Every field below sits at the top level.
| Field | Required | Type | Description |
|---|---|---|---|
| project | Yes | String | The Sozuri project that owns the API key making this request. |
| from | Yes | String | The sender ID registered to your project (defaults to Sozuri if omitted). |
| to | Yes | String |
A comma-separated list of recipient phone numbers in E.164 format,
e.g. 254700000001,254711000002. Numbers like 0722-503-129 are auto-formatted to 254722503129.
|
| message | Yes | String | The text body of the message. Required for SMS and WhatsApp text payloads. |
| type | Yes | String |
Either transactional or promotional. Must match the type of sender ID you’re using
— a sender ID is registered as one or the other, never both.
|
| channel | No | String | Channel to send on. Defaults to sms; set to whatsapp to use WhatsApp. |
| campaign | No | String | Optional label for grouping reports — e.g. Promo Nai. |
| apiKey | — | String |
Your project’s API key. Recommended: send as a Bearer token in the Authorization header instead — see request headers.
|
Postman screenshot:
Response parameters
The response is a JSON object summarising what Sozuri accepted for delivery:
| Field | Type | Description |
|---|---|---|
| messageData | Object | Summary of the request, including the count of messages accepted. |
| recipients | Array | One entry per recipient, each with the fields below. |
| recipients[].messageId | String | Unique Sozuri ID for this message. Use it to correlate status callbacks. |
| recipients[].to | String | The recipient’s phone number. |
| recipients[].status | String | Acceptance status (e.g. accepted, unsupported_number). Not the final delivery status. |
| recipients[].statusCode | String | Numeric code — see the status code table. |
| recipients[].bulkId | String | Identifier shared by every message in this request — useful for batch reporting. |
| recipients[].messagePart | Number | How many SMS parts the message uses. One full GSM-7 message is 160 characters. |
| recipients[].type | String | Echoes back the message type (transactional / promotional). |
Sample response from Postman:
Sample request
A single request can target one or thousands of recipients. Sozuri responds synchronously with a unique messageId per recipient, then pushes asynchronous status updates to your callback URL as each message progresses.
Required headers
POST /api/v1/messaging
Content-Type: application/json
Authorization: Bearer Your_Project_API_KEY
Sample GET request
https://sozuri.net/api/v1/messaging?apiKey=Your_Project_API_KEY&project=Your_Project_name&channel=sms&from=MySenderID&to=254722xxx675&message=SozuriTestSMS
Sample POST request
POST /api/v1/messaging HTTP/1.1
Host: sozuri.net
Authorization: Bearer LOx5JPdqf0lvf.......R9X9XDJ4PFxRqVrt9dx83cWiwfTQMF
Content-Type: application/json
Accept: application/json
{
"project": "my project",
"from": "Sozuri",
"to": "2547251642xx,2547326971xx",
"campaign": "Promo Nai",
"channel": "sms",
"message": "Test SMS.",
"type": "promotional"
}
POST /api/v1/messaging HTTP/1.1
Host: sozuri.net
Authorization: Bearer LOx5JPdqf0lvf45EZAQMJ.......SUzyxR9X9XDJ4PFxRqVrt9dx83cWiwfTQMF
Content-Type: application/json
Accept: application/xml
<request>
<project>my project</project>
<from>Sozuri</from>
<to>2547251642xx,2547326971xx</to>
<campaign>Promo Nai</campaign>
<channel>sms</channel>
<message>Test SMS.</message>
<type>promotional</type>
</request>
<?php
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => "https://sozuri.net/api/v1/messaging",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
"project" => "my project",
"from" => "Sozuri",
"to" => "2547251642xx,2547326971xx",
"campaign" => "Promo Nai",
"channel" => "sms",
"message" => "Test SMS.",
"type" => "promotional",
]),
CURLOPT_HTTPHEADER => [
"Accept: application/json",
"Authorization: Bearer LOx5JPdqf0lvf45EZAQMJm85OSUzyxR9X9XDJ4PFxRqVrt9dx83cWiwfTQMF",
"Content-Type: application/json",
],
));
$response = curl_exec($curl);
curl_close($curl);
echo $response;
const response = await fetch("https://sozuri.net/api/v1/messaging", {
method: "POST",
headers: {
"Authorization": "Bearer LOx5JPdqf0lvf45EZAQMJm85OSUzyxR9X9XDJ4PFxRqVrt9dx83cWiwfTQMF",
"Content-Type": "application/json",
"Accept": "application/json"
},
body: JSON.stringify({
project: "my project",
from: "Sozuri",
to: "2547251642xx,2547326971xx",
campaign: "Promo Nai",
channel: "sms",
message: "Test SMS.",
type: "promotional"
})
});
console.log(await response.json());
require 'uri'
require 'net/http'
require 'json'
uri = URI("https://sozuri.net/api/v1/messaging")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer LOx5JPdqf0lvf45EZAQMJm85OSUzyxR9X9XDJ4PFxRqVrt9dx83cWiwfTQMF"
request["Content-Type"] = "application/json"
request["Accept"] = "application/json"
request.body = {
project: "my project",
from: "Sozuri",
to: "2547251642xx,2547326971xx",
campaign: "Promo Nai",
channel: "sms",
message: "Test SMS.",
type: "promotional"
}.to_json
puts http.request(request).body
import requests
response = requests.post(
"https://sozuri.net/api/v1/messaging",
headers={
"Authorization": "Bearer LOx5JPdqf0lvf45EZAQMJm85OSUzyxR9X9XDJ4PFxRqVrt9dx83cWiwfTQMF",
"Content-Type": "application/json",
"Accept": "application/json",
},
json={
"project": "my project",
"from": "Sozuri",
"to": "2547251642xx,2547326971xx",
"campaign": "Promo Nai",
"channel": "sms",
"message": "Test SMS.",
"type": "promotional",
},
)
print(response.json())
HttpResponse<String> response = Unirest.post("https://sozuri.net/api/v1/messaging")
.header("Authorization", "Bearer LOx5JPdqf0lvf45EZAQMJm85OSUzyxR9X9XDJ4PFxRqVrt9dx83cWiwfTQMF")
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.body("{\"project\":\"my project\",\"from\":\"Sozuri\",\"to\":\"2547251642xx,2547326971xx\","
+ "\"campaign\":\"Promo Nai\",\"channel\":\"sms\",\"message\":\"Test SMS.\",\"type\":\"promotional\"}")
.asString();
var client = new RestClient("https://sozuri.net/api/v1/messaging");
var request = new RestRequest(Method.POST);
request.AddHeader("Accept", "application/json");
request.AddHeader("Content-Type", "application/json");
request.AddHeader("Authorization", "Bearer LOx5JPdqf0lvf45EZAQMJm85OSUzyxR9X9XDJ4PFxRqVrt9dx83cWiwfTQMF");
request.AddParameter("application/json",
"{\"project\":\"my project\",\"from\":\"Sozuri\",\"to\":\"2547251642xx,2547326971xx\","
+ "\"campaign\":\"Promo Nai\",\"channel\":\"sms\",\"message\":\"Test SMS.\",\"type\":\"promotional\"}",
ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
curl -X POST https://sozuri.net/api/v1/messaging \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer LOx5JPdqf0lvf45EZAQMJm85OSUzyxR9X9XDJ4PFxRqVrt9dx83cWiwfTQMF' \
-d '{
"project": "my project",
"from": "Sozuri",
"to": "2547251642xx,2547326971xx",
"campaign": "Promo Nai",
"channel": "sms",
"message": "Test SMS.",
"type": "promotional"
}'
Sample JSON response
{
"messageData": {
"messages": 2
},
"recipients": [
{
"messageId": "MSGBLK6012A7E8B90A21611835368",
"to": "2547251642xx",
"status": "accepted",
"statusCode": "11",
"bulkId": "bulk6012a7e8b904e1611835368",
"messagePart": 1,
"type": "promotional"
},
{
"messageId": "MSGBLK6012A7E8B90A41611835368",
"to": "2547326971xx",
"status": "accepted",
"statusCode": "11",
"bulkId": "bulk6012a7e8b904e1611835368",
"messagePart": 1,
"type": "promotional"
}
]
}
Delivery callback
Sozuri pushes a POST to your configured webhook each time a message changes state. Callbacks may arrive out of order — use the messageId and timestamp to update your records reliably.
Configure your callback URL from your project at Manage API › Callback URLs.
{
"project": "yourproject_name",
"messageId": "MSGBLK5F96B6A0CC2EB1603712672",
"channel": "sms",
"status": "success",
"network": "safaricom",
"type": "bulkDelivery",
"timestamp": "1603713484"
}
Optional callback authentication
You can predefine an Auth Key on your callback URL. Sozuri will include the key in every callback body so your server can verify the request is genuine before processing it.
Error responses
The most common errors you’ll see on the SMS endpoint. Each response uses the SMS error envelope — messageData.message. Authentication errors (Project not found., Unknown project.) are shared across channels and documented on the Authentication page.
| Condition | HTTP | Response body |
|---|---|---|
| Project credit balance is too low to send the message. | 400 | |
Custom bulk with template variables (e.g. {{fname}}) sent to recipients outside any contact group. |
400 | |
Required field (type, message, to) missing or malformed. |
422 | |
Request did not match any valid SMS processing branch — usually means type is missing or unrecognised. |
400 | |
| Internal error during dispatch. Rare and transient — retry idempotently. | 500 | |
statusCode table and how to interpret it across channels, see Status codes & response envelopes. For asynchronous delivery callbacks, see Delivery webhook statuses.
Retries & backoff
Network blips and brief routing transients are expected at any scale. Building correct retry behaviour into your client makes the difference between a smooth bulk run and a hand-managed incident. Branch your retry logic on error_code (stable) and retryable — never on the human-readable message text.
Which errors to retry
error_code | HTTP | Retryable | Action |
|---|---|---|---|
RATE_LIMITED | 429 | Yes | Honour the Retry-After header if present, otherwise wait 5 seconds + jitter and retry. |
SERVICE_UNAVAILABLE | 500 / 503 | Yes | Exponential backoff with jitter (see below). |
VALIDATION_FAILED | 422 | No | Fix the payload using the errors field. Do not retry the same request. |
AUTHENTICATION_FAILED | 401 | No | Verify your API key / bearer token and project value. |
AUTHORIZATION_FAILED | 403 | No | Escalate — the token does not permit this operation. |
NOT_FOUND / BAD_REQUEST | 404 / 400 | No | Fix the URL or payload. |
Recommended backoff curve
Exponential backoff with full jitter (uniform random between 0 and the upper bound). Full jitter prevents thundering-herd retries when many clients all hit a transient failure at the same instant.
| Attempt | Backoff window | Wait |
|---|---|---|
| 1st retry | 0 – 1 s | random(0, 1000) ms |
| 2nd retry | 0 – 2 s | random(0, 2000) ms |
| 3rd retry | 0 – 4 s | random(0, 4000) ms |
| 4th retry | 0 – 8 s | random(0, 8000) ms |
| 5th retry | 0 – 16 s | random(0, 16000) ms |
Cap each wait at 30 s. After 5 failed retries, route the request to a dead-letter for human review — log the request id for cross-referencing with Sozuri. | ||
Pseudocode
for attempt in 1..5:
response = POST /api/v1/messaging
if response.status == 200:
return success
if response.body.retryable == false:
return fail(response.body.error_code)
wait_ms = retry_after_header_ms
or min(30000, random(0, 1000 * 2^(attempt - 1)))
sleep(wait_ms)
return fail("MAX_RETRIES_EXCEEDED", request_id=response.body.id)
Submission rate (TPS)
Sozuri’s aggregate carrier capacity is 400 messages per second across all telcos combined (Safaricom + Airtel + Telkom). This is the platform ceiling; sustained submission above it triggers RATE_LIMITED (HTTP 429).
Within that aggregate, we recommend each project stay under 100 TPS sustained. That leaves headroom for several concurrent high-volume customers running large jobs at the same time, which is the normal pattern we see on the platform.
Pacing recommendations for bulk runs
| Run size | Recommended pacing | Sustained TPS |
|---|---|---|
| ≤ 10,000 | No pacing required — submit as a burst | up to 100 TPS |
| 10,000 – 50,000 | Spread over ≥ 8 minutes | ≈ 20–100 TPS |
| 50,000 – 200,000 | Spread over ≥ 35 minutes | ≈ 25–95 TPS |
| > 200,000 | Contact support to schedule a window | — |
Probing platform readiness before a scheduled run
Before launching a large bulk run, probe GET /api/v1/status (no authentication required). If the overall status is operational, proceed. If degraded with the relevant carrier still operational, the run will most likely succeed but consider deferring the start by a few minutes. If down, defer the run.
What to do on RATE_LIMITED
The retry/backoff logic above will work, but the better fix is pacing: repeatedly hitting 429 wastes round-trips and increases the chance some messages miss their delivery window. If you are consistently being rate-limited at the recommended pacing, your project may need a higher per-project allowance — contact support@sozuri.net.
Use cases
Bulk SMS is the workhorse channel in Kenya. Here’s where it shines.
OTPs & 2FA
Deliver one-time codes in seconds for login, signup, payment confirmation and password reset flows.
Marketing campaigns
Blast a promotional sender ID to thousands of contacts and watch the delivery rates in real time.
Transactional alerts
Order confirmations, dispatch notifications, appointment reminders — the workhorse of every modern business.
Reminders & nudges
Loan repayments, clinic appointments, KYC follow-ups — a well-timed SMS lifts conversion 20–40%.
Send your first SMS today.
Grab a code sample, drop in your API key, and watch a real SMS arrive on your phone.