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.

POST https://sozuri.net/api/v1/airtime/topup
Each request can contain up to 1,000 recipients. The minimum top-up per recipient is KES 10 and the maximum is KES 5,000. Sozuri responds synchronously with a 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

FieldRequiredTypeDescription
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:
  • number — the recipient’s phone number, e.g. +254711000111
  • amount — the airtime value as {currency} {amount}, e.g. KES 100.50
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:

FieldTypeDescription
recipientsCountNumberHow many top-up requests were accepted by the carrier.
totalAmountNumberTotal airtime value across the whole request.
totalDiscountStringTotal discount earned on this batch, e.g. KES 0.00.
responses[]ArrayOne entry per recipient with these fields:
  numberStringThe recipient’s phone number.
  amountStringAirtime value sent (e.g. KES 100.00).
  discountStringDiscount applied to this recipient.
  statusStringaccepted or failed. Not the final delivery status.
  requestIdStringUnique ID for this top-up. Use it to correlate the callback.
  errorMessageStringError 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:

Airtime request in Postman

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.

ConditionHTTPResponse body
More than 1,000 recipients in a single request. 400
{
    "data": { "message": "Error. Exceeded maximum allowed recipients" }
}
Number of recipients and number of airtimeValue entries don’t match. 400
{
    "data": { "message": "Error. Ensure all numbers have matching airtime value." }
}
Project balance below the KES 50 airtime minimum. 400
{
    "data": {
        "message": "Error. Insufficient balance. Min project balance for Airtime is 50KES. Top up your project then try again."
    }
}
A recipient phone number is malformed. 400
{
    "status": "failed",
    "errorMessage": "Invalid recipient number"
}
Top-up amount exceeds the KES 5,000 per-recipient cap. 400
{
    "status": "failed",
    "errorMessage": "Invalid airtime Value. Maximum limit per recipient is KES 5000"
}
Recipient is on a network we don’t currently support for airtime. 400
{
    "status": "failed",
    "errorMessage": "unsupported mobile network"
}
For status code reference see Status codes & response envelopes. For asynchronous delivery callbacks see Delivery webhook statuses.

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.

S
SAFARICOM
Airtime received
You have received KES 100 airtime from GALAXION REWARDS. New balance: KES 142.50. 12:31

Refunds & goodwill

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

S
SAFARICOM
Airtime received
You have received KES 50 airtime from GALAXION SUPPORT. We apologise for the delay on your order. 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 airtime from GALAXION FIELD OPS. Weekly stipend — agent ID 0427. New balance: KES 312.40. 17:00

Survey & research incentives

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

Reward, refund or pay &mdash; instantly.

Add airtime to your account and send your first top-up from the dashboard or API today.