Tanzania · Airtel Money Open API

pyairtel

Python SDK for Airtel Money Tanzania — Collection, Disbursement & Refunds

Get Started View on GitHub
$ pip install pyairtel
PyPI Python CI License

What's included

Everything you need to
accept & send mobile money

🔐

OAuth2 Authentication

Automatic token acquisition and refresh. Your credentials stay safe — tokens are managed internally and renewed 60 seconds before expiry.

📲

USSD Push Collection

Request payments from subscribers. A prompt is sent to their phone — they enter their Airtel Money PIN to approve. No redirects needed.

Transaction Status Polling

Poll transaction status with typed responses: is_successful, is_pending, is_failed. No raw string parsing required.

💸

Disbursement (B2C)

Transfer money directly to any Airtel Money wallet. Includes payee validation before sending so you never send to an invalid number.

↩️

Refunds

Reverse a completed collection using Airtel's internal airtel_money_id. One line of code — no extra configuration needed.

🔒

RSA PIN Encryption

Disbursements require your merchant PIN encrypted with Airtel's RSA public key. Handled automatically via pycryptodome.

📞

Phone Normalisation

Pass numbers in any format — +255…, 0…, or 255…. Automatically normalised to the 12-digit format Airtel expects.

⚠️

ESB Error Decoding

All 9 real Airtel Tanzania ESB error codes mapped to human-readable messages. CollectionError.esb_code and .esb_message on every failure.

🧪

Sandbox & Production

Toggle between UAT sandbox and live production with a single sandbox=True/False flag. Same code, different environment.

🔑

Secure Credentials

Credentials are never hardcoded. Use a .env file locally and environment variables in production — a .env.example template is included in the repo.


Code examples

From zero to
first transaction

from pyairtel import AirtelMoney

# Initialise — sandbox=True for testing, False for production
airtel = AirtelMoney(
    client_id=os.environ["AIRTEL_CLIENT_ID"],
    client_secret=os.environ["AIRTEL_CLIENT_SECRET"],
    sandbox=os.getenv("AIRTEL_SANDBOX", "true").lower() == "true,
)

# Send a USSD push — subscriber gets a PIN prompt on their phone
resp = airtel.collect(
    phone="+255681219610",   # +255, 0, or 255 formats all accepted
    amount=5000,              # Tanzanian Shillings
    reference="invoice-42",
)

print(resp.transaction_id)  # TXN-20240101120000-AB12CD34
print(resp.is_initiated)     # True
import time

# Wait for the subscriber to approve on their phone
time.sleep(15)

status = airtel.get_collection_status(resp.transaction_id)

if status.is_successful:
    print("✅ Payment confirmed!", status.airtel_money_id)

elif status.is_pending:
    print("⏳ Subscriber hasn't approved yet...")

elif status.is_failed:
    print("❌ Payment failed:", status.message)

# Transaction status codes:
# TS  — Transaction Successful
# TIP — Transaction In Progress
# TF  — Transaction Failed
# TA  — Transaction Ambiguous
# Use airtel_money_id from a successful collection status
status = airtel.get_collection_status(transaction_id)

if status.is_successful:
    refund = airtel.refund(status.airtel_money_id)

    if refund.is_successful:
        print("↩️  Refund initiated:", refund.message)
    else:
        print("Refund failed:", refund.message)
# Step 1 — check the payee can receive money
check = airtel.validate_payee("+255754123456")

if not check.is_valid:
    print("Cannot receive money:", check.message)
else:
    # Step 2 — transfer money (PIN is RSA-encrypted automatically)
    result = airtel.transfer(
        phone="+255681219610",
        amount=2000,
        pin="1234",                  # merchant PIN — encrypted before sending
        public_key_pem=open("airtel_pub.pem").read(),
        payer_first_name="Ronald",
        payer_last_name="Gosso",
        reference="payout-001",
    )

    print(result.is_successful, result.airtel_money_id)
from pyairtel import AirtelMoney, decode_esb_error
from pyairtel.exceptions import (
    AuthenticationError,
    CollectionError,
    DisbursementError,
    EncryptionError,
)

try:
    resp = airtel.collect(phone="+255754123456", amount=1000, reference="ref-1")

except AuthenticationError as e:
    print("Bad credentials:", e)

except CollectionError as e:
    print("ESB code:", e.esb_code)      # e.g. "ESB000014"
    print("Reason:",   e.esb_message)   # "Insufficient funds..."

# Decode any ESB code manually
print(decode_esb_error("ESB000039"))
# → "Transaction timed out. The subscriber did not respond..."
# Step 1 — copy the template and fill in your values
# cp .env.example .env

# .env (never commit this file — it's in .gitignore)
AIRTEL_CLIENT_ID=your-client-id-here
AIRTEL_CLIENT_SECRET=your-client-secret-here
AIRTEL_SANDBOX=true
AIRTEL_PUBLIC_KEY_PATH=airtel_pub.pem  # disbursement only

