Skip to content

nahom-network/tx-verify

Repository files navigation

tx-verify

PyPI Python License: ISC

Async Python library for verifying Ethiopian payment transactions across CBE, Telebirr, Dashen Bank, Bank of Abyssinia, CBE Birr, Awash Bank, Siinqee Bank, Abay Bank, Zemen Bank, and M-Pesa.

For every provider the library fetches the official receipt (PDF or HTML API response), parses it, and returns a strongly-typed result object. No headless browser is bundled — PDF parsing uses pypdf, HTML parsing uses BeautifulSoup, and M-Pesa uses the public Safaricom API — so it runs anywhere Python 3.10+ does.


Supported Providers

Provider Function Input (example) Result type
CBE verify_cbe() "FT23062669JJ", "12345678" (8-digit suffix) TransactionResult
CBE (mobile) verify_cbe() "fHCxyU3pPQIUBir8hu" (mobile receipt ID) TransactionResult
Telebirr verify_telebirr() "CE12345678" (10-char alphanumeric) TransactionResult
Dashen Bank verify_dashen() "1234567890123456" (16 digits) TransactionResult
Bank of Abyssinia verify_abyssinia() "FT23062669JJ", "90172" (5-digit suffix) TransactionResult
CBE Birr verify_cbe_birr() "AB1234CD56", "0911234567" (local phone) TransactionResult
Awash Bank verify_awash() "-2DBWYO2M4D-9UIFS" (receipt code) TransactionResult
Siinqee Bank verify_siinqee() "E29OIPS260300062" (reference) TransactionResult
Abay Bank verify_abay() "135FTRM25044000119176773010" (receipt code) TransactionResult
Zemen Bank verify_zemen() "108IBET252931385" (reference) TransactionResult
M-Pesa verify_mpesa() "UE20VG1GS8" (10-char alphanumeric) TransactionResult
Image (Mistral) verify_image() image_bytes (JPEG/PNG) ImageVerifyResult

Installation

pip install tx-verify

Or with uv:

uv pip install tx-verify

SOCKS proxy support

http:// and https:// proxies work out of the box. To route requests through a socks4://, socks5://, or socks5h:// proxy, install the extra:

pip install 'tx-verify[socks]'

This pulls in PySocks — the SOCKS backend used by requests[socks]. If PySocks is not installed and a SOCKS proxy URL is supplied, the client raises a clear ImportError with the install command — rather than failing later with a low-level network error.

Under the hood the library uses the synchronous requests HTTP client (which proxies users report as the most reliable choice for production SOCKS deployments) and dispatches each call through asyncio.to_thread, so the public verify_* API stays fully async.

verify_image() additionally requires the MISTRAL_API_KEY environment variable (see Image verification).


Quick Start

import asyncio
from tx_verify import verify_telebirr, verify_cbe

async def main():
    # --- Telebirr ---
    result = await verify_telebirr("CE12345678")
    if result.success:
        print(result.payer_name, result.amount)

    # --- CBE ---
    result = await verify_cbe("FT23062669JJ", "12345678")
    if result.success:
        print(f"Paid {result.amount} ETB to {result.receiver_name}")

asyncio.run(main())

Provider Reference

CBE — Commercial Bank of Ethiopia

CBE transaction references are 12 characters starting with FT (PDF receipts) and require the last 8 digits of the account number. Mobile banking receipt IDs (non-FT) are supported without an account suffix.

from tx_verify import verify_cbe

result = await verify_cbe("FT23062669JJ", "12345678")
# result.success               → bool
# result.payer_name            → str | None
# result.payer_account         → str | None
# result.receiver_name         → str | None
# result.receiver_account      → str | None
# result.amount                → float | None
# result.transaction_date      → datetime | None
# result.transaction_reference → str | None
# result.narrative             → str | None
# result.error                 → str | None

The verifier fetches a PDF receipt from CBE servers, extracts text with pypdf, and parses payer / receiver / amount / date fields.

Mobile receipts are fetched from CBE's mobile endpoint and mapped into the same TransactionResult structure.


Telebirr

Telebirr references are 10-character alphanumeric codes. The verifier scrapes the public Ethio Telecom receipt page and returns a TransactionResult. On failure it returns a result with success=False.

from tx_verify import verify_telebirr

result = await verify_telebirr("CE12345678")
if result.success:
    print(result.payer_name)            # str | None
    print(result.payer_account)         # str | None
    print(result.receiver_name)         # str | None
    print(result.receiver_account)      # str | None
    print(result.transaction_status)    # str | None
    print(result.receipt_number)        # str | None
    print(result.transaction_date)      # datetime | None
    print(result.amount)                # float | None
    print(result.service_charge)        # float | None
    print(result.vat)                   # float | None
    print(result.total_amount)          # float | None
    print(result.meta)                  # dict

