QR PKPay
Merchant login

Merchant API integration

Accept Raast QR payments on your website with two API calls and one callback.

How it works

  1. Your server creates a payment: POST /payments with the amount and your order ID.
  2. You receive payment_id, transaction_id, checkout_url and a short short_url. Redirect the customer to either — short_url is a short, anonymous link that does not reveal the payment id.
  3. The customer scans a dynamic QR with the exact amount. The QR expires after 5 minutes.
  4. When the payment is verified (or expires) we POST a signed callback to your server and the customer is sent back to your return_url.

Base URL: https://pay.pkpay.site/api/v1 · Currency: PKR · All bodies are JSON.

Authentication & signing

Sign in to the merchant portal with the login your gateway admin gave you. It shows your Merchant ID, API key and API secret, checks your request signatures, and runs a test Pay-In with a live QR. Never put the secret in browser or mobile code.

HeaderValue
X-Api-KeyYour API key (pk_…)
X-TimestampCurrent Unix time in seconds (must be within 300 s of server time)
X-SignatureHex HMAC-SHA256 of the string to sign, keyed with your API secret
Idempotency-KeyRequired on POST. A unique value per order attempt (8–128 chars). Retrying with the same key returns the same payment instead of creating a duplicate.

String to sign (four parts joined with a newline \n):

timestamp + "\n" + HTTP_METHOD + "\n" + REQUEST_URI + "\n" + RAW_BODY

REQUEST_URI is the path plus query string exactly as requested, e.g. /api/v1/payments. For GET the body is an empty string. A signature cannot be reused; the only exception is an exact retry of a POST with the same Idempotency-Key, which safely returns the original payment.

Requests are rate limited to about 60 per minute. Always use https://.

IP allowlist (required)

Your API is locked to the IP addresses your gateway admin sets for you, kept separately for pay-in (deposit) and pay-out (withdrawal). A request from any other address is refused:

HTTP 403
{ "error": { "code": "ip_not_allowed", "message": "This IP address is not allowed for this operation." } }
AllowlistApplies to
Pay-InPOST /payments, GET /payments/{id}
Pay-OutPOST /payouts, GET /payouts, GET /payouts/{ref}
EitherGET /balance (allowed from a pay-in or pay-out IP)

Strict: if a side's list is empty, that side is blocked until the admin adds an IP. Each entry is a single IP or a CIDR range (IPv4 or IPv6), for example 203.0.113.10 or 203.0.113.0/24. Your current IP and configured allowlists are shown on your portal dashboard — send the admin the server IP you call from.

Create a payment

POST https://pay.pkpay.site/api/v1/payments

{
  "amount": 1500.00,
  "order_id": "ORDER-10045",
  "description": "2 x T-shirt",
  "customer": { "name": "Ali Khan", "email": "ali@example.com", "phone": "+923001234567" },
  "callback_url": "https://shop.example.com/payments/callback",
  "return_url": "https://shop.example.com/orders/10045",
  "metadata": { "cart_id": "88123" }
}

Only amount and order_id are required. Amount must be between 1 and 500,000 PKR. Response 201 Created (or 200 with Idempotent-Replayed: true for a retry):

{
  "data": {
    "payment_id": "pay_01jbd3m4w2k8x9t7q5v6n3r2p1",
    "transaction_id": "TXN261028153045123456",
    "order_id": "ORDER-10045",
    "amount": "1500.00",
    "currency": "PKR",
    "fee": "0.00",
    "net_amount": "1500.00",
    "status": "pending",
    "checkout_url": "https://pay.pkpay.site/checkout/pay_01jbd3m4w2k8x9t7q5v6n3r2p1",
    "short_url": "https://pay.pkpay.site/s/k2x9qm4w7a",
    "expires_at": "2026-10-28T15:35:45+00:00",
    "paid_at": null,
    "created_at": "2026-10-28T15:30:45+00:00"
  }
}

Get payment status

GET https://pay.pkpay.site/api/v1/payments/{payment_id}

Returns the same object. Statuses: pending, paid, expired, failed, cancelled . Use this as a fallback if a callback is missed.

Balance

Your current balance, all amounts in PKR:

GET https://pay.pkpay.site/api/v1/balance
{
  "data": {
    "available": "4250.00",
    "frozen": "1000.00",
    "paid_out": "7500.00",
    "earned": "12750.00",
    "currency": "PKR"
  }
}
FieldMeaning
earnedNet of every paid payment (amount minus fee)
frozenHeld for withdrawals you have requested that are still pending
paid_outWithdrawals already sent to you
availableearned − frozen − paid_out — what you can withdraw now

Withdrawals (Pay-Out)

Request a withdrawal of your available balance. Allowed methods: easypaisa, jazzcash. The amount (between 100 and 100,000 PKR) is frozen immediately; an operator sends the money manually and then marks it paid.

POST https://pay.pkpay.site/api/v1/payouts

{
  "method": "easypaisa",            // one of: easypaisa, jazzcash
  "account_number": "03001234567",  // the mobile wallet number
  "account_name": "Ali Khan",       // optional
  "amount": 1000.00,
  "note": "weekly settlement"       // optional
}

Response 201 Created:

{
  "data": {
    "payout_ref": "PO2610301A2B3C",
    "method": "easypaisa",
    "account_number": "03001234567",
    "account_name": "Ali Khan",
    "amount": "1000.00",
    "currency": "PKR",
    "status": "pending",
    "note": "weekly settlement",
    "reference": null,
    "created_at": "2026-10-30T09:15:00+00:00",
    "processed_at": null
  }
}

