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.
| 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 |
pip install tx-verifyOr with uv:
uv pip install tx-verifyhttp:// 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).
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())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 | NoneThe 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 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) # dictTelebirrVerificationError may be raised when a proxy returns an explicit
error message (see Error Handling).
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 | NoneAbyssinia 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 | NoneCBE 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 → dictOn failure result.success is False and result.error contains the reason:
if not result.success:
print(result.error)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 | NoneAwash 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 receipts are PDFs referenced by a code.
from tx_verify import verify_siinqee
result = await verify_siinqee("E29OIPS260300062")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 receipts are PDF receipts referenced by a transaction code.
from tx_verify import verify_zemen
result = await verify_zemen("108IBET252931385")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 | NoneRequires the MISTRAL_API_KEY environment variable. The mistralai package
is already installed as a dependency.
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.
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.
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).
TelebirrVerificationErrormay 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| Variable | Purpose | Required by |
|---|---|---|
MISTRAL_API_KEY |
Mistral Vision API key | verify_image() |
LOG_LEVEL |
DEBUG or INFO (default INFO) |
Optional for all verifiers |
# 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 -vCI enforces lint → typecheck → test in this order. A one-liner that matches the CI gate locally:
ruff check . && ruff format --check . && mypy tx_verify/ && pytestSee 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 |
ISC © Nahom D