Comprehensive documentation for integrating with our payment system
The SHADOW PAY API allows you to integrate M-Pesa STK Push payments into your applications. This documentation provides details on how to configure your system, initiate payments, and track transactions in real-time.
All API endpoints are relative to the base URL:
All API requests require authentication using API keys. Include these in the request headers:
X-API-Key: Your_API_KeyX-API-Secret: Your_API_SecretContent-Type: application/jsonAll API responses are returned in JSON format with a consistent structure:
{
"success": true|false,
"message": "Descriptive message",
// Additional data fields depending on the endpoint
}
Use this interface to test the SHADOW PAY API endpoints with your credentials.
To use the SHADOW PAY API, you need to:
Here's a basic PHP implementation to get you started:
<?php
// Initialize payment
function initiatePayment($apiKey, $apiSecret, $paymentAccountId, $phone, $amount, $reference, $description) {
$url = "https://stk-push.top/api/v2/stkpush.php";
$data = [
'payment_account_id' => $paymentAccountId,
'phone' => $phone,
'amount' => $amount,
'reference' => $reference,
'description' => $description
];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'X-API-Key: ' . $apiKey,
'X-API-Secret: ' . $apiSecret,
'Content-Type: application/json'
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return json_decode($response, true);
}
// Your API credentials
$apiKey = "your_api_key_here";
$apiSecret = "your_api_secret_here";
$paymentAccountId = 14; // Your payment account ID
// Example usage
$result = initiatePayment($apiKey, $apiSecret, $paymentAccountId, "254712345678", 100, "ORDER123", "Test payment");
if ($result['success']) {
echo "STK push sent successfully. Checkout Request ID: " . $result['checkout_request_id'];
} else {
echo "Error: " . $result['message'];
}
?>
This endpoint initiates an M-Pesa STK Push payment request.
| Parameter | Type | Required | Description |
|---|---|---|---|
| payment_account_id | Integer | Yes | Your payment account ID |
| phone | String | Yes | Customer phone number (format: 254712345678) |
| amount | Float | Yes | Payment amount (minimum 1 KES) |
| reference | String | No | Your internal reference for the transaction |
| description | String | No | Description of the payment |
{
"payment_account_id": 17,
"phone": "254712345678",
"amount": 100,
"reference": "ORDER_12345",
"description": "Payment for order #12345"
}
{
"success": true,
"message": "STK push sent successfully",
"checkout_request_id": "ws_CO_20230101120000_abc123def456",
"merchant_request_id": "ws_MR_20230101120000_xyz789uvw012"
}
{
"success": false,
"message": "Error description"
}
This endpoint checks the status of a payment transaction.
| Parameter | Type | Required | Description |
|---|---|---|---|
| checkout_request_id | String | Yes | The checkout request ID from the STK Push response |
{
"checkout_request_id": "ws_CO_20230101120000_abc123def456"
}
{
"success": true,
"status": "completed",
"amount": 100,
"phone": "254712345678",
"transaction_code": "ABC123DEF456",
"created_at": "2023-01-01 12:00:00"
}
This endpoint retrieves a list of your transactions with pagination support.
| Parameter | Type | Required | Description |
|---|---|---|---|
| page | Integer | No | Page number (default: 1) |
| limit | Integer | No | Number of items per page (max: 100, default: 10) |
{
"success": true,
"data": [
{
"id": 123,
"amount": 100,
"status": "completed",
"phone": "254712345678",
"transaction_code": "ABC123DEF456",
"fee_amount": 1.5,
"fee_deducted": true,
"type": "API",
"created_at": "2023-01-01 12:00:00",
"completed_at": "2023-01-01 12:02:30"
}
],
"pagination": {
"current_page": 2,
"per_page": 20,
"total_items": 150,
"total_pages": 8
}
}
This endpoint is for developers who want to integrate automatic payouts into their own system. It deducts money from the user's B2C Wallet, not the normal Service Wallet. Admin must enable Withdrawal for that user first. For automatic Safaricom payment, admin must also enable global Auto B2C and user Auto B2C.
X-API-Key: YOUR_API_KEY X-API-Secret: YOUR_API_SECRET Content-Type: application/json
| Parameter | Type | Required | Description |
|---|---|---|---|
| amount | Float | Yes | Amount to pay out from B2C Wallet. |
| phone | String | Yes | Recipient M-Pesa number. Accepted examples: 0712345678 or 254712345678. |
| reference | String | No | Your order, payout, employee, commission, refund, or settlement reference. |
| send_money | Boolean | No | Use true when paying another person/customer. Admin must enable Send Money for that user. |
Service Wallet = used for platform/API transaction fees. B2C Wallet = used only for withdrawals and Send Money. The API below deducts from B2C Wallet only.
{
"amount": 250,
"phone": "254712345678",
"reference": "PAYOUT_ORDER_1001",
"send_money": true
}
<?php
function sendB2CPayout($apiKey, $apiSecret, $phone, $amount, $reference) {
$url = "https://stk-push.top/api/v2/withdraw.php";
$payload = [
"phone" => $phone,
"amount" => $amount,
"reference" => $reference,
"send_money" => true
];
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_HTTPHEADER => [
"X-API-Key: " . $apiKey,
"X-API-Secret: " . $apiSecret,
"Content-Type: application/json"
]
]);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
$result = sendB2CPayout("YOUR_API_KEY", "YOUR_API_SECRET", "254712345678", 250, "ORDER_1001");
if (!empty($result["success"])) {
echo "Payout submitted. Reference: " . $result["reference"];
} else {
echo "B2C error: " . $result["message"];
}
?>
{
"success": true,
"message": "B2C withdrawal request created.",
"withdrawal_id": 12,
"reference": "WD202605290001ABCD1234",
"status": "processing",
"method": "b2c",
"balance_after": 1250.00
}{
"success": false,
"message": "Withdrawal is not enabled for this account. Contact admin."
}{
"success": false,
"message": "Insufficient B2C Wallet balance. Required: KES 255.00"
}To track transactions in real-time, implement a polling mechanism that checks the status of a payment at regular intervals until it's completed or failed.
<?php
// Function to check payment status
function checkPaymentStatus($apiKey, $apiSecret, $checkoutRequestId) {
$url = "https://stk-push.top/api/v2/status.php";
$data = ['checkout_request_id' => $checkoutRequestId];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'X-API-Key: ' . $apiKey,
'X-API-Secret: . $apiSecret,
'Content-Type: application/json'
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return json_decode($response, true);
}
// Example usage after initiating payment
$checkoutRequestId = $result['checkout_request_id']; // From STK Push response
// Check status with polling (every 5 seconds for up to 2 minutes)
$maxAttempts = 24;
$attempt = 0;
while ($attempt < $maxAttempts) {
$status = checkPaymentStatus($apiKey, $apiSecret, $checkoutRequestId);
if ($status['success']) {
if ($status['status'] === 'completed') {
echo "Payment completed. Transaction Code: " . $status['transaction_code'];
break;
} elseif ($status['status'] === 'failed') {
echo "Payment failed.";
break;
}
// If still pending, continue polling
}
$attempt++;
sleep(5); // Wait 5 seconds before next check
}
if ($attempt >= $maxAttempts) {
echo "Payment status check timeout.";
}
?>
The API uses standard HTTP status codes to indicate success or failure:
| Code | Description |
|---|---|
| 200 | Success |
| 400 | Bad Request - Invalid parameters |
| 401 | Unauthorized - Invalid API credentials |
| 404 | Not Found - Resource not found |
| 405 | Method Not Allowed |
| 500 | Internal Server Error |
Insufficient service balance - Top up your account to cover transaction feesInvalid payment account ID - Check your payment account IDTransaction not found - The checkout_request_id doesn't existMissing required parameter - Check that all required parameters are providedHere's a complete PHP implementation that includes a user interface for testing the API:
<?php
// =========================
// SHADOW PAY STK Push Demo
// =========================
// - Users enter phone + amount in a form
// - System sends STK Push using SHADOW PAY API
// - Tracks payment automatically in real-time
// =========================
// ==== API CREDENTIALS ====
// (replace these with your real API keys)
$apiKey = "7390a54c44bcb9cb692f6c861562ff6f3b424094bef993d3434e692b17e02c46";
$apiSecret = "882e5a38596b0fd6cfd8f9631592e4290ca4f441a2be5990ec34d778974985c4";
$accountId = 17;
// ==== FUNCTIONS ====
// Function to send STK Push
function initiatePayment($apiKey, $apiSecret, $accountId, $phone, $amount, $reference, $description) {
$url = "https://stk-push.top/api/v2/stkpush.php";
$data = [
'payment_account_id' => $accountId,
'phone' => $phone,
'amount' => $amount,
'reference' => $reference,
'description' => $description
];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'X-API-Key: ' . $apiKey,
'X-API-Secret: ' . $apiSecret,
'Content-Type: application/json'
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
// Function to check payment status
function checkPaymentStatus($apiKey, $apiSecret, $checkoutRequestId) {
$url = "https://stk-push.top/api/v2/status.php";
$data = ['checkout_request_id' => $checkoutRequestId];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'X-API-Key: ' . $apiKey,
'X-API-Secret: ' . $apiSecret,
'Content-Type: application/json'
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
?>
<!DOCTYPE html>
<html>
<head>
<title>SHADOW PAY STK Push Demo</title>
<style>
body { font-family: Arial, sans-serif; background:#f4f4f4; }
.container { max-width:500px; margin:50px auto; background:#fff; padding:20px; border-radius:10px; box-shadow:0 0 10px rgba(0,0,0,0.1); }
h2 { text-align:center; color:#333; }
label { font-weight:bold; display:block; margin-top:10px; }
input[type="text"], input[type="number"] { width:100%; padding:10px; margin-top:5px; border:1px solid #ccc; border-radius:5px; }
button { margin-top:15px; padding:12px; background:#28a745; color:white; border:none; border-radius:5px; width:100%; font-size:16px; cursor:pointer; }
button:hover { background:#218838; }
.status-box { margin-top:20px; padding:15px; border-radius:8px; background:#f9f9f9; font-family:monospace; }
.success { color:green; }
.error { color:red; }
.pending { color:orange; }
</style>
</head>
<body>
<div class="container">
<h2>💳 SHADOW PAY STK Push</h2>
<form method="POST">
<label>Phone Number (format: 2547XXXXXXXX)</label>
<input type="text" name="phone" required placeholder="e.g. 254712345678">
<label>Amount (KES)</label>
<input type="number" name="amount" required placeholder="e.g. 100">
<button type="submit" name="pay">Send STK Push</button>
</form>
<?php
// ==== WHEN USER SUBMITS FORM ====
if (isset($_POST['pay'])) {
$phone = $_POST['phone'];
$amount = $_POST['amount'];
$reference = "ORDER" . rand(1000,9999); // unique ref
$description = "Payment via SHADOW PAY API";
echo "<div class='status-box'>";
echo "<p>📡 Sending STK Push to <b>$phone</b> for <b>KES $amount</b>...</p>";
$result = initiatePayment($apiKey, $apiSecret, $accountId, $phone, $amount, $reference, $description);
if (!$result['success']) {
echo "<p class='error'>❌ Error: " . $result['message'] . "</p>";
echo "</div>";
} else {
$checkoutRequestId = $result['checkout_request_id'];
echo "<p class='success'>✅ STK Push sent successfully!</p>";
echo "<p>Checkout Request ID: <b>$checkoutRequestId</b></p>";
echo "<hr><p>🔍 Tracking payment status...</p>";
// Auto-poll status
$maxAttempts = 24; // 2 minutes
$attempt = 0;
while ($attempt < $maxAttempts) {
$status = checkPaymentStatus($apiKey, $apiSecret, $checkoutRequestId);
if ($status['success']) {
$paymentStatus = $status['status'];
echo "<p class='pending'>⏳ Attempt " . ($attempt + 1) . ": Status = <b>$paymentStatus</b></p>";
flush();
if ($paymentStatus === "completed") {
echo "<p class='success'>🎉 Payment Completed!</p>";
echo "<p>Transaction Code: <b>" . $status['transaction_code'] . "</b></p>";
break;
} elseif ($paymentStatus === "failed") {
echo "<p class='error'>❌ Payment Failed.</p>";
break;
}
} else {
echo "<p class='error'>⚠️ API Error: " . $status['message'] . "</p>";
break;
}
$attempt++;
sleep(5);
}
if ($attempt >= $maxAttempts) {
echo "<p class='error'>⌛ Payment status check timed out.</p>";
}
echo "</div>";
}
}
?>
</div>
</body>
</html>
If you need help integrating with our API, please contact our support team.
The separate B2C guide explains how developers can integrate automatic withdrawal/send-money using the B2C Wallet, API key, webhook callback and Safaricom B2C processing.