Complete documentation for connecting your website or app to the payment system
The Shadow Pay API lets you connect M-Pesa STK Push payments to your website or app. Use this page to initiate payments, check transaction status, and list transactions.
All API endpoints are relative to:
Every request must include your API credentials as HTTP headers:
X-API-Key: your_api_keyX-API-Secret: your_api_secretContent-Type: application/jsonAll responses are JSON with a consistent structure:
{
"success": true | false,
"message": "Human-readable message",
// ...additional fields per endpoint
}
Test the Shadow Pay API endpoints directly with your credentials.
Initiate a payment and poll for completion in under 30 lines:
curl -X POST https://shadow-pay.top/api/v2/stkpush.php \
-H "X-API-Key: YOUR_API_KEY" \
-H "X-API-Secret: YOUR_API_SECRET" \
-H "Content-Type: application/json" \
-d '{
"payment_account_id": 17,
"phone": "0700100211",
"amount": 100,
"reference": "ORDER123",
"description": "Payment for order #123"
}'
<?php
$apiKey = "YOUR_API_KEY";
$apiSecret = "YOUR_API_SECRET";
$accountId = 17;
$baseUrl = "https://shadow-pay.top/api/v2";
function payflowPost(string $url, string $key, string $secret, array $body): array {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($body),
CURLOPT_HTTPHEADER => [
"X-API-Key: $key",
"X-API-Secret: $secret",
"Content-Type: application/json",
],
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 30,
]);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true) ?? [];
}
// 1. Send STK Push
$result = payflowPost(
"$baseUrl/stkpush.php", $apiKey, $apiSecret,
[
"payment_account_id" => $accountId,
"phone" => "0700100211",
"amount" => 100,
"reference" => "ORDER123",
"description" => "Payment for order #123",
]
);
if (!$result["success"]) {
die("STK Push error: " . $result["message"]);
}
$checkoutId = $result["checkout_request_id"];
echo "STK Push sent — Checkout ID: $checkoutId\n";
// 2. Poll for completion (every 5 s, up to 2 min)
for ($i = 0; $i < 24; $i++) {
sleep(5);
$status = payflowPost(
"$baseUrl/status.php", $apiKey, $apiSecret,
["checkout_request_id" => $checkoutId]
);
echo "Attempt " . ($i + 1) . ": " . $status["status"] . "\n";
if ($status["status"] === "completed") {
echo "Payment confirmed! Code: " . $status["transaction_code"] . "\n";
break;
} elseif (in_array($status["status"], ["failed", "cancelled", "expired"])) {
echo "Payment " . $status["status"] . ".\n";
break;
}
}
?>
import requests
import time
API_KEY = "YOUR_API_KEY"
API_SECRET = "YOUR_API_SECRET"
ACCOUNT_ID = 17
BASE_URL = "https://shadow-pay.top/api/v2"
HEADERS = {
"X-API-Key": API_KEY,
"X-API-Secret": API_SECRET,
"Content-Type": "application/json",
}
# 1. Send STK Push
resp = requests.post(
f"{BASE_URL}/stkpush.php",
headers=HEADERS,
json={
"payment_account_id": ACCOUNT_ID,
"phone": "0700100211",
"amount": 100,
"reference": "ORDER123",
"description": "Payment for order #123",
},
timeout=30,
)
result = resp.json()
if not result.get("success"):
raise SystemExit(f"STK Push error: {result.get('message')}")
checkout_id = result["checkout_request_id"]
print(f"STK Push sent — Checkout ID: {checkout_id}")
# 2. Poll for completion (every 5 s, up to 2 min)
for attempt in range(1, 25):
time.sleep(5)
status_resp = requests.post(
f"{BASE_URL}/status.php",
headers=HEADERS,
json={"checkout_request_id": checkout_id},
timeout=30,
)
data = status_resp.json()
print(f"Attempt {attempt}: {data.get('status')}")
if data.get("status") == "completed":
print(f"Payment confirmed! Code: {data.get('transaction_code')}")
break
elif data.get("status") in ("failed", "cancelled", "expired"):
print(f"Payment {data.get('status')}.")
break
// Browser JavaScript (fetch API)
const API_KEY = "YOUR_API_KEY";
const API_SECRET = "YOUR_API_SECRET";
const ACCOUNT_ID = 17;
const BASE_URL = "https://shadow-pay.top/api/v2";
const headers = {
"X-API-Key": API_KEY,
"X-API-Secret": API_SECRET,
"Content-Type": "application/json",
};
async function initiatePayment(phone, amount, reference, description) {
const resp = await fetch(`${BASE_URL}/stkpush.php`, {
method: "POST",
headers: headers,
body: JSON.stringify({
payment_account_id: ACCOUNT_ID,
phone, amount, reference, description,
}),
});
return resp.json();
}
async function checkStatus(checkoutRequestId) {
const resp = await fetch(`${BASE_URL}/status.php`, {
method: "POST",
headers: headers,
body: JSON.stringify({ checkout_request_id: checkoutRequestId }),
});
return resp.json();
}
// Usage
(async () => {
const result = await initiatePayment("0700100211", 100, "ORDER123", "Order payment");
if (!result.success) { console.error("Error:", result.message); return; }
console.log("STK Push sent:", result.checkout_request_id);
// Poll every 5 s, up to 24 attempts
for (let i = 0; i < 24; i++) {
await new Promise(r => setTimeout(r, 5000));
const status = await checkStatus(result.checkout_request_id);
console.log(`Attempt ${i + 1}: ${status.status}`);
if (status.status === "completed") {
console.log("Confirmed! Code:", status.transaction_code);
break;
} else if (["failed", "cancelled", "expired"].includes(status.status)) {
console.log("Payment", status.status);
break;
}
}
})();
// Node.js (built-in fetch available in Node 18+)
const API_KEY = "YOUR_API_KEY";
const API_SECRET = "YOUR_API_SECRET";
const ACCOUNT_ID = 17;
const BASE_URL = "https://shadow-pay.top/api/v2";
const HEADERS = {
"X-API-Key": API_KEY,
"X-API-Secret": API_SECRET,
"Content-Type": "application/json",
};
async function apiPost(endpoint, body) {
const res = await fetch(`${BASE_URL}/${endpoint}`, {
method: "POST",
headers: HEADERS,
body: JSON.stringify(body),
});
return res.json();
}
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
(async () => {
// 1. Send STK Push
const result = await apiPost("stkpush.php", {
payment_account_id: ACCOUNT_ID,
phone: "0700100211",
amount: 100,
reference: "ORDER123",
description: "Order payment",
});
if (!result.success) { console.error("STK Push failed:", result.message); return; }
console.log("STK Push sent:", result.checkout_request_id);
// 2. Poll for status
for (let i = 0; i < 24; i++) {
await sleep(5000);
const data = await apiPost("status.php", {
checkout_request_id: result.checkout_request_id,
});
console.log(`[${i + 1}] status: ${data.status}`);
if (data.status === "completed") {
console.log("Confirmed! Code:", data.transaction_code);
break;
}
if (["failed", "cancelled", "expired"].includes(data.status)) {
console.log("Payment ended:", data.status);
break;
}
}
})();
Sends an M-Pesa STK Push prompt to the customer's phone. Returns a checkout_request_id you use to track the payment.
| Parameter | Type | Required | Description |
|---|---|---|---|
payment_account_id | Integer | Yes | Your payment account ID (from dashboard) |
phone | String | Yes | Customer M-Pesa number — format: 0700100211 |
amount | Float | Yes | Amount in KES (minimum 1) |
reference | String | No | Your internal order/reference code |
description | String | No | Short description shown on the STK prompt |
curl -X POST https://shadow-pay.top/api/v2/stkpush.php \
-H "X-API-Key: YOUR_API_KEY" \
-H "X-API-Secret: YOUR_API_SECRET" \
-H "Content-Type: application/json" \
-d '{
"payment_account_id": 17,
"phone": "0700100211",
"amount": 100,
"reference": "ORDER_12345",
"description": "Payment for order #12345"
}'
<?php
$apiKey = "YOUR_API_KEY";
$apiSecret = "YOUR_API_SECRET";
$url = "https://shadow-pay.top/api/v2/stkpush.php";
$payload = [
"payment_account_id" => 17,
"phone" => "0700100211",
"amount" => 100,
"reference" => "ORDER_12345",
"description" => "Payment for order #12345",
];
$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",
],
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 30,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$result = json_decode($response, true);
if ($result["success"]) {
echo "STK Push sent!\n";
echo "Checkout Request ID: " . $result["checkout_request_id"] . "\n";
} else {
echo "Error: " . $result["message"] . "\n";
}
?>
import requests
API_KEY = "YOUR_API_KEY"
API_SECRET = "YOUR_API_SECRET"
URL = "https://shadow-pay.top/api/v2/stkpush.php"
headers = {
"X-API-Key": API_KEY,
"X-API-Secret": API_SECRET,
"Content-Type": "application/json",
}
payload = {
"payment_account_id": 17,
"phone": "0700100211",
"amount": 100,
"reference": "ORDER_12345",
"description": "Payment for order #12345",
}
response = requests.post(URL, headers=headers, json=payload, timeout=30)
result = response.json()
if result.get("success"):
print("STK Push sent!")
print("Checkout Request ID:", result["checkout_request_id"])
else:
print("Error:", result.get("message"))
const response = await fetch("https://shadow-pay.top/api/v2/stkpush.php", {
method: "POST",
headers: {
"X-API-Key": "YOUR_API_KEY",
"X-API-Secret": "YOUR_API_SECRET",
"Content-Type": "application/json",
},
body: JSON.stringify({
payment_account_id: 17,
phone: "0700100211",
amount: 100,
reference: "ORDER_12345",
description: "Payment for order #12345",
}),
});
const result = await response.json();
if (result.success) {
console.log("STK Push sent!");
console.log("Checkout ID:", result.checkout_request_id);
} else {
console.error("Error:", result.message);
}
// Node.js (built-in fetch, Node 18+)
const res = await fetch("https://shadow-pay.top/api/v2/stkpush.php", {
method: "POST",
headers: {
"X-API-Key": "YOUR_API_KEY",
"X-API-Secret": "YOUR_API_SECRET",
"Content-Type": "application/json",
},
body: JSON.stringify({
payment_account_id: 17,
phone: "0700100211",
amount: 100,
reference: "ORDER_12345",
description: "Payment for order #12345",
}),
});
const result = await res.json();
console.log(result.success ? result.checkout_request_id : result.message);
{
"success": true,
"message": "STK push initiated",
"transaction_id": 456,
"checkout_request_id": "ws_CO_20230101120000_abc123def456",
"merchant_request_id": "ws_MR_20230101120000_xyz789uvw012"
}
{
"success": false,
"message": "Invalid payment account ID or STK Push initiation failed."
}
Poll this endpoint after initiating an STK Push to find out whether the customer completed payment.
| Parameter | Type | Required | Description |
|---|---|---|---|
checkout_request_id | String | Yes | The checkout_request_id from the STK Push response |
curl -X POST https://shadow-pay.top/api/v2/status.php \
-H "X-API-Key: YOUR_API_KEY" \
-H "X-API-Secret: YOUR_API_SECRET" \
-H "Content-Type: application/json" \
-d '{"checkout_request_id": "ws_CO_20230101120000_abc123def456"}'
<?php
$apiKey = "YOUR_API_KEY";
$apiSecret = "YOUR_API_SECRET";
$url = "https://shadow-pay.top/api/v2/status.php";
$checkoutId = "ws_CO_20230101120000_abc123def456";
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(["checkout_request_id" => $checkoutId]),
CURLOPT_HTTPHEADER => [
"X-API-Key: $apiKey",
"X-API-Secret: $apiSecret",
"Content-Type: application/json",
],
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 30,
]);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
switch ($data["status"] ?? "") {
case "completed":
echo "Payment completed!\n";
echo "Transaction Code: " . $data["transaction_code"] . "\n";
echo "M-Pesa Receipt: " . ($data["mpesa_receipt_number"] ?? "Pending") . "\n";
break;
case "failed":
case "cancelled":
case "expired":
echo "Payment " . $data["status"] . ".\n";
break;
default:
echo "Still pending — check again in a few seconds.\n";
}
?>
import requests
API_KEY = "YOUR_API_KEY"
API_SECRET = "YOUR_API_SECRET"
URL = "https://shadow-pay.top/api/v2/status.php"
CHECKOUT_ID = "ws_CO_20230101120000_abc123def456"
headers = {
"X-API-Key": API_KEY,
"X-API-Secret": API_SECRET,
"Content-Type": "application/json",
}
response = requests.post(
URL,
headers=headers,
json={"checkout_request_id": CHECKOUT_ID},
timeout=30,
)
data = response.json()
status = data.get("status", "")
if status == "completed":
print("Payment completed!")
print("Transaction Code:", data.get("transaction_code"))
print("M-Pesa Receipt: ", data.get("mpesa_receipt_number", "Pending"))
elif status in ("failed", "cancelled", "expired"):
print(f"Payment {status}.")
else:
print("Still pending.")
const response = await fetch("https://shadow-pay.top/api/v2/status.php", {
method: "POST",
headers: {
"X-API-Key": "YOUR_API_KEY",
"X-API-Secret": "YOUR_API_SECRET",
"Content-Type": "application/json",
},
body: JSON.stringify({
checkout_request_id: "ws_CO_20230101120000_abc123def456",
}),
});
const data = await response.json();
if (data.status === "completed") {
console.log("Payment completed! Code:", data.transaction_code);
} else if (["failed", "cancelled", "expired"].includes(data.status)) {
console.log("Payment", data.status);
} else {
console.log("Pending — poll again shortly");
}
// Node.js (built-in fetch, Node 18+)
const res = await fetch("https://shadow-pay.top/api/v2/status.php", {
method: "POST",
headers: {
"X-API-Key": "YOUR_API_KEY",
"X-API-Secret": "YOUR_API_SECRET",
"Content-Type": "application/json",
},
body: JSON.stringify({
checkout_request_id: "ws_CO_20230101120000_abc123def456",
}),
});
const data = await res.json();
console.log("Status:", data.status, "| Code:", data.transaction_code ?? "–");
{
"success": true,
"status": "completed",
"amount": 100,
"fee_amount": 1,
"phone": "0700100211",
"transaction_code": "KS-7F3K9QRT2M",
"mpesa_receipt_number": "ABC123DEF456",
"checkout_request_id": "ws_CO_20230101120000_abc123def456",
"merchant_request_id": "ws_MR_20230101120000_xyz789uvw012",
"result_code": 0,
"result_desc": "The service request is processed successfully.",
"created_at": "2023-01-01 12:00:00",
"completed_at": "2023-01-01 12:01:45"
}
{
"success": true,
"status": "pending",
"amount": 100,
"fee_amount": 1,
"phone": "0700100211",
"transaction_code": null,
"mpesa_receipt_number": null,
"checkout_request_id": "ws_CO_20230101120000_abc123def456",
"merchant_request_id": "ws_MR_20230101120000_xyz789uvw012",
"result_code": null,
"result_desc": null,
"created_at": "2023-01-01 12:00:00",
"completed_at": null
}
Note: transaction_code and mpesa_receipt_number are null while the payment is pending. They are set the instant Safaricom confirms payment — never before.
| Status | Meaning |
|---|---|
| pending | Customer has not yet entered their PIN — keep polling |
| completed | Payment confirmed by Safaricom |
| failed | Payment was rejected or timed out |
| cancelled | Customer cancelled the STK prompt |
| expired | STK Push timed out — initiate a new one |
Retrieve a paginated list of your transactions.
| Parameter | Type | Required | Description |
|---|---|---|---|
page | Integer | No | Page number (default: 1) |
limit | Integer | No | Items per page (max 100, default 10) |
curl -X GET "https://shadow-pay.top/api/v2/transactions.php?page=1&limit=10" \ -H "X-API-Key: YOUR_API_KEY" \ -H "X-API-Secret: YOUR_API_SECRET"
<?php
$apiKey = "YOUR_API_KEY";
$apiSecret = "YOUR_API_SECRET";
$url = "https://shadow-pay.top/api/v2/transactions.php?page=1&limit=10";
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPGET => true,
CURLOPT_HTTPHEADER => [
"X-API-Key: $apiKey",
"X-API-Secret: $apiSecret",
],
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 30,
]);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
if ($data["success"]) {
echo "Total transactions: " . $data["pagination"]["total_items"] . "\n";
foreach ($data["transactions"] as $tx) {
echo "[" . $tx["id"] . "] "
. $tx["amount"] . " KES — "
. $tx["status"] . " — "
. $tx["created_at"] . "\n";
}
}
?>
import requests
API_KEY = "YOUR_API_KEY"
API_SECRET = "YOUR_API_SECRET"
URL = "https://shadow-pay.top/api/v2/transactions.php"
headers = {
"X-API-Key": API_KEY,
"X-API-Secret": API_SECRET,
}
params = {"page": 1, "limit": 10}
response = requests.get(URL, headers=headers, params=params, timeout=30)
data = response.json()
if data.get("success"):
print("Total:", data["pagination"]["total_items"])
for tx in data["data"]:
print(f"[{tx['id']}] {tx['amount']} KES — {tx['status']} — {tx['created_at']}")
const response = await fetch("https://shadow-pay.top/api/v2/transactions.php?page=1&limit=10", {
method: "GET",
headers: {
"X-API-Key": "YOUR_API_KEY",
"X-API-Secret": "YOUR_API_SECRET",
},
});
const data = await response.json();
if (data.success) {
console.log("Total:", data.pagination.total_items);
data.transactions.forEach(tx => console.log(`[${tx.id}] ${tx.amount} KES — ${tx.status}`));
}
// Node.js (built-in fetch, Node 18+)
const res = await fetch("https://shadow-pay.top/api/v2/transactions.php?page=1&limit=10", {
headers: {
"X-API-Key": "YOUR_API_KEY",
"X-API-Secret": "YOUR_API_SECRET",
},
});
const data = await res.json();
console.log(data.success ? data.transactions : data.message);
{
"success": true,
"transactions": [
{
"id": 123,
"kind": "api",
"type": "API",
"status": "completed",
"amount": 100,
"fee_amount": 1.5,
"fee_deducted": true,
"phone": "0700100211",
"transaction_code": "KS-7F3K9QRT2M",
"mpesa_receipt_number": "ABC123DEF456",
"checkout_request_id": "ws_CO_20230101120000_abc123def456",
"merchant_request_id": "ws_MR_20230101120000_xyz789uvw012",
"is_deposit": false,
"credit_target": null,
"payment_link_id": null,
"result_code": 0,
"result_desc": "The service request is processed successfully.",
"created_at": "2023-01-01 12:00:00",
"completed_at": "2023-01-01 12:01:45"
}
],
"summary": {
"total_completed_amount": 2350.00,
"completed_count": 18,
"pending_count": 2,
"failed_count": 1
},
"pagination": {
"current_page": 1,
"per_page": 10,
"total_items": 21,
"total_pages": 3
},
"filters_applied": {}
}
Note: The kind field classifies the transaction type: api, web, wallet_deposit, or credit_topup. You can filter by ?type=api, ?status=completed, ?date_from=2026-01-01, ?date_to=2026-12-31, ?search=254712, or ?id=123 for a single record.
After initiating an STK Push, poll the status endpoint every few seconds until the payment reaches a final state (completed, failed, cancelled, or expired). Recommended interval: 3–5 seconds.
<?php
$apiKey = "YOUR_API_KEY";
$apiSecret = "YOUR_API_SECRET";
$baseUrl = "https://shadow-pay.top/api/v2";
$checkoutId = "ws_CO_20230101120000_abc123def456"; // from STK Push response
$finalStatuses = ["completed", "failed", "cancelled", "expired"];
$maxAttempts = 24; // 2 minutes at 5 s intervals
function payflowPost(string $url, string $key, string $secret, array $body): array {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($body),
CURLOPT_HTTPHEADER => [
"X-API-Key: $key",
"X-API-Secret: $secret",
"Content-Type: application/json",
],
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 30,
]);
$resp = curl_exec($ch);
curl_close($ch);
return json_decode($resp, true) ?? [];
}
for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
sleep(5);
$data = payflowPost("$baseUrl/status.php", $apiKey, $apiSecret,
["checkout_request_id" => $checkoutId]);
echo "Attempt $attempt: " . ($data["status"] ?? "error") . "\n";
if (in_array($data["status"] ?? "", $finalStatuses)) {
if ($data["status"] === "completed") {
echo "Payment confirmed!\n";
echo "Transaction Code: " . $data["transaction_code"] . "\n";
echo "M-Pesa Receipt: " . ($data["mpesa_receipt_number"] ?? "Awaiting") . "\n";
} else {
echo "Payment ended with status: " . $data["status"] . "\n";
}
break;
}
}
if ($attempt > $maxAttempts) {
echo "Polling timed out after " . ($maxAttempts * 5) . " seconds.\n";
}
?>
import requests
import time
API_KEY = "YOUR_API_KEY"
API_SECRET = "YOUR_API_SECRET"
BASE_URL = "https://shadow-pay.top/api/v2"
CHECKOUT_ID = "ws_CO_20230101120000_abc123def456"
FINAL = {"completed", "failed", "cancelled", "expired"}
MAX_ATTEMPTS = 24 # 2 minutes at 5 s intervals
headers = {
"X-API-Key": API_KEY,
"X-API-Secret": API_SECRET,
"Content-Type": "application/json",
}
for attempt in range(1, MAX_ATTEMPTS + 1):
time.sleep(5)
resp = requests.post(
f"{BASE_URL}/status.php",
headers=headers,
json={"checkout_request_id": CHECKOUT_ID},
timeout=30,
)
data = resp.json()
status = data.get("status", "error")
print(f"Attempt {attempt}: {status}")
if status in FINAL:
if status == "completed":
print("Payment confirmed!")
print("Transaction Code:", data.get("transaction_code"))
print("M-Pesa Receipt: ", data.get("mpesa_receipt_number", "Awaiting"))
else:
print(f"Payment ended: {status}")
break
else:
print("Polling timed out.")
// Browser — poll with async/await
const API_KEY = "YOUR_API_KEY";
const API_SECRET = "YOUR_API_SECRET";
const BASE_URL = "https://shadow-pay.top/api/v2";
const CHECKOUT_ID = "ws_CO_20230101120000_abc123def456";
const FINAL = new Set(["completed", "failed", "cancelled", "expired"]);
const headers = {
"X-API-Key": API_KEY,
"X-API-Secret": API_SECRET,
"Content-Type": "application/json",
};
async function pollStatus(checkoutId, intervalMs = 5000, maxAttempts = 24) {
for (let i = 1; i <= maxAttempts; i++) {
await new Promise(r => setTimeout(r, intervalMs));
const resp = await fetch(`${BASE_URL}/status.php`, {
method: "POST", headers,
body: JSON.stringify({ checkout_request_id: checkoutId }),
});
const data = await resp.json();
console.log(`Attempt ${i}:`, data.status);
if (FINAL.has(data.status)) {
if (data.status === "completed") {
console.log("Confirmed! Code:", data.transaction_code);
} else {
console.log("Payment", data.status);
}
return data;
}
}
throw new Error("Polling timed out");
}
pollStatus(CHECKOUT_ID).catch(console.error);
// Node.js 18+ (built-in fetch)
const API_KEY = "YOUR_API_KEY";
const API_SECRET = "YOUR_API_SECRET";
const BASE_URL = "https://shadow-pay.top/api/v2";
const CHECKOUT_ID = "ws_CO_20230101120000_abc123def456";
const FINAL = new Set(["completed", "failed", "cancelled", "expired"]);
const HEADERS = {
"X-API-Key": API_KEY,
"X-API-Secret": API_SECRET,
"Content-Type": "application/json",
};
const sleep = ms => new Promise(r => setTimeout(r, ms));
async function pollStatus(checkoutId) {
for (let i = 1; i <= 24; i++) {
await sleep(5000);
const res = await fetch(`${BASE_URL}/status.php`, {
method: "POST",
headers: HEADERS,
body: JSON.stringify({ checkout_request_id: checkoutId }),
});
const data = await res.json();
console.log(`[${i}] ${data.status}`);
if (FINAL.has(data.status)) {
console.log(data.status === "completed"
? `Confirmed! Code: ${data.transaction_code}`
: `Payment ${data.status}`);
return data;
}
}
throw new Error("Timeout");
}
pollStatus(CHECKOUT_ID).catch(console.error);
A self-contained PHP page that accepts phone + amount, initiates an STK Push, and tracks payment in real-time — ready to paste into your project.
<?php
// ── Config ──────────────────────────────────────────
$API_KEY = "YOUR_API_KEY";
$API_SECRET = "YOUR_API_SECRET";
$ACCOUNT_ID = 17; // your payment account ID
$BASE_URL = "https://shadow-pay.top/api/v2";
function payflowPost(string $url, string $key, string $secret, array $body): array {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($body),
CURLOPT_HTTPHEADER => [
"X-API-Key: $key",
"X-API-Secret: $secret",
"Content-Type: application/json",
],
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 30,
]);
$resp = curl_exec($ch);
curl_close($ch);
return json_decode($resp, true) ?? [];
}
?>
<!DOCTYPE html>
<html>
<head>
<title>Shadow Pay STK Push Demo</title>
<style>
body { font-family: Arial, sans-serif; background: #f4f4f4; }
.box { max-width: 480px; margin: 50px auto; background: #fff;
padding: 24px; border-radius: 10px; box-shadow: 0 2px 12px rgba(0,0,0,.12); }
h2 { text-align: center; color: #333; }
label { font-weight: bold; display: block; margin-top: 12px; }
input { width: 100%; padding: 10px; margin-top: 4px; border: 1px solid #ccc;
border-radius: 5px; box-sizing: border-box; }
button { margin-top: 16px; padding: 12px; background: #28a745;
color: white; border: none; border-radius: 5px;
width: 100%; font-size: 16px; cursor: pointer; }
button:hover { background: #218838; }
.log { margin-top: 18px; padding: 14px; border-radius: 8px;
background: #f9f9f9; font-family: monospace; font-size: 13px; }
.ok { color: green; } .err { color: red; } .pend { color: orange; }
</style>
</head>
<body>
<div class="box">
<h2>💳 Shadow Pay STK Push</h2>
<form method="POST">
<label>Phone Number (2547XXXXXXXX)</label>
<input type="text" name="phone" required placeholder="e.g. 0700100211">
<label>Amount (KES)</label>
<input type="number" name="amount" required min="1" placeholder="e.g. 100">
<button type="submit" name="pay">Send STK Push</button>
</form>
<?php if (isset($_POST["pay"])): ?>
<div class="log">
<?php
$phone = htmlspecialchars($_POST["phone"]);
$amount = (int) $_POST["amount"];
$ref = "ORDER" . rand(1000, 9999);
echo "<p>📡 Sending STK to <b>$phone</b> for <b>KES $amount</b>…</p>";
global $API_KEY, $API_SECRET, $ACCOUNT_ID, $BASE_URL;
$result = payflowPost("$BASE_URL/stkpush.php", $API_KEY, $API_SECRET, [
"payment_account_id" => $ACCOUNT_ID,
"phone" => $phone,
"amount" => $amount,
"reference" => $ref,
"description" => "STK Push Demo",
]);
if (!($result["success"] ?? false)) {
echo "<p class='err'>❌ Error: " . htmlspecialchars($result["message"] ?? "Unknown") . "</p>";
} else {
$cid = $result["checkout_request_id"];
echo "<p class='ok'>✅ STK sent — Checkout ID: <b>$cid</b></p>";
echo "<p>⏳ Tracking payment…</p>";
$done = false;
for ($i = 1; $i <= 24; $i++) {
sleep(5);
$s = payflowPost("$BASE_URL/status.php", $API_KEY, $API_SECRET,
["checkout_request_id" => $cid]);
echo "<p class='pend'>Attempt $i: <b>" . htmlspecialchars($s["status"] ?? "error") . "</b></p>";
flush();
if (($s["status"] ?? "") === "completed") {
echo "<p class='ok'>🎉 Paid! Code: <b>" . htmlspecialchars($s["transaction_code"]) . "</b></p>";
$done = true; break;
} elseif (in_array($s["status"] ?? "", ["failed","cancelled","expired"])) {
echo "<p class='err'>❌ Payment " . htmlspecialchars($s["status"]) . ".</p>";
$done = true; break;
}
}
if (!$done) echo "<p class='err'>⌛ Timed out — check dashboard.</p>";
}
?>
</div>
<?php endif; ?>
</div>
</body>
</html>
The API uses standard HTTP status codes:
| Code | Meaning |
|---|---|
200 | Success |
400 | Bad Request — missing or invalid parameters |
401 | Unauthorized — invalid API credentials |
404 | Not Found — resource does not exist |
405 | Method Not Allowed — use the correct HTTP method |
500 | Internal Server Error |
Insufficient service balance — Top up your account to cover transaction feesInvalid payment account ID — Check your payment account ID in the dashboardTransaction not found — The checkout_request_id does not existMissing required parameter — Ensure all required fields are provided