UnifiedPay API Documentation

Integrate M-Pesa payments with a few lines of code.

🚀 Quick Start

  1. Sign up and log in to your dashboard
  2. Link a Paybill, Till, or Bank / SACCO account, then activate it with the KES 300 subscription (per 30 days)
  3. Copy the account's consumer_key and consumer_secret
  4. Optionally add a callback URL to receive payment results on your server
  5. Use the endpoints below to validate, send STK push, and check transaction status

Base URL

https://unifiedpay.co.ke

All requests must be HTTPS POST.

POST Validate Credentials

/auth/cred/{consumer_key}/{consumer_secret}/url

Check that your API credentials are valid and see the account's subscription status (active, unpaid, expired or suspended).

Request

No request body required.

Response

Success (200)
{
  "ResultCode": 0,
  "message": "Credentials valid",
  "shortcode": "4201775",
  "account_status": "active",
  "subscription_expires_at": "2026-10-28 14:30:22"
}
Error (401)
{
  "ResultCode": 401,
  "errorMessage": "Invalid API credentials"
}
curl -X POST https://unifiedpay.co.ke/auth/cred/YOUR_CONSUMER_KEY/YOUR_CONSUMER_SECRET/url
<?php
$url = 'https://unifiedpay.co.ke/auth/cred/YOUR_CONSUMER_KEY/YOUR_CONSUMER_SECRET/url';

$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response, true);
if ($result['ResultCode'] == 0) {
    echo "Credentials valid. Shortcode: " . $result['shortcode'];
} else {
    echo "Error: " . $result['errorMessage'];
}
?>
import requests

url = "https://unifiedpay.co.ke/auth/cred/YOUR_CONSUMER_KEY/YOUR_CONSUMER_SECRET/url"
response = requests.post(url)
data = response.json()

if data['ResultCode'] == 0:
    print(f"Valid. Shortcode: {data['shortcode']}")
else:
    print(f"Error: {data['errorMessage']}")
fetch('https://unifiedpay.co.ke/auth/cred/YOUR_CONSUMER_KEY/YOUR_CONSUMER_SECRET/url', {
    method: 'POST'
})
.then(res => res.json())
.then(data => {
    if (data.ResultCode === 0) {
        console.log('Valid. Shortcode:', data.shortcode);
    } else {
        console.error('Error:', data.errorMessage);
    }
});

POST Send STK Push

/auth/cred/{consumer_key}/{consumer_secret}/sendstk

Initiate M-Pesa payment prompt on customer's phone.

Request Body (JSON)

FieldTypeRequiredDescription
amountintegerYesAmount to pay (KES, whole numbers only)
msisdnstringYesCustomer phone number (e.g., 254712345678 or 0712345678)
referencestringYesYour payment reference, up to 100 characters (Safaricom documents a 12-character limit for the M-Pesa account reference, so keep Paybill references to 12 or fewer). See how it is used below.

How payments are routed

Account typePaid toTransactionTypeM-Pesa account reference
PaybillYour paybillCustomerPayBillOnlineThe reference you send
Till NumberYour tillCustomerBuyGoodsOnlineThe reference you send
Bank / SACCOThe bank's or SACCO's paybillCustomerPayBillOnlineAlways your account / member number (your reference is kept for your records)

The account must have an active subscription, otherwise the request is rejected with 403.

Response

Success (200)
{
  "ResponseCode": "0",
  "success": true,
  "message": "STK push initiated",
  "transaction_request_id": "FL20240315143022123",
  "MerchantRequestID": "2d5c-4e7b-8f1a-9c3d6e8f1a2b",
  "CheckoutRequestID": "ws_CO_1234567890",
  "party_b": "123456",
  "account_reference": "Test Payment",
  "transaction_type": "CustomerPayBillOnline"
}
Error (400/403/500)
{
  "ResultCode": 400,
  "errorMessage": "Missing required fields"
}
curl -X POST https://unifiedpay.co.ke/auth/cred/YOUR_KEY/YOUR_SECRET/sendstk \
  -H "Content-Type: application/json" \
  -d '{
    "amount": "1",
    "msisdn": "254712345678",
    "reference": "Test Payment"
  }'
<?php
$url = 'https://unifiedpay.co.ke/auth/cred/YOUR_KEY/YOUR_SECRET/sendstk';
$data = [
    'amount' => 1,
    'msisdn' => '254712345678',
    'reference' => 'Test Payment'
];

$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response, true);
if ($result['ResponseCode'] == '0') {
    echo "STK sent. Transaction ID: " . $result['transaction_request_id'];
} else {
    echo "Error: " . ($result['errorMessage'] ?? 'Unknown error');
}
?>
import requests
import json

url = "https://unifiedpay.co.ke/auth/cred/YOUR_KEY/YOUR_SECRET/sendstk"
payload = {
    "amount": 1,
    "msisdn": "254712345678",
    "reference": "Test Payment"
}
headers = {"Content-Type": "application/json"}

response = requests.post(url, json=payload, headers=headers)
data = response.json()

if data.get('ResponseCode') == '0':
    print(f"STK sent. Transaction ID: {data['transaction_request_id']}")
