One POST to create a payment, one endpoint to receive the webhook. No SDK to install, no sandbox that behaves differently from production.
PAKPAY gives you one API for EasyPaisa and JazzCash. Your server creates a payment, we return a hosted checkout URL, the customer approves it in their wallet, and we notify your server the moment the money lands.
Every endpoint is a POST that accepts
application/x-www-form-urlencoded or JSON, and always replies with JSON.
Base URL:
https://pakpay.org/api/v1
Amounts are in PKR. Fees are deducted from the amount collected, so a PKR 1,000 payment at 3.5% credits PKR 965 to your balance.
You get two keys in your merchant panel.
| Key | Prefix | Use |
|---|---|---|
| Public key | pk_live_… | Sent as api_key on every request. Identifies you. |
| Secret key | sk_live_… | Never transmitted. Used to compute the sign
on requests and to verify the sign on our webhooks. |
You may also send the public key as an X-Api-Key header and the
signature as X-Signature instead of in the body.
Build the signature from the parameters you are about to send:
sign itself, plus any empty or null values.key=value with & between.// PHP — this is the whole thing function pakpay_sign(array $params, $secret) { unset($params['sign']); ksort($params); $parts = []; foreach ($params as $k => $v) { if ($v === '' || $v === null) continue; $parts[] = $k . '=' . $v; } return hash_hmac('sha256', implode('&', $parts), $secret); }
For example, amount=1000, api_key=pk_live_abc,
order_id=INV-1 signs the string
amount=1000&api_key=pk_live_abc&order_id=INV-1.
POST https://pakpay.org/api/v1/create.php
| Parameter | Type | Required | Description |
|---|---|---|---|
| api_key | string | Yes | Your public key. |
| order_id | string | Yes | Your own reference, unique per merchant, max 80 characters. Sending the same one again returns the existing payment link instead of creating a duplicate. |
| amount | number | Yes | Amount in PKR, greater than zero and inside your account limits. |
| return_url | string | Yes | Where we send the customer's browser once the wallet step is over. |
| method | string | No | easypaisa or jazzcash. Omit to let
the customer choose on our checkout page. |
| callback_url | string | No | Overrides your saved webhook URL for this one payment. |
| customer_name | string | No | Shown on the checkout page. |
| customer_email | string | No | For your own records. |
| customer_phone | string | No | For your own records. |
| sign | string | Yes | See signing. |
// Request POST https://pakpay.org/api/v1/create.php Content-Type: application/x-www-form-urlencoded api_key=pk_live_xxx&amount=1000.00&order_id=INV-1042 &return_url=https://yourstore.pk/thanks&sign=a1b2c3… // Response 200 { "status": true, "order_id": "PK202609052336491840", "merchant_order_id": "INV-1042", "amount": 1000.00, "fee": 35.00, "net_amount": 965.00, "currency": "PKR", "payment_url": "https://pakpay.org/pay/?t=…", "expires_at": "2026-09-06 00:06:49" }
Redirect the customer to payment_url. Do not treat the payment as
paid until you receive the webhook — a customer reaching your
return_url proves nothing on its own.
POST https://pakpay.org/api/v1/status.php
Send api_key, order_id (yours or ours) and
sign. Use this to reconcile, never as a substitute for the webhook.
{
"status": true,
"order_id": "PK202609052336491840",
"merchant_order_id": "INV-1042",
"payment_status": "success",
"amount": 1000.00,
"fee": 35.00,
"net_amount": 965.00,
"method": "easypaisa",
"settled": true,
"paid_at": "2026-09-05 23:41:02"
}
| payment_status | Meaning |
|---|---|
| created | Link issued, the customer has not opened the wallet yet. |
| pending | Sent to the wallet, waiting for the customer to approve. |
| success | Paid and credited to your balance. Safe to fulfil. |
| failed | Declined or cancelled. No money moved. |
| expired | The link ran out before it was used. |
POST https://pakpay.org/api/v1/balance.php
Send api_key and sign. Returns your
available balance, money locked in withdrawals, lifetime totals and your current fee schedule.
POST https://pakpay.org/api/v1/payout.php
| Parameter | Required | Description |
|---|---|---|
| api_key | Yes | Your public key. |
| amount | Yes | Debited from your balance. The fee comes out of this. |
| method | Yes | easypaisa or jazzcash. |
| account_name | Yes | The account holder's name, as registered on the wallet. |
| account_number | Yes | Mobile number of the wallet, digits only. |
| sign | Yes | See signing. |
{
"status": true,
"reference_id": "PO202609052346509616",
"amount": 1000.00,
"fee": 40.00,
"net_amount": 960.00,
"payout_status": "processing"
}
Your balance is debited immediately. If the payout later fails, the full amount is returned to
your balance and you receive a payout.failed webhook.
Set your endpoint in the merchant panel. We POST JSON to it with a
Content-Type: application/json header. Reply
200 as soon as you have stored the event — anything else is treated as
a failure and retried after 1 minute, 5 minutes, 15 minutes, 1 hour and 6 hours.
| Event | Fires when |
|---|---|
| payment.success | A payment confirmed and your balance was credited. |
| payment.failed | A payment was declined or cancelled. |
| payout.success | A withdrawal reached the beneficiary. |
| payout.failed | A withdrawal failed; the amount is back in your balance. |
| test.ping | You pressed “Send test event” in the panel. |
// payment.success body { "order_id": "INV-1042", "pakpay_id": "PK202609052336491840", "amount": 1000.00, "fee": 35.00, "net_amount": 965.00, "currency": "PKR", "method": "easypaisa", "payment_status": "success", "event": "payment.success", "timestamp": 1757110862, "sign": "9f8c…" }
order_id before acting on it.// webhook.php on your server $secret = 'sk_live_your_secret'; $body = json_decode(file_get_contents('php://input'), true); $sign = $body['sign'] ?? ''; unset($body['sign']); ksort($body); $parts = []; foreach ($body as $k => $v) { if ($v === '' || $v === null) continue; $parts[] = $k . '=' . $v; } $expected = hash_hmac('sha256', implode('&', $parts), $secret); if (!hash_equals($expected, $sign)) { http_response_code(403); exit('bad signature'); } if ($body['event'] === 'payment.success') { // mark $body['order_id'] paid — only if it is not already paid } echo 'ok';
Booleans arrive as JSON true/false;
cast them to the same string form on both sides if your language stringifies them differently.
A failure always returns {"status": false, "error": "…"} with a
matching HTTP status.
| HTTP | Typical error | What to do |
|---|---|---|
| 400 | Missing or invalid parameter | Read the message; it names the field. |
| 401 | Invalid api_key / Invalid signature | Check your keys and the signing order. |
| 403 | Merchant account is suspended / IP not whitelisted | Contact support, or add the IP in your panel. |
| 404 | Order not found | The reference does not belong to your account. |
| 409 | order_id already used | That reference is already settled. Use a new one. |
// pakpay.php — a complete working client define('PAKPAY_BASE', 'https://pakpay.org/api/v1'); define('PAKPAY_PUBLIC', 'pk_live_your_public_key'); define('PAKPAY_SECRET', 'sk_live_your_secret_key'); function pakpay_call($endpoint, array $params) { $params['api_key'] = PAKPAY_PUBLIC; $params['sign'] = pakpay_sign($params, PAKPAY_SECRET); $ch = curl_init(PAKPAY_BASE . '/' . $endpoint . '.php'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => http_build_query($params), CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, ]); $out = json_decode(curl_exec($ch), true); curl_close($ch); return $out; } // create a payment and send the customer to it $r = pakpay_call('create', [ 'order_id' => 'INV-1042', 'amount' => '1000.00', 'return_url' => 'https://yourstore.pk/thanks', ]); if (!empty($r['status'])) { header('Location: ' . $r['payment_url']); exit; } die('PAKPAY error: ' . $r['error']);
sign before crediting anything.order_id.payment.success, never on the return URL alone.order_id is unique for every payment you create.Send us your merchant ID and the order reference you are stuck on and we will look at the exact request in our logs.