Documentation

Integrate PAKPAY in an afternoon

One POST to create a payment, one endpoint to receive the webhook. No SDK to install, no sandbox that behaves differently from production.

Overview

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.

Authentication

You get two keys in your merchant panel.

KeyPrefixUse
Public keypk_live_… Sent as api_key on every request. Identifies you.
Secret keysk_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.

Keep the secret key on your server only. Anyone holding it can create payments and request withdrawals on your account. Rotate it from the panel if it leaks.

Signing requests

Build the signature from the parameters you are about to send:

  1. Drop sign itself, plus any empty or null values.
  2. Sort the remaining parameters by key, ascending, byte order.
  3. Join them as key=value with & between.
  4. HMAC-SHA256 that string with your secret key. Send the hex digest, lowercase.
// 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.

Create a payment

POST https://pakpay.org/api/v1/create.php

ParameterTypeRequiredDescription
api_keystringYesYour public key.
order_idstringYes Your own reference, unique per merchant, max 80 characters. Sending the same one again returns the existing payment link instead of creating a duplicate.
amountnumberYes Amount in PKR, greater than zero and inside your account limits.
return_urlstringYes Where we send the customer's browser once the wallet step is over.
methodstringNo easypaisa or jazzcash. Omit to let the customer choose on our checkout page.
callback_urlstringNo Overrides your saved webhook URL for this one payment.
customer_namestringNoShown on the checkout page.
customer_emailstringNoFor your own records.
customer_phonestringNoFor your own records.
signstringYesSee 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.

Check a payment

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_statusMeaning
createdLink issued, the customer has not opened the wallet yet.
pendingSent to the wallet, waiting for the customer to approve.
successPaid and credited to your balance. Safe to fulfil.
failedDeclined or cancelled. No money moved.
expiredThe link ran out before it was used.

Balance & fees

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.

Request a withdrawal

POST https://pakpay.org/api/v1/payout.php

ParameterRequiredDescription
api_keyYesYour public key.
amountYesDebited from your balance. The fee comes out of this.
methodYeseasypaisa or jazzcash.
account_nameYesThe account holder's name, as registered on the wallet.
account_numberYesMobile number of the wallet, digits only.
signYesSee 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.

Webhooks

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.

EventFires when
payment.successA payment confirmed and your balance was credited.
payment.failedA payment was declined or cancelled.
payout.successA withdrawal reached the beneficiary.
payout.failedA withdrawal failed; the amount is back in your balance.
test.pingYou 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…"
}
Handle repeats. A webhook can arrive more than once — check whether you have already credited that order_id before acting on it.

Verifying a webhook

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

Errors

A failure always returns {"status": false, "error": "…"} with a matching HTTP status.

HTTPTypical errorWhat to do
400Missing or invalid parameterRead the message; it names the field.
401Invalid api_key / Invalid signatureCheck your keys and the signing order.
403Merchant account is suspended / IP not whitelistedContact support, or add the IP in your panel.
404Order not foundThe reference does not belong to your account.
409order_id already usedThat reference is already settled. Use a new one.

Full example

// 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']);

Go-live checklist

  • Webhook URL saved in the panel, and “Send test event” returns HTTP 200.
  • Your handler verifies sign before crediting anything.
  • Your handler is safe to call twice with the same order_id.
  • You fulfil on payment.success, never on the return URL alone.
  • order_id is unique for every payment you create.
  • Secret key lives in server config, never in client-side code or a repository.
  • Server IPs added to the whitelist once your addresses are stable.

Need a hand?

Send us your merchant ID and the order reference you are stuck on and we will look at the exact request in our logs.