else:
    print(f"Error: {data.get('errorMessage')}")
fetch('https://unifiedpay.co.ke/auth/cred/YOUR_KEY/YOUR_SECRET/sendstk', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
        amount: 1,
        msisdn: '254712345678',
        reference: 'Test Payment'
    })
})
.then(res => res.json())
.then(data => {
    if (data.ResponseCode === '0') {
        console.log('STK sent. ID:', data.transaction_request_id);
    } else {
        console.error('Error:', data.errorMessage);
    }
});

POST Transaction Status

/auth/cred/{consumer_key}/{consumer_secret}/sendstatus

Query the status of a previously initiated STK push transaction.

Request Body (JSON)

FieldTypeRequiredDescription
transaction_request_idstringYesThe ID returned from the /sendstk endpoint

Response

Success (200)
{
  "ResultCode": "200",
  "transaction_request_id": "FL20240315143022123",
  "TransactionStatus": "Completed",
  "TransactionCode": "0",
  "ResultDesc": "The service request is processed successfully.",
  "TransactionReceipt": "QKJ8H7G9L2",
  "TransactionAmount": "1.00",
  "Msisdn": "254712345678",
  "TransactionDate": "20240315143022",
  "TransactionReference": "Test Payment",
  "CheckoutRequestID": "ws_CO_1234567890",
  "MerchantRequestID": "2d5c-4e7b-8f1a-9c3d6e8f1a2b"
}
Pending
{
  "ResultCode": "200",
  "ResultDesc": "Transaction pending",
  "TransactionStatus": "Pending",
  ...
}
curl -X POST https://unifiedpay.co.ke/auth/cred/YOUR_KEY/YOUR_SECRET/sendstatus \
  -H "Content-Type: application/json" \
  -d '{"transaction_request_id": "FL20240315143022123"}'
<?php
$url = 'https://unifiedpay.co.ke/auth/cred/YOUR_KEY/YOUR_SECRET/sendstatus';
$data = ['transaction_request_id' => 'FL20240315143022123'];

$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response, true);
echo "Status: " . $result['TransactionStatus'];
if ($result['TransactionStatus'] == 'Completed') {
    echo " Receipt: " . $result['TransactionReceipt'];
}
?>
import requests

url = "https://unifiedpay.co.ke/auth/cred/YOUR_KEY/YOUR_SECRET/sendstatus"
payload = {"transaction_request_id": "FL20240315143022123"}
headers = {"Content-Type": "application/json"}

response = requests.post(url, json=payload, headers=headers)
data = response.json()

print(f"Status: {data['TransactionStatus']}")
if data['TransactionStatus'] == 'Completed':
    print(f"Receipt: {data['TransactionReceipt']}")
fetch('https://unifiedpay.co.ke/auth/cred/YOUR_KEY/YOUR_SECRET/sendstatus', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ transaction_request_id: 'FL20240315143022123' })
})
.then(res => res.json())
.then(data => {
    console.log('Status:', data.TransactionStatus);
    if (data.TransactionStatus === 'Completed') {
        console.log('Receipt:', data.TransactionReceipt);
    }
});

Error Codes Reference

0 - Success
1 - Insufficient balance
1032 - Cancelled by user
1037 - Timeout (user unreachable)
2001 - Invalid initiator info
1019 - Invalid receiving organization
401 - Invalid API credentials
403 - Account not subscribed, expired or suspended
400 - Missing required fields
404 - Transaction not found
500 - Server error

POST Payment Callbacks (to your server)

Add a callback URL for an account under Dashboard → Callbacks. When M-Pesa completes, fails or cancels a payment, UnifiedPay sends a POST with a JSON body to that URL. Reply with any 2xx status. Failed deliveries can be resent from the Transactions page.

Headers

HeaderDescription
X-UnifiedPay-Eventtransaction.completed, transaction.failed, transaction.cancelled or test
X-UnifiedPay-Signaturesha256= + HMAC-SHA256 of the raw body, keyed with the account's consumer secret

Body

{
  "event": "transaction.completed",
  "transaction_request_id": "FL20240315143022123",
  "TransactionStatus": "Completed",
  "TransactionCode": "0",
  "ResultDesc": "The service request is processed successfully.",
  "TransactionReceipt": "QKJ8H7G9L2",
  "TransactionAmount": "1.00",
  "Msisdn": "254712345678",
  "TransactionDate": "20240315143022",
  "TransactionReference": "Test Payment",
  "CheckoutRequestID": "ws_CO_1234567890",
  "MerchantRequestID": "2d5c-4e7b-8f1a-9c3d6e8f1a2b"
}

Verifying the signature (PHP)

<?php
$body = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $body, 'YOUR_CONSUMER_SECRET');

if (!hash_equals($expected, $_SERVER['HTTP_X_UNIFIEDPAY_SIGNATURE'] ?? '')) {
    http_response_code(401);
    exit;
}

$event = json_decode($body, true);
if ($event['TransactionStatus'] === 'Completed') {
    // Mark order $event['TransactionReference'] as paid
}
http_response_code(200);
?>