Shadow Pay
Full API Reference
v2 STK Docs
Full API Reference

Shadow Pay API

Integrate payments, manage accounts, generate payment links, and query transactions — all from a clean, consistent REST API.

12
Endpoint groups
4
Code languages
REST
JSON API
Base URL https://shadow-pay.top/api/v2/

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.

X-API-KEY
:
your_api_key
X-API-SECRET
:
your_api_secret
Content-Type
:
application/json
Base URL https://shadow-pay.top/api/v2/

Error Codes

Standard HTTP status codes

StatusMeaning
200Success
201Resource created
400Bad request — missing or invalid field
401Unauthorized — invalid or missing API credentials
403Forbidden — KYC not verified or account suspended
404Not found
409Conflict — duplicate resource
422Validation error — value out of range or wrong format
429Rate limit exceeded
500Internal server error
502Bad gateway — Safaricom unreachable
503Service unavailable — admin configuration missing
All error responses follow the shape: { "success": false, "message": "Human-readable description" }

API Credentials

Manage your API key pairs (max 5)

GET
https://shadow-pay.top/api/v2/credentials.php
List all your API keys
200 Response
{
  "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
PHP
JavaScript
Python
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"])
POST
https://shadow-pay.top/api/v2/credentials.php
Generate a new API key pair (max 5 per account)
FieldTypeRequiredDescription
descriptionstringoptionalA label for this key, e.g. "Production App"
201 Response
{
  "success": true,
  "credentials": {
    "id": 2,
    "api_key": "pk_new123...",
    "api_secret": "sk_new456...",
    "description": "Production App",
    "is_active": true
  }
}
cURL
PHP
JavaScript
Python
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"])
POST
https://shadow-pay.top/api/v2/credentials.php?action=toggle
Enable or disable an API key
FieldTypeRequiredDescription
idintegerrequiredID of the API key to toggle
cURL
PHP
JavaScript
Python
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()
DELETE
https://shadow-pay.top/api/v2/credentials.php
Permanently delete an API key (cannot delete the currently-authenticating key)
FieldTypeRequiredDescription
idintegerrequiredID of the API key to delete
cURL
PHP
JavaScript
Python
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

A payment account is your M-Pesa Paybill or Till number customers pay into. Add at least one before initiating STK Push payments.
GET
https://shadow-pay.top/api/v2/payment-accounts.php
List all your payment accounts
200 Response
{
  "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
PHP
JavaScript
Python
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"])
GET
https://shadow-pay.top/api/v2/payment-accounts.php?id=N
Get a single payment account by ID
cURL
PHP
JavaScript
Python
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()
POST
https://shadow-pay.top/api/v2/payment-accounts.php
Add a new Paybill or Till account
FieldTypeRequiredDescription
account_typestringrequiredpaybill or till
account_numberstringrequired4–10 digit Paybill / Till number
account_referencestringpaybill onlyAccount reference shown on M-Pesa prompt
201 Response
{"success":true,"message":"Payment account added.","account":{"id":5,"account_type":"paybill","account_number":"247247","account_reference":"My Business","is_active":true}}
cURL
PHP
JavaScript
Python
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()
DELETE
https://shadow-pay.top/api/v2/payment-accounts.php
Delete a payment account (only if no active payment links linked to it)
FieldTypeRequiredDescription
idintegerrequiredID of the account to delete
cURL
PHP
JavaScript
Python
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

Once approved, you can use your own M-Pesa shortcode for STK Push payments instead of the shared admin shortcode.
GET
https://shadow-pay.top/api/v2/daraja-request.php
Check the status of your pending Daraja request
200 Response
{
  "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
PHP
JavaScript
Python
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())
POST
https://shadow-pay.top/api/v2/daraja-request.php
Submit Daraja credentials for admin approval
FieldTypeRequiredDescription
consumer_keystringrequiredDaraja Consumer Key from Safaricom developer portal
consumer_secretstringrequiredDaraja Consumer Secret
shortcodestringrequiredYour M-Pesa Business Shortcode
passkeystringrequiredDaraja Passkey (LNM Passkey)
environmentstringoptionalsandbox (default) or production
cURL
PHP
JavaScript
Python
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()
DELETE
https://shadow-pay.top/api/v2/daraja-request.php
Cancel your pending Daraja credential request
cURL
PHP
JavaScript
Python
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

GET
https://shadow-pay.top/api/v2/sms-alerts.php
Get current SMS alert config and recent alert logs
200 Response
{
  "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
PHP
JavaScript
Python
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())
POST
https://shadow-pay.top/api/v2/sms-alerts.php
Set SMS alert phone number or toggle alerts on/off
FieldTypeRequiredDescription
phonestringoptionalSafaricom number to receive SMS alerts (07XXXXXXXX)
enabledbooleanoptionaltrue to enable, false to disable
cURL
PHP
JavaScript
Python
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

Requires KYC verification and sufficient service wallet balance to cover the transaction fee.
POST
https://shadow-pay.top/api/v2/stkpush.php
Initiate an STK Push payment request to a customer's phone
FieldTypeRequiredDescription
payment_account_idintegerrequiredYour payment account ID (from Payment Accounts endpoint)
phonestringrequiredCustomer's Safaricom number (07XX or 254XX)
amountnumberrequiredAmount in KES (min 1)
referencestringoptionalOrder/account reference shown on M-Pesa receipt
descriptionstringoptionalTransaction description
200 Response
{
  "success": true,
  "message": "STK push initiated",
  "transaction_id": 1042,
  "checkout_request_id": "ws_CO_20260716_abc123",
  "merchant_request_id": "ws_MR_20260716_xyz789"
}
Use the returned checkout_request_id to poll /api/v2/status.php for payment confirmation.
cURL
PHP
JavaScript
Python
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 $checkoutId
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: 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_id
Step 2 of 2  ·  After STK Push
Confirm Payment Status
POST https://shadow-pay.top/api/v2/status.php

Poll 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.

Completed response
{
  "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." }
cURL
PHP
JavaScript
Python
# 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

POST
https://shadow-pay.top/api/v2/status.php
Query a pending STK Push — polls Safaricom live and fires notifications when complete
FieldTypeRequiredDescription
checkout_request_idstringrequiredThe checkout_request_id returned from STK Push
200 Response — completed
{
  "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"
}
Poll every 2 seconds until status is completed, failed, cancelled, or expired. Most M-Pesa payments resolve within 10–15 seconds.
cURL
PHP
JavaScript
Python
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"])

Wallet

Service balance used to cover transaction fees

The wallet balance pays transaction fees for every STK Push. Top it up to keep payments flowing.
GET
https://shadow-pay.top/api/v2/wallet.php
Get current wallet balance and recent deposits
200 Response
{
  "success": true,
  "wallet": {
    "service_balance": 245.50,
    "withdrawal_credit_balance": 1000.00,
    "total_fees_paid": 54.50
  },
  "recent_deposits": [...]
}
cURL
PHP
JavaScript
Python
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"])
POST
https://shadow-pay.top/api/v2/wallet.php
Top up your wallet via M-Pesa STK Push
FieldTypeRequiredDescription
phonestringrequiredYour Safaricom number to charge (07XX or 254XX)
amountnumberrequiredAmount in KES (min 5)
200 Response
{
  "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
PHP
JavaScript
Python
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 confirmation
const 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 confirmation
r = 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_id
Step 2 of 2  ·  After Wallet Top-Up
Confirm Deposit Status
POST https://shadow-pay.top/api/v2/status.php

Poll 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.

Completed response
{
  "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"
}
cURL
PHP
JavaScript
Python
# 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

Top up the balance used to cover withdrawal fees. Uses the admin-configured credit collection account.
POST
https://shadow-pay.top/api/v2/credit-topup.php
Initiate an STK Push to top up withdrawal credit balance
Credit is added to the authenticated user account identified by your API key — no extra username field needed. The STK Push charges the M-Pesa number you supply.
FieldTypeRequiredDescription
phonestringrequiredYour Safaricom M-Pesa number to charge (07XXXXXXXX or 254XXXXXXXXX)
amountnumberrequiredAmount in KES to top up (minimum 10). Credited to your withdrawal balance on payment.
200 Response
{
  "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."
}
Error Responses
// 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." }
cURL
PHP
JavaScript
Python
# 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"))
Step 2 of 2  ·  After Credit Top-Up
Confirm Credit Top-Up Status
POST https://shadow-pay.top/api/v2/status.php

Poll 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.

Completed response
{
  "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"
}
cURL
PHP
JavaScript
Python
# 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

GET
https://shadow-pay.top/api/v2/transactions.php
List transactions with rich filtering — covers all types: API payments, credit top-ups, wallet deposits
ParamTypeDescription
idintegerFetch a single transaction by ID
pageintegerPage number (default 1)
limitintegerPer page, max 100 (default 20)
statusstringpending | completed | failed | cancelled | expired
typestringapi | web | credit_topup | wallet_deposit
date_fromdateYYYY-MM-DD start date
date_todateYYYY-MM-DD end date
searchstringSearch by phone / transaction code / M-Pesa receipt
200 Response
{
  "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 }
}
cURL
PHP
JavaScript
Python
# 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)

POST
https://shadow-pay.top/api/v2/withdraw.php
Submit a withdrawal request to M-Pesa (requires KYC-verified account)
Important: The reference field must exactly match your account username (case-insensitive). Requests with a mismatched reference are rejected with 422. KYC verification is required.
FieldTypeRequiredDescription
mpesa_namestringrequiredFull name of the M-Pesa account holder (as registered with Safaricom)
mpesa_numberstringrequiredM-Pesa number to receive funds (07XXXXXXXX or 254XXXXXXXXX)
amountnumberrequiredAmount to withdraw in KES (minimum 10). Must not exceed your withdrawal credit balance.
referencestringrequiredYour account username — must match exactly (case-insensitive). Used to verify the request belongs to you.
200 Response
{
  "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"
}
Error Responses
{ "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." }
cURL
PHP
JavaScript
Python
# 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"))
Step 2 of 2  ·  After Withdrawal Submit
Track Withdrawal Status
GET ?withdrawal_code=A3K7P92

Use 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).

Approved response
{
  "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." }
cURL
PHP
JavaScript
Python
# 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"])
GET
https://shadow-pay.top/api/v2/withdrawal-status.php
Check the status of a withdrawal request
ParamTypeRequiredDescription
withdrawal_codestringrequiredThe withdrawal code returned when the request was submitted
200 Response
{
  "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
PHP
JavaScript
Python
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"])