TeePayDocs
SDKs

Python SDK

Accept payments, create hosted checkouts, send payouts and verify webhooks from Python with sync and async clients.

Package
teepay
Install
pip install teepay
Runtime
Python 3.9 or newer; httpx is the only dependency
Clients
TeePay (sync) and AsyncTeePay (async), fully typed

Install

pip install teepay

Create a client

import os
from teepay import TeePay

teepay = TeePay(os.environ["TEEPAY_API_KEY"])  # or TeePay() to read TEEPAY_API_KEY

print(teepay.environment)  # "sandbox" or "live"
caps = teepay.capabilities()
print(caps.features.collection_create, caps.supported_countries)

Options: timeout (seconds, default 30), max_retries (default 2), base_url (default https://api.teepay.me), api_version (pins the TeePay-Version header, e.g. "2026-10-18") and http_client (your own httpx.Client). Every method also accepts timeout= and max_retries=.

Mobile money push

Sends a USSD prompt to the customer's phone. See Mobile money push.

payment = teepay.collections.create(
    money={"amount_minor": "500000", "currency": "TZS"},  # TZS 5,000.00
    customer={"country": "TZ", "phone": "+255712345678", "name": "Asha Juma"},
    description="Order 1001",
    idempotency_key="order-1001",
)
print(payment.public_reference, payment.status)  # "TP8K2M4QZ1" "pending"

# 202 means the prompt was sent, not that the customer paid.
latest = teepay.collections.retrieve(payment.public_reference)
if latest.status == "succeeded":
    fulfil_order()

Hosted checkout session

See Checkout sessions.

session = teepay.checkout_sessions.create(
    title="Order 1001",
    description="2 x Coffee bag",
    amount_minor="500000",
    currency="TZS",
    reference="order-1001",
    metadata={"cart": "c-77"},  # keys must be lowercase
    idempotency_key="checkout-order-1001",
)
redirect(session.checkout_url)

# Later, on your server:
session = teepay.checkout_sessions.retrieve(session.id)
if session.status == "paid":
    print(session.payment.reference)

Control Numbers and payouts

number = teepay.control_numbers.create(
    kind="bill",
    customer={"name": "Asha Juma", "phone": "0712345678"},
    description="School fees, term 1",
    amount_minor="5000000",
    idempotency_key="invoice-2026-0077",
)
if number.last_response.status == 202:
    ...  # not issued yet: call create again later with the SAME idempotency_key

quote = teepay.disbursements.quote(
    money={"amount": 2500000, "currency": "TZS"},  # payout quotes take an integer
    destination={"channel": "mobile", "name": "Juma Ali", "phone": "255712345678"},
    narration="Supplier payment October",
)
payout = teepay.disbursements.create(quote_id=quote.id, idempotency_key="payout-2026-10-0001")

Lists

page = teepay.collections.list(limit=50, status="succeeded")
for item in page["items"]:
    print(item.public_reference)

for payment in teepay.collections.list_all(status="succeeded"):  # follows next_cursor
    print(payment.public_reference)

Results are dicts with attribute access (obj.status == obj["status"]). Use brackets for items, which is also a dict method. Control Number lists put their rows in page["data"].

Async

import asyncio
from teepay import AsyncTeePay

async def main() -> None:
    async with AsyncTeePay() as teepay:
        session = await teepay.checkout_sessions.create(
            title="Order 1001", amount_minor="500000", currency="TZS", idempotency_key="checkout-order-1001",
        )
        print(session.checkout_url)
        async for payment in teepay.collections.list_all():
            print(payment.public_reference)

asyncio.run(main())

Verify webhooks

Pass the raw body bytes, before any JSON parsing.

import os
from flask import Flask, request
from teepay import WebhookVerificationError, verify_webhook

app = Flask(__name__)

@app.post("/webhooks/teepay")
def teepay_webhook():
    try:
        event = verify_webhook(request.get_data(), request.headers, os.environ["TEEPAY_WEBHOOK_SECRET"])
    except WebhookVerificationError:
        return "", 400
    if event.event == "payment.completed":
        mark_paid(event.data.reference)  # process each event.id once
    return "", 200

verify_webhook(payload, headers, secret, tolerance=300) checks the signature in constant time and returns the event. client.webhooks.verify(...) is the same; client.webhooks.is_valid(...) returns a bool, and generate_test_headers(payload, secret) signs a payload for your tests.

Errors

from teepay import AccountRestrictedError, ApiError, APIConnectionError, RateLimitError, ValidationError

try:
    teepay.collections.create(money=money, customer=customer, idempotency_key="order-1001")
except ValidationError as e:      # 422: invalid_request, unsupported_market, invalid_amount, ...
    print(e.code, e.message)
except AccountRestrictedError:    # 403 account_restricted: contact TeePay support
    ...
except RateLimitError as e:       # 429: wait e.retry_after seconds
    ...
except APIConnectionError:        # no response: retry with the SAME key, or read the payment
    ...
except ApiError as e:             # anything else, e.g. 503
    print(e.status, e.code, e.request_id)
ExceptionWhen
AuthenticationError401 invalid_credential
PermissionError (alias PermissionDeniedError)403 scope_denied, access_denied, developer_access_denied
AccountRestrictedError403 account_restricted (subclass of PermissionError)
NotFoundError404 not_found
ConflictError409 idempotency_conflict, expired payout quote
ValidationError422 codes such as invalid_request and unsupported_market
RateLimitError429 rate_limited, with retry_after
ApiErrorAny other HTTP error; base class of all of the above
ConnectionError, TimeoutError (aliases APIConnectionError, APITimeoutError)No HTTP response
WebhookVerificationErrorBad webhook signature or timestamp

PermissionError, ConnectionError and TimeoutError share names with Python built-ins; import the aliases if you prefer. Request keys that are not lowercase raise ValueError before anything is sent.

Idempotency and retries

Collections, checkout sessions and Control Numbers send an Idempotency-Key header; disbursements.create sends idempotency_key in the body. Without one, the SDK generates a key and reuses it for its own retries. Derive keys from your own records so a retry after a restart cannot create a second payment. See Idempotency.

Only GET requests and keyed creates are retried, on connection errors, timeouts, 408, 429 and 5xx, using Retry-After when present.

Response metadata

info = payment.last_response
info.status               # 202
info.request_id           # quote to support
info.rate_limit           # RateLimit(limit=120, remaining=117, reset=42)
info.idempotent_replayed  # True when an earlier result was returned

On this page