Skip to main content

Production-grade Python SDK for the ClickPesa API — sync & async, collections, payouts, BillPay and more

Project description

ClickPesa Python SDK

PyPI version Python License: MIT

Production-grade Python SDK for the ClickPesa API — supports both sync and async usage, with automatic token management, checksum injection, retry logic, and a full exception hierarchy.


Features

  • Sync & AsyncClickPesa for blocking code, AsyncClickPesa for asyncio / FastAPI / async frameworks
  • Auto Auth — JWT tokens are fetched and cached automatically (55-minute window, 1-hour API TTL)
  • Checksum injection — HMAC-SHA256 checksums added to every mutating request when a checksum_key is configured
  • Retries — exponential backoff on transient 5xx errors (configurable)
  • Thread-safe — safe to share a single client across threads or async tasks
  • Context managerwith / async with support for automatic cleanup
  • Typed exceptions — structured error hierarchy with status_code and response attributes
  • PEP 561 compliant — ships with py.typed for mypy / pyright support

Installation

pip install clickpesa-python-sdk

Requires Python 3.10+.


Quick Start

Sync

from clickpesa import ClickPesa

with ClickPesa(
    client_id="YOUR_CLIENT_ID",
    api_key="YOUR_API_KEY",
    checksum_key="YOUR_CHECKSUM_KEY",  # optional but recommended
    sandbox=True,                       # set False for production
) as client:
    balance = client.account.get_balance()
    print(balance)

Async

import asyncio
from clickpesa import AsyncClickPesa

async def main():
    async with AsyncClickPesa(
        client_id="YOUR_CLIENT_ID",
        api_key="YOUR_API_KEY",
        checksum_key="YOUR_CHECKSUM_KEY",
        sandbox=True,
    ) as client:
        balance = await client.account.get_balance()
        print(balance)

asyncio.run(main())

Configuration

Parameter Type Default Description
client_id str required Your ClickPesa application Client ID
api_key str required Your ClickPesa application API key
checksum_key str | None None Enables HMAC-SHA256 checksum on every mutating request
sandbox bool False Target sandbox (api-sandbox.clickpesa.com) instead of production
timeout float 30.0 Per-request timeout in seconds
max_retries int 3 Max retry attempts on transient server errors

Note: order_id / orderReference values must be alphanumeric only (no hyphens, underscores, or special characters). The API will reject any order reference containing non-alphanumeric characters.


Collections

USSD Push

# 1. Preview — check available methods and fees before charging
preview = client.payments.preview_ussd_push(
    amount="5000",
    order_id="ORD20240001",
    phone="255712345678",           # optional: include to get sender details
    fetch_sender_details=True,      # optional: returns accountName / accountProvider
)
print(preview["activeMethods"])     # [{"name": "TIGO-PESA", "status": "AVAILABLE", "fee": 580, ...}]

# 2. Initiate — triggers PIN prompt on the customer's phone
transaction = client.payments.initiate_ussd_push(
    amount="5000",
    phone="255712345678",
    order_id="ORD20240001",
    currency="TZS",                 # only TZS supported
)
print(transaction["id"], transaction["status"])

Card Payment

# 1. Preview — check card method availability
preview = client.payments.preview_card(amount="50", order_id="CARD001")

# 2. Initiate — generate a hosted payment link
result = client.payments.initiate_card(
    amount="50",
    order_id="CARD001",
    currency="USD",                 # only USD supported
    customer={
        "fullName": "John Doe",
        "email": "john@example.com",
        "phoneNumber": "255712345678",
    },
    # or use an existing customer ID:
    # customer={"id": "CUST_123"}
)
print(result["cardPaymentLink"])    # redirect customer here

Query Payments

# Single payment by order reference
payments = client.payments.get_status("ORD20240001")

# Paginated list with filters
page = client.payments.list_all(
    status="SUCCESS",
    collectedCurrency="TZS",
    startDate="2024-01-01",
    endDate="2024-12-31",
    limit=20,
    skip=0,
)
print(page["totalCount"], page["data"])

Disbursements

Mobile Money Payout

# 1. Preview — verify fees and recipient before sending
preview = client.payouts.preview_mobile_money(
    amount=10000,
    phone="255712345678",
    order_id="PAY20240001",
    currency="TZS",                 # TZS or USD; recipient always receives TZS
)
print(preview["fee"], preview["receiver"]["accountName"])

# 2. Create — disburse funds
payout = client.payouts.create_mobile_money(
    amount=10000,
    phone="255712345678",
    order_id="PAY20240001",
)
print(payout["id"], payout["status"])  # status: AUTHORIZED → PROCESSING → SUCCESS

