Guides / Python

Python integration guide

A thin requests-based client, plus Flask and FastAPI examples for receiving confirmations.

Prerequisites

  • Python 3.8+
  • The requests package (pip install requests)
  • Flask, Django or FastAPI, if you plan to receive C2B confirmations

1. Build a gateway client

gateway_client.py
import requests

class GatewayClient:
    def __init__(self, api_key: str | None = None):
        self.base_url = "https://payments.navipos.co.ke"
        self.session = requests.Session()
        if api_key:
            self.session.headers["X-API-Key"] = api_key

    def post(self, path: str, payload: dict) -> dict:
        response = self.session.post(f"{self.base_url}{path}", json=payload, timeout=15)
        response.raise_for_status()
        return response.json()

    def get(self, path: str, params: dict | None = None) -> dict:
        response = self.session.get(f"{self.base_url}{path}", params=params, timeout=15)
        response.raise_for_status()
        return response.json()

2. Register a client

Create an account. It's created inactive and a 6-digit OTP is sent by SMS and email — registering does not return an API key.

register.py
client = GatewayClient()

result = client.post("/api/v1/clients", {
    "name": "My Business Ltd",
    "businessType": "Retail",
    "email": "owner@mybusiness.com",
    "phone": "254712345678",
    "password": "S3cure!pass",
})
# Response: {"otp_required": True, "data": {"email": ..., "phone": ...}}
# No API key yet — the account is "inactive" and a 6-digit OTP has been sent.

This is the only step that returns your API key and client ID — save them now. The account moves to pending afterwards, awaiting admin approval before it can process live payments.

verify_otp.py
verified = client.post("/api/v1/auth/verify-otp", {
    "email": "owner@mybusiness.com",
    "otp": "482913",
})

api_key = verified["data"]["apiKey"]
client_id = verified["data"]["client"]["clientId"]
session_token = verified["data"]["token"]
# This is the only response that includes api_key/client_id — save them now.
# The account moves to "pending" afterwards, awaiting admin approval.

3. Store provider credentials

Secrets are encrypted at rest. consumerKey/consumerSecret are always required; shortCode, passkey and initiatorPassword are required unless provider is kcb.

credentials.py
authed = GatewayClient(api_key=api_key)

# consumerKey/consumerSecret are always required; shortCode, passkey and
# initiatorPassword are required unless provider is "kcb".
authed.post(f"/api/v1/clients/{client_id}/credentials", {
    "provider": "daraja",
    "consumerKey": "your_consumer_key",
    "consumerSecret": "your_consumer_secret",
    "shortCode": "174379",
    "passkey": "your_passkey",
    "initiatorPassword": "your_initiator_password",
    "environment": "sandbox",
})

4. Trigger an STK push

stk_push.py
# Only phoneNumber and amount are required — reference/description
# get server-generated defaults if omitted.
push = authed.post("/api/v1/stk-push", {
    "phoneNumber": "254712345678",
    "amount": 1000,
    "reference": "ORDER-1042",
    "description": "Payment for order #1042",
})

transaction_id = push["data"]["transactionId"]

5. Confirm the payment

STK results resolve asynchronously — poll until the transaction leaves pending.

poll_transaction.py
import time

def poll_transaction(client: GatewayClient, transaction_id: str, max_attempts: int = 10) -> str:
    for _ in range(max_attempts):
        result = client.get(f"/api/v1/transactions/{transaction_id}")
        status = result["data"]["status"]

        # Status values are lowercase: pending, initiated, completed, failed, cancelled.
        if status in ("completed", "failed", "cancelled"):
            return status
        time.sleep(2)

    raise TimeoutError("Transaction status could not be determined in time")

6. Receiving C2B (Pay Bill) payments

Register your own endpoint with POST /api/v1/c2b-register and Safaricom's confirmation payload is relayed to it:

app.py (Flask)
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.post("/payments/confirmation")
def mpesa_confirmation():
    # This is YOUR endpoint — register it with POST /api/v1/c2b-register
    # so Safaricom's C2B confirmation is relayed here.
    payload = request.get_json(force=True)

    trans_id = payload.get("TransID")
    amount = payload.get("TransAmount")
    bill_reference = payload.get("BillRefNumber")
    payer_phone = payload.get("MSISDN")

    # Persist / reconcile the payment against bill_reference here.

    return jsonify({"ResultCode": 0, "ResultDesc": "Accepted"})

The equivalent with FastAPI:

main.py (FastAPI)
from fastapi import FastAPI, Request

app = FastAPI()

@app.post("/payments/confirmation")
async def mpesa_confirmation(request: Request):
    payload = await request.json()
    bill_reference = payload.get("BillRefNumber")
    # Persist / reconcile the payment against bill_reference here.
    return {"ResultCode": 0, "ResultDesc": "Accepted"}

Response codes

CodeMeaningWhat to do
200 OK Request succeeded.
201 Created Resource created.
400 Bad Request Check the request payload.
401 Unauthorized Provide a valid API key or JWT.
404 Not Found The resource does not exist.
429 Too Many Requests Back off and retry — rate limit hit.
500 Server Error Retry, then contact support.
Full request/response schema for every endpoint: API reference.