Production Python SDK for Safaricom M-Pesa Daraja v3
Project description
daraja-v3
Formerly
mpesa-python— renamed todaraja-v3on PyPI (the original name was taken).
Production Python SDK for Safaricom M-Pesa Daraja v3.
Zero external dependencies. Type-annotated. Stripe-quality error handling.
from mpesa import MpesaClient
client = MpesaClient(
consumer_key="your_key",
consumer_secret="your_secret",
shortcode="174379",
passkey="your_lnm_passkey",
sandbox=True, # flip to False when you go live
)
# Prompt customer phone to enter PIN
result = client.stk_push(
phone="0712345678", # any Kenyan format — normalised automatically
amount=500,
reference="Order-001",
callback_url="https://yourapp.com/mpesa/callback",
)
print(result.checkout_request_id)
Why this exists
Every Kenya fintech project needs M-Pesa. Most of them roll their own wrapper — usually a 200-line mpesa_utils.py with no tests, swallowed exceptions, and phone number bugs. This is the library those teams should be importing instead.
Design goals (borrowed from Stripe's Python SDK):
- Fail before the network call when possible —
ValidationErrorcatches bad inputs locally - Typed results — no
response["Body"]["stkCallback"]["CallbackMetadata"]["Item"][0]["Value"] - Token caching — never burn a request on an expired token
- Composable exceptions — catch
MpesaErrorbroadly or specific subclasses narrowly - Zero dependencies — drop into any environment without conflicts
Installation
pip install daraja-v3
Or from source:
git clone https://github.com/gabrielmahia/mpesa-python
cd mpesa-python
pip install -e ".[dev]"
Supported APIs
| API | Method | What it does |
|---|---|---|
| STK Push | client.stk_push() |
Prompt customer phone to pay |
| STK Query | client.stk_query() |
Check STK push status |
| B2C | client.b2c() |
Send money to customer wallet |
| C2B Register | client.c2b_register_urls() |
Register payment notification URLs |
| Account Balance | client.account_balance() |
Query your shortcode balance |
| Webhook parsing | MpesaClient.parse_stk_callback() |
Parse Safaricom's webhook body |
Roadmap: Reversal, Transaction Status, QR Code, Ratiba (recurring payments)
Usage guide
STK Push + webhook
# 1. Initiate push
result = client.stk_push(
phone="0712345678",
amount=1500,
reference="Invoice-2024-042",
description="Subscription",
callback_url="https://yourapp.com/mpesa/stk/callback",
)
# 2. Store result.checkout_request_id against your order
# 3. In your webhook handler:
from mpesa import MpesaClient
def handle_stk_callback(request_body: dict):
payment = MpesaClient.parse_stk_callback(request_body)
if payment["paid"]:
receipt = payment["mpesa_receipt"] # e.g. "NLJ7RT61SV"
amount = payment["amount"] # e.g. 1500
phone = payment["phone"] # e.g. "254712345678"
# update your order, send confirmation email, etc.
B2C disbursement
result = client.b2c(
phone="0712345678",
amount=5000,
remarks="Monthly stipend",
initiator_name="testapi", # from your Daraja portal
security_credential="encrypted_cred",
callback_url="https://yourapp.com/mpesa/b2c/result",
queue_timeout_url="https://yourapp.com/mpesa/b2c/timeout",
)
print(result.conversation_id) # use to reconcile
Error handling
from mpesa import MpesaClient
from mpesa.exceptions import ValidationError, AuthenticationError, TransactionError, MpesaError
try:
result = client.stk_push(phone="bad-phone", amount=100, reference="ref")
except ValidationError as e:
# Bad input — no network call was made
print(f"Fix your input: {e.message} (code: {e.code})")
except AuthenticationError as e:
# Check your CONSUMER_KEY and CONSUMER_SECRET
print(f"Auth failed: {e.message}")
except TransactionError as e:
# M-Pesa returned a non-zero result (user cancelled, insufficient funds, etc.)
print(f"Transaction failed: {e.message} (M-Pesa code: {e.code})")
print(f"Raw response: {e.raw}")
except MpesaError as e:
# Catch-all for any other SDK error
print(f"Error: {e.message}")
Phone number normalisation
The SDK accepts any Kenyan phone format and normalises to 2547XXXXXXXX:
from mpesa.validators import phone
phone("0712345678") # → "254712345678"
phone("+254712345678") # → "254712345678"
phone("254712345678") # → "254712345678"
phone("712345678") # → "254712345678"
phone("bad-number") # → raises ValidationError
Development
# Clone and install dev dependencies
git clone https://github.com/gabrielmahia/mpesa-python
cd mpesa-python
pip install -e ".[dev]"
# Run tests
pytest
# Lint
ruff check .
# Type check
mypy mpesa/
Running integration tests
Integration tests require Daraja sandbox credentials. Set environment variables:
export MPESA_CONSUMER_KEY="your_sandbox_key"
export MPESA_CONSUMER_SECRET="your_sandbox_secret"
export MPESA_SHORTCODE="174379"
export MPESA_PASSKEY="your_sandbox_passkey"
export MPESA_CALLBACK_URL="https://your-ngrok-tunnel.ngrok.io/mpesa/callback"
pytest tests/integration/ -v
Get sandbox credentials free at developer.safaricom.co.ke.
Contributing
Issues and PRs welcome. See CONTRIBUTING.md.
This SDK follows the project conventions in nairobi-stack — the engineering guide for building products in East Africa.
License
MIT — use freely in commercial and open source projects. Attribution appreciated.
Built by a Kenyan engineer who has written this wrapper from scratch one too many times.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file daraja_v3-0.1.0.tar.gz.
File metadata
- Download URL: daraja_v3-0.1.0.tar.gz
- Upload date:
- Size: 13.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3b1ead56f64b55b08a877fc54b54012a8de4913a4023e28694de8f26fb4255bc
|
|
| MD5 |
d7d1b56bb1b90449ba397ed26b876ba2
|
|
| BLAKE2b-256 |
03fcb4b15329ad0d17b607e2875111b600b27f7268c819d7e65a760ae3802042
|
Provenance
The following attestation bundles were made for daraja_v3-0.1.0.tar.gz:
Publisher:
publish.yml on gabrielmahia/mpesa-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
daraja_v3-0.1.0.tar.gz -
Subject digest:
3b1ead56f64b55b08a877fc54b54012a8de4913a4023e28694de8f26fb4255bc - Sigstore transparency entry: 1123029262
- Sigstore integration time:
-
Permalink:
gabrielmahia/mpesa-python@84aa815052eba89c3a4ef229282d3910cada2491 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/gabrielmahia
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@84aa815052eba89c3a4ef229282d3910cada2491 -
Trigger Event:
push
-
Statement type:
File details
Details for the file daraja_v3-0.1.0-py3-none-any.whl.
File metadata
- Download URL: daraja_v3-0.1.0-py3-none-any.whl
- Upload date:
- Size: 12.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
110af4ad6caa8199b1f31c2b4041e99ec4d001ec0078b57d3dcf156b885c4947
|
|
| MD5 |
1fed303bcc64c7be1a00e8aa7893d120
|
|
| BLAKE2b-256 |
3b813beb21534da25136c7035c858e584440fd50a38cdd0fc1f5dc2fea9cba73
|
Provenance
The following attestation bundles were made for daraja_v3-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on gabrielmahia/mpesa-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
daraja_v3-0.1.0-py3-none-any.whl -
Subject digest:
110af4ad6caa8199b1f31c2b4041e99ec4d001ec0078b57d3dcf156b885c4947 - Sigstore transparency entry: 1123029268
- Sigstore integration time:
-
Permalink:
gabrielmahia/mpesa-python@84aa815052eba89c3a4ef229282d3910cada2491 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/gabrielmahia
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@84aa815052eba89c3a4ef229282d3910cada2491 -
Trigger Event:
push
-
Statement type: