Skip to main content

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.

PyPI version PyPI downloads CI License: MIT Python Versions Django | Flask | FastAPI

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/EcitizenClient core (signing, verification, HTTP submission) uses only hmac, hashlib, base64, and urllib from the standard library - installing it never conflicts with your Django/Flask/FastAPI project's own dependency pins.
  • Interactive CLI Setup: Run ecitizen-pesaflow init to interactively configure your credentials, write .env entries, 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) so 0712345678 / 0112345678 / +254712345678 all become 2547XXXXXXXX / 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:

  1. Prompt for your eCitizen credentials (API Client ID, API Key, Merchant Secret, Service ID).
  2. Ask for your target framework (Django, Flask, FastAPI, or Standalone).
  3. Write your .env file.
  4. 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)

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

  1. Keep Secrets Private: Store your API key and merchant secret in .env or your platform's secret manager. Never commit secrets to git.
  2. CSRF Exemption: eCitizen servers POST webhook notifications from outside your application's domain. create_django_webhook_view is already @csrf_exempt; if you build your own view, exempt the notification route explicitly.
  3. Timing-Safe Comparison: Webhook signatures are checked using hmac.compare_digest to prevent side-channel timing attacks.
  4. Phone Formatting: Always use PhoneHelper.normalize() so phone numbers match Safaricom / Airtel STK push formats (2547XXXXXXXX or 2541XXXXXXXX).

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.


  • ecitizen-pesaflow-gateway on 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.1

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

Source distribution (sdist)

Source distribution for ecitizen-pesaflow-gateway 0.1.1
File Size Uploaded
ecitizen_pesaflow_gateway-0.1.1.tar.gz 28.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for ecitizen-pesaflow-gateway 0.1.1
File Interpreter ABI Platform
ecitizen_pesaflow_gateway-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 54.3 kB

Release files / ecitizen_pesaflow_gateway-0.1.1.tar.gz

Download URL ecitizen_pesaflow_gateway-0.1.1.tar.gz
Size 28.6 kB
Tags Source
SHA-256 checksum
How to use checksums
8f27852e4dd492190e45baddace8969f308d82bf216a6fa0fb29d43d26cbe64e
BLAKE2b-256 checksum
How to use checksums
4aa30a9bca71a53c334a9c8075d40c738d0dec2b6095517d6d80e345df33872d
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.1-py3-none-any.whl

Download URL ecitizen_pesaflow_gateway-0.1.1-py3-none-any.whl
Size 25.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b18a585b05760b9452a5fb6747766c3bbf4ca8117e37ef18f20c57b21e13f4b9
BLAKE2b-256 checksum
How to use checksums
d0b5be476ddb781556e6c2e7a4a81e713f7b3ba9a559106e7a92dda427bb8b3e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.4

Release history Release notifications | RSS feed

0.1.2

2 release files

This release

0.1.1 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