Send airtime
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.
- Service status
- Request parameters
- Response parameters
- Sample request
- Delivery callback
- Error responses
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
| Field | Required | Type | Description |
|---|---|---|---|
| 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:
|
| 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:
| Field | Type | Description |
|---|---|---|
| recipientsCount | Number | Number of entries in responses — accepted and failed. |
| totalAmount | String | The total of every amount requested, including recipients that failed, e.g. KES 21.00. |
| totalDiscount | String | Always KES 0.00. |
| responses[] | Array | One entry per recipient, in the order sent, with these fields: |
| number | String | The recipient’s number as 2547XXXXXXXX. |
| amount | String | The amount requested, e.g. KES 100.00. |
| discount | String | Always KES 0.00. |
| status | String | accepted or failed. Not the final delivery status — that arrives in the callback. |
| requestId | Number | A 7-digit number identifying this top-up. The callback carries the same value. |
| errorMessage | String | none 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"
}
| Field | Type | Description |
|---|---|---|
| number | String | The recipient, as 2547XXXXXXXX. |
| amount | String | The whole-shilling amount sent, e.g. KES 100.00. |
| requestId | Number | The requestId from the immediate response. |
| discount | String | Always KES 0.00. |
| status | String | success or failed. |
| type | String | Always airtimeDelivery. |
| description | String | A description of the result, free text. |
| timestamp | Number | When Sozuri sent the callback, in Unix seconds. |
| authKey | String | Your 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
| Condition | HTTP | Response body |
|---|---|---|
| Missing or wrong API key or project name. | 401 | |
| Airtime sending is unavailable (today, every authenticated request). | 503 | |
recipients is not a string (for example, a JSON array sent as an array). |
422 | |
| More than 1,000 recipients in a single request. | 200 | |
A recipient is missing its number or its amount. |
200 | |
| Project balance is less than the total requested plus KES 50. | 200 | |
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 | |
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.
| Condition | errorMessage |
|---|---|
| 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.
Refunds & goodwill
When a service hiccup costs a customer time, send airtime as an apology — no payment rail needed.
Agent & field-staff payouts
Pay commissions, stipends or research incentives directly as airtime — the only ‘wallet’ that works on every phone.
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.