Shadow Pay API

Complete documentation for connecting your website or app to the payment system

Withdrawal API Docs

Introduction

The Shadow Pay API lets you connect M-Pesa STK Push payments to your website or app. Use this page to initiate payments, check transaction status, and list transactions.

Base URL

All API endpoints are relative to:

https://shadow-pay.top/api/v2/

Authentication

Every request must include your API credentials as HTTP headers:

Response Format

All responses are JSON with a consistent structure:

{
    "success": true | false,
    "message": "Human-readable message",
    // ...additional fields per endpoint
}

Live API Test Interface

Test the Shadow Pay API endpoints directly with your credentials.

Test results will appear here…

Getting Started

1. Obtain API Credentials

  1. Create an account on the Shadow Pay platform
  2. Generate your API Key and Secret from the dashboard
  3. Set up at least one Payment Account

2. Quick Integration Example

Initiate a payment and poll for completion in under 30 lines:

curl -X POST https://shadow-pay.top/api/v2/stkpush.php \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-API-Secret: YOUR_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "payment_account_id": 17,
    "phone": "0700100211",
    "amount": 100,
    "reference": "ORDER123",
    "description": "Payment for order #123"
  }'
<?php
$apiKey    = "YOUR_API_KEY";
$apiSecret = "YOUR_API_SECRET";
$accountId = 17;
$baseUrl   = "https://shadow-pay.top/api/v2";

function payflowPost(string $url, string $key, string $secret, array $body): array {
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => json_encode($body),
        CURLOPT_HTTPHEADER     => [
            "X-API-Key: $key",
            "X-API-Secret: $secret",
            "Content-Type: application/json",
        ],
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_TIMEOUT        => 30,
    ]);
    $response = curl_exec($ch);
    curl_close($ch);
    return json_decode($response, true) ?? [];
}

// 1. Send STK Push
$result = payflowPost(
    "$baseUrl/stkpush.php", $apiKey, $apiSecret,
    [
        "payment_account_id" => $accountId,
        "phone"              => "0700100211",
        "amount"             => 100,
        "reference"          => "ORDER123",
        "description"        => "Payment for order #123",
    ]
);

if (!$result["success"]) {
    die("STK Push error: " . $result["message"]);
}

$checkoutId = $result["checkout_request_id"];
echo "STK Push sent — Checkout ID: $checkoutId\n";

// 2. Poll for completion (every 5 s, up to 2 min)
for ($i = 0; $i < 24; $i++) {
    sleep(5);
    $status = payflowPost(
        "$baseUrl/status.php", $apiKey, $apiSecret,
        ["checkout_request_id" => $checkoutId]
    );
    echo "Attempt " . ($i + 1) . ": " . $status["status"] . "\n";

    if ($status["status"] === "completed") {
        echo "Payment confirmed! Code: " . $status["transaction_code"] . "\n";
        break;
    } elseif (in_array($status["status"], ["failed", "cancelled", "expired"])) {
        echo "Payment " . $status["status"] . ".\n";
        break;
    }
}
?>
import requests
import time

API_KEY    = "YOUR_API_KEY"
API_SECRET = "YOUR_API_SECRET"
ACCOUNT_ID = 17
BASE_URL   = "https://shadow-pay.top/api/v2"

HEADERS = {
    "X-API-Key":    API_KEY,
    "X-API-Secret": API_SECRET,
    "Content-Type": "application/json",
}

# 1. Send STK Push
resp = requests.post(
    f"{BASE_URL}/stkpush.php",
    headers=HEADERS,
    json={
        "payment_account_id": ACCOUNT_ID,
        "phone":              "0700100211",
        "amount":             100,
        "reference":          "ORDER123",
        "description":        "Payment for order #123",
    },
    timeout=30,
)
result = resp.json()

if not result.get("success"):
    raise SystemExit(f"STK Push error: {result.get('message')}")

checkout_id = result["checkout_request_id"]
print(f"STK Push sent — Checkout ID: {checkout_id}")

