GOOD AFTERNOON
Loading…
Profile
Customers
People and contacts linked to your payments
| TYPE / NAME | CONTACT | DATE |
|---|---|---|
| Loading… | ||
Payment channels
Manage the paybill and till accounts your customers can pay into
| CHANNEL ID | CHANNEL TYPE | ACCOUNT NUMBER | STATUS | DATE CREATED | ACTION |
|---|---|---|---|---|---|
| Loading… | |||||
Add a payment channel
Connect your bank, paybill or till so you can receive payments
● Channel preview
See how your payment channels appear when you collect money.
You can add multiple channels and choose which one to use when initiating payments.
Create STK Push
Initiate an M-Pesa payment request to a customer's phone
● Fast, secure payment
The customer receives an M-Pesa PIN prompt on their phone.
Successful payments are credited to your configured channel.
Transactions
Search, filter and export your payment history
| TYPE | AMOUNT | CHANNEL | PROVIDER REF | EXTERNAL REF | DESCRIPTION | DATE |
|---|---|---|---|---|---|---|
| Loading… | ||||||
Pricing
Transaction fees by amount range
| AMOUNT FROM | AMOUNT TO | TRANSACTION FEE |
|---|---|---|
| Loading… | ||
API Keys
Manage the credentials your systems use to access the API
| NAME | API USERNAME | ACCOUNT ID | CREATED | ACTIONS |
|---|---|---|---|---|
| Loading… | ||||
Identity Verification
Verify your identity to unlock payment channels, STK Push, and API keys
Admin Overview
Real-time system activity and key metrics
All Users
Manage accounts and KYC status across the platform
All Transactions
Every STK Push transaction across all accounts
KYC Reviews
Review and verify identity documents submitted by users
API Documentation
Integrate ImaraPay M-Pesa payments into your application
Overview
ImaraPay lets you trigger M-Pesa STK Push payments directly to your customers from your backend. Money is deposited straight into your registered payment channel (Till, Paybill, or Bank account) — you are charged a small per-transaction service fee from your ImaraPay service wallet.
All API requests use HTTPS. The base URL for every endpoint is:
Authentication
Every API request (except public endpoints like /api/pricing) requires an API key pair. You can create keys on the API Keys page inside your ImaraPay dashboard.
When you create a key you receive an API Username and an API Password. Combine them into an HTTP Basic Auth header:
Authorization: Basic base64(API_USERNAME:API_PASSWORD)
Getting your token
const API_USERNAME = 'your_api_username';
const API_PASSWORD = 'your_api_password';
const BASE_URL = 'https://YOUR_DOMAIN/api';
// Build the Basic Auth header once, reuse for every request
const AUTH_HEADER = 'Basic ' + btoa(API_USERNAME + ':' + API_PASSWORD);
const headers = {
'Content-Type': 'application/json',
'Authorization': AUTH_HEADER,
};
import requests
import base64
API_USERNAME = "your_api_username"
API_PASSWORD = "your_api_password"
BASE_URL = "https://YOUR_DOMAIN/api"
credentials = base64.b64encode(f"{API_USERNAME}:{API_PASSWORD}".encode()).decode()
AUTH_HEADER = f"Basic {credentials}"
HEADERS = {
"Content-Type": "application/json",
"Authorization": AUTH_HEADER,
}
Quick Start
Get a payment working in under 5 minutes:
- Create an account on ImaraPay and complete identity verification (KYC).
- Go to Payment Channels and add your Till, Paybill, or Bank account.
- Go to API Keys and create a key — copy the Username, Password, and Basic Auth token.
- Top up your service wallet with a small amount to cover transaction fees.
- Call POST /api/stk-push with your channel ID, the customer's phone, and the amount.
Initiate STK Push
Sends an M-Pesa payment prompt to the customer's phone. Money is deposited directly into your payment channel. The fee is deducted from your service wallet at the time of the request.
Request parameters
| Field | Type | Description |
|---|---|---|
| account_id required | integer | ID of the payment channel to deposit funds into. Retrieve from GET /api/accounts. |
| phone_number required | string | Customer's M-Pesa phone number. Accepts formats: 0712345678, 254712345678, +254712345678. |
| amount required | integer | Amount in KES (whole numbers only). Minimum: 1, Maximum: 999,999. |
| external_reference optional | string | Your own order/invoice reference. Stored on the transaction for reconciliation. |
| description optional | string | Short description shown in M-Pesa. Defaults to Payment. |
async function stkPush({ accountId, phone, amount, reference, description }) {
const res = await fetch(`${BASE_URL}/stk-push`, {
method: 'POST',
headers,
body: JSON.stringify({
account_id: accountId,
phone_number: phone,
amount: amount,
external_reference: reference,
description: description,
}),
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.error || 'STK push failed');
}
return res.json();
// { success: true, transaction_id: 42, checkout_request_id: "ws_CO_...", fee_to_be_charged: 6 }
}
// Example usage
const result = await stkPush({
accountId: 1,
phone: '0712345678',
amount: 500,
reference: 'INV-001',
description: 'Order #1001',
});
console.log('Checkout ID:', result.checkout_request_id);
def stk_push(account_id, phone, amount, reference=None, description="Payment"):
payload = {
"account_id": account_id,
"phone_number": phone,
"amount": amount,
"external_reference": reference,
"description": description,
}
response = requests.post(f"{BASE_URL}/stk-push", json=payload, headers=HEADERS)
response.raise_for_status()
return response.json()
# { "success": True, "transaction_id": 42,
# "checkout_request_id": "ws_CO_...", "fee_to_be_charged": 6 }
# Example usage
result = stk_push(
account_id=1,
phone="0712345678",
amount=500,
reference="INV-001",
description="Order #1001",
)
print("Checkout ID:", result["checkout_request_id"])
Success response
{
"success": true,
"message": "STK push sent successfully",
"transaction_id": 42,
"checkout_request_id": "ws_CO_123456789_20240615123456_1_254712345678",
"fee_to_be_charged": 6
}
Check Transaction Status
M-Pesa payments are asynchronous. After initiating an STK Push, poll this endpoint every 3–5 seconds until the status is completed or failed. Stop polling after 90 seconds.
async function pollTransactionStatus(checkoutRequestId, timeoutMs = 90000) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const res = await fetch(
`${BASE_URL}/transaction/status/${checkoutRequestId}`,
{ headers }
);
const data = await res.json();
if (data.status === 'completed') {
console.log('Payment successful! Receipt:', data.mpesa_receipt);
return data;
}
if (data.status === 'failed') {
throw new Error(data.result_desc || 'Payment failed');
}
// Still pending — wait 3 seconds and try again
await new Promise(r => setTimeout(r, 3000));
}
throw new Error('Payment timed out — please ask the customer to check M-Pesa');
}
import time
def poll_transaction_status(checkout_request_id, timeout=90):
deadline = time.time() + timeout
while time.time() < deadline:
response = requests.get(
f"{BASE_URL}/transaction/status/{checkout_request_id}",
headers=HEADERS,
)
data = response.json()
if data["status"] == "completed":
print("Payment successful! Receipt:", data["mpesa_receipt"])
return data
if data["status"] == "failed":
raise Exception(data.get("result_desc", "Payment failed"))
time.sleep(3) # wait 3 seconds before polling again
raise Exception("Payment timed out")
Status response
{
"status": "completed",
"amount": 500,
"mpesa_receipt": "RGH8KXXXX",
"result_desc": "The service request is processed successfully.",
"balance": 94
}
The status field will be one of: pending · completed · failed
Webhooks (Callbacks)
Instead of polling, you can set a Callback URL on your account (under Account Settings). ImaraPay will POST the payment result to your URL as soon as M-Pesa confirms it.
Callback payload
{
"Body": {
"stkCallback": {
"MerchantRequestID": "29115-34620561-1",
"CheckoutRequestID": "ws_CO_123456789",
"ResultCode": 0,
"ResultDesc": "The service request is processed successfully.",
"CallbackMetadata": {
"Item": [
{ "Name": "Amount", "Value": 500 },
{ "Name": "MpesaReceiptNumber", "Value": "RGH8KXXXX" },
{ "Name": "TransactionDate", "Value": 20240615123456 },
{ "Name": "PhoneNumber", "Value": 254712345678 }
]
}
}
}
}
When ResultCode is 0 the payment succeeded. Any other value is a failure — use ResultDesc as the error message to show the user.
Handling the callback
app.post('/payment/callback', express.json(), (req, res) => {
// Always respond 200 immediately so M-Pesa doesn't retry
res.json({ ResultCode: 0, ResultDesc: 'Accept' });
const cb = req.body?.Body?.stkCallback;
if (!cb) return;
const { CheckoutRequestID, ResultCode, ResultDesc, CallbackMetadata } = cb;
if (ResultCode === 0) {
const items = CallbackMetadata?.Item || [];
const get = name => items.find(i => i.Name === name)?.Value;
const receipt = get('MpesaReceiptNumber');
const amount = get('Amount');
// TODO: update your DB using CheckoutRequestID
console.log(`Payment confirmed: ${receipt} — KES ${amount}`);
} else {
console.log(`Payment failed: ${ResultDesc}`);
}
});
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/payment/callback', methods=['POST'])
def payment_callback():
# Always respond 200 immediately so M-Pesa doesn't retry
data = request.get_json(force=True)
cb = (data or {}).get('Body', {}).get('stkCallback', {})
checkout_id = cb.get('CheckoutRequestID')
result_code = cb.get('ResultCode')
result_desc = cb.get('ResultDesc', '')
if result_code == 0:
items = cb.get('CallbackMetadata', {}).get('Item', [])
get_val = lambda name: next((i['Value'] for i in items if i['Name'] == name), None)
receipt = get_val('MpesaReceiptNumber')
amount = get_val('Amount')
# TODO: update your DB using checkout_id
print(f"Payment confirmed: {receipt} — KES {amount}")
else:
print(f"Payment failed: {result_desc}")
return jsonify(ResultCode=0, ResultDesc="Accept")
Payment Channels
A payment channel is a Till, Paybill, or Bank account where funds are deposited. You must pass a channel's id when initiating an STK Push.
List your channels
const res = await fetch(`${BASE_URL}/accounts`, { headers });
const channels = await res.json();
// Find your active till channel
const till = channels.find(c => c.account_type === 'till' && c.is_active);
console.log('Till channel ID:', till?.id);
response = requests.get(f"{BASE_URL}/accounts", headers=HEADERS)
channels = response.json()
# Find your active paybill channel
paybill = next((c for c in channels if c["account_type"] == "paybill" and c["is_active"]), None)
print("Paybill channel ID:", paybill["id"] if paybill else "None")
Channel object
{
"id": 1,
"name": "Paybill 247247",
"account_type": "paybill", // "till" | "paybill" | "bank"
"account_number": "247247",
"account_reference": "247247",
"bank_name": null, // populated only for bank channels
"is_active": true,
"created_at": "2024-06-15T08:00:00.000Z"
}
Add a channel
| Field | Type | Description |
|---|---|---|
| account_type required | string | till · paybill · bank |
| account_number required | string | Till number, Paybill number, or Bank account number. |
| bank_name bank only | string | Bank name (e.g. Equity Bank). Call GET /api/banks for the full list. |
| account_reference optional | string | Account reference sent to M-Pesa. Defaults to account_number. |
Transactions
Query past transactions for reconciliation, reporting, or showing payment history.
List transactions
| Query param | Type | Description |
|---|---|---|
| status optional | string | pending · completed · failed |
| account_id optional | integer | Filter by channel ID. |
| limit optional | integer | Max results (default 100). |
// Get last 50 completed transactions
const res = await fetch(`${BASE_URL}/transactions?status=completed&limit=50`, { headers });
const txs = await res.json();
txs.forEach(tx => {
console.log(`${tx.mpesa_receipt} | KES ${tx.amount} | ${tx.phone_number} | ${tx.external_reference}`);
});
params = {"status": "completed", "limit": 50}
response = requests.get(f"{BASE_URL}/transactions", headers=HEADERS, params=params)
txs = response.json()
for tx in txs:
print(f"{tx['mpesa_receipt']} | KES {tx['amount']} | {tx['phone_number']}")
Transaction object
{
"id": 42,
"phone_number": "254712345678",
"amount": 500,
"fee_charged": 6,
"status": "completed",
"mpesa_receipt": "RGH8KXXXX",
"external_reference": "INV-001",
"description": "Order #1001",
"checkout_request_id": "ws_CO_...",
"result_desc": "The service request is processed successfully.",
"account_name": "Till 3118761",
"channel": "Till - 3118761",
"created_at": "2024-06-15T12:34:56.000Z",
"completed_at": "2024-06-15T12:35:14.000Z"
}
Error Codes
HTTP errors
| Status | Code | Meaning |
|---|---|---|
| 400 | Bad Request | Missing or invalid parameters. Check the error field for details. |
| 401 | Unauthorized | Missing or invalid API credentials. |
| 402 | Payment Required | Insufficient service wallet balance to cover the fee. |
| 403 | Forbidden | KYC verification required, or resource belongs to another account. |
| 404 | Not Found | Channel, transaction, or resource does not exist. |
| 500 | Server Error | Unexpected error. Retry once — if it persists, contact support. |
M-Pesa result codes
These appear in the result_code / ResultCode field of a callback or transaction status response.
| Code | Cause | Action |
|---|---|---|
| 0 | Success | Payment confirmed. Credit the customer. |
| 1032 | Request cancelled by user | Ask the customer to try again. |
| 1037 | No response from customer (timeout) | Prompt timed out on their phone. Ask them to try again. |
| 1 | Insufficient balance | Customer has insufficient M-Pesa balance. |
| 17 | M-Pesa system error | Retry after a few seconds. |
| 26 | Request throttled | Too many requests. Back off and retry. |
| 2001 | Invalid initiator | Check your Daraja credentials configuration. |
Handling errors in code
try {
const result = await stkPush({ accountId: 1, phone: '0712345678', amount: 500 });
const status = await pollTransactionStatus(result.checkout_request_id);
console.log('Done:', status.mpesa_receipt);
} catch (err) {
if (err.message.includes('Insufficient service balance')) {
// Redirect merchant to top up their ImaraPay wallet
console.error('Top up your service wallet first');
} else {
// Show the M-Pesa error to the customer
console.error('Payment error:', err.message);
}
}
import requests
try:
result = stk_push(account_id=1, phone="0712345678", amount=500)
status = poll_transaction_status(result["checkout_request_id"])
print("Done:", status["mpesa_receipt"])
except requests.HTTPError as e:
body = e.response.json()
if e.response.status_code == 402:
print("Top up your service wallet first")
else:
print("API error:", body.get("error"))
except Exception as e:
print("Payment error:", str(e))
Pricing
ImaraPay charges a flat per-transaction fee based on the payment amount. There are no monthly fees, setup fees, or percentage-based charges.
Fees are deducted from your service wallet at the time the STK push is sent, and refunded automatically if the customer cancels or the push fails.
The fee schedule is available at:
// Public endpoint — no auth required
const res = await fetch(`${BASE_URL}/pricing`);
const tiers = await res.json();
// Find the fee for a given amount
function getFee(amount) {
const tier = tiers.find(t => amount >= t.min && amount <= t.max);
return tier ? tier.fee : 0;
}
console.log(getFee(500)); // 6
console.log(getFee(1000)); // 15
console.log(getFee(10000)); // 50
# Public endpoint — no auth required
response = requests.get(f"{BASE_URL}/pricing")
tiers = response.json()
def get_fee(amount):
tier = next((t for t in tiers if t["min"] <= amount <= t["max"]), None)
return tier["fee"] if tier else 0
print(get_fee(500)) # 6
print(get_fee(1000)) # 15
print(get_fee(10000)) # 50
View the full fee table inside your dashboard on the Pricing page.