Accept 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 and checkout_url. Redirect the customer to the checkout URL.return_url.Base URL: https://pkpay.site/api/v1 · Currency: PKR · All bodies are JSON.
Get your API key and API secret from the gateway admin. 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.
POST https://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. 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://pkpay.site/checkout/pay_01jbd3m4w2k8x9t7q5v6n3r2p1",
"expires_at": "2026-10-28T15:35:45+00:00",
"paid_at": null,
"created_at": "2026-10-28T15:30:45+00:00"
}
}
GET https://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.
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, invalid_signature, timestamp_out_of_range, replayed_request | Authentication failed |
| 404 | not_found | Unknown payment |
| 409 | idempotency_conflict | Same Idempotency-Key used with a different body |
| 422 | validation_error | Invalid input (see fields) |
| 429 | rate_limited | Too many requests; retry later |
| 503 | no_account_available, payin_disabled | Temporarily unable to accept payments |
<?php
$apiKey = 'pk_your_key';
$apiSecret = 'sk_your_secret';
$baseUrl = 'https://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']);
// 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://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" }
Security: secrets are stored encrypted, every request is signed and rate limited, and payment requests are idempotent.