M-Pesa
M-Pesa integration supports STK Push (Lipa Na M-Pesa), transaction status queries, reversals (refunds), and automatic reconciliation via the Pull Transactions API.
Getting Started — Step by Step
Follow these steps to go from zero to accepting M-Pesa payments.
flowchart TB
A["1. Create Daraja Account"] --> B["2. Create App & Get Keys"]
B --> C["3. Get API Key from Payments Service"]
C --> D["4. Configure M-Pesa Credentials"]
D --> E["5. Set Webhook URL"]
E --> F["6. Make Your First Payment"]
F --> G["7. Handle Webhook Callback"]
style A fill:#e3f2fd
style F fill:#e8f5e9
style G fill:#e8f5e9
Step 1: Create a Daraja Account
- Go to Safaricom Developer Portal
- Click Sign Up and create an account
- Verify your email
Step 2: Create an App & Get Credentials
- Log in to the Daraja portal
- Go to My Apps → Add a New App
- Select the APIs you need:
- Lipa Na M-Pesa Online (for STK Push)
- Transaction Status (for status queries)
- Reversal (for refunds)
- Copy your Consumer Key and Consumer Secret
Sandbox vs Production
Start with sandbox credentials for testing. You'll get test shortcodes and passkeys automatically. For production, you need a live paybill/till number — see Go Live below.
Step 3: Get an API Key from Payments Service
If you don't have one yet, ask your admin to create one:
make createapikey
# Enter your merchant UUID
# Scopes: payments:read,payments:write,merchant:read,merchant:write,webhooks:read
Step 4: Configure M-Pesa Credentials
{
"provider": "mpesa",
"mpesa": {
"env": "sandbox",
"consumer_key": "your_consumer_key_from_daraja",
"consumer_secret": "your_consumer_secret_from_daraja",
"passkey": "bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919",
"shortcode": "174379"
}
}
Sandbox test values
For sandbox testing, use:
- Shortcode:
174379 - Passkey:
bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919 - Test phone:
254708374149
You should see a response with masked credentials:
{
"success": true,
"data": {
"provider": "mpesa",
"config_summary": {
"env": "sandbox",
"consumer_key": "****abcd",
"consumer_secret": "****ef12",
"passkey": "****c919",
"shortcode": "174379"
}
}
}
Step 5: Set Your Webhook URL
Save the secret!
The response contains webhook_secret — store it securely. It's shown once.
Step 6: Make Your First Payment
POST /v1/payments
X-API-Key: sk_live_your_key
Idempotency-Key: test-payment-001
Content-Type: application/json
You should get:
{
"success": true,
"data": {
"id": "txn-uuid-...",
"status": "pending",
"provider_ref": "ws_CO_..."
}
}
In sandbox, the STK push is simulated. In production, the customer's phone will display a payment prompt.
Step 7: Handle the Webhook
When the customer completes payment, you'll receive a webhook:
POST https://yourapp.com/webhooks/payments
X-Event-Type: payment.completed
X-Payments-Signature: sha256=...
X-Payments-Timestamp: 1717500000
{
"type": "payment.completed",
"data": {
"payment_id": "txn-uuid-...",
"amount": "1.00",
"currency": "KES",
"provider": "mpesa",
"status": "completed"
}
}
Verify the signature and fulfill the order. See the Webhook Integration Guide for full details.
Verify It Worked
Check the transaction:
Going Live
To switch from sandbox to production:
- Get a live paybill or till number from Safaricom
- Create Business Admin/Manager operators on the M-Pesa Org Portal
- Go to Daraja portal → Go Live tab → fill in your live shortcode
- Update your credentials:
{
"provider": "mpesa",
"mpesa": {
"env": "live",
"consumer_key": "your_live_consumer_key",
"consumer_secret": "your_live_consumer_secret",
"passkey": "your_live_passkey",
"shortcode": "your_live_shortcode",
"initiator": "your_api_operator_username",
"security_credential": "your_encrypted_credential",
"result_url": "https://yourdomain.com/v1/webhooks/mpesa/transactionstatus/result",
"timeout_url": "https://yourdomain.com/v1/webhooks/mpesa/transactionstatus/timeout"
}
}
Production certificate
Place Safaricom's production certificate at certs/ProductionCertificate.cer. In production, webhook signatures are verified using this RSA certificate instead of the passkey hash.
Additional fields for production
| Field | When needed | Description |
|---|---|---|
initiator |
Reversals, status queries | API operator username from M-Pesa Org Portal |
security_credential |
Reversals, status queries | Encrypted password for the operator |
result_url |
Reversals, status queries | Where Daraja sends async results |
timeout_url |
Reversals, status queries | Where Daraja sends timeout notifications |
nominated_number |
Pull Transactions (reconciliation) | Phone number linked to the paybill account |
STK Push Payment Flow
sequenceDiagram
participant Merchant
participant API as Payments API
participant Daraja as Daraja API
participant Phone as Customer Phone
participant Webhook as Merchant Webhook
Merchant->>API: POST /v1/payments<br/>{ provider: "mpesa", phone: "254712..." }
API->>Daraja: OAuth token (Basic Auth)
Daraja-->>API: { access_token: "..." }
API->>Daraja: POST /mpesa/stkpush/v1/processrequest
Daraja-->>API: { CheckoutRequestID: "ws_CO_..." }
API-->>Merchant: { status: "pending", provider_ref: "ws_CO_..." }
Daraja->>Phone: STK Push prompt
Phone->>Daraja: Customer enters PIN
Daraja->>API: POST /v1/webhooks/mpesa<br/>(callback with result)
API->>API: Verify signature + IP whitelist + dedup
API->>Webhook: POST merchant_url<br/>{ type: "payment.completed" }
Create Payment (STK Push)
Request
{
"amount": 500.00,
"currency": "KES",
"provider": "mpesa",
"phone": "254712345678",
"callback_url": "https://yourdomain.com/v1/webhooks/mpesa"
}
| Field | Type | Required | Description |
|---|---|---|---|
amount |
decimal | ✅ | Amount in KES (whole numbers for M-Pesa) |
currency |
string | ✅ | Must be KES |
provider |
string | ✅ | Must be mpesa |
phone |
string | ✅ | Customer phone in 254XXXXXXXXX format |
callback_url |
string | ❌ | Override the default STK callback URL |
Response
{
"success": true,
"data": {
"id": "txn-uuid-...",
"merchant_id": "m1m2m3-...",
"amount": "500.00",
"currency": "KES",
"provider": "mpesa",
"status": "pending",
"provider_ref": "ws_CO_123456789",
"idempotency_key": "order-456",
"created_at": "2025-01-01T12:00:00Z"
}
}
Status is always pending
M-Pesa STK Push is async. The customer receives a prompt on their phone. The final status arrives via the callback webhook.
Inbound Webhook (M-Pesa → Us)
Safaricom sends the STK callback to POST /v1/webhooks/mpesa.
Security
-
IP Whitelisting — only Safaricom's IPs are allowed:
-
Signature verification — sandbox uses passkey hash, production uses Safaricom's RSA certificate
- Replay protection — 24h Redis dedup by CheckoutRequestID
Callback Payload (from Safaricom)
{
"Body": {
"stkCallback": {
"MerchantRequestID": "29115-34620561-1",
"CheckoutRequestID": "ws_CO_123456789",
"ResultCode": 0,
"ResultDesc": "The service request is processed successfully."
}
}
}
| ResultCode | Meaning |
|---|---|
0 |
Success → payment.completed |
| Any other | Failure → payment.failed |
Transaction Status Check
When callbacks are delayed or missing, query Daraja directly:
Where NEF61H8J60 is the M-Pesa receipt number.
sequenceDiagram
participant Merchant
participant API as Payments API
participant Daraja
Merchant->>API: POST /v1/payments/mpesa/check-status/NEF61H8J60
API->>Daraja: POST /mpesa/transactionstatus/v1/query
Daraja-->>API: { ResponseCode: "0", ResponseDescription: "Accept..." }
API-->>Merchant: { success: true, data: { ... } }
Note over Daraja,API: Daraja processes async
Daraja->>API: POST /v1/webhooks/mpesa/transactionstatus/result
API->>API: Update transaction + enqueue merchant webhook
Response
{
"success": true,
"data": {
"OriginatorConversationID": "1236-7134259-1",
"ConversationID": "AG_20210709_1234409f86436c583e3f",
"ResponseCode": "0",
"ResponseDescription": "Accept the service request successfully."
}
}
Async API
This response only confirms the query was accepted. The actual result arrives via callback to your configured result_url.
Reversal (Refund)
M-Pesa refunds are processed as reversals through the Daraja Reversal API. Use the standard refund endpoint:
sequenceDiagram
participant Merchant
participant API as Payments API
participant Daraja
participant Webhook as Merchant Webhook
Merchant->>API: POST /v1/refunds<br/>{ payment_id: "..." }
API->>Daraja: POST /mpesa/reversal/v1/request
Daraja-->>API: { ResponseCode: "0" }
API-->>Merchant: { status: "pending" }
Note over Daraja,API: Daraja processes async
Daraja->>API: POST /v1/webhooks/mpesa/reversal/result
API->>API: Update transaction to "reversed"
API->>Webhook: { type: "payment.reversed" }
Reversal Result Codes
| Code | Description |
|---|---|
0 |
Success |
R000001 |
Transaction already reversed |
R000002 |
Invalid transaction ID |
1 |
Insufficient balance |
21 |
Initiator not allowed (missing API role) |
2001 |
Invalid initiator credentials |
Reconciliation
The Pull Transactions API recovers missed callbacks from the last 48 hours.
One-time Registration
Manual Reconciliation
Automatic Reconciliation
An ARQ cron job runs every 15 minutes, pulling the last 2 hours of transactions and syncing any that are still pending in our DB.
flowchart LR
Cron["ARQ Cron<br/>every 15 min"] --> Pull["Pull Transactions<br/>from Daraja"]
Pull --> Compare{"Transaction<br/>in our DB?"}
Compare -->|"Yes + pending"| Update["Update to completed<br/>+ enqueue webhook"]
Compare -->|"Yes + completed"| Skip["Skip"]
Compare -->|"No"| Log["Log as not_found"]
Response
Daraja Error Codes
| Code | Description | Mitigation |
|---|---|---|
400.008.01 |
Invalid auth type | Use Authorization: Basic <base64> |
400.008.02 |
Invalid grant type | Use grant_type=client_credentials |
400.003.02 |
Bad request | Check request payload format |
500.003.03 |
Quota violation | Reduce request rate |
500.003.02 |
Spike arrest | Fix endpoint errors |
Viewing M-Pesa Transactions
List and filter all M-Pesa transactions:
GET /v1/payments?provider=mpesa&status=completed
GET /v1/payments?provider=mpesa&since=2025-06-01T00:00:00Z
GET /v1/payments?search=NEF61H8J60
GET /v1/payments?provider=mpesa&status=pending&min_amount=1000
See Payments API → List & Filter for all available filters.