ecitizen-pesaflow-gateway (Python)
Kenya eCitizen / PesaFlow payment gateway SDK for Python — signed M-Pesa STK Push checkout, HMAC-SHA256 webhook/IPN verification, and ready-made adapters for Django, Flask, and FastAPI. Zero dependencies, Python 3.7–3.13.
A beginner-friendly Kenya eCitizen / PesaFlow payment gateway SDK and CLI for Django, Flask, FastAPI, and plain Python. Build signed checkout payloads, render an instant payment button, and verify webhook callbacks with plain-English fields.
Keywords: eCitizen payment gateway Python, PesaFlow SDK, Kenya payment gateway Python, M-Pesa STK Push Python, Django payment gateway, Flask payment gateway, FastAPI payment gateway, HMAC webhook verification, Safaricom M-Pesa integration, Kenyan government payments API.
This is the Python sibling of ecitizen-pesaflow-gateway on npm (source). Both packages implement the exact same HMAC-SHA256 signing/verification algorithm and are tested against shared cross-language golden test vectors, so a payload signed by one is verifiable by the other.
- Forward & Backward Compatible: Pure standard library, no C extensions. Tested on Python 3.7 through 3.13.
- Zero Runtime Dependencies: The
EcitizenGateway/EcitizenClientcore (signing, verification, HTTP submission) uses onlyhmac,hashlib,base64, andurllibfrom the standard library - installing it never conflicts with your Django/Flask/FastAPI project's own dependency pins. - Interactive CLI Setup: Run
ecitizen-pesaflow initto interactively configure your credentials, write.enventries, and scaffold a framework-specific payment view in seconds. - Headless Payment CLI: Sign and submit payments straight from the terminal with
ecitizen-pesaflow pay/status- no browser, no HTML, no scaffolding required. - Instant Payment Button: Render a ready-to-use, HMAC-signed payment form in one call (
client.pay_button(...)) or get the raw payload (client.checkout(...)) for a custom UI. - Safaricom M-Pesa STK Push: Built-in Kenyan phone normalization (
PhoneHelper) so0712345678/0112345678/+254712345678all become2547XXXXXXXX/2541XXXXXXXX. - Timing-Safe Cryptographic Verification: Validate server-to-server IPN notifications with
hmac.compare_digest. - Pre-Built Adapters: Webhook views for Django, Flask, and FastAPI - or call
client.verify(payload)directly from any other framework (Bottle, Pyramid, Sanic, Tornado, plain WSGI/ASGI).
Compatibility
- Python: >= 3.7 (tested on 3.7, 3.8, 3.9, 3.10, 3.11, 3.12, 3.13 in CI).
- Web Frameworks: Django, Flask, FastAPI out of the box via dedicated adapters; any other Python framework by calling
client.verify(payload_dict)directly.
Installation
pip install ecitizen-pesaflow-gateway
Optional .env auto-loading for the CLI:
pip install "ecitizen-pesaflow-gateway[dotenv]"
Interactive CLI Setup
ecitizen-pesaflow init
The wizard will:
- Prompt for your eCitizen credentials (API Client ID, API Key, Merchant Secret, Service ID).
- Ask for your target framework (Django, Flask, FastAPI, or Standalone).
- Write your
.envfile. - Scaffold a ready-to-run payment view for your selected framework.
Non-interactive / CI:
ecitizen-pesaflow init --client-id "MY_ID" --api-key "MY_KEY" --secret "MY_SEC" --service-id "MY_SVC" --framework flask --yes
Verify your environment and HMAC algorithm against a known-good test vector:
ecitizen-pesaflow test
Sign and submit a payment directly from the terminal:
ecitizen-pesaflow pay --amount 500 --reference INV-0001 --description "School fees" \
--name "Jane Doe" --id-number 12345678 --phone 0712345678
Add --dry-run to print the signed payload without sending it. Pass --yes/-y to skip the confirmation prompt in scripts/CI.
Check settlement status:
ecitizen-pesaflow status --reference INV-0001 --status-url https://your-status-endpoint
Run ecitizen-pesaflow help for the full flag reference.
Quickstart: Flask
from flask import Flask
from ecitizen_pesaflow_gateway import EcitizenClient, PhoneHelper
from ecitizen_pesaflow_gateway.adapters.flask_adapter import create_flask_webhook_view
app = Flask(__name__)
client = EcitizenClient() # reads ECITIZEN_* from the environment
@app.get("/payment/pay")
def pay():
return client.pay_button({
"amount": 1500,
"reference": "INV-0001",
"description": "Land Rates Clearance",
"name": "John Doe",
"idNumber": "28374619",
"phone": PhoneHelper.normalize("0712345678"),
"sendStkPush": True,
"notifyUrl": "https://yourdomain.com/payment/notify",
}, "Proceed to eCitizen", {"class": "btn btn-success btn-lg"})
def on_success(result):
print(f"Payment confirmed for {result.reference}, amount {result.amount_paid}")
# Order.query.filter_by(reference=result.reference).update({"status": "paid"})
app.add_url_rule(
"/payment/notify",
view_func=create_flask_webhook_view(client, on_success=on_success),
methods=["POST"],
)
Quickstart: FastAPI
eCitizen POSTs webhooks as application/x-www-form-urlencoded, and FastAPI/Starlette's request.form() needs python-multipart to parse that - install with the fastapi extra:
pip install "ecitizen-pesaflow-gateway[fastapi]"
from fastapi import FastAPI
from ecitizen_pesaflow_gateway import EcitizenClient, PhoneHelper
from ecitizen_pesaflow_gateway.adapters.fastapi_adapter import create_fastapi_webhook_route
app = FastAPI()
client = EcitizenClient()
async def on_success(result):
print("Confirmed:", result.reference, result.amount_paid)
app.include_router(create_fastapi_webhook_route(client, on_success=on_success), prefix="/payment")
Quickstart: Django
# views.py
from django.http import HttpResponse
from ecitizen_pesaflow_gateway import EcitizenClient, PhoneHelper
from ecitizen_pesaflow_gateway.adapters.django_adapter import create_django_webhook_view
client = EcitizenClient()
def pay(request):
return HttpResponse(client.pay_button({
"amount": 500,
"reference": "INV-0001",
"description": "School fees",
"name": "Jane Doe",
"idNumber": "12345678",
"phone": PhoneHelper.normalize("0712345678"),
"sendStkPush": True,
"notifyUrl": request.build_absolute_uri("/payment/notify"),
}))
notify = create_django_webhook_view(client, on_success=lambda r: print("paid", r.reference))
# urls.py
from django.urls import path
from . import views
urlpatterns = [
path("payment/pay", views.pay),
path("payment/notify", views.notify),
]
Standalone Usage (any other framework, or none)
from ecitizen_pesaflow_gateway import EcitizenClient, PhoneHelper
client = EcitizenClient({
"apiClientID": "YOUR_API_CLIENT_ID",
"apiKey": "YOUR_API_KEY",
"secret": "YOUR_SECRET",
"serviceID": "YOUR_SERVICE_ID",
})
# 1. Generate a signed checkout payload
checkout = client.checkout({
"amount": 500,
"reference": "INV-0001",
"description": "School fees",
"name": "Jane Doe",
"idNumber": "12345678",
"phone": "0712345678", # normalized automatically to 254712345678
})
# 2. Verify an inbound webhook notification
result = client.verify(webhook_payload) # e.g. request.POST.dict(), request.form, etc.
if result.success:
print(f"Payment confirmed for {result.reference} ({result.amount_paid} KES)")
else:
print(f"Verification failed: {result.description}")
# 3. Or skip the browser/HTML entirely and prompt the payment directly
submission = client.initiate_payment({
"amount": 500,
"reference": "INV-0002",
"description": "School fees",
"name": "Jane Doe",
"idNumber": "12345678",
"phone": "0712345678",
"sendStkPush": True,
})
print(submission.http_status, submission.response_body)
# 4. Poll settlement status (requires statusUrl / ECITIZEN_STATUS_URL)
status = client.check_payment_status("INV-0002")
print(status.http_status, status.response_body)
Examples
Full, runnable projects for each supported framework live in examples/ - real venv + requirements.txt + .env.example + working /pay and /notify routes, not just snippets:
examples/django_demo/- Django project with apaymentsapp.examples/flask_demo/- single-file Flask app.examples/fastapi_demo/- single-file FastAPI app (with interactive/docs).
Each installs the real published package from PyPI (not local source) and has its own README with exact pip install / run commands. Copy .env.example to .env and fill in your own eCitizen credentials to run one.
API Reference Overview
| Method / Utility | Description |
|---|---|
client.checkout(...) |
Generates a signed checkout payload and endpoint URL |
client.pay_button(...) |
Generates a self-contained HTML form and payment button |
client.initiate_payment(...) |
Directly submits payment without browser/HTML |
client.check_payment_status(...) |
Polls payment settlement status |
client.verify(payload) |
Verifies IPN webhook HMAC signature |
client.is_paid(payload) |
Returns a boolean verification result |
PhoneHelper.normalize(phone) |
Normalizes Kenyan phone numbers (07..., 01... -> 254...) |
ecitizen-pesaflow init |
Interactive project setup wizard |
Security & Best Practices
- Keep Secrets Private: Store your API key and merchant secret in
.envor your platform's secret manager. Never commit secrets to git. - CSRF Exemption: eCitizen servers POST webhook notifications from outside your application's domain.
create_django_webhook_viewis already@csrf_exempt; if you build your own view, exempt the notification route explicitly. - Timing-Safe Comparison: Webhook signatures are checked using
hmac.compare_digestto prevent side-channel timing attacks. - Phone Formatting: Always use
PhoneHelper.normalize()so phone numbers match Safaricom / Airtel STK push formats (2547XXXXXXXXor2541XXXXXXXX).
Development
pip install -e ".[test]"
pytest -q
tox.ini runs the suite across every supported Python version if you have multiple interpreters installed locally; CI runs the same matrix (3.7-3.13) plus a zero-dependency install check on every push.
Related Projects
ecitizen-pesaflow-gatewayon npm — the Node.js / TypeScript sibling (source) for Express, Fastify, Next.js, and NestJS. Both packages share the same signing algorithm and cross-language golden test vectors.
License
MIT License. See LICENSE for details.
Metadata
Release files for ecitizen-pesaflow-gateway 0.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| ecitizen_pesaflow_gateway-0.1.2.tar.gz | 29.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ecitizen_pesaflow_gateway-0.1.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 55.3 kB
Release files / ecitizen_pesaflow_gateway-0.1.2.tar.gz
| Download URL | ecitizen_pesaflow_gateway-0.1.2.tar.gz |
|---|---|
| Size | 29.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6dd08016b5f416ac733c234e0cd5248433ec53303fb03eca8b1d11c688900685
|
|
BLAKE2b-256 checksum How to use checksums |
9df81cd917f11be4d244f77eca15d28180071744fa8a40abaed641140ad41584
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.4
|
Release files / ecitizen_pesaflow_gateway-0.1.2-py3-none-any.whl
| Download URL | ecitizen_pesaflow_gateway-0.1.2-py3-none-any.whl |
|---|---|
| Size | 25.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7c537b44c2bb47a7b9fd01f96e2a836b9ae847ffc872e602b7eecfe5867f19dc
|
|
BLAKE2b-256 checksum How to use checksums |
412f428d94b13939907ad23bad2abc7f5d29374919e036eb94063ca48cf5e9c8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.4
|