Send airtime to Safaricom, Airtel and Telkom subscribers from your own system. Airtime sending is currently unavailable — read the notice below before you build on this endpoint.

POST https://sozuri.net/api/v1/airtime/topup
Airtime sending is currently unavailable. Every authenticated request to GET or POST /api/v1/airtime/topup answers HTTP 503 with the body below. Nothing is sent and nothing is charged. A request with a missing or wrong API key or project name gets the 401 authentication error instead.
{
    "data": {
        "status": "unavailable",
        "message": "Airtime topup is not currently available. The service is being migrated to a new provider."
    }
}
The reference below is the contract for when the service returns.
When the service is available: each request can contain up to 1,000 recipients, and each recipient up to KES 5,000. Your project balance must be at least KES 50 more than the total you request. Sozuri answers at once with a requestId per recipient, then posts each top-up’s final result to your callback URL.
Required headers
POST /api/v1/airtime/topup HTTP/1.1
Host: sozuri.net
Accept: application/json
Content-Type: application/x-www-form-urlencoded
Authorization: Bearer YOUR_PROJECT_API_KEY

GET works too, with the same fields in the query string. Send the API key as a Bearer token or as an apiKey field — an apiKey header is not read — and always send project. See Authentication.

Request parameters

FieldRequiredTypeDescription
project Yes String Your project’s exact name.
recipients Yes String A JSON array encoded as a string — as a form field, or as a string value in a JSON body. Sending it as a JSON array instead of a string is refused. Each element is an object with:
  • number — a Kenyan phone number, e.g. 0700000001, 254700000001 or +254700000001. It is converted to 2547XXXXXXXX.
  • amount — the airtime value, either KES 100 or 100. Use whole shillings: a decimal part is shown in the response but dropped when the airtime is sent and charged.
apiKey — String Your project API key, if you do not send it as a Bearer token in the Authorization header (recommended).

Each recipient that passes its checks is charged from your project balance when the request is accepted. A recipient that fails its checks is not charged and nothing is sent to it.

Response parameters

The immediate JSON response (HTTP 200) summarises the request and includes one entry per recipient:

FieldTypeDescription
recipientsCountNumberNumber of entries in responses — accepted and failed.
totalAmountStringThe total of every amount requested, including recipients that failed, e.g. KES 21.00.
totalDiscountStringAlways KES 0.00.
responses[]ArrayOne entry per recipient, in the order sent, with these fields:
  numberStringThe recipient’s number as 2547XXXXXXXX.
  amountStringThe amount requested, e.g. KES 100.00.
  discountStringAlways KES 0.00.
  statusStringaccepted or failed. Not the final delivery status — that arrives in the callback.
  requestIdNumberA 7-digit number identifying this top-up. The callback carries the same value.
  errorMessageStringnone when accepted, otherwise the reason (see Error responses).

Sample request

POST /api/v1/airtime/topup HTTP/1.1
Host: sozuri.net
Accept: application/json
Content-Type: application/x-www-form-urlencoded
Authorization: Bearer YOUR_PROJECT_API_KEY

project=Your_Project_Name&recipients=%5B%7B%22number%22%3A%22254700000001%22%2C%22amount%22%3A%22KES%2010%22%7D%5D
<?php

$curl = curl_init();

curl_setopt_array($curl, [
    CURLOPT_URL => 'https://sozuri.net/api/v1/airtime/topup',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_POSTFIELDS => http_build_query([
        'project'    => 'Your_Project_Name',
        'recipients' => json_encode([
            ['number' => '254700000001', 'amount' => 'KES 10'],
            ['number' => '0700000002',   'amount' => 'KES 11'],
        ]),
    ]),
    CURLOPT_HTTPHEADER => [
        'Accept: application/json',
        'Content-Type: application/x-www-form-urlencoded',
        'Authorization: Bearer YOUR_PROJECT_API_KEY',
    ],
]);

echo curl_exec($curl);
curl_close($curl);
const body = new URLSearchParams();
body.append("project", "Your_Project_Name");
body.append("recipients", JSON.stringify([
    { number: "254700000001", amount: "KES 10" },
    { number: "0700000002",   amount: "KES 11" },
]));