# 2. Poll for completion (every 5 s, up to 2 min)
for attempt in range(1, 25):
    time.sleep(5)
    status_resp = requests.post(
        f"{BASE_URL}/status.php",
        headers=HEADERS,
        json={"checkout_request_id": checkout_id},
        timeout=30,
    )
    data = status_resp.json()
    print(f"Attempt {attempt}: {data.get('status')}")

    if data.get("status") == "completed":
        print(f"Payment confirmed! Code: {data.get('transaction_code')}")
        break
    elif data.get("status") in ("failed", "cancelled", "expired"):
        print(f"Payment {data.get('status')}.")
        break
// Browser JavaScript (fetch API)
const API_KEY    = "YOUR_API_KEY";
const API_SECRET = "YOUR_API_SECRET";
const ACCOUNT_ID = 17;
const BASE_URL   = "https://shadow-pay.top/api/v2";

const headers = {
    "X-API-Key":    API_KEY,
    "X-API-Secret": API_SECRET,
    "Content-Type": "application/json",
};

async function initiatePayment(phone, amount, reference, description) {
    const resp = await fetch(`${BASE_URL}/stkpush.php`, {
        method:  "POST",
        headers: headers,
        body:    JSON.stringify({
            payment_account_id: ACCOUNT_ID,
            phone, amount, reference, description,
        }),
    });
    return resp.json();
}

async function checkStatus(checkoutRequestId) {
    const resp = await fetch(`${BASE_URL}/status.php`, {
        method:  "POST",
        headers: headers,
        body:    JSON.stringify({ checkout_request_id: checkoutRequestId }),
    });
    return resp.json();
}

// Usage
(async () => {
    const result = await initiatePayment("0700100211", 100, "ORDER123", "Order payment");
    if (!result.success) { console.error("Error:", result.message); return; }

    console.log("STK Push sent:", result.checkout_request_id);

    // Poll every 5 s, up to 24 attempts
    for (let i = 0; i < 24; i++) {
        await new Promise(r => setTimeout(r, 5000));
        const status = await checkStatus(result.checkout_request_id);
        console.log(`Attempt ${i + 1}: ${status.status}`);

        if (status.status === "completed") {
            console.log("Confirmed! Code:", status.transaction_code);
            break;
        } else if (["failed", "cancelled", "expired"].includes(status.status)) {
            console.log("Payment", status.status);
            break;
        }
    }
})();
// Node.js (built-in fetch available in Node 18+)
const API_KEY    = "YOUR_API_KEY";
const API_SECRET = "YOUR_API_SECRET";
const ACCOUNT_ID = 17;
const BASE_URL   = "https://shadow-pay.top/api/v2";

const HEADERS = {
    "X-API-Key":    API_KEY,
    "X-API-Secret": API_SECRET,
    "Content-Type": "application/json",
};

async function apiPost(endpoint, body) {
    const res = await fetch(`${BASE_URL}/${endpoint}`, {
        method:  "POST",
        headers: HEADERS,
        body:    JSON.stringify(body),
    });
    return res.json();
}

function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }

(async () => {
    // 1. Send STK Push
    const result = await apiPost("stkpush.php", {
        payment_account_id: ACCOUNT_ID,
        phone:              "0700100211",
        amount:             100,
        reference:          "ORDER123",
        description:        "Order payment",
    });

    if (!result.success) { console.error("STK Push failed:", result.message); return; }
    console.log("STK Push sent:", result.checkout_request_id);

    // 2. Poll for status
    for (let i = 0; i < 24; i++) {
        await sleep(5000);
        const data = await apiPost("status.php", {
            checkout_request_id: result.checkout_request_id,
        });
        console.log(`[${i + 1}] status: ${data.status}`);

        if (data.status === "completed") {
            console.log("Confirmed! Code:", data.transaction_code);
            break;
        }
        if (["failed", "cancelled", "expired"].includes(data.status)) {
            console.log("Payment ended:", data.status);
            break;
        }
    }
})();

API Endpoints

Initiate STK Push

Sends an M-Pesa STK Push prompt to the customer's phone. Returns a checkout_request_id you use to track the payment.

POST /api/v2/stkpush.php

