Skip to content

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

  1. Go to Safaricom Developer Portal
  2. Click Sign Up and create an account
  3. Verify your email

Step 2: Create an App & Get Credentials

  1. Log in to the Daraja portal
  2. Go to My AppsAdd a New App
  3. Select the APIs you need:
    • Lipa Na M-Pesa Online (for STK Push)
    • Transaction Status (for status queries)
    • Reversal (for refunds)
  4. 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

PUT /v1/merchant/providers
X-API-Key: sk_live_your_key
Content-Type: application/json
{
  "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

PUT /v1/merchant/webhook
X-API-Key: sk_live_your_key
Content-Type: application/json
{
  "webhook_url": "https://yourapp.com/webhooks/payments"
}

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
{
  "amount": 1.00,
  "currency": "KES",
  "provider": "mpesa",
  "phone": "254708374149"
}

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:

GET /v1/payments?provider=mpesa&status=completed
X-API-Key: sk_live_your_key

Going Live

To switch from sandbox to production:

  1. Get a live paybill or till number from Safaricom
  2. Create Business Admin/Manager operators on the M-Pesa Org Portal
  3. Go to Daraja portal → Go Live tab → fill in your live shortcode
  4. 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

POST /v1/payments
X-API-Key: sk_live_...
Idempotency-Key: order-456
{
  "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:

    196.201.214.200, 196.201.214.206, 196.201.213.114,
    196.201.214.207, 196.201.214.208, 196.201.213.44,
    196.201.212.127, 196.201.212.138, 196.201.212.129,
    196.201.212.136, 196.201.212.74, 196.201.212.69
    
  • 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:

POST /v1/payments/mpesa/check-status/NEF61H8J60
X-API-Key: sk_live_...

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:

POST /v1/refunds
X-API-Key: sk_live_...
{
  "payment_id": "txn-uuid-...",
  "amount": 500
}
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

POST /v1/payments/mpesa/register-pull
X-API-Key: sk_live_...

Manual Reconciliation

POST /v1/payments/mpesa/reconcile
X-API-Key: sk_live_...

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

{
  "success": true,
  "data": {
    "synced": 3,
    "skipped": 12,
    "not_found": 1
  }
}

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.