QR PKPay

Merchant API integration

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

1. 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 and checkout_url. Redirect the customer to the checkout URL.
  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://pkpay.site/api/v1 · Currency: PKR · All bodies are JSON.

2. Authentication & signing

Get your API key and API secret from the gateway admin. 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.

3. Create a 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"
  }
}

4. Get payment status

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.

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

6. Errors

{ "error": { "code": "validation_error", "message": "...", "fields": { "amount": ["..."] } } }
HTTPcodeMeaning
400idempotency_key_requiredMissing or invalid Idempotency-Key header
401unauthenticated, invalid_signature, timestamp_out_of_range, replayed_requestAuthentication failed
404not_foundUnknown payment
409idempotency_conflictSame Idempotency-Key used with a different body
422validation_errorInvalid input (see fields)
429rate_limitedToo many requests; retry later
503no_account_available, payin_disabledTemporarily unable to accept payments

7. PHP example

<?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)

8. Payment verification API (gateway operators)

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.