Request Parameters

ParameterTypeRequiredDescription
payment_account_idIntegerYesYour payment account ID (from dashboard)
phoneStringYesCustomer M-Pesa number — format: 0700100211
amountFloatYesAmount in KES (minimum 1)
referenceStringNoYour internal order/reference code
descriptionStringNoShort description shown on the STK prompt
curl -X POST https://shadow-pay.top/api/v2/stkpush.php \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-API-Secret: YOUR_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "payment_account_id": 17,
    "phone": "0700100211",
    "amount": 100,
    "reference": "ORDER_12345",
    "description": "Payment for order #12345"
  }'
<?php
$apiKey    = "YOUR_API_KEY";
$apiSecret = "YOUR_API_SECRET";
$url       = "https://shadow-pay.top/api/v2/stkpush.php";

$payload = [
    "payment_account_id" => 17,
    "phone"              => "0700100211",
    "amount"             => 100,
    "reference"          => "ORDER_12345",
    "description"        => "Payment for order #12345",
];

$ch = curl_init($url);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => json_encode($payload),
    CURLOPT_HTTPHEADER     => [
        "X-API-Key: $apiKey",
        "X-API-Secret: $apiSecret",
        "Content-Type: application/json",
    ],
    CURLOPT_CONNECTTIMEOUT => 10,
    CURLOPT_TIMEOUT        => 30,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

$result = json_decode($response, true);

if ($result["success"]) {
    echo "STK Push sent!\n";
    echo "Checkout Request ID: " . $result["checkout_request_id"] . "\n";
} else {
    echo "Error: " . $result["message"] . "\n";
}
?>
import requests

API_KEY    = "YOUR_API_KEY"
API_SECRET = "YOUR_API_SECRET"
URL        = "https://shadow-pay.top/api/v2/stkpush.php"

headers = {
    "X-API-Key":    API_KEY,
    "X-API-Secret": API_SECRET,
    "Content-Type": "application/json",
}

payload = {
    "payment_account_id": 17,
    "phone":              "0700100211",
    "amount":             100,
    "reference":          "ORDER_12345",
    "description":        "Payment for order #12345",
}

response = requests.post(URL, headers=headers, json=payload, timeout=30)
result   = response.json()

if result.get("success"):
    print("STK Push sent!")
    print("Checkout Request ID:", result["checkout_request_id"])
else:
    print("Error:", result.get("message"))
const response = await fetch("https://shadow-pay.top/api/v2/stkpush.php", {
    method: "POST",
    headers: {
        "X-API-Key":    "YOUR_API_KEY",
        "X-API-Secret": "YOUR_API_SECRET",
        "Content-Type": "application/json",
    },
    body: JSON.stringify({
        payment_account_id: 17,
        phone:              "0700100211",
        amount:             100,
        reference:          "ORDER_12345",
        description:        "Payment for order #12345",
    }),
});

const result = await response.json();

if (result.success) {
    console.log("STK Push sent!");
    console.log("Checkout ID:", result.checkout_request_id);
} else {
    console.error("Error:", result.message);
}
// Node.js (built-in fetch, Node 18+)
const res = await fetch("https://shadow-pay.top/api/v2/stkpush.php", {
    method:  "POST",
    headers: {
        "X-API-Key":    "YOUR_API_KEY",
        "X-API-Secret": "YOUR_API_SECRET",
        "Content-Type": "application/json",
    },
    body: JSON.stringify({
        payment_account_id: 17,
        phone:              "0700100211",
        amount:             100,
        reference:          "ORDER_12345",
        description:        "Payment for order #12345",
    }),
});

const result = await res.json();
console.log(result.success ? result.checkout_request_id : result.message);

Success Response

✓ 200 OK — STK Push accepted
{
    "success": true,
    "message": "STK push initiated",
    "transaction_id": 456,
    "checkout_request_id": "ws_CO_20230101120000_abc123def456",
    "merchant_request_id": "ws_MR_20230101120000_xyz789uvw012"
}
✗ Error Response
{
    "success": false,
    "message": "Invalid payment account ID or STK Push initiation failed."
}

