Skip to main content

lenco-py

Unofficial Python SDK for the Lenco API v2 — accounts, transfers, collections, settlements, transactions, and webhooks. Framework-agnostic: use it from Django, FastAPI, Flask, Celery, or plain scripts.

Not affiliated with or endorsed by Lenco.

PyPI PyPI - Python Version PyPI - Downloads License: BSD-3-Clause

CI Coverage Ruff Code style: black Checked with mypy

Python Pydantic Pytest

Contents generated with DocToc

Documentation: https://engineervix.github.io/lenco-py/

  • Sync and async clients with identical resource APIs
  • Fully typed (mypy strict), pydantic v2 models
  • Webhook signature verification with the standard library only
  • JWE card-payload encryption as an optional extra
  • Automatic retry with jittered backoff for transient failures — GET only, never POST
  • Zambian phone number normalization as an optional extra

Installation

pip install lenco-py
pip install "lenco-py[card]"    # adds jwcrypto for card collections
pip install "lenco-py[phone]"   # adds phonenumbers for phone normalization

Quickstart

from lenco import LencoClient

with LencoClient(token="your-api-token") as client:
    # Accounts
    for account in client.accounts.list().items:
        print(account.id, account.currency, account.available_balance)

    balance = client.accounts.balance("b176cda5-7d97-4a3f-b4dd-ab0234e9e08c")

    # Verify a recipient before sending money
    resolved = client.resolve.bank_account(account_number="9130000000000", bank_id="002")
    print(resolved.account_name)  # "Beata Jean"

    # Send money (mobile money — Zambia)
    transfer = client.transfers.to_mobile_money(
        account_id="your-account-uuid",
        amount=20.00,
        reference="order-1234",       # unique per transfer
        phone="0977433571",
        operator="airtel",
        country="zm",
    )
    # Transfers always return HTTP 200 — inspect the status:
    assert transfer.status == "successful", transfer.reason_for_failure

Async (FastAPI)

from fastapi import FastAPI
from lenco import AsyncLencoClient

app = FastAPI()
client = AsyncLencoClient(token="your-api-token")

@app.post("/payments/request")
async def request_payment(phone: str, amount: float):
    collection = await client.collections.from_mobile_money(
        amount=amount,
        reference="order-5678",
        phone=phone,
        operator="mtn",
        country="zm",
        bearer="merchant",
    )
    # status is "pay-offline" until the customer approves on their phone
    return {"reference": collection.reference, "status": collection.status}

Django

# payments/services.py
from lenco import LencoClient
from django.conf import settings

def charge_mobile_money(phone: str, operator: str, amount: float, reference: str):
    with LencoClient(token=settings.LENCO_API_TOKEN) as client:
        return client.collections.from_mobile_money(
            amount=amount,
            reference=reference,
            phone=phone,
            operator=operator,
            country="zm",
        )

For long-running use, prefer instantiating one client at module level and reusing it (the underlying httpx client pools connections).

Webhooks

Lenco signs every webhook with X-Lenco-Signature (HMAC-SHA512 of the raw body, keyed by the SHA256 of your API token). Always verify before processing:

from django.http import HttpRequest, JsonResponse
from django.views.decorators.csrf import csrf_exempt
from lenco.exceptions import LencoWebhookVerificationError
from lenco.webhooks import parse_event, verify_signature

@csrf_exempt
def lenco_webhook(request: HttpRequest):
    try:
        verify_signature(
            request.body,
            request.headers["X-Lenco-Signature"],
            api_token=settings.LENCO_API_TOKEN,
        )
    except LencoWebhookVerificationError:
        return JsonResponse({"detail": "invalid signature"}, status=400)

    event = parse_event(request.body)
    if event.event == "collection.successful":
        fulfill_order(event.data["reference"])
    elif event.event == "transfer.failed":
        alert_ops(event.data)

    # Always ack quickly — Lenco retries every 30 min for 24 h otherwise.
    return JsonResponse({"ok": True})

Event types: transfer.successful, transfer.failed, collection.successful, collection.failed, collection.settled, transaction.credit, transaction.debit.

Card collections (PCI DSS)

Card payloads are JWE-encrypted end-to-end. The SDK fetches a fresh RSA key per payload (Lenco rotates keys):

from lenco import LencoClient

