Skip to main content

daraja-mock

Local test server for the Safaricom M-Pesa Daraja v3 API.

CI Python Tests Zero deps License

Test your M-Pesa integration without a Safaricom account, sandbox credentials, or internet connection. Configure scenarios to simulate user cancellation, insufficient funds, timeouts, and more — all from a single in-process server.


Install

pip install daraja-mock

Quickstart

from daraja_mock import DarajaMock, Scenario

mock = DarajaMock()

def test_stk_push_success():
    with mock.run() as base_url:
        # Point your MpesaClient at base_url instead of api.safaricom.co.ke
        response = requests.post(
            f"{base_url}/mpesa/stkpush/v1/processrequest",
            json={
                "BusinessShortCode": "174379",
                "Amount": 100,
                "PhoneNumber": "254712345678",
                "CallBackURL": "https://yourapp.com/callback",
                "AccountReference": "Order001",
                "TransactionDesc": "Payment",
            }
        )
    assert response.json()["ResponseCode"] == "0"
    assert mock.last_stk_checkout_id  # store this to query status later

def test_stk_push_user_cancels():
    # STK initiated OK, but user cancels on phone
    mock.queue_scenarios(Scenario.SUCCESS, Scenario.USER_CANCELLED)

    with mock.run() as base_url:
        init = requests.post(f"{base_url}/mpesa/stkpush/v1/processrequest", json={"Amount": 100})
        status = requests.post(f"{base_url}/mpesa/stkpushquery/v1/query", json={
            "CheckoutRequestID": init.json()["CheckoutRequestID"]
        })

    assert status.json()["ResultCode"] == "1032"  # user cancelled

Scenarios

Scenario ResultCode Use for
SUCCESS 0 Happy path
USER_CANCELLED 1032 User dismissed STK prompt
INSUFFICIENT_FUNDS 1 Balance too low
TIMED_OUT 1037 User did not respond in time
WRONG_PIN 2001 Wrong M-Pesa PIN entered
SYSTEM_ERROR 17 Safaricom internal error
AUTH_FAILURE OAuth returns HTTP 400
# Single scenario — all calls use this
mock.set_scenario(Scenario.INSUFFICIENT_FUNDS)

# Queue — each call consumes one, then falls back to set_scenario
mock.queue_scenarios(Scenario.SUCCESS, Scenario.USER_CANCELLED, Scenario.TIMED_OUT)

Endpoints implemented

Endpoint Method Notes
/oauth/v1/generate GET Returns access_token
/mpesa/stkpush/v1/processrequest POST STK Push initiation
/mpesa/stkpushquery/v1/query POST Poll STK status
/mpesa/b2c/v3/paymentrequest POST B2C disbursement
/mpesa/c2b/v1/registerurl POST C2B URL registration
/mpesa/accountbalance/v1/query POST Balance enquiry

Callback simulation

For webhook-based flows, build a realistic callback payload and POST it to your handler:

# Simulate Safaricom posting to your callback URL
payload = mock.build_stk_callback(
    checkout_request_id="ws_CO_123",
    scenario=Scenario.USER_CANCELLED,
)

# POST to your FastAPI/Flask/Django handler
response = test_client.post("/mpesa/stk/callback", json=payload)
assert response.status_code == 200

Inspect calls

with mock.run() as base_url:
    # ... make calls ...
    pass

# After the context
assert len(mock.calls) == 2
assert mock.calls[0].endpoint == "/oauth/v1/generate"
assert mock.calls[1].body["Amount"] == 100

Standalone server

# Default port 8765
python -m daraja_mock

# Custom port
python -m daraja_mock --port 9000

Then point any HTTP client (Postman, curl, your app) at http://localhost:8765.


Use with mpesa-python

import pytest
from daraja_mock import DarajaMock, Scenario
from mpesa import MpesaClient  # github.com/gabrielmahia/mpesa-python

@pytest.fixture
def mpesa_client():
    mock = DarajaMock()
    with mock.run() as base_url:
        client = MpesaClient(
            consumer_key="test_key",
            consumer_secret="test_secret",
            shortcode="174379",
            passkey="test_passkey",
            base_url=base_url,
        )
        yield client, mock

def test_full_stk_flow(mpesa_client):
    client, mock = mpesa_client
    result = client.stk_push("0712345678", 100, "Order001")
    assert result.checkout_request_id == mock.last_stk_checkout_id

Design decisions

No external dependencies. The server runs on Python's stdlib HTTPServer. No FastAPI, no httpx, no pytest-asyncio. This means it works in any test environment without dependency conflicts.

Thread-safe context manager. Each mock.run() starts a server in a daemon thread and tears it down cleanly on exit. Multiple mocks can run concurrently on different ports.

Queue-based scenarios. Real M-Pesa flows have two steps (initiate + query). queue_scenarios lets you specify each step independently: SUCCESS initiation followed by USER_CANCELLED status.


Part of the nairobi-stack East Africa engineering ecosystem. Maintained by Gabriel Mahia. Kenya × USA.

Release files for daraja-mock 1.0.0

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

Source distribution (sdist)

Source distribution for daraja-mock 1.0.0
File Size Uploaded
daraja_mock-1.0.0.tar.gz 7.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for daraja-mock 1.0.0
File Interpreter ABI Platform
daraja_mock-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size:15.5 kB

Release files / daraja_mock-1.0.0.tar.gz

Download URL daraja_mock-1.0.0.tar.gz
Size 7.8 kB
Tags Source
SHA-256 checksum
How to use checksums
155458948bb83b47210d6030d40835bdd0f98e63ae14a0e62b190d9eaf74b3af
BLAKE2b-256 checksum
How to use checksums
ad24835288600c24583daa80e307d30e46da60a351bfeeb4e3dcdde39e70c6f0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Mar 18, 2026.

Transparency log

Release files / daraja_mock-1.0.0-py3-none-any.whl

Download URL daraja_mock-1.0.0-py3-none-any.whl
Size 7.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0c1f422c0807219800a87a05959f40e5dea7dcb42f23157364993c896a31f4ba
BLAKE2b-256 checksum
How to use checksums
c83f9c2cea3f6d0a8a765d1882dcf49c38a82d78fff1d4b19ca8a70b59cf6383
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Mar 18, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.0.0 This release

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