Check Payment Status

Poll this endpoint after initiating an STK Push to find out whether the customer completed payment.

POST /api/v2/status.php

Request Parameters

ParameterTypeRequiredDescription
checkout_request_idStringYesThe checkout_request_id from the STK Push response
curl -X POST https://shadow-pay.top/api/v2/status.php \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-API-Secret: YOUR_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"checkout_request_id": "ws_CO_20230101120000_abc123def456"}'
<?php
$apiKey         = "YOUR_API_KEY";
$apiSecret      = "YOUR_API_SECRET";
$url            = "https://shadow-pay.top/api/v2/status.php";
$checkoutId     = "ws_CO_20230101120000_abc123def456";

$ch = curl_init($url);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => json_encode(["checkout_request_id" => $checkoutId]),
    CURLOPT_HTTPHEADER     => [
        "X-API-Key: $apiKey",
        "X-API-Secret: $apiSecret",
        "Content-Type: application/json",
    ],
    CURLOPT_CONNECTTIMEOUT => 10,
    CURLOPT_TIMEOUT        => 30,
]);
$response = curl_exec($ch);
curl_close($ch);

$data = json_decode($response, true);

switch ($data["status"] ?? "") {
    case "completed":
        echo "Payment completed!\n";
        echo "Transaction Code:  " . $data["transaction_code"] . "\n";
        echo "M-Pesa Receipt:    " . ($data["mpesa_receipt_number"] ?? "Pending") . "\n";
        break;
    case "failed":
    case "cancelled":
    case "expired":
        echo "Payment " . $data["status"] . ".\n";
        break;
    default:
        echo "Still pending — check again in a few seconds.\n";
}
?>
import requests

API_KEY       = "YOUR_API_KEY"
API_SECRET    = "YOUR_API_SECRET"
URL           = "https://shadow-pay.top/api/v2/status.php"
CHECKOUT_ID   = "ws_CO_20230101120000_abc123def456"

headers = {
    "X-API-Key":    API_KEY,
    "X-API-Secret": API_SECRET,
    "Content-Type": "application/json",
}

response = requests.post(
    URL,
    headers=headers,
    json={"checkout_request_id": CHECKOUT_ID},
    timeout=30,
)
data = response.json()

status = data.get("status", "")
if status == "completed":
    print("Payment completed!")
    print("Transaction Code:", data.get("transaction_code"))
    print("M-Pesa Receipt: ", data.get("mpesa_receipt_number", "Pending"))
elif status in ("failed", "cancelled", "expired"):
    print(f"Payment {status}.")
else:
    print("Still pending.")
const response = await fetch("https://shadow-pay.top/api/v2/status.php", {
    method: "POST",
    headers: {
        "X-API-Key":    "YOUR_API_KEY",
        "X-API-Secret": "YOUR_API_SECRET",
        "Content-Type": "application/json",
    },
    body: JSON.stringify({
        checkout_request_id: "ws_CO_20230101120000_abc123def456",
    }),
});

const data = await response.json();

if (data.status === "completed") {
    console.log("Payment completed! Code:", data.transaction_code);
} else if (["failed", "cancelled", "expired"].includes(data.status)) {
    console.log("Payment", data.status);
} else {
    console.log("Pending — poll again shortly");
}
// Node.js (built-in fetch, Node 18+)
const res  = await fetch("https://shadow-pay.top/api/v2/status.php", {
    method:  "POST",
    headers: {
        "X-API-Key":    "YOUR_API_KEY",
        "X-API-Secret": "YOUR_API_SECRET",
        "Content-Type": "application/json",
    },
    body: JSON.stringify({
        checkout_request_id: "ws_CO_20230101120000_abc123def456",
    }),
});
const data = await res.json();
console.log("Status:", data.status, "| Code:", data.transaction_code ?? "–");

Success Response

