Send airtime
Send airtime to any Safaricom, Airtel or Telkom subscriber programmatically. Top up one number or one thousand in a single API call — perfect for rewards, refunds, agent commissions and incentives.
requestId per recipient, then pushes the final delivery status to your webhook as the carrier confirms.
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
Request parameters
| Field | Required | Type | Description |
|---|---|---|---|
| project | Yes | String | The name of the project that owns the API key making this request. |
| recipients | Yes | URL-encoded JSON array |
Each element is an object with:
|
| apiKey | — | String | Your project API key. Recommended: pass as a Bearer token in the Authorization header instead. |
Response parameters
The synchronous JSON response summarises the batch and includes one entry per recipient:
| Field | Type | Description |
|---|---|---|
| recipientsCount | Number | How many top-up requests were accepted by the carrier. |
| totalAmount | Number | Total airtime value across the whole request. |
| totalDiscount | String | Total discount earned on this batch, e.g. KES 0.00. |
| responses[] | Array | One entry per recipient with these fields: |
| number | String | The recipient’s phone number. |
| amount | String | Airtime value sent (e.g. KES 100.00). |
| discount | String | Discount applied to this recipient. |
| status | String | accepted or failed. Not the final delivery status. |
| requestId | String | Unique ID for this top-up. Use it to correlate the callback. |
| errorMessage | String | Error detail, or none if the request was accepted. |
Sample request
POST /api/v1/airtime/topup HTTP/1.1
Host: sozuri.net
Accept: application/json
Content-Type: application/x-www-form-urlencoded
project=Law&recipients=%5B%7B%22number%22%3A%22%2B254725164293%22%2C%22amount%22%3A%22KES%2010%22%7D%5D&apiKey=YI1uKEDbI6SOtzPX4W4VHICRP2oCuL1BmCJmTeKQrEKEYy5NuNI30l7L86LE
<?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' => 'Law',
'recipients' => json_encode([
['number' => '+254725164293', 'amount' => 'KES 10'],
['number' => '0725164293', 'amount' => 'KES 11'],
]),
'apiKey' => 'YI1uKEDbI6SOtzPX4W4VHICRP2oCuL1BmCJmTeKQrEKEYy5NuNI30l7L86LE',
]),
CURLOPT_HTTPHEADER => [
'Accept: application/json',
'Content-Type: application/x-www-form-urlencoded',
],
]);
echo curl_exec($curl);
curl_close($curl);
const body = new URLSearchParams();
body.append("project", "Law");
body.append("recipients", JSON.stringify([
{ number: "+254725164293", amount: "KES 10" },
{ number: "0725164293", amount: "KES 11" },
]));
body.append("apiKey", "YI1uKEDbI6SOtzPX4W4VHICRP2oCuL1BmCJmTeKQrEKEYy5NuNI30l7L86LE");
const response = await fetch("https://sozuri.net/api/v1/airtime/topup", {
method: "POST",
headers: {
"Accept": "application/json",
"Content-Type": "application/x-www-form-urlencoded"
},
body
});
console.log(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.body = URI.encode_www_form({
project: "Law",
recipients: '[{"number":"+254725164293","amount":"KES 10"}]',
apiKey: "YI1uKEDbI6SOtzPX4W4VHICRP2oCuL1BmCJmTeKQrEKEYy5NuNI30l7L86LE"
})
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",
},
data={
"project": "Law",
"recipients": json.dumps([
{"number": "+254725164293", "amount": "KES 10"},
{"number": "0725164293", "amount": "KES 11"},
]),
"apiKey": "YI1uKEDbI6SOtzPX4W4VHICRP2oCuL1BmCJmTeKQrEKEYy5NuNI30l7L86LE",
},
)
print(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")
.field("project", "Law")
.field("recipients", "[{\"number\":\"+254725164293\",\"amount\":\"KES 10\"}]")
.field("apiKey", "YI1uKEDbI6SOtzPX4W4VHICRP2oCuL1BmCJmTeKQrEKEYy5NuNI30l7L86LE")
.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.AddParameter("project", "Law");
request.AddParameter("recipients", "[{\"number\":\"+254725164293\",\"amount\":\"KES 10\"}]");
request.AddParameter("apiKey", "YI1uKEDbI6SOtzPX4W4VHICRP2oCuL1BmCJmTeKQrEKEYy5NuNI30l7L86LE");
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' \
--data-urlencode 'project=Law' \
--data-urlencode 'recipients=[{"number":"+254725164293","amount":"KES 10"}]' \
--data-urlencode 'apiKey=YI1uKEDbI6SOtzPX4W4VHICRP2oCuL1BmCJmTeKQrEKEYy5NuNI30l7L86LE'
Sample JSON response
{
"recipientsCount": 2,
"totalAmount": 21,
"totalDiscount": "KES 0.00",
"responses": [
{
"number": "254725164293",
"amount": "KES 10",
"discount": "KES 0.00",
"status": "accepted",
"requestId": 4971724,
"errorMessage": "none"
},
{
"number": "254725164293",
"amount": "KES 11",
"discount": "KES 0.00",
"status": "accepted",
"requestId": 5015342,
"errorMessage": "none"
}
]
}
Postman screenshot:
Delivery callback
Sozuri pushes a POST to your configured webhook the moment the carrier confirms (or rejects) each top-up. Match the requestId back to your synchronous response to update your records.
Configure the URL from your project at Manage API › Callback URLs.
{
"number": "254725164293",
"amount": "KES 1000.00",
"requestId": "5015342",
"discount": "KES 0.00",
"status": "success",
"type": "airtimeDelivery",
"description": "success",
"timestamp": "1603713484"
}
Error responses
Airtime topup uses two distinct error envelopes: data.message for request-level errors before per-recipient processing begins, and status + errorMessage for per-recipient validation inside the request. Auth errors (Unknown project.) are documented on the Authentication page.
| Condition | HTTP | Response body |
|---|---|---|
| More than 1,000 recipients in a single request. | 400 | |
Number of recipients and number of airtimeValue entries don’t match. |
400 | |
| Project balance below the KES 50 airtime minimum. | 400 | |
| A recipient phone number is malformed. | 400 | |
| Top-up amount exceeds the KES 5,000 per-recipient cap. | 400 | |
| Recipient is on a network we don’t currently support for airtime. | 400 | |
Use cases
Where programmatic airtime delivers real ROI.
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, push instant 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 instant they finish your form — response rates climb when reward is immediate.
Reward, refund or pay — instantly.
Add airtime to your account and send your first top-up from the dashboard or API today.