Accept Raast QR payments on your website with two API calls and one callback.
POST /payments with the amount and your order ID.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.return_url.Base URL: https://pay.pkpay.site/api/v1 · Currency: PKR · All bodies are JSON.
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.
| Header | Value |
|---|---|
X-Api-Key | Your API key (pk_…) |
X-Timestamp | Current Unix time in seconds (must be within 300 s of server time) |
X-Signature | Hex HMAC-SHA256 of the string to sign, keyed with your API secret |
Idempotency-Key | Required 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://.
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." } }
| Allowlist | Applies to |
|---|---|
| Pay-In | POST /payments, GET /payments/{id} |
| Pay-Out | POST /payouts, GET /payouts, GET /payouts/{ref} |
| Either | GET /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.
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 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.
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"
}
}
| Field | Meaning |
|---|---|
earned | Net of every paid payment (amount minus fee) |
frozen | Held for withdrawals you have requested that are still pending |
paid_out | Withdrawals already sent to you |
available | earned − frozen − paid_out — what you can withdraw now |
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.
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.
{ "error": { "code": "validation_error", "message": "...", "fields": { "amount": ["..."] } } }
| HTTP | code | Meaning |
|---|---|---|
| 400 | idempotency_key_required | Missing or invalid Idempotency-Key header |
| 401 | unauthenticated | Authentication failed (missing/invalid key) |
| 401 | invalid_signature | The X-Signature did not match |
| 401 | timestamp_out_of_range | X-Timestamp is outside the allowed window |
| 401 | replayed_request | This signature was already used (replay) |
| 403 | ip_not_allowed | Request IP is not in your allowlist for this side |
| 403 | merchant_disabled | The merchant account is disabled |
| 404 | not_found | Unknown payment or payout |
| 409 | idempotency_conflict | Same Idempotency-Key used with a different body |
| 422 | validation_error | Invalid input (see fields) |
| 422 | invalid_method | Unsupported payout method |
| 422 | amount_out_of_range | Amount is outside the configured min/max |
| 422 | insufficient_balance | Withdrawal exceeds your available balance |
| 429 | rate_limited | Too many requests; retry later |
| 503 | no_account_available | No payment account can accept right now |
| 503 | payin_disabled | Pay-in is disabled |
| 503 | payout_disabled | Pay-out is disabled |
<?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)
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" }
| HTTP | code | Meaning |
|---|---|---|
| 401 | unauthenticated | Authentication failed (missing/invalid key) |
| 404 | not_found | Unknown payment or payout |
| 404 | no_match | No payment matches the reference/amount |
| 409 | amount_mismatch | Amount does not match the payment |
| 409 | ambiguous_match | More than one payment matches; be more specific |
| 409 | verification_window_closed | Payment can no longer be auto-verified |
| 409 | not_verifiable | Payment status cannot be verified |
| 422 | validation_error | Invalid input (see fields) |
| 429 | rate_limited | Too many requests; retry later |
| 503 | verification_disabled | Verification 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.