✓ 200 OK — Payment completed
{
    "success": true,
    "status": "completed",
    "amount": 100,
    "fee_amount": 1,
    "phone": "0700100211",
    "transaction_code": "KS-7F3K9QRT2M",
    "mpesa_receipt_number": "ABC123DEF456",
    "checkout_request_id": "ws_CO_20230101120000_abc123def456",
    "merchant_request_id": "ws_MR_20230101120000_xyz789uvw012",
    "result_code": 0,
    "result_desc": "The service request is processed successfully.",
    "created_at": "2023-01-01 12:00:00",
    "completed_at": "2023-01-01 12:01:45"
}
⏳ Still pending
{
    "success": true,
    "status": "pending",
    "amount": 100,
    "fee_amount": 1,
    "phone": "0700100211",
    "transaction_code": null,
    "mpesa_receipt_number": null,
    "checkout_request_id": "ws_CO_20230101120000_abc123def456",
    "merchant_request_id": "ws_MR_20230101120000_xyz789uvw012",
    "result_code": null,
    "result_desc": null,
    "created_at": "2023-01-01 12:00:00",
    "completed_at": null
}

Note: transaction_code and mpesa_receipt_number are null while the payment is pending. They are set the instant Safaricom confirms payment — never before.

Status Values

StatusMeaning
pendingCustomer has not yet entered their PIN — keep polling
completedPayment confirmed by Safaricom
failedPayment was rejected or timed out
cancelledCustomer cancelled the STK prompt
expiredSTK Push timed out — initiate a new one

List Transactions

Retrieve a paginated list of your transactions.

GET /api/v2/transactions.php?page=1&limit=10

Query Parameters

ParameterTypeRequiredDescription
pageIntegerNoPage number (default: 1)
limitIntegerNoItems per page (max 100, default 10)
curl -X GET "https://shadow-pay.top/api/v2/transactions.php?page=1&limit=10" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-API-Secret: YOUR_API_SECRET"
<?php
$apiKey    = "YOUR_API_KEY";
$apiSecret = "YOUR_API_SECRET";
$url       = "https://shadow-pay.top/api/v2/transactions.php?page=1&limit=10";

$ch = curl_init($url);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPGET        => true,
    CURLOPT_HTTPHEADER     => [
        "X-API-Key: $apiKey",
        "X-API-Secret: $apiSecret",
    ],
    CURLOPT_CONNECTTIMEOUT => 10,
    CURLOPT_TIMEOUT        => 30,
]);
$response = curl_exec($ch);
curl_close($ch);

$data = json_decode($response, true);

if ($data["success"]) {
    echo "Total transactions: " . $data["pagination"]["total_items"] . "\n";
    foreach ($data["transactions"] as $tx) {
        echo "[" . $tx["id"] . "] "
            . $tx["amount"] . " KES — "
            . $tx["status"] . " — "
            . $tx["created_at"] . "\n";
    }
}
?>
import requests

API_KEY    = "YOUR_API_KEY"
API_SECRET = "YOUR_API_SECRET"
URL        = "https://shadow-pay.top/api/v2/transactions.php"

headers = {
    "X-API-Key":    API_KEY,
    "X-API-Secret": API_SECRET,
}
params = {"page": 1, "limit": 10}

response = requests.get(URL, headers=headers, params=params, timeout=30)
data     = response.json()

if data.get("success"):
    print("Total:", data["pagination"]["total_items"])
    for tx in data["data"]:
        print(f"[{tx['id']}] {tx['amount']} KES — {tx['status']} — {tx['created_at']}")
const response = await fetch("https://shadow-pay.top/api/v2/transactions.php?page=1&limit=10", {
    method: "GET",
    headers: {
        "X-API-Key":    "YOUR_API_KEY",
        "X-API-Secret": "YOUR_API_SECRET",
    },
});

const data = await response.json();
if (data.success) {
    console.log("Total:", data.pagination.total_items);
    data.transactions.forEach(tx => console.log(`[${tx.id}] ${tx.amount} KES — ${tx.status}`));
}
// Node.js (built-in fetch, Node 18+)
const res  = await fetch("https://shadow-pay.top/api/v2/transactions.php?page=1&limit=10", {
    headers: {
        "X-API-Key":    "YOUR_API_KEY",
        "X-API-Secret": "YOUR_API_SECRET",
    },
});
const data = await res.json();
console.log(data.success ? data.transactions : data.message);