Bank Payout (ACH / RTGS)

# Get list of supported banks and their BIC codes
banks = client.payouts.get_banks()
# [{"name": "EQUITY BANK TANZANIA LIMITED", "value": "equity_bank_tanzania_limited", "bic": "EQBLTZTZ"}, ...]

# 1. Preview
preview = client.payouts.preview_bank(
    amount=500000,
    account_number="1234567890",
    bic="EQBLTZTZ",
    order_id="BANK20240001",
    transfer_type="ACH",            # "ACH" or "RTGS"
    currency="TZS",
)

# 2. Create
payout = client.payouts.create_bank(
    amount=500000,
    account_number="1234567890",
    account_name="Jane Doe",
    bic="EQBLTZTZ",
    order_id="BANK20240001",
    transfer_type="RTGS",
    currency="TZS",
)

Query Payouts

# Single payout by order reference
payouts = client.payouts.get_status("PAY20240001")

# All payouts with filters
page = client.payouts.list_all(
    channel="MOBILE MONEY",
    status="SUCCESS",
    limit=50,
)

BillPay

ClickPesa BillPay lets customers pay using a numeric control number through mobile money, SIM banking, and CRDB Wakalas. There are two types of control numbers:

  • Order — one-time, closes after payment. Ideal for invoices and e-commerce orders.
  • Customer — static and reusable per customer. Ideal for subscriptions and recurring payments.

Note: Every ClickPesa merchant has a 4-digit Merchant BillPay-Namba visible on the dashboard. Order control numbers can also be generated offline (no API call) by concatenating your Merchant BillPay-Namba with any internal order reference (e.g. 1122 + 231256 = 1122231256). The SDK only covers API-based generation.

Create Control Numbers

# Order control number (one-time)
cn = client.billpay.create_order_control_number(
    bill_reference="INVOICE001",    # optional — becomes the control number; auto-generated if omitted
    amount=90900,
    description="Water Bill - July 2024",
    payment_mode="EXACT",           # "EXACT" or "ALLOW_PARTIAL_AND_OVER_PAYMENT"
)
print(cn["billPayNumber"])          # share this with your customer

# Customer control number (persistent / recurring)
cn = client.billpay.create_customer_control_number(
    customer_name="John Doe",
    phone="255712345678",           # phone or email required
    email="john@example.com",
    amount=50000,
    payment_mode="ALLOW_PARTIAL_AND_OVER_PAYMENT",
)

Bulk Create (up to 50 per request)

# Bulk order control numbers
result = client.billpay.bulk_create_order_numbers([
    {"billAmount": 10000, "billDescription": "Invoice #001", "billPaymentMode": "EXACT"},
    {"billAmount": 20000, "billDescription": "Invoice #002"},
    {"billReference": "MYREF003", "billAmount": 5000},
])
print(result["created"], result["failed"])
print(result["billPayNumbers"])

# Bulk customer control numbers
result = client.billpay.bulk_create_customer_numbers([
    {"customerName": "Alice", "customerPhone": "255712345678", "billAmount": 15000},
    {"customerName": "Bob",   "customerEmail": "bob@example.com"},
])

Manage Existing Numbers

# Query details
details = client.billpay.get_details("55042914871931")

# Update amount, description or payment mode
client.billpay.update_reference(
    "55042914871931",
    amount=120000,
    description="Updated Water Bill",
    payment_mode="EXACT",
)

# Activate / deactivate
client.billpay.update_status("55042914871931", "INACTIVE")
client.billpay.update_status("55042914871931", "ACTIVE")

Hosted Links

# Checkout link — customer chooses their payment method
result = client.links.generate_checkout(
    order_id="LINK001",
    order_currency="TZS",
    total_price="15000",
    customer_name="Jane Doe",
    customer_email="jane@example.com",
    customer_phone="255712345678",
    description="Order LINK001",
)
print(result["checkoutLink"])       # redirect customer here

# With itemised order instead of a flat total
result = client.links.generate_checkout(
    order_id="LINK002",
    order_currency="USD",
    order_items=[
        {"name": "Widget A", "price": "25.00", "quantity": 2},
        {"name": "Widget B", "price": "10.00", "quantity": 1},
    ],
)

# Payout link — recipient enters their own bank / mobile details
result = client.links.generate_payout(amount="50000", order_id="POUT001")
print(result["payoutLink"])

Account & Exchange

# Account balances
result = client.account.get_balance()
# {"balances": [{"currency": "TZS", "balance": 39700}, {"currency": "USD", "balance": 0}]}
print(result["balances"])