TelebirrVerificationError may be raised when a proxy returns an explicit error message (see Error Handling).


Dashen Bank

Dashen references are 16-digit numbers starting with 3 digits (e.g. 1234567890123456). The verifier fetches a PDF with built-in retry logic (up to 5 attempts).

from tx_verify import verify_dashen

result = await verify_dashen("1234567890123456")
# result.success               → bool
# result.payer_name            → str | None
# result.payer_account         → str | None
# result.payment_channel       → str | None
# result.transaction_type      → str | None
# result.narrative             → str | None
# result.receiver_name         → str | None
# result.receiver_account      → str | None
# result.transaction_reference → str | None
# result.transaction_date      → datetime | None
# result.amount                → float | None
# result.service_charge        → float | None
# result.vat                   → float | None
# result.total_amount          → float | None
# result.amount_in_words       → str | None
# result.meta                  → dict[str, Any]
# result.error                 → str | None

Bank of Abyssinia

Abyssinia references are also 12 characters starting with FT, but the suffix is the last 5 digits of the account number. The bank returns JSON rather than a PDF, so parsing is done directly from the API response.

from tx_verify import verify_abyssinia

result = await verify_abyssinia("FT23062669JJ", "90172")
# result.success               → bool
# result.transaction_reference → str | None
# result.payer_name            → str | None
# result.payer_account         → str | None
# result.receiver_name         → str | None
# result.receiver_account      → str | None
# result.amount                → float | None
# result.total_amount          → float | None
# result.vat                   → float | None
# result.service_charge        → float | None
# result.currency              → str | None
# result.transaction_type      → str | None
# result.narrative             → str | None
# result.transaction_date      → datetime | None
# result.amount_in_words       → str | None
# result.meta                  → dict[str, Any]
# result.error                 → str | None

CBE Birr

CBE Birr receipts are 10-character alphanumeric codes. You also need the wallet phone number in local Ethiopian format starting with 09 and 10 digits long (e.g. 0911234567).

from tx_verify import verify_cbe_birr

result = await verify_cbe_birr("AB1234CD56", "0911234567")
# result.success            → bool
# result.payer_name         → str | None
# result.payer_account      → str | None
# result.receiver_account   → str | None
# result.receiver_name      → str | None
# result.transaction_reference → str | None
# result.transaction_status → str | None
# result.receipt_number     → str | None
# result.transaction_date   → datetime | None
# result.amount             → float | None
# result.service_charge     → float | None
# result.vat                → float | None
# result.total_amount       → float | None
# result.narrative          → str | None
# result.payment_channel    → str | None
# result.meta               → dict

On failure result.success is False and result.error contains the reason:

if not result.success:
    print(result.error)

M-Pesa

M-Pesa references are 10-character alphanumeric codes. The verifier calls the Safaricom primary API, decodes a Base64-encoded PDF from the response, and parses it.

from tx_verify import verify_mpesa

result = await verify_mpesa("UE20VG1GS8")
# result.success               → bool
# result.transaction_reference → str | None
# result.receipt_number        → str | None
# result.transaction_date      → datetime | None
# result.amount                → float | None
# result.service_charge        → float | None
# result.vat                   → float | None
# result.payer_name            → str | None
# result.payer_account         → str | None
# result.payment_method        → str | None
# result.transaction_type      → str | None
# result.payment_channel       → str | None
# result.amount_in_words       → str | None
# result.meta                  → dict
# result.error                 → str | None

Awash Bank

Awash receipts are public HTML pages. Pass the receipt code or full URL.

from tx_verify import verify_awash

result = await verify_awash("-2DBWYO2M4D-9UIFS")

Siinqee Bank

Siinqee receipts are PDFs referenced by a code.

from tx_verify import verify_siinqee

result = await verify_siinqee("E29OIPS260300062")

Abay Bank

Abay receipts are public HTML pages. Pass the receipt code or full URL.

from tx_verify import verify_abay

result = await verify_abay("135FTRM25044000119176773010")

Zemen Bank

Zemen receipts are PDF receipts referenced by a transaction code.

from tx_verify import verify_zemen

result = await verify_zemen("108IBET252931385")

Image verification (Mistral Vision)

Upload a receipt image (JPEG/PNG) and Mistral Vision AI will detect whether it is a CBE or Telebirr receipt, extract the reference, and optionally verify it automatically.

from tx_verify import verify_image

with open("receipt.jpg", "rb") as f:
    image_bytes = f.read()

# Detect only
info = await verify_image(image_bytes, auto_verify=False)
print(info.type)          # "telebirr" | "cbe" | None
print(info.reference)     # e.g. "CE12345678"
print(info.forward_to)    # "/verify-telebirr" | "/verify-cbe"

# Auto-verify (account_suffix required for CBE)
info = await verify_image(
    image_bytes,
    auto_verify=True,
    account_suffix="12345678",
)
print(info.verified)    # bool | None
print(info.details)     # TransactionResult | None

