Shadow Pay API
Integrate payments, manage accounts, generate payment links, and query transactions — all from a clean, consistent REST API.
Authentication
Required on every request
Two headers, every request
Generate your API key pair from your dashboard or via the API Credentials endpoint. Both headers are required — requests missing either return 401.
https://shadow-pay.top/api/v2/
Error Codes
Standard HTTP status codes
| Status | Meaning |
|---|---|
200 | Success |
201 | Resource created |
400 | Bad request — missing or invalid field |
401 | Unauthorized — invalid or missing API credentials |
403 | Forbidden — KYC not verified or account suspended |
404 | Not found |
409 | Conflict — duplicate resource |
422 | Validation error — value out of range or wrong format |
429 | Rate limit exceeded |
500 | Internal server error |
502 | Bad gateway — Safaricom unreachable |
503 | Service unavailable — admin configuration missing |
{ "success": false, "message": "Human-readable description" }API Credentials
Manage your API key pairs (max 5)
{
"success": true,
"credentials": [
{
"id": 1,
"api_key": "pk_abc123...",
"api_secret": "sk_xyz789...",
"description": "My App",
"is_active": true,
"last_used": "2026-07-16 10:00:00",
"created_at": "2026-07-01 09:00:00"
}
],
"total": 1
}
curl -X GET "https://shadow-pay.top/api/v2/credentials.php" \ -H "X-API-KEY: your_api_key" \ -H "X-API-SECRET: your_api_secret"
<?php
$ch = curl_init("https://shadow-pay.top/api/v2/credentials.php");
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["X-API-KEY: your_api_key", "X-API-SECRET: your_api_secret"],
CURLOPT_RETURNTRANSFER => true,
]);
$res = json_decode(curl_exec($ch), true);
print_r($res["credentials"]);const res = await fetch("https://shadow-pay.top/api/v2/credentials.php", {
headers: {
"X-API-KEY": "your_api_key",
"X-API-SECRET": "your_api_secret"
}
});
const data = await res.json();
console.log(data.credentials);import requests
r = requests.get("https://shadow-pay.top/api/v2/credentials.php",
headers={"X-API-KEY": "your_api_key", "X-API-SECRET": "your_api_secret"})
print(r.json()["credentials"])| Field | Type | Required | Description |
|---|---|---|---|
| description | string | optional | A label for this key, e.g. "Production App" |
{
"success": true,
"credentials": {
"id": 2,
"api_key": "pk_new123...",
"api_secret": "sk_new456...",
"description": "Production App",
"is_active": true
}
}
curl -X POST "https://shadow-pay.top/api/v2/credentials.php" \
-H "X-API-KEY: your_api_key" \
-H "X-API-SECRET: your_api_secret" \
-H "Content-Type: application/json" \
-d '{"description":"Production App"}'<?php
$ch = curl_init("https://shadow-pay.top/api/v2/credentials.php");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(["description" => "Production App"]),
CURLOPT_HTTPHEADER => ["X-API-KEY: your_api_key", "X-API-SECRET: your_api_secret", "Content-Type: application/json"],
CURLOPT_RETURNTRANSFER => true,
]);
$res = json_decode(curl_exec($ch), true);
echo $res["credentials"]["api_key"];const res = await fetch("https://shadow-pay.top/api/v2/credentials.php", {
method: "POST",
headers: {
"X-API-KEY": "your_api_key",
"X-API-SECRET": "your_api_secret",
"Content-Type": "application/json"
},
body: JSON.stringify({ description: "Production App" })
});
const data = await res.json();
console.log(data.credentials.api_key);import requests
r = requests.post("https://shadow-pay.top/api/v2/credentials.php",
headers={"X-API-KEY": "your_api_key", "X-API-SECRET": "your_api_secret"},
json={"description": "Production App"})
print(r.json()["credentials"]["api_key"])| Field | Type | Required | Description |
|---|---|---|---|
| id | integer | required | ID of the API key to toggle |
curl -X POST "https://shadow-pay.top/api/v2/credentials.php?action=toggle" \
-H "X-API-KEY: your_api_key" \
-H "X-API-SECRET: your_api_secret" \
-H "Content-Type: application/json" \
-d '{"id":2}'<?php
$ch = curl_init("https://shadow-pay.top/api/v2/credentials.php?action=toggle");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(["id" => 2]),
CURLOPT_HTTPHEADER => ["X-API-KEY: your_api_key", "X-API-SECRET: your_api_secret", "Content-Type: application/json"],
CURLOPT_RETURNTRANSFER => true,
]);
echo curl_exec($ch);await fetch("https://shadow-pay.top/api/v2/credentials.php?action=toggle", {
method: "POST",
headers: { "X-API-KEY": "your_api_key", "X-API-SECRET": "your_api_secret", "Content-Type": "application/json" },
body: JSON.stringify({ id: 2 })
});requests.post("https://shadow-pay.top/api/v2/credentials.php?action=toggle",
headers={"X-API-KEY": "your_api_key", "X-API-SECRET": "your_api_secret"},
json={"id": 2}).json()| Field | Type | Required | Description |
|---|---|---|---|
| id | integer | required | ID of the API key to delete |
curl -X DELETE "https://shadow-pay.top/api/v2/credentials.php" \
-H "X-API-KEY: your_api_key" \
-H "X-API-SECRET: your_api_secret" \
-H "Content-Type: application/json" \
-d '{"id":2}'<?php
$ch = curl_init("https://shadow-pay.top/api/v2/credentials.php");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "DELETE",
CURLOPT_POSTFIELDS => json_encode(["id" => 2]),
CURLOPT_HTTPHEADER => ["X-API-KEY: your_api_key", "X-API-SECRET: your_api_secret", "Content-Type: application/json"],
CURLOPT_RETURNTRANSFER => true,
]);
echo curl_exec($ch);await fetch("https://shadow-pay.top/api/v2/credentials.php", {
method: "DELETE",
headers: { "X-API-KEY": "your_api_key", "X-API-SECRET": "your_api_secret", "Content-Type": "application/json" },
body: JSON.stringify({ id: 2 })
});requests.delete("https://shadow-pay.top/api/v2/credentials.php",
headers={"X-API-KEY": "your_api_key", "X-API-SECRET": "your_api_secret"},
json={"id": 2}).json()Payment Accounts
Your M-Pesa Paybill or Till numbers
{
"success": true,
"accounts": [
{
"id": 5,
"account_type": "paybill",
"account_number": "247247",
"account_reference": "My Business",
"is_active": true,
"created_at": "2026-07-01 09:00:00"
}
],
"total": 1
}curl -X GET "https://shadow-pay.top/api/v2/payment-accounts.php" \ -H "X-API-KEY: your_api_key" \ -H "X-API-SECRET: your_api_secret"
<?php
$ch = curl_init("https://shadow-pay.top/api/v2/payment-accounts.php");
curl_setopt_array($ch, [CURLOPT_HTTPHEADER => ["X-API-KEY: your_api_key", "X-API-SECRET: your_api_secret"], CURLOPT_RETURNTRANSFER => true]);
$r = json_decode(curl_exec($ch), true);
print_r($r["accounts"]);const r = await fetch("https://shadow-pay.top/api/v2/payment-accounts.php", {
headers: { "X-API-KEY": "your_api_key", "X-API-SECRET": "your_api_secret" }
});
console.log((await r.json()).accounts);import requests
r = requests.get("https://shadow-pay.top/api/v2/payment-accounts.php", headers={"X-API-KEY": "your_api_key", "X-API-SECRET": "your_api_secret"})
print(r.json()["accounts"])curl -X GET "https://shadow-pay.top/api/v2/payment-accounts.php?id=5" \ -H "X-API-KEY: your_api_key" \ -H "X-API-SECRET: your_api_secret"
<?php
$ch = curl_init("https://shadow-pay.top/api/v2/payment-accounts.php?id=5");
curl_setopt_array($ch, [CURLOPT_HTTPHEADER => ["X-API-KEY: your_api_key", "X-API-SECRET: your_api_secret"], CURLOPT_RETURNTRANSFER => true]);
echo curl_exec($ch);const r = await fetch("https://shadow-pay.top/api/v2/payment-accounts.php?id=5", { headers: {"X-API-KEY":"your_api_key","X-API-SECRET":"your_api_secret"} });
console.log(await r.json());requests.get("https://shadow-pay.top/api/v2/payment-accounts.php?id=5", headers={"X-API-KEY":"your_api_key","X-API-SECRET":"your_api_secret"}).json()| Field | Type | Required | Description |
|---|---|---|---|
| account_type | string | required | paybill or till |
| account_number | string | required | 4–10 digit Paybill / Till number |
| account_reference | string | paybill only | Account reference shown on M-Pesa prompt |
{"success":true,"message":"Payment account added.","account":{"id":5,"account_type":"paybill","account_number":"247247","account_reference":"My Business","is_active":true}}curl -X POST "https://shadow-pay.top/api/v2/payment-accounts.php" \
-H "X-API-KEY: your_api_key" \
-H "X-API-SECRET: your_api_secret" \
-H "Content-Type: application/json" \
-d '{"account_type":"paybill","account_number":"247247","account_reference":"My Business"}'<?php
$data = ["account_type" => "paybill", "account_number" => "247247", "account_reference" => "My Business"];
$ch = curl_init("https://shadow-pay.top/api/v2/payment-accounts.php");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($data),
CURLOPT_HTTPHEADER => ["X-API-KEY: your_api_key", "X-API-SECRET: your_api_secret", "Content-Type: application/json"],
CURLOPT_RETURNTRANSFER => true,
]);
print_r(json_decode(curl_exec($ch), true));const res = await fetch("https://shadow-pay.top/api/v2/payment-accounts.php", {
method: "POST",
body: JSON.stringify({ account_type:"paybill", account_number:"247247", account_reference:"My Business" }),
headers: { "X-API-KEY":"your_api_key", "X-API-SECRET":"your_api_secret", "Content-Type":"application/json" }
});
console.log(await res.json());requests.post("https://shadow-pay.top/api/v2/payment-accounts.php",
json={"account_type":"paybill","account_number":"247247","account_reference":"My Business"},
headers={"X-API-KEY":"your_api_key","X-API-SECRET":"your_api_secret"}).json()| Field | Type | Required | Description |
|---|---|---|---|
| id | integer | required | ID of the account to delete |
curl -X DELETE "https://shadow-pay.top/api/v2/payment-accounts.php" \
-H "X-API-KEY: your_api_key" \
-H "X-API-SECRET: your_api_secret" \
-H "Content-Type: application/json" \
-d '{"id":5}'<?php
$ch = curl_init("https://shadow-pay.top/api/v2/payment-accounts.php");
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => "DELETE", CURLOPT_POSTFIELDS => json_encode(["id" => 5]),
CURLOPT_HTTPHEADER => ["X-API-KEY: your_api_key", "X-API-SECRET: your_api_secret", "Content-Type: application/json"], CURLOPT_RETURNTRANSFER => true]);
echo curl_exec($ch);await fetch("https://shadow-pay.top/api/v2/payment-accounts.php", {
method: "DELETE",
body: JSON.stringify({ id: 5 }),
headers: { "X-API-KEY":"your_api_key","X-API-SECRET":"your_api_secret","Content-Type":"application/json" }
});requests.delete("https://shadow-pay.top/api/v2/payment-accounts.php", json={"id":5}, headers={"X-API-KEY":"your_api_key","X-API-SECRET":"your_api_secret"}).json()Daraja Credential Request
Submit your Safaricom Daraja credentials for admin approval
{
"success": true,
"request": {
"id": 3,
"environment": "production",
"status": "pending",
"admin_notes": null,
"reviewed_at": null,
"submitted_at": "2026-07-15 10:30:00"
},
"has_active_credentials": false
}curl -X GET "https://shadow-pay.top/api/v2/daraja-request.php" \ -H "X-API-KEY: your_api_key" \ -H "X-API-SECRET: your_api_secret"
<?php
$ch = curl_init("https://shadow-pay.top/api/v2/daraja-request.php");
curl_setopt_array($ch, [CURLOPT_HTTPHEADER => ["X-API-KEY: your_api_key", "X-API-SECRET: your_api_secret"], CURLOPT_RETURNTRANSFER => true]);
echo curl_exec($ch);const r = await fetch("https://shadow-pay.top/api/v2/daraja-request.php", { headers: {"X-API-KEY":"your_api_key","X-API-SECRET":"your_api_secret"} });
console.log(await r.json());print(requests.get("https://shadow-pay.top/api/v2/daraja-request.php", headers={"X-API-KEY":"your_api_key","X-API-SECRET":"your_api_secret"}).json())| Field | Type | Required | Description |
|---|---|---|---|
| consumer_key | string | required | Daraja Consumer Key from Safaricom developer portal |
| consumer_secret | string | required | Daraja Consumer Secret |
| shortcode | string | required | Your M-Pesa Business Shortcode |
| passkey | string | required | Daraja Passkey (LNM Passkey) |
| environment | string | optional | sandbox (default) or production |
curl -X POST "https://shadow-pay.top/api/v2/daraja-request.php" \
-H "X-API-KEY: your_api_key" \
-H "X-API-SECRET: your_api_secret" \
-H "Content-Type: application/json" \
-d '{"consumer_key":"Bq...","consumer_secret":"rZ...","shortcode":"174379","passkey":"bfb2...","environment":"sandbox"}'<?php
$d = ["consumer_key" => "Bq...", "consumer_secret" => "rZ...", "shortcode" => "174379", "passkey" => "bfb2...", "environment" => "sandbox"];
$ch = curl_init("https://shadow-pay.top/api/v2/daraja-request.php");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($d),
CURLOPT_HTTPHEADER => ["X-API-KEY: your_api_key", "X-API-SECRET: your_api_secret", "Content-Type: application/json"], CURLOPT_RETURNTRANSFER => true]);
echo curl_exec($ch);await fetch("https://shadow-pay.top/api/v2/daraja-request.php", {
method: "POST",
body: JSON.stringify({ consumer_key:"Bq...", consumer_secret:"rZ...", shortcode:"174379", passkey:"bfb2...", environment:"sandbox" }),
headers: { "X-API-KEY":"your_api_key","X-API-SECRET":"your_api_secret","Content-Type":"application/json" }
});requests.post("https://shadow-pay.top/api/v2/daraja-request.php",
json={"consumer_key":"Bq...","consumer_secret":"rZ...","shortcode":"174379","passkey":"bfb2...","environment":"sandbox"},
headers={"X-API-KEY":"your_api_key","X-API-SECRET":"your_api_secret"}).json()curl -X DELETE "https://shadow-pay.top/api/v2/daraja-request.php" \ -H "X-API-KEY: your_api_key" \ -H "X-API-SECRET: your_api_secret"
<?php
$ch = curl_init("https://shadow-pay.top/api/v2/daraja-request.php");
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => "DELETE", CURLOPT_HTTPHEADER => ["X-API-KEY: your_api_key", "X-API-SECRET: your_api_secret"], CURLOPT_RETURNTRANSFER => true]);
echo curl_exec($ch);await fetch("https://shadow-pay.top/api/v2/daraja-request.php", { method:"DELETE", headers:{"X-API-KEY":"your_api_key","X-API-SECRET":"your_api_secret"} });requests.delete("https://shadow-pay.top/api/v2/daraja-request.php", headers={"X-API-KEY":"your_api_key","X-API-SECRET":"your_api_secret"}).json()SMS Alerts
Configure payment SMS notifications
{
"success": true,
"sms_alerts": { "enabled": true, "phone": "0700100211", "total_charged": 12.00 },
"recent_logs": [
{ "phone": "0700100211", "status": "sent", "created_at": "2026-07-15 10:00:00" }
]
}curl -X GET "https://shadow-pay.top/api/v2/sms-alerts.php" \ -H "X-API-KEY: your_api_key" \ -H "X-API-SECRET: your_api_secret"
<?php
$ch = curl_init("https://shadow-pay.top/api/v2/sms-alerts.php");
curl_setopt_array($ch, [CURLOPT_HTTPHEADER => ["X-API-KEY: your_api_key", "X-API-SECRET: your_api_secret"], CURLOPT_RETURNTRANSFER => true]);
echo curl_exec($ch);const r = await fetch("https://shadow-pay.top/api/v2/sms-alerts.php", { headers:{"X-API-KEY":"your_api_key","X-API-SECRET":"your_api_secret"} });
console.log(await r.json());print(requests.get("https://shadow-pay.top/api/v2/sms-alerts.php", headers={"X-API-KEY":"your_api_key","X-API-SECRET":"your_api_secret"}).json())| Field | Type | Required | Description |
|---|---|---|---|
| phone | string | optional | Safaricom number to receive SMS alerts (07XXXXXXXX) |
| enabled | boolean | optional | true to enable, false to disable |
curl -X POST "https://shadow-pay.top/api/v2/sms-alerts.php" \
-H "X-API-KEY: your_api_key" \
-H "X-API-SECRET: your_api_secret" \
-H "Content-Type: application/json" \
-d '{"phone":"0700100211","enabled":true}'<?php
$ch = curl_init("https://shadow-pay.top/api/v2/sms-alerts.php");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode(["phone" => "0700100211", "enabled" => true]),
CURLOPT_HTTPHEADER => ["X-API-KEY: your_api_key", "X-API-SECRET: your_api_secret", "Content-Type: application/json"], CURLOPT_RETURNTRANSFER => true]);
echo curl_exec($ch);await fetch("https://shadow-pay.top/api/v2/sms-alerts.php", {
method: "POST",
body: JSON.stringify({ phone:"0700100211", enabled:true }),
headers: { "X-API-KEY":"your_api_key","X-API-SECRET":"your_api_secret","Content-Type":"application/json" }
});requests.post("https://shadow-pay.top/api/v2/sms-alerts.php", json={"phone":"0700100211","enabled":True},
headers={"X-API-KEY":"your_api_key","X-API-SECRET":"your_api_secret"}).json()STK Push
Trigger M-Pesa PIN prompts on customer phones
| Field | Type | Required | Description |
|---|---|---|---|
| payment_account_id | integer | required | Your payment account ID (from Payment Accounts endpoint) |
| phone | string | required | Customer's Safaricom number (07XX or 254XX) |
| amount | number | required | Amount in KES (min 1) |
| reference | string | optional | Order/account reference shown on M-Pesa receipt |
| description | string | optional | Transaction description |
{
"success": true,
"message": "STK push initiated",
"transaction_id": 1042,
"checkout_request_id": "ws_CO_20260716_abc123",
"merchant_request_id": "ws_MR_20260716_xyz789"
}checkout_request_id to poll /api/v2/status.php for payment confirmation.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":5,"phone":"0700100211","amount":500,"reference":"ORDER-001","description":"Payment for order"}'<?php
$data = [
"payment_account_id" => 5,
"phone" => "0700100211",
"amount" => 500,
"reference" => "ORDER-001",
"description" => "Payment for order",
];
$ch = curl_init("https://shadow-pay.top/api/v2/stkpush.php");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($data),
CURLOPT_HTTPHEADER => ["X-API-KEY: your_api_key", "X-API-SECRET: your_api_secret", "Content-Type: application/json"],
CURLOPT_RETURNTRANSFER => true,
]);
$res = json_decode(curl_exec($ch), true);
$checkoutId = $res["checkout_request_id"];
// Now poll /api/v2/status.php with $checkoutIdconst 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: 5,
phone: "0700100211",
amount: 500,
reference: "ORDER-001",
description: "Payment for order"
})
});
const data = await res.json();
// Poll data.checkout_request_id via /api/v2/status.php
console.log(data.checkout_request_id);import requests
res = requests.post("https://shadow-pay.top/api/v2/stkpush.php",
headers={"X-API-KEY": "your_api_key", "X-API-SECRET": "your_api_secret"},
json={"payment_account_id": 5, "phone": "0700100211", "amount": 500, "reference": "ORDER-001"})
data = res.json()
checkout_id = data["checkout_request_id"]
# Poll /api/v2/status.php with checkout_idhttps://shadow-pay.top/api/v2/status.phpPoll every 2–3 seconds with the checkout_request_id from Step 1. Stop when status is completed, failed, cancelled, or expired. Notifications and fee deductions fire automatically on the first completed poll.
{
"success": true,
"status": "completed",
"amount": 500,
"fee_amount": 5,
"charged_amount": 500,
"phone": "0700100211",
"transaction_code": "KS-A3BCD12345",
"mpesa_receipt_number": "SL12ABC345",
"checkout_request_id": "ws_CO_20260716_abc123",
"merchant_request_id": "ws_MR_20260716_xyz789",
"result_code": 0,
"result_desc": "The service request is processed successfully.",
"created_at": "2026-07-16 11:00:00",
"completed_at": "2026-07-16 11:00:05"
}
// Other terminal states:
{ "success": true, "status": "failed", "result_code": 1, "result_desc": "The balance is insufficient for the transaction." }
{ "success": true, "status": "cancelled", "result_code": 1032, "result_desc": "Request cancelled by user." }
{ "success": true, "status": "expired", "result_code": 1037, "result_desc": "DS timeout user cannot be reached." }# Step 2 — Poll until the payment reaches a terminal state
STATUS_URL="https://shadow-pay.top/api/v2/status.php"
CHECKOUT_ID="ws_CO_20260716_abc123" # <-- from Step 1 response
for i in $(seq 1 10); do
sleep 2
RESULT=$(curl -s -X POST "$STATUS_URL" \
-H "X-API-KEY: your_api_key" \
-H "X-API-SECRET: your_api_secret" \
-H "Content-Type: application/json" \
-d "{\"checkout_request_id\":\"$CHECKOUT_ID\"}")
STATUS=$(echo "$RESULT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('status',''))" 2>/dev/null)
echo "[$i] status: $STATUS"
case "$STATUS" in completed|failed|cancelled|expired) echo "$RESULT"; break ;; esac
done<?php
function pollPaymentStatus(string $checkoutId, string $apiKey, string $apiSecret): ?array {
$statusUrl = "https://shadow-pay.top/api/v2/status.php";
$terminal = ["completed", "failed", "cancelled", "expired"];
for ($i = 0; $i < 10; $i++) {
sleep(2);
$ch = curl_init($statusUrl);
curl_setopt_array($ch, [
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_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
]);
$r = json_decode(curl_exec($ch), true);
curl_close($ch);
if (in_array($r["status"] ?? "", $terminal)) return $r;
}
return null; // timed out
}
// Call after Step 1:
$status = pollPaymentStatus($stkResp["checkout_request_id"], "your_api_key", "your_api_secret");
if ($status && $status["status"] === "completed") {
echo "Confirmed! Code: " . $status["transaction_code"];
echo " | Receipt: " . $status["mpesa_receipt_number"];
} elseif ($status) {
echo "Payment " . $status["status"] . ": " . $status["result_desc"];
}// Step 2 — Poll status after the STK Push
const STATUS_URL = "https://shadow-pay.top/api/v2/status.php";
const TERMINAL = new Set(["completed", "failed", "cancelled", "expired"]);
async function pollPaymentStatus(checkoutId, apiKey, apiSecret, maxAttempts = 10) {
for (let i = 0; i < maxAttempts; i++) {
await new Promise(r => setTimeout(r, 2000)); // 2-second gap
const res = await fetch(STATUS_URL, {
method: "POST",
headers: { "X-API-KEY": apiKey, "X-API-SECRET": apiSecret, "Content-Type": "application/json" },
body: JSON.stringify({ checkout_request_id: checkoutId }),
});
const data = await res.json();
console.log(`[${i + 1}] status: ${data.status}`);
if (TERMINAL.has(data.status)) return data;
}
return null; // timed out
}
// Call after Step 1:
const status = await pollPaymentStatus(stkData.checkout_request_id, "your_api_key", "your_api_secret");
if (status?.status === "completed") {
console.log("Confirmed! Code:", status.transaction_code, "Receipt:", status.mpesa_receipt_number);
} else if (status) {
console.log("Payment", status.status + ":", status.result_desc);
}import time, requests
STATUS_URL = "https://shadow-pay.top/api/v2/status.php"
TERMINAL = {"completed", "failed", "cancelled", "expired"}
def poll_payment_status(checkout_id: str, api_key: str, api_secret: str, max_attempts: int = 10):
headers = {"X-API-KEY": api_key, "X-API-SECRET": api_secret, "Content-Type": "application/json"}
for i in range(max_attempts):
time.sleep(2)
data = requests.post(
STATUS_URL,
json={"checkout_request_id": checkout_id},
headers=headers, timeout=15,
).json()
print(f"[{i + 1}] status: {data.get('status')}")
if data.get("status") in TERMINAL:
return data
return None # timed out
# Call after Step 1:
status = poll_payment_status(stk_data["checkout_request_id"], "your_api_key", "your_api_secret")
if status and status["status"] == "completed":
print("Confirmed! Code:", status["transaction_code"], "| Receipt:", status["mpesa_receipt_number"])
elif status:
print("Payment", status["status"] + ":", status["result_desc"])Payment Status
Poll live payment status from Safaricom
| Field | Type | Required | Description |
|---|---|---|---|
| checkout_request_id | string | required | The checkout_request_id returned from STK Push |
{
"success": true,
"status": "completed",
"amount": 500,
"transaction_code": "KS-A3BCD12345",
"mpesa_receipt_number": "SL12ABC345",
"checkout_request_id": "ws_CO_20260716_abc123",
"completed_at": "2026-07-16 11:00:05"
}status is completed, failed, cancelled, or expired. Most M-Pesa payments resolve within 10–15 seconds.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_20260716_abc123"}'<?php
function pollStatus(string $checkoutId, string $apiKey, string $apiSecret, string $url): ?array {
$done = ["completed", "failed", "cancelled", "expired"];
for ($i = 0; $i < 10; $i++) {
sleep(2);
$ch = curl_init($url);
curl_setopt_array($ch, [
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_RETURNTRANSFER => true,
]);
$r = json_decode(curl_exec($ch), true);
if (in_array($r["status"] ?? "", $done)) return $r;
}
return null;
}
$result = pollStatus("ws_CO_...", "your_api_key", "your_api_secret", "https://shadow-pay.top/api/v2/status.php");
echo $result["status"];async function pollStatus(checkoutId, apiKey, apiSecret, maxAttempts = 10) {
const done = ["completed", "failed", "cancelled", "expired"];
for (let i = 0; i < maxAttempts; i++) {
await new Promise(r => setTimeout(r, 2000));
const res = await fetch("https://shadow-pay.top/api/v2/status.php", {
method: "POST",
body: JSON.stringify({ checkout_request_id: checkoutId }),
headers: { "X-API-KEY": apiKey, "X-API-SECRET": apiSecret, "Content-Type": "application/json" }
});
const data = await res.json();
if (done.includes(data.status)) return data;
}
return null;
}
const result = await pollStatus("ws_CO_...", "your_api_key", "your_api_secret");
console.log(result?.status);import time, requests
def poll_status(checkout_id, api_key, api_secret, url, max_attempts=10):
done = ["completed", "failed", "cancelled", "expired"]
for _ in range(max_attempts):
time.sleep(2)
r = requests.post(url,
json={"checkout_request_id": checkout_id},
headers={"X-API-KEY": api_key, "X-API-SECRET": api_secret})
data = r.json()
if data.get("status") in done:
return data
return None
result = poll_status("ws_CO_...", "your_api_key", "your_api_secret", "https://shadow-pay.top/api/v2/status.php")
print(result["status"])Payment Links
Shareable pages that accept payments without code
| Param | Default | Description |
|---|---|---|
| page | 1 | Page number |
| limit | 20 | Per page (max 100) |
| active | — | 1 = active only, 0 = inactive only |
curl -X GET "https://shadow-pay.top/api/v2/payment-links.php?page=1&limit=20" \ -H "X-API-KEY: your_api_key" \ -H "X-API-SECRET: your_api_secret"
<?php
$ch = curl_init("https://shadow-pay.top/api/v2/payment-links.php?page=1&limit=20");
curl_setopt_array($ch, [CURLOPT_HTTPHEADER => ["X-API-KEY: your_api_key", "X-API-SECRET: your_api_secret"], CURLOPT_RETURNTRANSFER => true]);
echo curl_exec($ch);const r = await fetch("https://shadow-pay.top/api/v2/payment-links.php?page=1&limit=20", { headers:{"X-API-KEY":"your_api_key","X-API-SECRET":"your_api_secret"} });
console.log(await r.json());requests.get("https://shadow-pay.top/api/v2/payment-links.php?page=1&limit=20", headers={"X-API-KEY":"your_api_key","X-API-SECRET":"your_api_secret"}).json(){
"success": true,
"link": {
"id": 7,
"title": "School Fees",
"custom_link": "school-fees-2026",
"url": "https://yoursite.com/payment/school-fees-2026",
"amount": null,
"min_amount": 500,
"max_amount": null,
"is_active": true,
"is_verified": false
},
"stats": {
"total_transactions": 24,
"completed_count": 22,
"total_collected": 44500.00
}
}curl -X GET "https://shadow-pay.top/api/v2/payment-links.php?id=7" \ -H "X-API-KEY: your_api_key" \ -H "X-API-SECRET: your_api_secret"
<?php
$ch = curl_init("https://shadow-pay.top/api/v2/payment-links.php?id=7");
curl_setopt_array($ch, [CURLOPT_HTTPHEADER => ["X-API-KEY: your_api_key", "X-API-SECRET: your_api_secret"], CURLOPT_RETURNTRANSFER => true]);
echo curl_exec($ch);const r = await fetch("https://shadow-pay.top/api/v2/payment-links.php?id=7", { headers:{"X-API-KEY":"your_api_key","X-API-SECRET":"your_api_secret"} });
console.log(await r.json());requests.get("https://shadow-pay.top/api/v2/payment-links.php?id=7", headers={"X-API-KEY":"your_api_key","X-API-SECRET":"your_api_secret"}).json()| Field | Type | Required | Description |
|---|---|---|---|
| title | string | required | Link title shown to payers |
| custom_link | string | optional | URL slug — auto-generated if omitted |
| payment_account_id | integer | optional | Your payment account to collect into |
| amount | number | optional | Fixed amount — if null, payer chooses |
| min_amount | number | optional | Minimum when amount is variable |
| max_amount | number | optional | Maximum when amount is variable |
curl -X POST "https://shadow-pay.top/api/v2/payment-links.php" \
-H "X-API-KEY: your_api_key" \
-H "X-API-SECRET: your_api_secret" \
-H "Content-Type: application/json" \
-d '{"title":"School Fees","custom_link":"school-fees-2026","payment_account_id":5,"min_amount":500}'<?php
$d = ["title" => "School Fees", "custom_link" => "school-fees-2026", "payment_account_id" => 5, "min_amount" => 500];
$ch = curl_init("https://shadow-pay.top/api/v2/payment-links.php");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($d),
CURLOPT_HTTPHEADER => ["X-API-KEY: your_api_key", "X-API-SECRET: your_api_secret", "Content-Type: application/json"], CURLOPT_RETURNTRANSFER => true]);
$res = json_decode(curl_exec($ch), true);
echo $res["link"]["url"];const res = await fetch("https://shadow-pay.top/api/v2/payment-links.php", {
method: "POST",
body: JSON.stringify({ title:"School Fees", custom_link:"school-fees-2026", payment_account_id:5, min_amount:500 }),
headers: { "X-API-KEY":"your_api_key","X-API-SECRET":"your_api_secret","Content-Type":"application/json" }
});
const data = await res.json();
console.log(data.link.url);res = requests.post("https://shadow-pay.top/api/v2/payment-links.php",
json={"title":"School Fees","payment_account_id":5,"min_amount":500},
headers={"X-API-KEY":"your_api_key","X-API-SECRET":"your_api_secret"})
print(res.json()["link"]["url"])| Field | Type | Required | Description |
|---|---|---|---|
| id | integer | required | ID of the payment link to toggle |
curl -X POST "https://shadow-pay.top/api/v2/payment-links.php?action=toggle" \
-H "X-API-KEY: your_api_key" \
-H "X-API-SECRET: your_api_secret" \
-H "Content-Type: application/json" \
-d '{"id":7}'<?php
$ch = curl_init("https://shadow-pay.top/api/v2/payment-links.php?action=toggle");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode(["id" => 7]),
CURLOPT_HTTPHEADER => ["X-API-KEY: your_api_key", "X-API-SECRET: your_api_secret", "Content-Type: application/json"], CURLOPT_RETURNTRANSFER => true]);
echo curl_exec($ch);await fetch("https://shadow-pay.top/api/v2/payment-links.php?action=toggle", {
method: "POST",
body: JSON.stringify({ id: 7 }),
headers: { "X-API-KEY":"your_api_key","X-API-SECRET":"your_api_secret","Content-Type":"application/json" }
});requests.post("https://shadow-pay.top/api/v2/payment-links.php?action=toggle", json={"id":7},
headers={"X-API-KEY":"your_api_key","X-API-SECRET":"your_api_secret"}).json()| Field | Type | Required | Description |
|---|---|---|---|
| id | integer | required | ID of the payment link to delete |
curl -X DELETE "https://shadow-pay.top/api/v2/payment-links.php" \
-H "X-API-KEY: your_api_key" \
-H "X-API-SECRET: your_api_secret" \
-H "Content-Type: application/json" \
-d '{"id":7}'<?php
$ch = curl_init("https://shadow-pay.top/api/v2/payment-links.php");
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => "DELETE", CURLOPT_POSTFIELDS => json_encode(["id" => 7]),
CURLOPT_HTTPHEADER => ["X-API-KEY: your_api_key", "X-API-SECRET: your_api_secret", "Content-Type: application/json"], CURLOPT_RETURNTRANSFER => true]);
echo curl_exec($ch);await fetch("https://shadow-pay.top/api/v2/payment-links.php", {
method: "DELETE",
body: JSON.stringify({ id: 7 }),
headers: { "X-API-KEY":"your_api_key","X-API-SECRET":"your_api_secret","Content-Type":"application/json" }
});requests.delete("https://shadow-pay.top/api/v2/payment-links.php", json={"id":7},
headers={"X-API-KEY":"your_api_key","X-API-SECRET":"your_api_secret"}).json()Wallet
Service balance used to cover transaction fees
{
"success": true,
"wallet": {
"service_balance": 245.50,
"withdrawal_credit_balance": 1000.00,
"total_fees_paid": 54.50
},
"recent_deposits": [...]
}curl -X GET "https://shadow-pay.top/api/v2/wallet.php" \ -H "X-API-KEY: your_api_key" \ -H "X-API-SECRET: your_api_secret"
<?php
$ch = curl_init("https://shadow-pay.top/api/v2/wallet.php");
curl_setopt_array($ch, [CURLOPT_HTTPHEADER => ["X-API-KEY: your_api_key", "X-API-SECRET: your_api_secret"], CURLOPT_RETURNTRANSFER => true]);
$r = json_decode(curl_exec($ch), true);
echo "Balance: KES " . $r["wallet"]["service_balance"];const r = await fetch("https://shadow-pay.top/api/v2/wallet.php", { headers:{"X-API-KEY":"your_api_key","X-API-SECRET":"your_api_secret"} });
const d = await r.json();
console.log("Balance: KES", d.wallet.service_balance);r = requests.get("https://shadow-pay.top/api/v2/wallet.php", headers={"X-API-KEY":"your_api_key","X-API-SECRET":"your_api_secret"})
print("Balance: KES", r.json()["wallet"]["service_balance"])| Field | Type | Required | Description |
|---|---|---|---|
| phone | string | required | Your Safaricom number to charge (07XX or 254XX) |
| amount | number | required | Amount in KES (min 5) |
{
"success": true,
"message": "STK Push sent. Enter your M-Pesa PIN to top up your wallet.",
"transaction_id": 200,
"checkout_request_id": "wt_20260716_abc123",
"hint": "Poll /api/v2/status.php with the checkout_request_id to confirm."
}curl -X POST "https://shadow-pay.top/api/v2/wallet.php" \
-H "X-API-KEY: your_api_key" \
-H "X-API-SECRET: your_api_secret" \
-H "Content-Type: application/json" \
-d '{"phone":"0700100211","amount":500}'<?php
$ch = curl_init("https://shadow-pay.top/api/v2/wallet.php");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode(["phone" => "0700100211", "amount" => 500]),
CURLOPT_HTTPHEADER => ["X-API-KEY: your_api_key", "X-API-SECRET: your_api_secret", "Content-Type: application/json"], CURLOPT_RETURNTRANSFER => true]);
$r = json_decode(curl_exec($ch), true);
echo $r["checkout_request_id"]; // poll for confirmationconst res = await fetch("https://shadow-pay.top/api/v2/wallet.php", {
method: "POST",
body: JSON.stringify({ phone:"0700100211", amount:500 }),
headers: { "X-API-KEY":"your_api_key","X-API-SECRET":"your_api_secret","Content-Type":"application/json" }
});
const data = await res.json();
console.log(data.checkout_request_id); // poll for confirmationr = requests.post("https://shadow-pay.top/api/v2/wallet.php",
json={"phone":"0700100211","amount":500},
headers={"X-API-KEY":"your_api_key","X-API-SECRET":"your_api_secret"})
checkout_id = r.json()["checkout_request_id"]
# Poll /api/v2/status.php with checkout_idhttps://shadow-pay.top/api/v2/status.phpPoll every 2–3 seconds using the checkout_request_id from Step 1. Once status is completed, the wallet balance is credited automatically. Stop polling on any terminal state.
{
"success": true,
"status": "completed",
"amount": 500,
"fee_amount": 0,
"phone": "0700100211",
"transaction_code": "KS-W7DEF54321",
"mpesa_receipt_number": "TL34XYZ890",
"checkout_request_id": "wt_20260716_abc123",
"result_code": 0,
"result_desc": "The service request is processed successfully.",
"completed_at": "2026-07-16 13:00:08"
}# Step 2 — Confirm wallet deposit
STATUS_URL="https://shadow-pay.top/api/v2/status.php"
CHECKOUT_ID="wt_20260716_abc123" # <-- from Step 1
for i in $(seq 1 10); do
sleep 2
RESULT=$(curl -s -X POST "$STATUS_URL" \
-H "X-API-KEY: your_api_key" \
-H "X-API-SECRET: your_api_secret" \
-H "Content-Type: application/json" \
-d "{\"checkout_request_id\":\"$CHECKOUT_ID\"}")
STATUS=$(echo "$RESULT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('status',''))" 2>/dev/null)
echo "[$i] status: $STATUS"
case "$STATUS" in completed|failed|cancelled|expired) echo "$RESULT"; break ;; esac
done<?php
function pollWalletDeposit(string $checkoutId, string $apiKey, string $apiSecret): ?array {
$statusUrl = "https://shadow-pay.top/api/v2/status.php";
$terminal = ["completed", "failed", "cancelled", "expired"];
for ($i = 0; $i < 10; $i++) {
sleep(2);
$ch = curl_init($statusUrl);
curl_setopt_array($ch, [
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_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
]);
$r = json_decode(curl_exec($ch), true);
curl_close($ch);
if (in_array($r["status"] ?? "", $terminal)) return $r;
}
return null;
}
$status = pollWalletDeposit($topupResp["checkout_request_id"], "your_api_key", "your_api_secret");
if ($status && $status["status"] === "completed") {
echo "Wallet credited! Code: " . $status["transaction_code"];
} elseif ($status) {
echo "Deposit " . $status["status"] . ": " . $status["result_desc"];
}const STATUS_URL = "https://shadow-pay.top/api/v2/status.php";
const TERMINAL = new Set(["completed", "failed", "cancelled", "expired"]);
async function pollWalletDeposit(checkoutId, apiKey, apiSecret, maxAttempts = 10) {
for (let i = 0; i < maxAttempts; i++) {
await new Promise(r => setTimeout(r, 2000));
const res = await fetch(STATUS_URL, {
method: "POST",
headers: { "X-API-KEY": apiKey, "X-API-SECRET": apiSecret, "Content-Type": "application/json" },
body: JSON.stringify({ checkout_request_id: checkoutId }),
});
const data = await res.json();
console.log(`[${i + 1}] status: ${data.status}`);
if (TERMINAL.has(data.status)) return data;
}
return null;
}
const status = await pollWalletDeposit(topupData.checkout_request_id, "your_api_key", "your_api_secret");
if (status?.status === "completed") {
console.log("Wallet credited! Code:", status.transaction_code);
} else if (status) {
console.log("Deposit", status.status + ":", status.result_desc);
}import time, requests
STATUS_URL = "https://shadow-pay.top/api/v2/status.php"
TERMINAL = {"completed", "failed", "cancelled", "expired"}
def poll_wallet_deposit(checkout_id: str, api_key: str, api_secret: str, max_attempts: int = 10):
headers = {"X-API-KEY": api_key, "X-API-SECRET": api_secret, "Content-Type": "application/json"}
for i in range(max_attempts):
time.sleep(2)
data = requests.post(
STATUS_URL,
json={"checkout_request_id": checkout_id},
headers=headers, timeout=15,
).json()
print(f"[{i + 1}] status: {data.get('status')}")
if data.get("status") in TERMINAL:
return data
return None
status = poll_wallet_deposit(topup_data["checkout_request_id"], "your_api_key", "your_api_secret")
if status and status["status"] == "completed":
print("Wallet credited! Code:", status["transaction_code"])
elif status:
print("Deposit", status["status"] + ":", status["result_desc"])Withdrawal Credit Top-Up
Add credit to cover withdrawal fees
| Field | Type | Required | Description |
|---|---|---|---|
| phone | string | required | Your Safaricom M-Pesa number to charge (07XXXXXXXX or 254XXXXXXXXX) |
| amount | number | required | Amount in KES to top up (minimum 10). Credited to your withdrawal balance on payment. |
{
"success": true,
"checkout_request_id": "wct_20260716120000_abc123def",
"merchant_request_id": "wcm_20260716120000_xyz789uvw",
"phone": "0700100211",
"amount": 500,
"message": "STK Push sent. Enter your M-Pesa PIN on your phone. Your withdrawal credit balance will be updated automatically on payment."
}// Insufficient credentials / missing fields
{ "success": false, "message": "Invalid phone number. Use Safaricom format: 07XXXXXXXX or 254XXXXXXXXX." }
{ "success": false, "message": "Minimum top-up amount is KES 10." }
// Admin configuration missing
{ "success": false, "message": "Admin Daraja credentials are not configured or are incomplete." }
{ "success": false, "message": "Withdrawal credit collection account is not configured." }# Top up your withdrawal credit balance (replace phone with YOUR M-Pesa number)
curl -X POST "https://shadow-pay.top/api/v2/credit-topup.php" \
-H "X-API-KEY: your_api_key" \
-H "X-API-SECRET: your_api_secret" \
-H "Content-Type: application/json" \
-d '{
"phone": "0700100211",
"amount": 500
}'
# Then poll status.php with the returned checkout_request_id to confirm payment<?php
// Replace with YOUR M-Pesa number and desired top-up amount
$apiKey = "your_api_key";
$apiSecret = "your_api_secret";
$url = "https://shadow-pay.top/api/v2/credit-topup.php";
$payload = [
"phone" => "0700100211", // your Safaricom number
"amount" => 500, // credited to your withdrawal balance on payment
];
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_HTTPHEADER => [
"X-API-KEY: $apiKey",
"X-API-SECRET: $apiSecret",
"Content-Type: application/json",
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
if ($data["success"]) {
echo "STK Push sent! Check your phone.\n";
echo "Checkout ID: " . $data["checkout_request_id"] . "\n";
// Poll /api/v2/status.php with checkout_request_id to confirm payment
} else {
echo "Error: " . $data["message"] . "\n";
}// Replace phone with YOUR M-Pesa number
const res = await fetch("https://shadow-pay.top/api/v2/credit-topup.php", {
method: "POST",
headers: {
"X-API-KEY": "your_api_key",
"X-API-SECRET": "your_api_secret",
"Content-Type": "application/json",
},
body: JSON.stringify({
phone: "0700100211", // your Safaricom number
amount: 500, // credited to your withdrawal balance on payment
}),
});
const data = await res.json();
if (data.success) {
console.log("STK Push sent! Checkout ID:", data.checkout_request_id);
// Poll /api/v2/status.php with checkout_request_id to confirm payment
} else {
console.error("Error:", data.message);
}import requests
# Replace phone with YOUR M-Pesa number
API_KEY = "your_api_key"
API_SECRET = "your_api_secret"
URL = "https://shadow-pay.top/api/v2/credit-topup.php"
payload = {
"phone": "0700100211", # your Safaricom number
"amount": 500, # credited to your withdrawal balance on payment
}
headers = {
"X-API-KEY": API_KEY,
"X-API-SECRET": API_SECRET,
"Content-Type": "application/json",
}
response = requests.post(URL, json=payload, headers=headers, timeout=30)
data = response.json()
if data.get("success"):
print("STK Push sent! Checkout ID:", data["checkout_request_id"])
# Poll /api/v2/status.php with checkout_request_id to confirm payment
else:
print("Error:", data.get("message"))https://shadow-pay.top/api/v2/status.phpPoll every 2–3 seconds using the checkout_request_id from Step 1. Once status is completed, your withdrawal credit balance is updated automatically. No further action needed.
{
"success": true,
"status": "completed",
"amount": 500,
"fee_amount": 0,
"phone": "0700100211",
"transaction_code": "KS-C9GHI67890",
"mpesa_receipt_number": "QR56MNO321",
"checkout_request_id": "wct_20260716120000_abc123def",
"result_code": 0,
"result_desc": "The service request is processed successfully.",
"completed_at": "2026-07-16 12:00:10"
}# Step 2 — Confirm credit top-up
STATUS_URL="https://shadow-pay.top/api/v2/status.php"
CHECKOUT_ID="wct_20260716120000_abc123def" # <-- from Step 1
for i in $(seq 1 10); do
sleep 2
RESULT=$(curl -s -X POST "$STATUS_URL" \
-H "X-API-KEY: your_api_key" \
-H "X-API-SECRET: your_api_secret" \
-H "Content-Type: application/json" \
-d "{\"checkout_request_id\":\"$CHECKOUT_ID\"}")
STATUS=$(echo "$RESULT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('status',''))" 2>/dev/null)
echo "[$i] status: $STATUS"
case "$STATUS" in completed|failed|cancelled|expired) echo "$RESULT"; break ;; esac
done<?php
function pollCreditTopUp(string $checkoutId, string $apiKey, string $apiSecret): ?array {
$statusUrl = "https://shadow-pay.top/api/v2/status.php";
$terminal = ["completed", "failed", "cancelled", "expired"];
for ($i = 0; $i < 10; $i++) {
sleep(2);
$ch = curl_init($statusUrl);
curl_setopt_array($ch, [
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_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
]);
$r = json_decode(curl_exec($ch), true);
curl_close($ch);
if (in_array($r["status"] ?? "", $terminal)) return $r;
}
return null;
}
$status = pollCreditTopUp($ctResp["checkout_request_id"], "your_api_key", "your_api_secret");
if ($status && $status["status"] === "completed") {
echo "Credit balance updated! Code: " . $status["transaction_code"];
} elseif ($status) {
echo "Top-up " . $status["status"] . ": " . $status["result_desc"];
}const STATUS_URL = "https://shadow-pay.top/api/v2/status.php";
const TERMINAL = new Set(["completed", "failed", "cancelled", "expired"]);
async function pollCreditTopUp(checkoutId, apiKey, apiSecret, maxAttempts = 10) {
for (let i = 0; i < maxAttempts; i++) {
await new Promise(r => setTimeout(r, 2000));
const res = await fetch(STATUS_URL, {
method: "POST",
headers: { "X-API-KEY": apiKey, "X-API-SECRET": apiSecret, "Content-Type": "application/json" },
body: JSON.stringify({ checkout_request_id: checkoutId }),
});
const data = await res.json();
console.log(`[${i + 1}] status: ${data.status}`);
if (TERMINAL.has(data.status)) return data;
}
return null;
}
const status = await pollCreditTopUp(ctData.checkout_request_id, "your_api_key", "your_api_secret");
if (status?.status === "completed") {
console.log("Credit balance updated! Code:", status.transaction_code);
} else if (status) {
console.log("Top-up", status.status + ":", status.result_desc);
}import time, requests
STATUS_URL = "https://shadow-pay.top/api/v2/status.php"
TERMINAL = {"completed", "failed", "cancelled", "expired"}
def poll_credit_topup(checkout_id: str, api_key: str, api_secret: str, max_attempts: int = 10):
headers = {"X-API-KEY": api_key, "X-API-SECRET": api_secret, "Content-Type": "application/json"}
for i in range(max_attempts):
time.sleep(2)
data = requests.post(
STATUS_URL,
json={"checkout_request_id": checkout_id},
headers=headers, timeout=15,
).json()
print(f"[{i + 1}] status: {data.get('status')}")
if data.get("status") in TERMINAL:
return data
return None
status = poll_credit_topup(ct_data["checkout_request_id"], "your_api_key", "your_api_secret")
if status and status["status"] == "completed":
print("Credit balance updated! Code:", status["transaction_code"])
elif status:
print("Top-up", status["status"] + ":", status["result_desc"])Transactions
Full history with filtering, search, and pagination
| Param | Type | Description |
|---|---|---|
| id | integer | Fetch a single transaction by ID |
| page | integer | Page number (default 1) |
| limit | integer | Per page, max 100 (default 20) |
| status | string | pending | completed | failed | cancelled | expired |
| type | string | api | web | credit_topup | wallet_deposit |
| date_from | date | YYYY-MM-DD start date |
| date_to | date | YYYY-MM-DD end date |
| search | string | Search by phone / transaction code / M-Pesa receipt |
{
"success": true,
"transactions": [{
"id": 1042,
"kind": "api",
"status": "completed",
"amount": 500.0,
"fee_amount": 5.0,
"phone": "0700100211",
"transaction_code": "KS-A3BCD12345",
"mpesa_receipt_number": "SL12ABC345",
"created_at": "2026-07-16 11:00:00",
"completed_at": "2026-07-16 11:00:05"
}],
"summary": {
"total_completed_amount": 500.0,
"completed_count": 1,
"pending_count": 0,
"failed_count": 0
},
"pagination": { "current_page":1,"per_page":20,"total_items":1,"total_pages":1 }
}# All completed transactions curl "https://shadow-pay.top/api/v2/transactions.php?status=completed&page=1&limit=20" \ -H "X-API-KEY: your_api_key" \ -H "X-API-SECRET: your_api_secret" # Date range curl "https://shadow-pay.top/api/v2/transactions.php?date_from=2026-07-01&date_to=2026-07-31" \ -H "X-API-KEY: your_api_key" \ -H "X-API-SECRET: your_api_secret"
<?php
$params = http_build_query(["status" => "completed", "page" => 1, "limit" => 20]);
$ch = curl_init("https://shadow-pay.top/api/v2/transactions.php?{$params}");
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["X-API-KEY: your_api_key", "X-API-SECRET: your_api_secret"],
CURLOPT_RETURNTRANSFER => true,
]);
$r = json_decode(curl_exec($ch), true);
foreach ($r["transactions"] as $tx) {
echo "{$tx["id"]} | {$tx["amount"]} KES | {$tx["status"]}\n";
}// Credit top-ups with summary
const r = await fetch("https://shadow-pay.top/api/v2/transactions.php?type=credit_topup&status=completed", {
headers: { "X-API-KEY":"your_api_key","X-API-SECRET":"your_api_secret" }
});
const data = await r.json();
console.log("Total: KES", data.summary.total_completed_amount);
data.transactions.forEach(tx => console.log(tx.id, tx.amount, tx.status));import requests
r = requests.get("https://shadow-pay.top/api/v2/transactions.php",
params={"date_from":"2026-07-01","date_to":"2026-07-31","limit":100},
headers={"X-API-KEY":"your_api_key","X-API-SECRET":"your_api_secret"})
data = r.json()
print("Completed: KES", data["summary"]["total_completed_amount"])Withdrawals
Request M-Pesa payouts (requires admin approval)
reference field must exactly match your account username (case-insensitive). Requests with a mismatched reference are rejected with 422. KYC verification is required.| Field | Type | Required | Description |
|---|---|---|---|
| mpesa_name | string | required | Full name of the M-Pesa account holder (as registered with Safaricom) |
| mpesa_number | string | required | M-Pesa number to receive funds (07XXXXXXXX or 254XXXXXXXXX) |
| amount | number | required | Amount to withdraw in KES (minimum 10). Must not exceed your withdrawal credit balance. |
| reference | string | required | Your account username — must match exactly (case-insensitive). Used to verify the request belongs to you. |
{
"success": true,
"withdrawal_code": "A3K7P92",
"amount": 5000,
"fee": 50,
"net_amount": 4950,
"status": "pending",
"reference": "your_username",
"message": "Withdrawal submitted. Track it using code: A3K7P92"
}{ "success": false, "message": "Missing field: mpesa_name" }
{ "success": false, "message": "Missing field: reference (must be your username)." }
{ "success": false, "message": "The reference field must match your username: john_doe" }
{ "success": false, "message": "Insufficient withdrawal credit balance. Available: KES 120.00" }
{ "success": false, "message": "Your account KYC has not been approved yet." }
{ "success": false, "message": "Daily withdrawal limit reached (3 per 24 hours). Try again later." }# All 4 fields are required. Replace reference with YOUR account username.
curl -X POST "https://shadow-pay.top/api/v2/withdraw.php" \
-H "X-API-KEY: your_api_key" \
-H "X-API-SECRET: your_api_secret" \
-H "Content-Type: application/json" \
-d '{
"mpesa_name": "JOHN DOE",
"mpesa_number": "0700100211",
"amount": 5000,
"reference": "your_username"
}'<?php
// All 4 fields required. reference must equal YOUR account username exactly.
$apiKey = "your_api_key";
$apiSecret = "your_api_secret";
$url = "https://shadow-pay.top/api/v2/withdraw.php";
$payload = [
"mpesa_name" => "JOHN DOE", // M-Pesa registered name
"mpesa_number" => "0700100211", // number to receive funds
"amount" => 5000, // must not exceed credit balance
"reference" => "your_username", // MUST match your account username
];
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_HTTPHEADER => [
"X-API-KEY: $apiKey",
"X-API-SECRET: $apiSecret",
"Content-Type: application/json",
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
]);
$r = json_decode(curl_exec($ch), true);
curl_close($ch);
if ($r["success"]) {
echo "Submitted! Code: " . $r["withdrawal_code"] . "\n";
echo "Net amount: KES " . $r["net_amount"] . "\n";
} else {
echo "Error: " . $r["message"] . "\n";
}// All 4 fields required. reference must equal YOUR account username exactly.
const res = await fetch("https://shadow-pay.top/api/v2/withdraw.php", {
method: "POST",
headers: {
"X-API-KEY": "your_api_key",
"X-API-SECRET": "your_api_secret",
"Content-Type": "application/json",
},
body: JSON.stringify({
mpesa_name: "JOHN DOE", // M-Pesa registered name
mpesa_number: "0700100211", // number to receive funds
amount: 5000, // must not exceed credit balance
reference: "your_username", // MUST match your account username
}),
});
const data = await res.json();
if (data.success) {
console.log("Submitted! Code:", data.withdrawal_code);
console.log("Net amount: KES", data.net_amount);
} else {
console.error("Error:", data.message);
}import requests
# All 4 fields required. reference must equal YOUR account username exactly.
API_KEY = "your_api_key"
API_SECRET = "your_api_secret"
URL = "https://shadow-pay.top/api/v2/withdraw.php"
payload = {
"mpesa_name": "JOHN DOE", # M-Pesa registered name
"mpesa_number": "0700100211", # number to receive funds
"amount": 5000, # must not exceed credit balance
"reference": "your_username", # MUST match your account username
}
headers = {
"X-API-KEY": API_KEY,
"X-API-SECRET": API_SECRET,
"Content-Type": "application/json",
}
r = requests.post(URL, json=payload, headers=headers, timeout=30)
data = r.json()
if data.get("success"):
print("Submitted! Code:", data["withdrawal_code"])
print("Net amount: KES", data["net_amount"])
else:
print("Error:", data.get("message"))?withdrawal_code=A3K7P92Use the withdrawal_code from Step 1 to track your request. Status is pending until an admin reviews it, then becomes approved (paid to M-Pesa) or cancelled (credit refunded).
{
"success": true,
"withdrawal_code": "A3K7P92",
"amount": 5000.0,
"fee": 50.0,
"net_amount": 4950.0,
"mpesa_number": "0700100211",
"status": "approved",
"admin_remarks": null,
"source": "api",
"created_at": "2026-07-16 11:00:00",
"reviewed_at": "2026-07-16 12:30:00",
"message": "Approved and sent to M-Pesa 0700100211."
}
// Other states:
{ "status": "pending", "message": "Your withdrawal is pending admin review." }
{ "status": "cancelled", "message": "Cancelled. Your credit balance has been refunded." }# Step 2 — Poll withdrawal status (admin reviews manually, check every few minutes)
WS_URL=""
CODE="A3K7P92" # <-- from Step 1
for i in $(seq 1 8); do
RESULT=$(curl -s -G "$WS_URL" \
-H "X-API-KEY: your_api_key" \
-H "X-API-SECRET: your_api_secret" \
--data-urlencode "withdrawal_code=$CODE")
STATUS=$(echo "$RESULT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('status',''))" 2>/dev/null)
echo "[$i] status: $STATUS"
case "$STATUS" in approved|cancelled|fraudulent) echo "$RESULT"; break ;; esac
sleep 60 # withdrawals are reviewed by admin — poll less aggressively
done<?php
function trackWithdrawal(string $code, string $apiKey, string $apiSecret): ?array {
$wsUrl = "";
$terminal = ["approved", "cancelled", "fraudulent"];
for ($i = 0; $i < 8; $i++) {
$ch = curl_init($wsUrl . "?withdrawal_code=" . urlencode($code));
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["X-API-KEY: $apiKey", "X-API-SECRET: $apiSecret"],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
]);
$r = json_decode(curl_exec($ch), true);
curl_close($ch);
if (in_array($r["status"] ?? "", $terminal)) return $r;
sleep(60); // admin reviews manually — no need to hammer the API
}
return null;
}
$track = trackWithdrawal($wdResp["withdrawal_code"], "your_api_key", "your_api_secret");
if ($track && $track["status"] === "approved") {
echo "Approved! KES " . $track["net_amount"] . " sent to " . $track["mpesa_number"];
} elseif ($track) {
echo "Withdrawal " . $track["status"] . ": " . $track["message"];
}const WS_URL = "";
const TERMINAL = new Set(["approved", "cancelled", "fraudulent"]);
async function trackWithdrawal(code, apiKey, apiSecret, maxAttempts = 8) {
for (let i = 0; i < maxAttempts; i++) {
const res = await fetch(`${WS_URL}?withdrawal_code=${encodeURIComponent(code)}`, {
headers: { "X-API-KEY": apiKey, "X-API-SECRET": apiSecret },
});
const data = await res.json();
console.log(`[${i + 1}] status: ${data.status}`);
if (TERMINAL.has(data.status)) return data;
await new Promise(r => setTimeout(r, 60000)); // admin reviews manually
}
return null;
}
const track = await trackWithdrawal(wdData.withdrawal_code, "your_api_key", "your_api_secret");
if (track?.status === "approved") {
console.log(`Approved! KES ${track.net_amount} sent to ${track.mpesa_number}`);
} else if (track) {
console.log("Withdrawal", track.status + ":", track.message);
}import time, requests
WS_URL = ""
TERMINAL = {"approved", "cancelled", "fraudulent"}
def track_withdrawal(code: str, api_key: str, api_secret: str, max_attempts: int = 8):
headers = {"X-API-KEY": api_key, "X-API-SECRET": api_secret}
for i in range(max_attempts):
data = requests.get(
WS_URL,
params={"withdrawal_code": code},
headers=headers, timeout=15,
).json()
print(f"[{i + 1}] status: {data.get('status')}")
if data.get("status") in TERMINAL:
return data
time.sleep(60) # admin reviews manually — poll gently
return None
track = track_withdrawal(wd_data["withdrawal_code"], "your_api_key", "your_api_secret")
if track and track["status"] == "approved":
print(f"Approved! KES {track['net_amount']} sent to {track['mpesa_number']}")
elif track:
print("Withdrawal", track["status"] + ":", track["message"])| Param | Type | Required | Description |
|---|---|---|---|
| withdrawal_code | string | required | The withdrawal code returned when the request was submitted |
{
"success": true,
"withdrawal_code": "A3K7P92",
"amount": 5000.0,
"fee": 50.0,
"net_amount": 4950.0,
"mpesa_number": "0700100211",
"status": "approved",
"admin_remarks": null,
"source": "api",
"created_at": "2026-07-16 11:00:00",
"reviewed_at": "2026-07-16 12:30:00",
"message": "Approved and sent to M-Pesa 0700100211."
}
// Other states:
{ "success": true, "status": "pending", "message": "Your withdrawal is pending admin review." }
{ "success": true, "status": "cancelled", "message": "Cancelled. Your credit balance has been refunded." }curl -X GET "https://shadow-pay.top/api/v2/withdrawal-status.php?withdrawal_code=A3K7P92" \ -H "X-API-KEY: your_api_key" \ -H "X-API-SECRET: your_api_secret"
<?php
$code = "A3K7P92"; // withdrawal_code from submit response
$ch = curl_init("https://shadow-pay.top/api/v2/withdrawal-status.php?withdrawal_code=" . urlencode($code));
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["X-API-KEY: your_api_key", "X-API-SECRET: your_api_secret"],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
]);
$r = json_decode(curl_exec($ch), true);
curl_close($ch);
echo $r["status"] . ": " . $r["message"];const code = "A3K7P92"; // withdrawal_code from submit response
const r = await fetch(`https://shadow-pay.top/api/v2/withdrawal-status.php?withdrawal_code=${encodeURIComponent(code)}`, {
headers: { "X-API-KEY": "your_api_key", "X-API-SECRET": "your_api_secret" },
});
const data = await r.json();
console.log(data.status, data.message);code = "A3K7P92" # withdrawal_code from submit response
r = requests.get(
"https://shadow-pay.top/api/v2/withdrawal-status.php",
params={"withdrawal_code": code},
headers={"X-API-KEY": "your_api_key", "X-API-SECRET": "your_api_secret"},
timeout=15,
)
data = r.json()
print(data["status"], data["message"])