pyairtel — Airtel Money Python SDK

● Tanzania · Airtel Money Open API

# pyairtel

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

Get Started View on GitHub
$ pip install pyairtel

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.