Requires the MISTRAL_API_KEY environment variable. The mistralai package is already installed as a dependency.


Proxy Support

All receipt verifiers accept an explicit proxies argument. Environment variables are never read automatically — you must pass the proxy yourself.

Supported schemes:

Scheme Description
http Plain HTTP forward proxy
https HTTPS proxy (CONNECT tunnel)
socks4 SOCKS4 proxy
socks5 SOCKS5 proxy (client resolves DNS)
socks5h SOCKS5 proxy (proxy resolves DNS)

Authentication is embedded in the URL:

# Single global proxy
proxies = "http://user:pass@proxy.example.com:8080"

# Per-scheme mapping
proxies = {
    "http://":  "http://proxy.example.com:8080",
    "https://": "socks5://localhost:1080",
}

Pass it to any verifier:

from tx_verify import verify_telebirr, verify_cbe, verify_mpesa

# Telebirr through an HTTP proxy
receipt = await verify_telebirr("CE12345678", proxies="http://proxy:8080")

# CBE through SOCKS5
result = await verify_cbe(
    "FT23062669JJ", "12345678", proxies="socks5://127.0.0.1:1080"
)

# M-Pesa with per-scheme mapping
result = await verify_mpesa("UE20VG1GS8", proxies={
    "http://": "http://proxy:8080",
    "https://": "socks5h://proxy:1080",
})

verify_image also forward proxies to the underlying provider automatically.

SOCKS5 vs SOCKS5h — read this if your proxy fails

If your SOCKS5 proxy works with curl but the library reports "Host unreachable", "Network unreachable", or "general SOCKS server failure", you almost certainly need socks5h:// instead of socks5://:

Scheme Who resolves DNS? When to use
socks5:// The client Client and proxy share the same network / DNS view
socks5h:// The proxy Client and proxy are on different networks (the common case)

With socks5://, your machine resolves transactioninfo.ethiotelecom.et to an IP, then asks the proxy to connect to that IP. If the proxy's network can't route to that IP (very common for VPN / mobile / cross-region proxies) the proxy replies "Host unreachable" — even though curl to the same URL would work because curl often sends the hostname.

# ❌ Often fails with "Host unreachable" on cross-network SOCKS5 proxies
proxies = "socks5://user:pass@192.168.220.121:11280"

# ✅ Works in the same setup — proxy does the DNS lookup itself
proxies = "socks5h://user:pass@192.168.220.121:11280"

The library emits a runtime hint in your logs when it detects this exact failure mode.


Error Handling

All verifiers return a TransactionResult with success and error fields. Inspect result.success and result.error for expected failures (network errors, missing receipts, parsing failures).

  • TelebirrVerificationError may be raised for proxy-level errors. Catch it if you want to show the user a friendly message:
from tx_verify import TelebirrVerificationError, verify_telebirr

try:
    result = await verify_telebirr("INVALID_REF")
    if not result.success:
        print("Receipt not found.")
except TelebirrVerificationError as exc:
    print(f"Telebirr error: {exc}")
    if exc.details:
        print(f"Details: {exc.details}")

The library also provides a generic error handler for wrapping internal errors:

from tx_verify.utils.error_handler import AppError, ErrorType

Environment Variables

Variable Purpose Required by
MISTRAL_API_KEY Mistral Vision API key verify_image()
LOG_LEVEL DEBUG or INFO (default INFO) Optional for all verifiers

Development

# Clone
git clone https://github.com/nahom-network/tx-verify.git
cd tx-verify

# Install with dev dependencies (uv is recommended because uv.lock exists)
uv pip install -e ".[dev]"

# Or pip
pip install -e ".[dev]"

# Install pre-commit hooks
pre-commit install

# Lint & format
ruff check . --fix
ruff format .

# Type-check
mypy tx_verify/

# Run tests
pytest -v

CI enforces lint → typecheck → test in this order. A one-liner that matches the CI gate locally:

ruff check . && ruff format --check . && mypy tx_verify/ && pytest

Examples

See the examples/ directory for a runnable example per provider:

File What it shows
telebirr.py Verify a Telebirr receipt by reference number
cbe.py Fetch and parse a CBE PDF receipt
cbe_birr.py Verify a CBE Birr wallet transaction
dashen.py Verify a Dashen Bank receipt with retry logic
abyssinia.py Verify a Bank of Abyssinia transaction
mpesa.py Verify an Ethiopian M-Pesa transaction
image.py Analyse a receipt image with Mistral Vision AI
error_handling.py Catch provider-specific errors gracefully
awash.py Verify an Awash Bank receipt
siinqee.py Verify a Siinqee Bank receipt
abay.py Verify an Abay Bank receipt
zemen.py Verify a Zemen Bank receipt

License

ISC © Nahom D

About

No description, website, or topics provided.

Resources

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages