Python SDK for Airtel Money Tanzania — Collection, Disbursement & Refunds
What's included
Automatic token acquisition and refresh. Your credentials stay safe — tokens are managed internally and renewed 60 seconds before expiry.
Request payments from subscribers. A prompt is sent to their phone — they enter their Airtel Money PIN to approve. No redirects needed.
Poll transaction status with typed responses: is_successful, is_pending, is_failed. No raw string parsing required.
Transfer money directly to any Airtel Money wallet. Includes payee validation before sending so you never send to an invalid number.
Reverse a completed collection using Airtel's internal airtel_money_id. One line of code — no extra configuration needed.
Disbursements require your merchant PIN encrypted with Airtel's RSA public key. Handled automatically via pycryptodome.
Pass numbers in any format — +255…, 0…, or 255…. Automatically normalised to the 12-digit format Airtel expects.
All 9 real Airtel Tanzania ESB error codes mapped to human-readable messages. CollectionError.esb_code and .esb_message on every failure.
Toggle between UAT sandbox and live production with a single sandbox=True/False flag. Same code, different environment.
Code examples
from pyairtel import AirtelMoney # Initialise — sandbox=True for testing, False for production airtel = AirtelMoney( client_id="your-client-id", client_secret="your-client-secret", sandbox=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..."
API Reference
Local Development
git clone https://github.com/ronaldgosso/pyairtel.git cd pyairtel
# 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
pip install -e ".[dev]"
# For disbursement with RSA PIN encryption
pip install -e ".[dev,encryption]"
# Auto-fix ruff issues (imports, whitespace, deprecated types) ruff check pyairtel tests --fix --unsafe-fixes # Format with black black pyairtel tests
# 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
# 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"
deactivate
Error Codes