Success Response

✓ 200 OK
{
    "success": true,
    "transactions": [
        {
            "id": 123,
            "kind": "api",
            "type": "API",
            "status": "completed",
            "amount": 100,
            "fee_amount": 1.5,
            "fee_deducted": true,
            "phone": "0700100211",
            "transaction_code": "KS-7F3K9QRT2M",
            "mpesa_receipt_number": "ABC123DEF456",
            "checkout_request_id": "ws_CO_20230101120000_abc123def456",
            "merchant_request_id": "ws_MR_20230101120000_xyz789uvw012",
            "is_deposit": false,
            "credit_target": null,
            "payment_link_id": null,
            "result_code": 0,
            "result_desc": "The service request is processed successfully.",
            "created_at": "2023-01-01 12:00:00",
            "completed_at": "2023-01-01 12:01:45"
        }
    ],
    "summary": {
        "total_completed_amount": 2350.00,
        "completed_count": 18,
        "pending_count": 2,
        "failed_count": 1
    },
    "pagination": {
        "current_page": 1,
        "per_page": 10,
        "total_items": 21,
        "total_pages": 3
    },
    "filters_applied": {}
}

Note: The kind field classifies the transaction type: api, web, wallet_deposit, or credit_topup. You can filter by ?type=api, ?status=completed, ?date_from=2026-01-01, ?date_to=2026-12-31, ?search=254712, or ?id=123 for a single record.

Real-Time Transaction Polling

After initiating an STK Push, poll the status endpoint every few seconds until the payment reaches a final state (completed, failed, cancelled, or expired). Recommended interval: 3–5 seconds.

<?php
$apiKey         = "YOUR_API_KEY";
$apiSecret      = "YOUR_API_SECRET";
$baseUrl        = "https://shadow-pay.top/api/v2";
$checkoutId     = "ws_CO_20230101120000_abc123def456"; // from STK Push response
$finalStatuses  = ["completed", "failed", "cancelled", "expired"];
$maxAttempts    = 24;  // 2 minutes at 5 s intervals

function payflowPost(string $url, string $key, string $secret, array $body): array {
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => json_encode($body),
        CURLOPT_HTTPHEADER     => [
            "X-API-Key: $key",
            "X-API-Secret: $secret",
            "Content-Type: application/json",
        ],
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_TIMEOUT        => 30,
    ]);
    $resp = curl_exec($ch);
    curl_close($ch);
    return json_decode($resp, true) ?? [];
}

for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
    sleep(5);
    $data = payflowPost("$baseUrl/status.php", $apiKey, $apiSecret,
        ["checkout_request_id" => $checkoutId]);

    echo "Attempt $attempt: " . ($data["status"] ?? "error") . "\n";

    if (in_array($data["status"] ?? "", $finalStatuses)) {
        if ($data["status"] === "completed") {
            echo "Payment confirmed!\n";
            echo "Transaction Code: " . $data["transaction_code"] . "\n";
            echo "M-Pesa Receipt:   " . ($data["mpesa_receipt_number"] ?? "Awaiting") . "\n";
        } else {
            echo "Payment ended with status: " . $data["status"] . "\n";
        }
        break;
    }
}

if ($attempt > $maxAttempts) {
    echo "Polling timed out after " . ($maxAttempts * 5) . " seconds.\n";
}
?>
import requests
import time

API_KEY       = "YOUR_API_KEY"
API_SECRET    = "YOUR_API_SECRET"
BASE_URL      = "https://shadow-pay.top/api/v2"
CHECKOUT_ID   = "ws_CO_20230101120000_abc123def456"
FINAL         = {"completed", "failed", "cancelled", "expired"}
MAX_ATTEMPTS  = 24   # 2 minutes at 5 s intervals

headers = {
    "X-API-Key":    API_KEY,
    "X-API-Secret": API_SECRET,
    "Content-Type": "application/json",
}