with LencoClient(token="...") as client:
    encrypted = client.encryption.encrypt({
        "email": "customer@example.com",
        "reference": "order-9012",
        "amount": 13.00,
        "currency": "ZMW",
        "customer": {"firstName": "Haim", "lastName": "Hasegawa"},
        "billing": {
            "streetAddress": "1 Independence Ave",
            "city": "Lusaka",
            "postalCode": "10101",
            "country": "ZM",
        },
        "card": {
            "number": "5555555555554444",
            "expiryMonth": "12",
            "expiryYear": "2030",
            "cvv": "123",
        },
    })
    result = client.collections.from_card(encrypted_payload=encrypted)

if result.collection.status == "3ds-auth-required":
    redirect_customer(result.authorization.redirect)

encrypt() also accepts a typed CardCollectionPayload instead of a dict, so a typo'd field name fails at construction instead of as an opaque 400 — see Card collections.

Phone normalization

Lenco's mobile money APIs expect a Zambian number in local 0XXXXXXXXX shape. normalize_zambian_phone() converts +260..., 260..., and spaced local numbers to that shape. It rejects anything that is not a valid Zambian mobile number, including landlines:

from lenco.phone import normalize_zambian_phone

normalize_zambian_phone("+260966123456")  # "0966123456"
normalize_zambian_phone("0211234567")     # ValueError — landline, not mobile

It is a standalone function with no client involved, so you can call it wherever you collect a phone number. See Phone normalization.

Error handling

from lenco import LencoAuthError, LencoNotFoundError, LencoValidationError, LencoError

try:
    client.resolve.bank_account(account_number="0000", bank_id="002")
except LencoValidationError as e:
    print(e.message)          # "Account details was not found"
except LencoAuthError:
    # 401 — check your token
    ...
except LencoNotFoundError:
    # 404
    ...
except LencoError:
    # network failure, 5xx, anything else
    ...

Note the envelope gotcha: Lenco can return HTTP 200 with {"status": false}. The SDK treats that as an error and raises — you never have to check the envelope yourself. But for transfer and collection initiation, a 200 with "status": true means the request was accepted. The outcome lives in transfer.status / collection.status ("successful", "pending", "failed", "pay-offline", …).

Retries

A failed GET — a connection error, or a 429/5xx response — is retried automatically: 3 attempts by default, jittered exponential backoff, honoring Retry-After on a 429.

with LencoClient(token="...", max_retries=5) as client:
    ...

with LencoClient(token="...", max_retries=0) as client:  # disable retries
    ...

POST (transfer/collection initiation, card charges) is never auto-retried — Lenco has no idempotency keys, so a blind retry risks a second transfer instead of a safe re-read. Catch LencoDuplicateReferenceError to retry one safely yourself — see Retrying safely and Retries.

Development

python3.11 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest              # test suite (mocked transport, no network)
mypy src            # strict type checking
ruff check src      # lint
black src tests     # format
lefthook install    # one-time: run ruff/black/mypy on commit, lint commit messages

Docs (VitePress, Node 22+ and just — docs/ has its own package.json, wrapped by the root justfile):

just docs-install
just docs-dev      # local dev server
just docs-build    # production build

Contributing

See CONTRIBUTING.md.

License

BSD-3-Clause. See LICENSE.

Metadata

Release files for lenco-py 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for lenco-py 0.2.0
File Size Uploaded
lenco_py-0.2.0.tar.gz 27.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for lenco-py 0.2.0
File Interpreter ABI Platform
lenco_py-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 60.7 kB

Release files / lenco_py-0.2.0.tar.gz

Download URL lenco_py-0.2.0.tar.gz
Size 27.8 kB
Tags Source
SHA-256 checksum
How to use checksums
15a29b52e63d54ea4d883ae198fdca2b8dcca5806cac54f7f0a3a3e7e083c01f
BLAKE2b-256 checksum
How to use checksums
489a0ca02160c820c6ec8c1be470b6aee23ae1b85eb14ff7b83b039e856d47b6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 27, 2026.

Transparency log

Release files / lenco_py-0.2.0-py3-none-any.whl

Download URL lenco_py-0.2.0-py3-none-any.whl
Size 33.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e82cd726f2e4d1b9b0ef9e14f870648e647cdd5fdf6ccab9006419bbbec21d72
BLAKE2b-256 checksum
How to use checksums
d553ad99578ae78eb0b63ef645c1c13db05809dcdf5f551bb687b24bfc971a43
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 27, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page