Skip to main content

Overview

The Airtime service allows you to programmatically send airtime to phone numbers, validate transactions, and receive delivery status notifications.

Service Status

Check Airtime Service Status

curl http://localhost:8000/airtime/
service
string
Service name (“airtime”)
status
string
Service status (“ready”)
Response Example
{
  "service": "airtime",
  "status": "ready"
}

Sending Airtime

Send Airtime to Phone Number

Send airtime credit to a mobile phone number.
curl "http://localhost:8000/airtime/invoke-send-airtime?phone=254711XXXYYY&amount=100&currency=KES"
phone
string
required
Recipient phone number (without + prefix)Example: 254711XXXYYY
amount
string
default:"10"
Amount of airtime to send (must be positive number)Example: 100
currency
string
default:"KES"
Currency codeExample: KES, UGX, TZS
idempotencyKey
string
default:"ABCDEF"
Unique key to prevent duplicate transactionsExample: unique-txn-123
Implementation
# From routes/airtime.py:23-45
@airtime_bp.route("/invoke-send-airtime", methods=["GET"])
def invoke_send_airtime():
    phone = "+" + request.args.get("phone", "").strip()
    amount = request.args.get("amount", "10").strip()
    currency = request.args.get("currency", "KES").strip()
    idempotencyKey = request.args.get("idempotencyKey", "ABCDEF").strip()

    print(f"📲 Request to send airtime to: {phone} with amount: {amount}")
    if not phone:
        return {"error": "Missing 'phone' query parameter"}, 400

    try:
        amount_value = float(amount)
        if amount_value <= 0:
            return {"error": "'amount' must be a positive number"}, 400
    except ValueError:
        return {"error": "'amount' must be a valid number"}, 400

    try:
        response = send_airtime(phone, amount_value, currency, idempotencyKey or None)
        return {"message": f"Airtime sent to {phone}", "response": response}
    except Exception as e:
        return {"error": str(e)}, 500
Response Example
{
  "message": "Airtime sent to +254711XXXYYY",
  "response": {
    "errorMessage": "None",
    "numSent": 1,
    "totalAmount": "KES 100.0000",
    "totalDiscount": "KES 0.6000",
    "responses": [
      {
        "phoneNumber": "+254711XXXYYY",
        "amount": "KES 100.0000",
        "discount": "KES 0.6000",
        "status": "Sent",
        "requestId": "ATQid_...",
        "errorMessage": "None"
      }
    ]
  }
}
Error Responses
{
  "error": "Missing 'phone' query parameter"
}
{
  "error": "'amount' must be a positive number"
}
{
  "error": "'amount' must be a valid number"
}

Webhooks

Airtime Validation

Validate airtime transaction requests before they are processed.
curl -X POST http://localhost:8000/airtime/validation \
  -H "Content-Type: application/json" \
  -d '{
    "transactionId": "SomeTransactionID",
    "phoneNumber": "+254711XXXYYY",
    "sourceIpAddress": "127.12.32.24",
    "currencyCode": "KES",
    "amount": 500.00
  }'
transactionId
string
required
Unique transaction identifier
phoneNumber
string
required
Recipient’s phone number
sourceIpAddress
string
required
Source IP address of the request
currencyCode
string
required
Currency code (e.g., “KES”)
amount
number
required
Amount to validate
Implementation
# From routes/airtime.py:48-75
@airtime_bp.route("/validation", methods=["POST"])
def airtime_validation():
    data = request.get_json(force=True)

    transaction_id = data.get("transactionId")
    phone_number = data.get("phoneNumber")
    source_ip = data.get("sourceIpAddress")
    currency = data.get("currencyCode")
    amount = data.get("amount")

    # Basic validation logic (you can replace with real checks)
    if transaction_id and phone_number and currency and amount and source_ip:
        status = "Validated"
    else:
        status = "Failed"

    return jsonify({"status": status})
Response Example
{
  "status": "Validated"
}
Failed Validation
{
  "status": "Failed"
}

Airtime Status Callback

Receive delivery status notifications for airtime transactions.
curl -X POST http://localhost:8000/airtime/status \
  -H "Content-Type: application/json" \
  -d '{
    "phoneNumber": "+254711XXXYYY",
    "description": "Airtime Delivered Successfully",
    "status": "Success",
    "requestId": "ATQid_SampleTxnId123",
    "discount": "KES 0.6000",
    "value": "KES 100.0000"
  }'
phoneNumber
string
required
Recipient’s phone number
description
string
required
Human-readable status description
status
string
required
Delivery status (e.g., “Success”, “Failed”)
requestId
string
required
Africa’s Talking request ID
discount
string
required
Discount amount with currency
value
string
required
Airtime value with currency
Implementation
# From routes/airtime.py:78-108
@airtime_bp.route("/status", methods=["POST"])
def airtime_status():
    data = request.get_json(force=True)

    phone_number = data.get("phoneNumber")
    description = data.get("description")
    status = data.get("status")
    request_id = data.get("requestId")
    discount = data.get("discount")
    value = data.get("value")

    if not phone_number or not status or not request_id and not discount and not value:
        return "BAD", 400

    # Log or process the status here
    print(f"📲 Airtime status update for {phone_number}: {status} ({description})")

    # Respond with 200 OK and body "OK"
    return "OK", 200
Payload Example
{
  "phoneNumber": "+254711XXXYYY",
  "description": "Airtime Delivered Successfully",
  "status": "Success",
  "requestId": "ATQid_SampleTxnId123",
  "discount": "KES 0.6000",
  "value": "KES 100.0000"
}
Response
  • Success: "OK" with status code 200
  • Error: "BAD" with status code 400

Airtime Flow

1

Send Airtime

Make a GET request to /airtime/invoke-send-airtime with phone and amount
2

Validation (Optional)

If configured, AT calls your /airtime/validation endpoint for approval
3

Transaction Processed

Africa’s Talking processes the airtime transfer
4

Receive Status

Your /airtime/status endpoint receives delivery confirmation
5

Handle Result

Update your database and notify the user of success or failure

Best Practices

Always provide unique idempotencyKey values to prevent duplicate charges if requests are retried.
curl "http://localhost:8000/airtime/invoke-send-airtime?
  phone=254711XXXYYY&
  amount=100&
  idempotencyKey=txn-$(date +%s)"
Implement server-side validation to ensure amounts are within acceptable ranges and prevent fraud.
MIN_AMOUNT = 10
MAX_AMOUNT = 1000

if not (MIN_AMOUNT <= amount_value <= MAX_AMOUNT):
    return {"error": f"Amount must be between {MIN_AMOUNT} and {MAX_AMOUNT}"}, 400
Always respond with 200 OK to status callbacks, even if processing fails internally. Log errors and process asynchronously if needed.
try:
    # Process the status update
    update_database(data)
    notify_user(data)
except Exception as e:
    # Log error but still return 200 OK
    logger.error(f"Failed to process airtime status: {e}")

return "OK", 200
Keep track of request IDs and match them with status callbacks to ensure all transactions are accounted for.
# Store request ID when sending
transaction = {
    "request_id": response["responses"][0]["requestId"],
    "phone": phone,
    "amount": amount,
    "status": "pending"
}
db.save(transaction)

# Update when status callback is received
db.update({"request_id": request_id}, {"status": status})

Next Steps

SMS Service

Send confirmation SMS when airtime is delivered

SIM Swap

Check SIM swap status before sending airtime

Build docs developers (and LLMs) love