# Step 2 — install python-dotenv
# pip install python-dotenv

# Step 3 — load in your app
from dotenv import load_dotenv
import os
from pyairtel import AirtelMoney

load_dotenv()  # reads .env into environment variables

airtel = AirtelMoney(
    client_id=os.environ["AIRTEL_CLIENT_ID"],
    client_secret=os.environ["AIRTEL_CLIENT_SECRET"],
    sandbox=os.getenv("AIRTEL_SANDBOX", "true").lower() == "true",
)

API Reference

AirtelMoney methods

Method HTTP Description Returns
collect(phone, amount, reference) POST Send USSD push payment request to subscriber CollectionResponse
get_collection_status(transaction_id) GET Poll status of a collection — TS / TIP / TF / TA TransactionStatusResponse
refund(airtel_money_id) POST Reverse a completed collection transaction RefundResponse
validate_payee(phone) GET Check if a number is a valid Airtel Money wallet ValidationResponse
transfer(phone, amount, pin, ...) POST Transfer money from merchant wallet to subscriber DisbursementResponse

Security

Credentials &
environment variables

01

Never hardcode credentials

# ❌ Don't do this
airtel = AirtelMoney(client_id="abc123", client_secret="secret")

# ✅ Do this instead — load from environment
airtel = AirtelMoney(
    client_id=os.environ["AIRTEL_CLIENT_ID"],
    client_secret=os.environ["AIRTEL_CLIENT_SECRET"],
)
02

Copy .env.example.env

cp .env.example .env
# .env — fill in your real values from the Airtel developer portal
AIRTEL_CLIENT_ID=your-client-id-here
AIRTEL_CLIENT_SECRET=your-client-secret-here
AIRTEL_SANDBOX=true
AIRTEL_PUBLIC_KEY_PATH=airtel_pub.pem  # disbursement only
03

Load in your app with python-dotenv

pip install python-dotenv
from dotenv import load_dotenv
import os
from pyairtel import AirtelMoney

load_dotenv()

airtel = AirtelMoney(
    client_id=os.environ["AIRTEL_CLIENT_ID"],
    client_secret=os.environ["AIRTEL_CLIENT_SECRET"],
    sandbox=os.getenv("AIRTEL_SANDBOX", "true").lower() == "true",
)
04

Files that must never be committed

# .gitignore — already included in the repo
.env
*.pem          # your RSA public key for disbursement

Production deployments

On servers and CI/CD pipelines, skip .env files entirely. Set AIRTEL_CLIENT_ID and AIRTEL_CLIENT_SECRET as native environment variables or secrets (e.g. GitHub Actions Secrets, Railway variables, Render environment). os.environ[...] picks them up automatically — no code changes needed.


Local Development

Run the package locally
step by step

01

Clone the repo

git clone https://github.com/ronaldgosso/pyairtel.git
cd pyairtel
02

Create & activate virtual environment

# Create venv
python -m venv venv

# Activate — Linux / Mac
source venv/bin/activate

# Activate — Windows
venv\Scripts\activate

# Confirm — should show venv path
which python
03

Install package + dev dependencies

pip install -e ".[dev]"

# For disbursement with RSA PIN encryption
pip install -e ".[dev,encryption]"
04

Fix lint errors automatically

# Auto-fix ruff issues (imports, whitespace, deprecated types)
ruff check pyairtel tests --fix --unsafe-fixes

# Format with black
black pyairtel tests
05

Verify full quality gate

# Linting — should print: All checks passed!
ruff check pyairtel tests

# Formatting — should print: X files would be left unchanged
black --check pyairtel tests

# Type checking — should print: Success: no issues found
mypy pyairtel
06

Run all tests

# Run all 25 tests with verbose output
pytest tests/ -v

# Run a specific test class
pytest tests/ -v -k "TestCollection"

# Run a single test
pytest tests/ -v -k "test_collect_success"
07

Deactivate venv when done

deactivate

Error Codes

Airtel Tanzania
ESB error reference

ESB000001
General ErrorTransaction failed — general error. Check credentials and try again.
ESB000004
Service UnavailableAirtel Money service is temporarily down.
ESB000008
Invalid TransactionThe request parameters are incorrect.
ESB000011
Subscriber Not FoundPhone number is not registered on Airtel Money.
ESB000014
Insufficient FundsSubscriber's Airtel Money balance is too low.
ESB000033
Transaction Limit ExceededAmount is above the subscriber's transaction limit.
ESB000036
Daily Limit ExceededSubscriber has reached their daily transaction limit.
ESB000039
Transaction Timed OutSubscriber did not respond to the USSD prompt in time.
ESB000041
PIN LockedToo many incorrect PIN attempts by the subscriber.
ESB000045
Duplicate TransactionA transaction with this ID was already processed.