const response = await fetch("https://sozuri.net/api/v1/airtime/topup", {
    method: "POST",
    headers: {
        "Accept": "application/json",
        "Content-Type": "application/x-www-form-urlencoded",
        "Authorization": "Bearer YOUR_PROJECT_API_KEY"
    },
    body
});
console.log(response.status, await response.text());
require "uri"
require "net/http"

uri  = URI("https://sozuri.net/api/v1/airtime/topup")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true

request = Net::HTTP::Post.new(uri)
request["Accept"]        = "application/json"
request["Content-Type"]  = "application/x-www-form-urlencoded"
request["Authorization"] = "Bearer YOUR_PROJECT_API_KEY"
request.body = URI.encode_www_form({
    project:    "Your_Project_Name",
    recipients: '[{"number":"254700000001","amount":"KES 10"}]'
})

puts http.request(request).body
import requests, json

response = requests.post(
    "https://sozuri.net/api/v1/airtime/topup",
    headers={
        "Accept": "application/json",
        "Content-Type": "application/x-www-form-urlencoded",
        "Authorization": "Bearer YOUR_PROJECT_API_KEY",
    },
    data={
        "project": "Your_Project_Name",
        "recipients": json.dumps([
            {"number": "254700000001", "amount": "KES 10"},
            {"number": "0700000002",   "amount": "KES 11"},
        ]),
    },
)
print(response.status_code, response.text)
HttpResponse<String> response = Unirest.post("https://sozuri.net/api/v1/airtime/topup")
    .header("Accept", "application/json")
    .header("Content-Type", "application/x-www-form-urlencoded")
    .header("Authorization", "Bearer YOUR_PROJECT_API_KEY")
    .field("project", "Your_Project_Name")
    .field("recipients", "[{\"number\":\"254700000001\",\"amount\":\"KES 10\"}]")
    .asString();
var client  = new RestClient("https://sozuri.net/api/v1/airtime/topup");
var request = new RestRequest(Method.POST);
request.AddHeader("Accept",        "application/json");
request.AddHeader("Content-Type",  "application/x-www-form-urlencoded");
request.AddHeader("Authorization", "Bearer YOUR_PROJECT_API_KEY");
request.AddParameter("project",    "Your_Project_Name");
request.AddParameter("recipients", "[{\"number\":\"254700000001\",\"amount\":\"KES 10\"}]");
IRestResponse response = client.Execute(request);
curl --location --request POST 'https://sozuri.net/api/v1/airtime/topup' \
    --header 'Accept: application/json' \
    --header 'Content-Type: application/x-www-form-urlencoded' \
    --header 'Authorization: Bearer YOUR_PROJECT_API_KEY' \
    --data-urlencode 'project=Your_Project_Name' \
    --data-urlencode 'recipients=[{"number":"254700000001","amount":"KES 10"}]'
Sample JSON response (when the service is available)
{
    "recipientsCount": 2,
    "totalAmount": "KES 21.00",
    "totalDiscount": "KES 0.00",
    "responses": [
        {
            "number": "254700000001",
            "amount": "KES 10.00",
            "discount": "KES 0.00",
            "status": "accepted",
            "requestId": 4971724,
            "errorMessage": "none"
        },
        {
            "number": "254700000002",
            "amount": "KES 11.00",
            "discount": "KES 0.00",
            "status": "accepted",
            "requestId": 5015342,
            "errorMessage": "none"
        }
    ]
}

Delivery callback

For each accepted top-up, Sozuri POSTs the result to your project’s airtime callback URL once it is known. Match the requestId back to the immediate response to update your records. If no result is received, no callback is sent. While airtime is unavailable, the Manage API › Callback URLs page does not offer an airtime callback setting.

Request headers
Content-Type: application/json
Accept: application/json
X-Sozuri-Signature: t=1762585200,v1=<hex HMAC-SHA256>

X-Sozuri-Signature is present when your project’s callback settings have a signing secret: v1 is the HMAC-SHA256, keyed with that secret, of the timestamp t, a full stop, and the raw request body. authKey is added to the body when your callback settings have an auth key.