# Transaction statement
statement = client.account.get_statement(
    currency="TZS",
    start_date="2024-01-01",
    end_date="2024-12-31",
)
print(statement["accountDetails"])
print(statement["transactions"])

# Exchange rates
rates = client.exchange.get_rates()                          # all pairs
rates = client.exchange.get_rates(source="USD", target="TZS")  # specific pair
# [{"source": "USD", "target": "TZS", "rate": 2510, "date": "..."}]

Async Usage

Every method on AsyncClickPesa is the await-able equivalent:

import asyncio
from clickpesa import AsyncClickPesa

async def run_payments():
    async with AsyncClickPesa(
        client_id="YOUR_CLIENT_ID",
        api_key="YOUR_API_KEY",
        sandbox=True,
    ) as client:
        # Run multiple API calls concurrently
        balance, rates = await asyncio.gather(
            client.account.get_balance(),
            client.exchange.get_rates(source="USD"),
        )
        print(balance, rates)

        # Collections
        tx = await client.payments.initiate_ussd_push(
            amount="3000",
            phone="255712345678",
            order_id="ASYNC001",
        )

        # Disbursements
        payout = await client.payouts.create_mobile_money(
            amount=3000,
            phone="255712345678",
            order_id="ASYNC002",
        )

asyncio.run(run_payments())

Webhook Verification

from clickpesa import WebhookValidator

# In your webhook endpoint (Flask / FastAPI / Django etc.)
def webhook_handler(request):
    is_valid = WebhookValidator.verify(
        payload=request.json,
        signature=request.headers["X-ClickPesa-Signature"],
        checksum_key="YOUR_CHECKSUM_KEY",
    )
    if not is_valid:
        return {"error": "Invalid signature"}, 401

    # Process event ...

Error Handling

All errors are subclasses of ClickPesaError and carry .status_code and .response:

from clickpesa.exceptions import (
    AuthenticationError,    # 401 — invalid credentials / expired token
    ForbiddenError,         # 403 — feature not enabled on your account
    ValidationError,        # 400 — bad payload
    InsufficientFundsError, # 400 — not enough balance (subclass of ValidationError)
    NotFoundError,          # 404 — resource not found
    ConflictError,          # 409 — duplicate orderReference / billReference
    RateLimitError,         # 429 — payout request already in progress
    ServerError,            # 5xx — ClickPesa server error
    ClickPesaError,         # base class — catches all of the above
)

try:
    client.payments.initiate_ussd_push("5000", "255712345678", "ORD001")

except InsufficientFundsError as e:
    print(f"Not enough balance: {e}")

except ConflictError as e:
    print(f"Order reference already used: {e}")
    print(f"HTTP {e.status_code}{e.response}")

except ClickPesaError as e:
    print(f"Unexpected API error [{e.status_code}]: {e}")

Health Check

# Returns True if the API is reachable and credentials are valid
if client.is_healthy():
    print("Connected to ClickPesa")
else:
    print("API unreachable or credentials invalid")

# Async equivalent
healthy = await client.is_healthy()

Development

git clone https://github.com/JAXPARROW/clickpesa-python-sdk
cd clickpesa-python-sdk

# Install with dev dependencies
pip install -e ".[dev]"

# Run tests
pytest

# Run tests with coverage
pytest --cov=clickpesa --cov-report=term-missing

License

MIT — see LICENSE for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

clickpesa_python_sdk-0.1.2.tar.gz (31.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

clickpesa_python_sdk-0.1.2-py3-none-any.whl (28.6 kB view details)

Uploaded Python 3

File details

Details for the file clickpesa_python_sdk-0.1.2.tar.gz.

File metadata

  • Download URL: clickpesa_python_sdk-0.1.2.tar.gz
  • Upload date:
  • Size: 31.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.5

File hashes

Hashes for clickpesa_python_sdk-0.1.2.tar.gz
Algorithm Hash digest
SHA256 dda9a882eb73ef18298cd82b3adf7600b4776ee4e47acb0f0dbb0d18b09b2196
MD5 ceae85317d3a5adea2d521e7775a4e56
BLAKE2b-256 630ade561830da2923d3330b71007bc33496eb5591fdc2031de35f4b9ec11f59

See more details on using hashes here.

File details

Details for the file clickpesa_python_sdk-0.1.2-py3-none-any.whl.

File metadata

File hashes

Hashes for clickpesa_python_sdk-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 af7be109bd90f76e4ae59d67ca1f7c55c3feac80ed0bc5e1c7bd0d6cf05e20ae
MD5 4d93cfbfe3e9094a4455be9a50c6192c
BLAKE2b-256 f80e14e3b451f89a58520fa0804aa774285dfb2f62308e239845c6e03c4d10a8

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page