Skip to main content

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

Project description

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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

daraja_mock-1.0.0.tar.gz (7.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

daraja_mock-1.0.0-py3-none-any.whl (7.7 kB view details)

Uploaded Python 3

File details

Details for the file daraja_mock-1.0.0.tar.gz.

File metadata

  • Download URL: daraja_mock-1.0.0.tar.gz
  • Upload date:
  • Size: 7.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for daraja_mock-1.0.0.tar.gz
Algorithm Hash digest
SHA256 155458948bb83b47210d6030d40835bdd0f98e63ae14a0e62b190d9eaf74b3af
MD5 70a9426c033e7a691bde9b7276974ba1
BLAKE2b-256 ad24835288600c24583daa80e307d30e46da60a351bfeeb4e3dcdde39e70c6f0

See more details on using hashes here.

Provenance

The following attestation bundles were made for daraja_mock-1.0.0.tar.gz:

Publisher: publish.yml on gabrielmahia/daraja-mock

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file daraja_mock-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: daraja_mock-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 7.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for daraja_mock-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0c1f422c0807219800a87a05959f40e5dea7dcb42f23157364993c896a31f4ba
MD5 7b1f6bce9a0570baaddc5936fad98397
BLAKE2b-256 c83f9c2cea3f6d0a8a765d1882dcf49c38a82d78fff1d4b19ca8a70b59cf6383

See more details on using hashes here.

Provenance

The following attestation bundles were made for daraja_mock-1.0.0-py3-none-any.whl:

Publisher: publish.yml on gabrielmahia/daraja-mock

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page