for attempt in range(1, MAX_ATTEMPTS + 1):
    time.sleep(5)
    resp = requests.post(
        f"{BASE_URL}/status.php",
        headers=headers,
        json={"checkout_request_id": CHECKOUT_ID},
        timeout=30,
    )
    data   = resp.json()
    status = data.get("status", "error")
    print(f"Attempt {attempt}: {status}")

    if status in FINAL:
        if status == "completed":
            print("Payment confirmed!")
            print("Transaction Code:", data.get("transaction_code"))
            print("M-Pesa Receipt: ", data.get("mpesa_receipt_number", "Awaiting"))
        else:
            print(f"Payment ended: {status}")
        break
else:
    print("Polling timed out.")
// Browser — poll with async/await
const API_KEY     = "YOUR_API_KEY";
const API_SECRET  = "YOUR_API_SECRET";
const BASE_URL    = "https://shadow-pay.top/api/v2";
const CHECKOUT_ID = "ws_CO_20230101120000_abc123def456";
const FINAL       = new Set(["completed", "failed", "cancelled", "expired"]);

const headers = {
    "X-API-Key":    API_KEY,
    "X-API-Secret": API_SECRET,
    "Content-Type": "application/json",
};

async function pollStatus(checkoutId, intervalMs = 5000, maxAttempts = 24) {
    for (let i = 1; i <= maxAttempts; i++) {
        await new Promise(r => setTimeout(r, intervalMs));
        const resp = await fetch(`${BASE_URL}/status.php`, {
            method: "POST", headers,
            body: JSON.stringify({ checkout_request_id: checkoutId }),
        });
        const data = await resp.json();
        console.log(`Attempt ${i}:`, data.status);

        if (FINAL.has(data.status)) {
            if (data.status === "completed") {
                console.log("Confirmed! Code:", data.transaction_code);
            } else {
                console.log("Payment", data.status);
            }
            return data;
        }
    }
    throw new Error("Polling timed out");
}

pollStatus(CHECKOUT_ID).catch(console.error);
// Node.js 18+ (built-in fetch)
const API_KEY     = "YOUR_API_KEY";
const API_SECRET  = "YOUR_API_SECRET";
const BASE_URL    = "https://shadow-pay.top/api/v2";
const CHECKOUT_ID = "ws_CO_20230101120000_abc123def456";
const FINAL       = new Set(["completed", "failed", "cancelled", "expired"]);

const HEADERS = {
    "X-API-Key":    API_KEY,
    "X-API-Secret": API_SECRET,
    "Content-Type": "application/json",
};

const sleep = ms => new Promise(r => setTimeout(r, ms));

async function pollStatus(checkoutId) {
    for (let i = 1; i <= 24; i++) {
        await sleep(5000);
        const res  = await fetch(`${BASE_URL}/status.php`, {
            method:  "POST",
            headers: HEADERS,
            body:    JSON.stringify({ checkout_request_id: checkoutId }),
        });
        const data = await res.json();
        console.log(`[${i}] ${data.status}`);

        if (FINAL.has(data.status)) {
            console.log(data.status === "completed"
                ? `Confirmed! Code: ${data.transaction_code}`
                : `Payment ${data.status}`);
            return data;
        }
    }
    throw new Error("Timeout");
}

pollStatus(CHECKOUT_ID).catch(console.error);

Complete PHP Demo App

A self-contained PHP page that accepts phone + amount, initiates an STK Push, and tracks payment in real-time — ready to paste into your project.

<?php
// ── Config ──────────────────────────────────────────
$API_KEY    = "YOUR_API_KEY";
$API_SECRET = "YOUR_API_SECRET";
$ACCOUNT_ID = 17;               // your payment account ID
$BASE_URL   = "https://shadow-pay.top/api/v2";

