Python client for Airtel Money Tanzania — Collection & Disbursement APIs
Project description
pyairtel
Python client for Airtel Money Tanzania — Collection & Disbursement APIs.
📖 Full documentation & examples → ronaldgosso.github.io/pyairtel
Features
- 🔐 OAuth2 authentication with automatic token refresh
- 📲 Collection — USSD push payments (request money from subscribers)
- ✅ Transaction status polling
- 💸 Disbursement — transfer money to Airtel Money wallets
- ↩️ Refunds — reverse completed transactions by
airtel_money_id - 🔒 RSA PIN encryption for disbursement security
- 📞 Phone number normalisation (handles
+255…,0…,255…formats) - ⚠️ ESB error decoding — all 9 Airtel Tanzania error codes mapped to human-readable messages
- 🧪 Sandbox & Production environments
Installation
pip install pyairtel
For disbursement with PIN encryption, install the encryption extra:
pip install "pyairtel[encryption]"
Quick Start
1. Get credentials
Create an account at developers.airtel.co.tz, create an application, and add Collection and Disbursement APIs. Copy your client_id and client_secret from Key Management.
2. Collect money from a subscriber
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", # set False for production
)
response = airtel.collect(
phone="+255681219610",
amount=5000,
reference="invoice-42",
)
print(response.transaction_id) # TXN-20240101120000123456-AB12CD34
print(response.is_initiated) # True
3. Check transaction status
import time
time.sleep(15) # give the subscriber time to approve
status = airtel.get_collection_status(response.transaction_id)
if status.is_successful:
print("Payment confirmed!", status.airtel_money_id)
elif status.is_pending:
print("Still waiting for subscriber to approve...")
elif status.is_failed:
print("Payment failed:", status.message)
4. Transfer money to a subscriber (Disbursement)
# First validate the payee
check = airtel.validate_payee("+255681219610")
if not check.is_valid:
print("Payee cannot receive money:", check.message)
else:
result = airtel.transfer(
phone="+255681219610",
amount=2000,
pin="1234", # your merchant PIN
public_key_pem=open("airtel_pub.pem").read(), # from Key Management
payer_first_name="Ronald",
payer_last_name="Gosso",
reference="payout-001",
)
print(result.is_successful, result.airtel_money_id)
5. Refund a transaction
# 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)
print(refund.is_successful, refund.message)
Environment
| Parameter | Sandbox | Production |
|---|---|---|
sandbox |
True |
False |
| Base URL | https://openapiuat.airtel.africa |
https://openapi.airtel.africa |
Credentials & Environment Variables
Never hardcode credentials in your code. Copy .env.example to .env and fill in your values:
cp .env.example .env
.env:
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
Then load it in your project:
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",
)
⚠️
.envand*.pemare in.gitignore— never commit them.
Error Handling
from pyairtel import AirtelMoney, decode_esb_error
from pyairtel.exceptions import AuthenticationError, CollectionError, EncryptionError
try:
resp = airtel.collect(phone="+255681219610", amount=1000, reference="ref-1")
except AuthenticationError as e:
print("Bad credentials or token expired:", e)
except CollectionError as e:
print("Collection failed:", 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 to the USSD prompt in time."
| Exception | When raised |
|---|---|
AuthenticationError |
Token acquisition fails (bad credentials, network) |
CollectionError |
USSD push, status check, or refund fails — includes .esb_code and .esb_message |
DisbursementError |
Transfer or payee validation fails |
EncryptionError |
RSA PIN encryption fails (e.g. missing pycryptodome) |
ValidationError |
Invalid phone number format |
Airtel Tanzania ESB Error Codes
| Code | Meaning |
|---|---|
ESB000001 |
General error — check credentials and try again |
ESB000004 |
Service unavailable — Airtel Money is temporarily down |
ESB000008 |
Invalid transaction — request parameters are incorrect |
ESB000011 |
Subscriber not found — number not registered on Airtel Money |
ESB000014 |
Insufficient funds — subscriber balance too low |
ESB000033 |
Transaction limit exceeded — amount above subscriber's limit |
ESB000036 |
Daily limit exceeded — subscriber has hit their daily cap |
ESB000039 |
Transaction timed out — subscriber didn't respond to USSD prompt |
ESB000041 |
PIN locked — too many incorrect PIN attempts |
ESB000045 |
Duplicate transaction — this ID was already processed |
Local Development
Follow these steps to run pyairtel locally, run tests, and contribute.
1. Clone the repository
git clone https://github.com/ronaldgosso/pyairtel.git
cd pyairtel
2. Create and activate a virtual environment
# Create venv
python -m venv venv
# Activate — Linux / Mac
source venv/bin/activate
# Activate — Windows
venv\Scripts\activate
# Confirm — should show the venv path
which python
3. Install the package and dev dependencies
pip install -e ".[dev]"
# Also install encryption support for disbursement tests
pip install -e ".[dev,encryption]"
# install from requirements.txt
pip install -r requirements.txt
4. Fix lint errors automatically
# Auto-fix ruff issues (import order, trailing whitespace, deprecated types)
ruff check pyairtel tests --fix --unsafe-fixes
# Format with black
black pyairtel tests
5. Verify the 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 in 7 source files
mypy pyairtel
6. Run the test suite
# Run all 25 tests with verbose output
pytest tests/ -v
# Run a specific test class
pytest tests/ -v -k "TestCollection"
# Run a single test by name
pytest tests/ -v -k "test_collect_success"
7. Deactivate the venv when done
deactivate
Contributing
Contributions are welcome! Please follow these steps:
- Fork the repository on GitHub
- Create a feature branch:
git checkout -b feat/your-feature-name - Make your changes — all new code must include tests
- Run the full quality gate (steps 4–6 above) and ensure everything passes
- Commit with a descriptive message:
git commit -m "feat: add your feature" - Push and open a Pull Request against
main
Please keep PRs focused — one feature or fix per PR. If you're unsure whether something is in scope, open an issue first.
License
MIT © Ronald Isack Gosso
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pyairtel-0.1.0.tar.gz.
File metadata
- Download URL: pyairtel-0.1.0.tar.gz
- Upload date:
- Size: 263.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
49d9a9c157db4c8fc314264ce0fce32400f46e74aefd7ce5076cd774e9e4639a
|
|
| MD5 |
eedb5849f7f101700546ff2ee336e0a1
|
|
| BLAKE2b-256 |
e2f0e894e0e00e43ed37f138df00951c7067841100ac87ccf6025e5e1bf90c5f
|
Provenance
The following attestation bundles were made for pyairtel-0.1.0.tar.gz:
Publisher:
ci.yml on ronaldgosso/pyairtel
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyairtel-0.1.0.tar.gz -
Subject digest:
49d9a9c157db4c8fc314264ce0fce32400f46e74aefd7ce5076cd774e9e4639a - Sigstore transparency entry: 1154458441
- Sigstore integration time:
-
Permalink:
ronaldgosso/pyairtel@47672033584add763e7ed52a73f6bfdf6755f7af -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/ronaldgosso
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@47672033584add763e7ed52a73f6bfdf6755f7af -
Trigger Event:
push
-
Statement type:
File details
Details for the file pyairtel-0.1.0-py3-none-any.whl.
File metadata
- Download URL: pyairtel-0.1.0-py3-none-any.whl
- Upload date:
- Size: 16.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
38c62721e5991fe804d7acf5174e543fb1375f9b63e1b136456c0bf937f88de6
|
|
| MD5 |
8455723cf95d1671b7187090efd48ebd
|
|
| BLAKE2b-256 |
baff4520dce3474c24b051dc01783b10260a42f843958eac29348b6ad999a046
|
Provenance
The following attestation bundles were made for pyairtel-0.1.0-py3-none-any.whl:
Publisher:
ci.yml on ronaldgosso/pyairtel
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyairtel-0.1.0-py3-none-any.whl -
Subject digest:
38c62721e5991fe804d7acf5174e543fb1375f9b63e1b136456c0bf937f88de6 - Sigstore transparency entry: 1154458443
- Sigstore integration time:
-
Permalink:
ronaldgosso/pyairtel@47672033584add763e7ed52a73f6bfdf6755f7af -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/ronaldgosso
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@47672033584add763e7ed52a73f6bfdf6755f7af -
Trigger Event:
push
-
Statement type: