API Documentation — SmartPay | M-Pesa STK Push Integration Guide

SmartPay API Reference

Integrate M-Pesa STK Push, send B2C payouts, manage API keys, query transactions, and send SMS — all from one authenticated REST API. No SDK required.

https://api.smartpaypesa.com/v1
REST / JSON
Bearer Token Auth
Rate Limited
Idempotent
Open Source SDK

Getting Started

SmartPay gives you a single endpoint to collect M-Pesa payments via STK Push, send B2C payouts, manage multiple API accounts, query transaction history, and send SMS — all authenticated with a Bearer API key.

Quickstart — live in 5 steps
1
Get your bootstrap key
Sign up at smartpaypesa.com/signup.php → open the API tab in your dashboard → copy your default API key. This is the only time you need the dashboard.
2
Create a dedicated key for your app
POST /v1/keys with a name. You get back a fresh api_key — store it in your secrets manager, never in code.
3
Set where payments land
PUT /v1/keys/{id}/destination — choose wallet, phone, paybill, till, or bank. Set your callback URL here too.
4
Optionally enable payment notifications
PUT /v1/keys/{id}/notifications — get email and/or SMS on every completed payment.
5
Send your first STK Push
POST /v1/stk/push with a phone and amount. The customer gets a prompt on their phone within seconds. Results arrive at your callback URL in real time.
PHP SDK on GitHub

Authentication

Every request must include your API key as a Bearer token in the Authorization header. Keys are 64 hex characters, cryptographically random.

http
Authorization: Bearer a3f8c1e9b2d4f6a0e7c5b8d1f3a9e2c6b4d7f0a2e5c8b1d3f6a9e0c2b5d8f1a4
Never expose your API key in client-side JavaScript, public repos, or logs. Rotate it immediately via POST /v1/keys/{id}/regenerate if compromised.

Base URL & Versioning

All endpoints are versioned under /v1/. We maintain backward compatibility within a major version.

base url
https://api.smartpaypesa.com/v1

All requests and responses use application/json. Every response includes a success boolean and, on errors, an error_code machine-readable string.

A diagnostic endpoint is available at GET /v1/ping — returns {"success":true,"message":"pong"} without authentication. Use it to verify connectivity.

API Key Management

Create and manage multiple API accounts — useful for separating production, staging, or multiple client integrations. Each account has its own key, rate limit, payment destination, and notification settings.

All endpoints
GET/v1/keysList all accounts
POST/v1/keysCreate account
GET/v1/keys/{id}Get account + key
DEL/v1/keys/{id}Delete account
POST/v1/keys/{id}/set-defaultSet as default
POST/v1/keys/{id}/regenerateRegenerate key
GET/v1/keys/{id}/destinationGet destination
PUT/v1/keys/{id}/destinationSet destination
PUT/v1/keys/{id}/notificationsSet notifications
List All Accounts — GET /v1/keys
GET /v1/keys
json — response
{
  "success": true,
  "accounts": [
    {
      "id": 1,
      "name": "Default Account",
      "is_default": true,
      "active": true,
      "limit": 4850,
      "fraud_suspended": false,
      "destination": { "method": "paybill", "paybill_number": "123456", "account_number": "ACC001" },
      "callbackurl": "https://myapp.com/callback",
      "notify_email": "pay@myapp.com",
      "notify_phone": "",
      "created_at": "2025-06-01 09:00:00",
      "last_used_at": "2025-06-03 14:22:11"
    }
  ],
  "count": 1
}
Create Account — POST /v1/keys
POST /v1/keys
FieldTypeRequiredDescription
namestringrequiredUnique label for this account (max 80 chars)
http — request
POST /v1/keys HTTP/1.1
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

{ "name": "Production App" }
json — response 201
{
  "success": true,
  "message": "API account created successfully.",
  "account": {
    "id": 4,
    "name": "Production App",
    "is_default": false,
    "active": false,
    "limit": 0,
    "api_key": "a3f8c1e9b2d4f6a0e7c5b8d1f3a9e2c6b4d7f0a2e5c8b1d3f6a9e0c2b5d8f1a4",
    "destination": { "method": "wallet" },
    "callbackurl": "",
    "created_at": "2025-06-03 15:00:00"
  }
}
The api_key is returned in full only on create and regenerate. Store it securely — GET /v1/keys never includes it.
Set Payment Destination — PUT /v1/keys/{id}/destination
PUT /v1/keys/{id}/destination

Controls where incoming payments are routed. Choose one of five methods:

methodRequired extra fieldsDescription
walletPayments go to your SmartPay wallet
phonephoneM-Pesa number (2547XXXXXXXX)
paybillpaybill_number, account_numberPaybill business number + account
tilltill_numberBuy Goods till (5–7 digits)
bankpaybillbank_code, bank_accountBank paybill — see Supported Banks
Paybill example
json
{
  "method": "paybill",
  "paybill_number": "123456",
  "account_number": "ACC001",
  "callbackurl": "https://myapp.com/mpesa/callback"
}
Bank account example
json
{
  "method": "bankpaybill",
  "bank_code": "247247",
  "bank_account": "0123456789",
  "callbackurl": "https://myapp.com/mpesa/callback"
}
Notifications — PUT /v1/keys/{id}/notifications
PUT /v1/keys/{id}/notifications

Receive an email and/or SMS on every completed payment. Pass an empty string to disable either channel.

json — request body
{
  "notify_email": "payments@myapp.com",
  "notify_phone": "254712345678"
}

Initiate STK Push

Send an M-Pesa payment prompt to a customer's phone. Where the money goes is determined by the destination you configured on the API key. The customer sees the SmartPay prompt and enters their PIN.

The key must have an active subscription and remaining call limit > 0 before STK Push works.
POST /v1/stk/push
Request body
ParameterTypeRequiredDescription
phonestringrequiredCustomer phone — 2547XXXXXXXX, 2541XXXXXXXX, 07XX, or 01XX
amountintegerrequiredAmount in KES (1 – 300,000)
account_referencestringoptionalReference shown on M-Pesa prompt (default: SMARTPAY)
descriptionstringoptionalTransaction description (default: SmartPay Payment)
cURL example
bash
curl -X POST https://api.smartpaypesa.com/v1/stk/push \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d '{"phone":"254712345678","amount":150,"account_reference":"ORDER_101"}'
PHP example
php
<?php
$ch = curl_init('https://api.smartpaypesa.com/v1/stk/push');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        'Content-Type: application/json',
        'Authorization: Bearer ' . $YOUR_API_KEY,
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'phone'  => '254712345678',
        'amount' => 150,
    ]),
]);
$res = json_decode(curl_exec($ch));
// $res->checkout_request_id — poll or wait for callback
Success response
json
{
  "success": true,
  "message": "STK Push initiated successfully.",
  "checkout_request_id": "ws_CO_03062025160832822727856009",
  "merchant_request_id": "aea5-4f44-b4a1-d9044446ee461957335"
}
Save checkout_request_id — use it to query GET /v1/transactions/{ref} or match incoming webhook callbacks.
Rate limits
LimitValueResponse header
Per IP200 req / min
Per key (minute)60 req / minX-RateLimit-Remaining
Per key (day)10,000 req / dayX-RateLimit-Day-Remaining
Per phone number3 pushes per 5 min

B2C Payouts

Send money out of your SmartPay wallet directly to an M-Pesa phone number. Because B2C moves funds out of your account rather than in, it requires a second authentication factor on top of your API key, and the wallet balance is deducted immediately when the payout is queued.

B2C requests must come from your backend only. Never call these endpoints from client-side code — you would be exposing your dashboard password.
All endpoints
POST/v1/b2c/sendQueue a payout
GET/v1/b2c/{ref}Check payout status
Second-factor authentication

Every B2C request needs your normal Bearer API key plus two extra headers carrying your SmartPay dashboard login. These must belong to the same account that owns the API key — a valid login for a different account will be rejected.

HeaderRequiredDescription
X-UsernamerequiredYour dashboard login — email or phone (2547XXXXXXXX / 07XX / 01XX)
X-PasswordrequiredYour dashboard password
Your account must be email-verified and not suspended for these credentials to be accepted.
Queue a Payout — POST /v1/b2c/send
POST /v1/b2c/send
Request body
ParameterTypeRequiredDescription
phonestringrequiredRecipient phone — 2547XXXXXXXX, 07XX, or 01XX format
amountnumberrequiredAmount in KES — KSh 10 minimum
cURL example
bash
curl -X POST https://api.smartpaypesa.com/v1/b2c/send \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "X-Username: you@myapp.com" \
  -H "X-Password: YOUR_DASHBOARD_PASSWORD" \
  -d '{"phone":"254712345678","amount":500}'
Success response — 202 Accepted
json
{
  "success": true,
  "message": "Payment initiated.",
  "reference": "AG_20250617_2010a1b2c3d4e5f6a7b8",
  "withdrawal_id": 8842,
  "conversation_id": "AG_20250617_1234567890ab",
  "phone": "254712345678",
  "amount": 500,
  "fee": 15,
  "status": "PROCESSING"
}
A 202 response means the payout was accepted and queued, not that it succeeded. Always poll GET /v1/b2c/{ref} using the returned reference until status reaches a final state.
Transfer fees

A fee is added on top of amount and deducted from your wallet together with it. Both are reversed automatically if the payout ultimately fails.

Amount rangeFee
KSh 10 – 100Free
KSh 101 – 1,000KSh 10
KSh 1,001 – 5,000KSh 15
KSh 5,001 – 20,000KSh 20
Above KSh 20,000KSh 25
Check Payout Status — GET /v1/b2c/{ref}
GET /v1/b2c/{ref}

{ref} is the reference value returned by POST /v1/b2c/send. Same auth as above — Bearer key plus X-Username / X-Password.

json — response
{
  "success": true,
  "reference": "AG_20250617_2010a1b2c3d4e5f6a7b8",
  "phone": "254712345678",
  "amount": 500,
  "fee": 15,
  "payout_amount": 500,
  "status": "COMPLETED",
  "response_code": "0",
  "response_description": "The service request is processed successfully.",
  "transaction_id": "TFH774CBH2",
  "conversation_id": "AG_20250617_1234567890ab",
  "created_at": "2025-06-17 00:55:54",
  "updated_at": "2025-06-17 00:56:03"
}
Payout status values
statusMeaning
PROCESSINGAccepted and sent to Safaricom — awaiting the result callback
COMPLETEDMoney delivered to the recipient's M-Pesa account
FAILEDSafaricom rejected or could not complete the payout — balance reversed
TIMED_OUTNo result received from Safaricom in time — balance reversed
REVERSEDAmount + fee returned to your wallet
B2C results are not delivered to your callbackurl — they land on SmartPay's internal endpoints and update this record directly. Polling GET /v1/b2c/{ref} is the only way to observe the final outcome. See Webhooks.

Transaction Status

Query a payment by its checkout_request_id. Prefer webhooks for real-time results — use this for reconciliation or as a fallback.

GET/v1/transactions/{checkout_request_id}
http
GET /v1/transactions/ws_CO_03062025142200123456789 HTTP/1.1
Authorization: Bearer YOUR_API_KEY
List transactions — GET /v1/transactions
GET/v1/transactions
Query paramDefaultDescription
limit20Records per page (max 100)
offset0Skip N records for pagination
statusFilter: pending, completed, or failed

Account Balance

Retrieve your SmartPay wallet balance and account info.

GET/v1/account
json — response
{
  "success": true,
  "account": {
    "account_number": "SP36413506",
    "fullname": "John Kamau",
    "verified": true
  },
  "active_key": {
    "id": 4,
    "name": "Production App",
    "plan": "Professional",
    "limit_remaining": 4850,
    "fraud_suspended": false
  },
  "subscription": {
    "plan": "Professional",
    "status": "active",
    "expires_at": "2025-07-01 00:00:00",
    "api_calls": 5000
  },
  "transaction_stats": {
    "total": 312, "completed": 298, "pending": 4, "failed": 10
  }
}

SMS API

Send single or bulk SMS to Kenyan numbers. Uses the X-API-Key header. Each 160-character block costs 1 credit.

Send SMS — POST /v1/sms/
POST/v1/sms/
ParameterRequiredDescription
phonerequiredSupports 07XX, 01XX, or 254XX formats
messagerequiredSMS content — auto-split at 160 chars
bash
curl -X POST https://api.smartpaypesa.com/v1/sms/ \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -d '{"phone":"0712345678","message":"Hello from SmartPay!"}'
json — response
{
  "status": "success",
  "data": {
    "transaction_id": "5713595232279350695",
    "mobile": "254712345678",
    "sms_parts": 1,
    "characters": 22,
    "cost": 1,
    "remaining_balance": 99
  }
}
Check SMS balance — GET /v1/sms/balance
GET/v1/sms/balance
SMS pricing
Message lengthSMS partsCredits
1–160 chars1 SMS1 credit
161–320 chars2 SMS2 credits
321–480 chars3 SMS3 credits
481–640 chars4 SMS4 credits

Webhooks

SmartPay POSTs real-time payment results to your callback URL the moment a transaction completes — no polling required.

Configuration
  1. Include callbackurl in your PUT /v1/keys/{id}/destination call.
  2. Or set it in the dashboard: API → select account → destination → Callback URL.
Your endpoint must return HTTP 200 within 5 seconds. SmartPay retries failed deliveries up to 3 times with exponential back-off.
Webhook payload
json — POST body to your server
{
  "Body": {
    "stkCallback": {
      "MerchantRequestID": "aea5-4f44-b4a1-d9044446ee463545184",
      "CheckoutRequestID": "ws_CO_17062025005554749727856009",
      "ResultCode": 0,
      "ResultDesc": "The service request is processed successfully.",
      "CallbackMetadata": {
        "Item": [
          { "Name": "Amount",             "Value": 150 },
          { "Name": "MpesaReceiptNumber", "Value": "TFH774CBH1" },
          { "Name": "TransactionDate",    "Value": 20250617005603 },
          { "Name": "PhoneNumber",        "Value": 254712345678 }
        ]
      }
    }
  }
}
Always check ResultCode === 0 before crediting an account. A callback with ResultCode: 1032 means the customer cancelled. 1032, 1037, and 9999 are the most common non-zero codes.
B2C payouts do not deliver a webhook to your callback URL. Safaricom's result/timeout callbacks go directly to SmartPay's internal endpoints to update the payout record. Poll GET /v1/b2c/{ref} for the final status — see B2C Payouts.

Error Codes

API errors
error_codeHTTPMeaning
MISSING_AUTH401Authorization header missing
INVALID_API_KEY401Key not found, wrong format, or inactive
KEY_SUSPENDED403Suspended due to fraud activity
KEY_INACTIVE403Subscription not active on this key
LIMIT_REACHED403Monthly API call limit exhausted
MISSING_FIELDS400Required fields missing from body
INVALID_PHONE400Phone number format invalid
INVALID_AMOUNT400Amount out of range (1–300,000)
DUPLICATE_NAME409Account with that name already exists
CANNOT_DELETE_DEFAULT400Set another key as default first
IP_RATE_LIMIT429Too many requests from this IP
RATE_LIMIT_MINUTE429Per-key per-minute limit hit
RATE_LIMIT_DAY429Per-key daily limit hit
PHONE_RATE_LIMIT4293 pushes already sent to this number in 5 min
FRAUD_BLOCK_FAILURE_RATE403Abnormally high failure rate detected
FRAUD_BLOCK_PHONE_DIVERSITY403Suspicious random-phone pattern detected
FRAUD_BLOCK_BURST429High-volume burst + high failure rate
FRAUD_BLOCK_CANCEL_RATE403High customer rejection/cancel rate
PAYMENT_FAILURE500Daraja rejected the STK Push
NOT_FOUND404Resource or endpoint does not exist
B2C errors
error_codeHTTPMeaning
B2C_AUTH_REQUIRED401X-Username or X-Password header missing
B2C_AUTH_INVALID401X-Username is not a valid email or phone format
B2C_AUTH_FAILED401Username/password combination is incorrect
B2C_AUTH_MISMATCH403Those credentials belong to a different account than this API key
B2C_ACCOUNT_UNVERIFIED403Account email is not verified
B2C_ACCOUNT_SUSPENDED403Account is suspended
KEY_INACTIVE403This API key is not active
FRAUD_SUSPENDED403Account is suspended pending fraud review
VALIDATION_ERROR422Invalid phone format or amount below the KSh 10 minimum
INSUFFICIENT_BALANCE402Wallet balance can't cover amount + fee
ACCOUNT_ERROR500Account is missing required configuration (e.g. no account number on file)
GATEWAY_UNREACHABLE502Could not reach the payout gateway — balance was not touched
GATEWAY_ERRORvariesPayout rejected by Safaricom — balance is automatically reversed
CONFIG_ERROR500Server-side configuration issue — contact support
M-Pesa ResultCodes
ResultCodeMeaning
0Success — payment accepted
1Insufficient balance
1001Unable to lock subscriber — another transaction in progress
1019Transaction has expired
1032Cancelled by user
1037DS timeout — user cannot be reached
2001Initiator information is invalid
9999General error while sending push

Supported Banks

Use these bank_code values when setting a bankpaybill destination.

bank_codeBank name
111777ABC Bank
303030Absa Bank
972900Bank of Africa
247247Equity Bank
222111Family Bank Ltd
542542I&M Bank
522522KCB (522522)
522533KCB (522533)
547700National Bank
488488NIC Bank
600100Stanbic Bank
329329Standard Chartered Bank
516600Diamond Trust Bank (DTB)
700201Eco Bank
400200Co-operative Bank (400200)
400222Co-operative Bank (400222)
100400Housing Finance (HFC)
4156839SMARTPAY WALLET
200999Post Office Savings Bank
982800Prime Bank
559900UBA Bank
888600Century Microfinance Bank
508400Consolidated Bank
972700Credit Bank
498100Equatorial Commercial Bank
328585Faulu Microfinance Bank
919700First Community Bank
344500Guardian Bank
985050Gulf African Bank
529901Jamii Bora Bank
910200JT Bank
111999K-Rep Bank
101200Kenya Women Microfinance Bank
333222Mkopa
514000Musoni Microfinance
802200Rafiki Microfinance Bank
777001SMEP Microfinance Bank
862862Transnational Bank
504600Uwezo Microfinance Bank
200555Vision Fund Kenya

Best Practices

Security
  • Always use HTTPS for all API requests.
  • Never expose your API key in client-side code, logs, or public repositories. Rotate via POST /v1/keys/{id}/regenerate if compromised.
  • Create separate keys for production, staging, and each client — use POST /v1/keys.
  • Validate all webhook payloads — check ResultCode === 0 before crediting an account.
  • Never send your X-Password from client-side code — B2C requests must originate from your backend only, alongside your API key.
Performance
  • Use webhooks instead of polling GET /v1/transactions/{ref}.
  • Respond to webhook requests immediately with HTTP 200 and process asynchronously.
  • Implement exponential back-off for 429 responses.
Phone & retry logic
  • Handle PHONE_RATE_LIMIT (3 pushes / 5 min per number) — back off for at least 5 minutes before retrying the same number.
  • Store checkout_request_id from every STK Push for later reconciliation.
  • Keep SMS messages under 160 characters to use 1 credit per message.
  • Store the reference from every B2C send and poll GET /v1/b2c/{ref} until status reaches COMPLETED, FAILED, or REVERSED — don't assume success just because the initial call returned 202.
On first deploy: create a dedicated key via POST /v1/keys and store it in your secrets manager — never use the bootstrap key in production code.