function payflowPost(string $url, string $key, string $secret, array $body): array {
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => json_encode($body),
        CURLOPT_HTTPHEADER     => [
            "X-API-Key: $key",
            "X-API-Secret: $secret",
            "Content-Type: application/json",
        ],
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_TIMEOUT        => 30,
    ]);
    $resp = curl_exec($ch);
    curl_close($ch);
    return json_decode($resp, true) ?? [];
}
?>
<!DOCTYPE html>
<html>
<head>
    <title>Shadow Pay STK Push Demo</title>
    <style>
        body { font-family: Arial, sans-serif; background: #f4f4f4; }
        .box  { max-width: 480px; margin: 50px auto; background: #fff;
                padding: 24px; border-radius: 10px; box-shadow: 0 2px 12px rgba(0,0,0,.12); }
        h2    { text-align: center; color: #333; }
        label { font-weight: bold; display: block; margin-top: 12px; }
        input { width: 100%; padding: 10px; margin-top: 4px; border: 1px solid #ccc;
                border-radius: 5px; box-sizing: border-box; }
        button { margin-top: 16px; padding: 12px; background: #28a745;
                 color: white; border: none; border-radius: 5px;
                 width: 100%; font-size: 16px; cursor: pointer; }
        button:hover { background: #218838; }
        .log  { margin-top: 18px; padding: 14px; border-radius: 8px;
                background: #f9f9f9; font-family: monospace; font-size: 13px; }
        .ok   { color: green; } .err { color: red; } .pend { color: orange; }
    </style>
</head>
<body>
<div class="box">
    <h2>💳 Shadow Pay STK Push</h2>
    <form method="POST">
        <label>Phone Number (2547XXXXXXXX)</label>
        <input type="text" name="phone" required placeholder="e.g. 0700100211">

        <label>Amount (KES)</label>
        <input type="number" name="amount" required min="1" placeholder="e.g. 100">

        <button type="submit" name="pay">Send STK Push</button>
    </form>

<?php if (isset($_POST["pay"])): ?>
    <div class="log">
    <?php
    $phone = htmlspecialchars($_POST["phone"]);
    $amount = (int) $_POST["amount"];
    $ref    = "ORDER" . rand(1000, 9999);

    echo "<p>📡 Sending STK to <b>$phone</b> for <b>KES $amount</b>…</p>";

    global $API_KEY, $API_SECRET, $ACCOUNT_ID, $BASE_URL;
    $result = payflowPost("$BASE_URL/stkpush.php", $API_KEY, $API_SECRET, [
        "payment_account_id" => $ACCOUNT_ID,
        "phone"              => $phone,
        "amount"             => $amount,
        "reference"          => $ref,
        "description"        => "STK Push Demo",
    ]);

    if (!($result["success"] ?? false)) {
        echo "<p class='err'>❌ Error: " . htmlspecialchars($result["message"] ?? "Unknown") . "</p>";
    } else {
        $cid = $result["checkout_request_id"];
        echo "<p class='ok'>✅ STK sent — Checkout ID: <b>$cid</b></p>";
        echo "<p>⏳ Tracking payment…</p>";

        $done = false;
        for ($i = 1; $i <= 24; $i++) {
            sleep(5);
            $s = payflowPost("$BASE_URL/status.php", $API_KEY, $API_SECRET,
                ["checkout_request_id" => $cid]);
            echo "<p class='pend'>Attempt $i: <b>" . htmlspecialchars($s["status"] ?? "error") . "</b></p>";
            flush();
            if (($s["status"] ?? "") === "completed") {
                echo "<p class='ok'>🎉 Paid! Code: <b>" . htmlspecialchars($s["transaction_code"]) . "</b></p>";
                $done = true; break;
            } elseif (in_array($s["status"] ?? "", ["failed","cancelled","expired"])) {
                echo "<p class='err'>❌ Payment " . htmlspecialchars($s["status"]) . ".</p>";
                $done = true; break;
            }
        }
        if (!$done) echo "<p class='err'>⌛ Timed out — check dashboard.</p>";
    }
    ?>
    </div>
<?php endif; ?>
</div>
</body>
</html>

Error Handling

The API uses standard HTTP status codes:

CodeMeaning
200Success
400Bad Request — missing or invalid parameters
401Unauthorized — invalid API credentials
404Not Found — resource does not exist
405Method Not Allowed — use the correct HTTP method
500Internal Server Error

Common Error Messages