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.

POST https://sozuri.net/api/v1/messaging

Request parameters

The request body is JSON (or form-encoded). Every field below sits at the top level.

FieldRequiredTypeDescription
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:

SMS request in Postman

Response parameters

The response is a JSON object summarising what Sozuri accepted for delivery:

FieldTypeDescription
messageDataObjectSummary of the request, including the count of messages accepted.
recipientsArrayOne entry per recipient, each with the fields below.
recipients[].messageIdStringUnique Sozuri ID for this message. Use it to correlate status callbacks.
recipients[].toStringThe recipient’s phone number.
recipients[].statusStringAcceptance status (e.g. accepted, unsupported_number). Not the final delivery status.
recipients[].statusCodeStringNumeric code — see the status code table.
recipients[].bulkIdStringIdentifier shared by every message in this request — useful for batch reporting.
recipients[].messagePartNumberHow many SMS parts the message uses. One full GSM-7 message is 160 characters.
recipients[].typeStringEchoes back the message type (transactional / promotional).

Sample response from Postman:

SMS response in 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.

Message length — one SMS holds 160 GSM-7 characters or 70 UCS-2 (Unicode) characters. Longer messages are split into parts and each part is billed separately.
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.

ConditionHTTPResponse body
Project credit balance is too low to send the message. 400
{
    "messageData": {
        "message": "Error. Insufficient balance. Top up and try again."
    }
}
Custom bulk with template variables (e.g. {{fname}}) sent to recipients outside any contact group. 400
{
    "messageData": {
        "message": "Error. Failure loading group contacts for custom bulk sms. When you include contact parameters like {{fname}} in the message, ensure to ONLY include recipients from existing Contact Groups"
    }
}
Required field (type, message, to) missing or malformed. 422
{
    "message": "The given data was invalid.",
    "errors": {
        "type": ["The type field is required."]
    }
}
Request did not match any valid SMS processing branch — usually means type is missing or unrecognised. 400
{
    "messageData": {
        "message": "Error. No valid SMS processing branch found. Please check your request parameters."
    }
}
Internal error during dispatch. Rare and transient — retry idempotently. 500
{
    "messageData": {
        "message": "Error. A system error was encountered. Sozuri support has been notified of the same."
    }
}
For the full numeric 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_codeHTTPRetryableAction
RATE_LIMITED429YesHonour the Retry-After header if present, otherwise wait 5 seconds + jitter and retry.
SERVICE_UNAVAILABLE500 / 503YesExponential backoff with jitter (see below).
VALIDATION_FAILED422NoFix the payload using the errors field. Do not retry the same request.
AUTHENTICATION_FAILED401NoVerify your API key / bearer token and project value.
AUTHORIZATION_FAILED403NoEscalate — the token does not permit this operation.
NOT_FOUND / BAD_REQUEST404 / 400NoFix 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.

AttemptBackoff windowWait
1st retry0 – 1 srandom(0, 1000) ms
2nd retry0 – 2 srandom(0, 2000) ms
3rd retry0 – 4 srandom(0, 4000) ms
4th retry0 – 8 srandom(0, 8000) ms
5th retry0 – 16 srandom(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 sizeRecommended pacingSustained TPS
≤ 10,000No pacing required — submit as a burstup to 100 TPS
10,000 – 50,000Spread over ≥ 8 minutes≈ 20–100 TPS
50,000 – 200,000Spread over ≥ 35 minutes≈ 25–95 TPS
> 200,000Contact support to schedule a window
Rule of thumb: for any run over 10,000 messages, divide the recipient count by 100 to get the minimum number of seconds to spread submission over. 50,000 ÷ 100 = 500 s ≈ 8 minutes.
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.

S
Sozuri
SMS · just now
Your verification code is 482910. It expires in 5 minutes. 10:24

Marketing campaigns

Blast a promotional sender ID to thousands of contacts and watch the delivery rates in real time.

J
JUMIA
Promo
Black Friday is HERE. Up to 70% off electronics. Shop now: jumia.co.ke/bf. STOP to opt out. 08:00

Transactional alerts

Order confirmations, dispatch notifications, appointment reminders — the workhorse of every modern business.

G
GALAXION
Order update
Order #4821 confirmed. Total KES 2,450. Delivery tomorrow 9-11am. Track: galaxion.co.ke/t/4821 14:12

Reminders & nudges

Loan repayments, clinic appointments, KYC follow-ups — a well-timed SMS lifts conversion 20–40%.

T
TALA
Repayment
Hi Wanjiru, your KES 1,200 loan repayment is due tomorrow. Pay via Paybill 851900, account 254712345678. 09:00

Send your first SMS today.

Grab a code sample, drop in your API key, and watch a real SMS arrive on your phone.