Statuses: pending, paid, rejected — pending (requested, amount frozen), paid (money sent — moves from frozen to paid out), rejected (amount returns to your available balance). List or fetch:

GET https://pay.pkpay.site/api/v1/payouts
GET https://pay.pkpay.site/api/v1/payouts/{payout_ref}

A 422 with code insufficient_balance means the amount is more than your available balance. Withdrawals can also be requested from the merchant portal.

Callbacks

On every status change we POST to your callback_url:

{
  "event": "payment.paid",
  "data": { …same payment object… },
  "sent_at": "2026-10-28T15:32:10+00:00"
}

Verify headers X-Timestamp and X-Signature = hex HMAC-SHA256 of timestamp + "." + raw_body with your API secret. Respond with HTTP 2xx. Failed deliveries are retried with exponential backoff. Always treat callbacks idempotently.

Errors

{ "error": { "code": "validation_error", "message": "...", "fields": { "amount": ["..."] } } }
HTTPcodeMeaning
400idempotency_key_requiredMissing or invalid Idempotency-Key header
401unauthenticatedAuthentication failed (missing/invalid key)
401invalid_signatureThe X-Signature did not match
401timestamp_out_of_rangeX-Timestamp is outside the allowed window
401replayed_requestThis signature was already used (replay)
403ip_not_allowedRequest IP is not in your allowlist for this side
403merchant_disabledThe merchant account is disabled
404not_foundUnknown payment or payout
409idempotency_conflictSame Idempotency-Key used with a different body
422validation_errorInvalid input (see fields)
422invalid_methodUnsupported payout method
422amount_out_of_rangeAmount is outside the configured min/max
422insufficient_balanceWithdrawal exceeds your available balance
429rate_limitedToo many requests; retry later
503no_account_availableNo payment account can accept right now
503payin_disabledPay-in is disabled
503payout_disabledPay-out is disabled

PHP example

<?php
$apiKey    = 'pk_your_key';
$apiSecret = 'sk_your_secret';
$baseUrl   = 'https://pay.pkpay.site/api/v1';

function gatewayRequest(string $method, string $path, ?array $payload = null): array
{
    global $apiKey, $apiSecret, $baseUrl;

    $url  = $baseUrl . $path;
    $body = $payload === null ? '' : json_encode($payload);
    $ts   = (string) time();
    $uri  = parse_url($url, PHP_URL_PATH) . (parse_url($url, PHP_URL_QUERY) ? '?' . parse_url($url, PHP_URL_QUERY) : '');
    $sig  = hash_hmac('sha256', $ts . "\n" . $method . "\n" . $uri . "\n" . $body, $apiSecret);

    $headers = ["Content-Type: application/json", "Accept: application/json",
                "X-Api-Key: $apiKey", "X-Timestamp: $ts", "X-Signature: $sig"];
    if ($method === 'POST') {
        $headers[] = 'Idempotency-Key: order-' . $payload['order_id'];
    }

    $ch = curl_init($url);
    curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $headers,
                            CURLOPT_POSTFIELDS => $body ?: null, CURLOPT_RETURNTRANSFER => true]);
    $response = json_decode(curl_exec($ch), true);
    curl_close($ch);

    return $response;
}

$payment = gatewayRequest('POST', '/payments', ['amount' => 1500, 'order_id' => 'ORDER-10045',
    'return_url' => 'https://shop.example.com/orders/10045']);

header('Location: ' . $payment['data']['checkout_url']);

// Query your balance / request a withdrawal:
$balance = gatewayRequest('GET', '/balance');
// $payout = gatewayRequest('POST', '/payouts', ['method' => 'easypaisa',
//     'account_number' => '03001234567', 'amount' => 1000]);

// callback.php: verify before trusting
$raw = file_get_contents('php://input');
$expected = hash_hmac('sha256', $_SERVER['HTTP_X_TIMESTAMP'] . '.' . $raw, $apiSecret);
if (!hash_equals($expected, $_SERVER['HTTP_X_SIGNATURE'] ?? '')) { http_response_code(401); exit; }
$event = json_decode($raw, true);
// mark order paid when $event['data']['status'] === 'paid' (check amount + order_id)

Payment verification API (gateway operators)

A trusted verifier can confirm a received payment. Enable it in Admin → Settings → Verification.

POST https://pay.pkpay.site/api/v1/verifications
X-Verifier-Key: vk_…

{ "transaction_id": "TXN261028153045123456", "reference": "BANK-REF-123" }
// or match by exact amount (optionally per account):
{ "amount": 1500.00, "account_id": 3, "reference": "BANK-REF-123" }
HTTPcodeMeaning
401unauthenticatedAuthentication failed (missing/invalid key)
404not_foundUnknown payment or payout
404no_matchNo payment matches the reference/amount
409amount_mismatchAmount does not match the payment
409ambiguous_matchMore than one payment matches; be more specific
409verification_window_closedPayment can no longer be auto-verified
409not_verifiablePayment status cannot be verified
422validation_errorInvalid input (see fields)
429rate_limitedToo many requests; retry later
503verification_disabledVerification API is disabled

Security: requests are signed (HMAC-SHA256) and single-use, timestamped within a 300 s window, IP-allowlisted per side, rate limited and idempotent on POST; secrets are stored encrypted and never returned.