Body
{
    "number": "254700000001",
    "amount": "KES 100.00",
    "requestId": 5015342,
    "discount": "KES 0.00",
    "status": "success",
    "type": "airtimeDelivery",
    "description": "Successful",
    "timestamp": 1762585200,
    "authKey": "YOUR_CALLBACK_AUTH_KEY"
}
FieldTypeDescription
numberStringThe recipient, as 2547XXXXXXXX.
amountStringThe whole-shilling amount sent, e.g. KES 100.00.
requestIdNumberThe requestId from the immediate response.
discountStringAlways KES 0.00.
statusStringsuccess or failed.
typeStringAlways airtimeDelivery.
descriptionStringA description of the result, free text.
timestampNumberWhen Sozuri sent the callback, in Unix seconds.
authKeyStringYour callback auth key. Present only when one is set.

Error responses

While the service is unavailable, every authenticated request gets the 503 answer above. When it returns, request-level problems are answered with HTTP 200 and a data.message body, before any recipient is processed or charged. Problems with a single recipient do not fail the request: that recipient appears in responses[] with "status": "failed" and a reason in errorMessage.

Request-level errors
ConditionHTTPResponse body
Missing or wrong API key or project name. 401
{
    "error_code": "AUTHENTICATION_FAILED",
    "message": "Authentication failed. Check your API key or bearer token.",
    "retryable": false
}
Airtime sending is unavailable (today, every authenticated request). 503
{
    "data": {
        "status": "unavailable",
        "message": "Airtime topup is not currently available. The service is being migrated to a new provider."
    }
}
recipients is not a string (for example, a JSON array sent as an array). 422
{
    "error_code": "VALIDATION_FAILED",
    "message": "The submitted data failed validation.",
    "retryable": false,
    "errors": { "recipients": ["The recipients must be a string."] }
}
More than 1,000 recipients in a single request. 200
{
    "data": { "message": "Error. Exceeded maximum allowed recipients" }
}
A recipient is missing its number or its amount. 200
{
    "data": { "message": "Error. Ensure all numbers have matching airtime value." }
}
Project balance is less than the total requested plus KES 50. 200
{
    "data": {
        "message": "Error. Insufficient balance. Min project balance for Airtime is 50KES. Top up your project then try again."
    }
}
An amount is not a number once a leading currency code and space are removed (for example KES 1,000). Recipients listed before it have already been accepted and charged. 500
{
    "error_code": "SERVICE_UNAVAILABLE",
    "message": "An unexpected error occurred. Please retry.",
    "retryable": true
}
Per-recipient errors

Each appears inside responses[], for example { "number": "254700000001", "amount": "KES 6,000.00", "discount": "KES 0.00", "status": "failed", "requestId": 4971724, "errorMessage": "Invalid airtime Value. Maximum limit per recipient is KES 5000" }. When a recipient fails more than one check, the reason from the last check in this table is reported.

ConditionerrorMessage
The number is not a valid 12-digit Kenyan number once converted.Invalid recipient number
The amount is over KES 5,000.Invalid airtime Value. Maximum limit per recipient is KES 5000
The number is on a network airtime cannot be sent to.unsupported mobile network

Use cases

Where programmatic airtime delivers real ROI, once the service is back.

Customer rewards

Top up customers who hit a milestone — sign-up bonus, loyalty tiers, NPS survey completion.

S
SAFARICOM
Airtime received
You have received KES 100.00 airtime. New balance: KES 142.50. 12:31

Refunds & goodwill

When a service hiccup costs a customer time, send airtime as an apology — no payment rail needed.

S
SAFARICOM
Airtime received
You have received KES 50.00 airtime. New balance: KES 88.20. 15:07

Agent & field-staff payouts

Pay commissions, stipends or research incentives directly as airtime — the only ‘wallet’ that works on every phone.

A
AIRTEL
Airtime received
You have received KES 300.00 airtime. New balance: KES 312.40. 17:00

Survey & research incentives

Compensate respondents the moment they finish your form — response rates climb when the reward is immediate.

Reward, refund or pay with airtime.

Airtime sending is currently unavailable. Talk to us to hear when it returns, or start reaching your customers by SMS today.