Hosted QR checkout
Create a checkout
One server call per order. The answer is the link the customer pays on.
The request
POST https://api.antpay.network/api/v1/checkouts with your X-Api-Key.
| Field | Required | What to send |
|---|---|---|
reference | yes | Your order id. Unique per API key; a repeat returns 400. 1 to 120 characters from letters, digits, ., _ and -, and no ... The API accepts other characters when you create, but a reference with /, ;, \ or .. in it cannot be read back with a status call, so the samples refuse it. |
amount | yes | PKR, whole rupees, minimum 300: 1500, not 1500.50. |
paymentMethod | yes | Always "qr". |
paymentType | yes | Always "onetime". |
returnUrl | yes | Where the page sends the customer after a final result. Use https. No parameters are added, so that page must read the order status from your own system. |
expiresIn | no | Minutes the customer has to submit their details. Default 15, at most 1380. |
user | no | Any object of your own. It comes back in the postback as user. |
accountNumber, walletName | no | The customer's wallet number and wallet, if you already know them. See Two ways the page opens. |
successUrl | no | Used as the postback URL only when none is set on your key or your account. |
cURL
# Step 1: create the checkout. Put the request in a file, so the JSON is exactly what you wrote, for example
# {"reference":"ORD-10021","amount":1500,"paymentMethod":"qr","paymentType":"onetime","returnUrl":"https://shop.example.com/orders/ORD-10021"}
# The answer carries checkoutUrl: send the customer there.
antpay_create_checkout() { # antpay_create_checkout FILE_WITH_JSON_BODY
antpay_call POST /api/v1/checkouts "$(cat "$1")"
}Part of antpay.sh, the complete tested file
Node.js
// Step 1: create the checkout on your server, store the checkoutId, redirect the customer.
async function createCheckout(order) {
if (!REFERENCE.test(order.id)) throw new Error('reference: use letters, digits, ".", "_" and "-" only (1 to 120 characters)');
const { http: status, json } = await call('POST', '/api/v1/checkouts', {
reference: order.id, // your order id, unique per API key
amount: order.total, // whole rupees, minimum 300
paymentMethod: 'qr',
paymentType: 'onetime',
returnUrl: `https://shop.example.com/orders/${encodeURIComponent(order.id)}`,
});
if (status === 200 && json && json.success) {
return { checkoutId: json.checkoutId, checkoutUrl: json.checkoutUrl, expiresAt: json.expiresAt };
}
// 4xx: nothing was created (a duplicate reference already exists: look it up).
// 5xx or no answer: the checkout may exist. Ask by reference before you retry.
const error = new Error(`checkout not created (HTTP ${status})`);
error.status = status;
error.body = json;
throw error;
}Part of antpay.js, the complete tested file
PHP
// Step 1: create the checkout on your server, store the checkoutId, redirect the customer.
function antpay_create_checkout(string $orderId, int $total): array {
if (!preg_match(ANTPAY_REFERENCE, $orderId)) throw new InvalidArgumentException('reference: use letters, digits, ".", "_" and "-" only (1 to 120 characters)');
[$status, $data] = antpay_call('POST', '/api/v1/checkouts', [
'reference' => $orderId, // your order id, unique per API key
'amount' => $total, // whole rupees, minimum 300
'paymentMethod' => 'qr',
'paymentType' => 'onetime',
'returnUrl' => 'https://shop.example.com/orders/' . rawurlencode($orderId),
]);
if ($status === 200 && is_array($data) && !empty($data['success'])) {
return ['checkoutId' => $data['checkoutId'], 'checkoutUrl' => $data['checkoutUrl'], 'expiresAt' => $data['expiresAt']];
}
// 4xx: nothing was created (a duplicate reference already exists: look it up).
// 5xx or no answer: the checkout may exist. Ask by reference before you retry.
throw new RuntimeException("checkout not created (HTTP $status): " . json_encode($data));
}Part of antpay.php, the complete tested file
Python
# Step 1: create the checkout on your server, store the checkoutId, redirect the customer.
def create_checkout(order_id, total):
if not REFERENCE.match(order_id):
raise ValueError('reference: use letters, digits, ".", "_" and "-" only (1 to 120 characters)')
status, data = call("POST", "/api/v1/checkouts", {
"reference": order_id, # your order id, unique per API key
"amount": total, # whole rupees, minimum 300
"paymentMethod": "qr",
"paymentType": "onetime",
"returnUrl": "https://shop.example.com/orders/" + urllib.parse.quote(order_id, safe=""),
})
if status == 200 and data and data.get("success"):
return {"checkoutId": data["checkoutId"], "checkoutUrl": data["checkoutUrl"], "expiresAt": data["expiresAt"]}
# 4xx: nothing was created (a duplicate reference already exists: look it up).
# 5xx or no answer: the checkout may exist. Ask by reference before you retry.
raise RuntimeError(f"checkout not created (HTTP {status}): {data}")Part of antpay.py, the complete tested file
The answer
{
"success": true,
"checkoutUrl": "https://pay.antpay.network/qr/3f6c1d2e-8a4b-4c1e-9d7a-2b5e6f7a8c90",
"checkoutId": "3f6c1d2e-8a4b-4c1e-9d7a-2b5e6f7a8c90",
"expiresAt": "2026-11-01T10:15:00.000Z"
}
checkoutUrl: send the customer here, exactly as returned.checkoutId: store it. It identifies the postback and rebuilds the link ashttps://pay.antpay.network/qr/<checkoutId>.expiresAt: when the link stops accepting customer details. It is not the payment deadline.
The answer has no transaction id. That arrives in the postback as transactionId, and in the status calls as txnId.
Redirect the customer
Use a full-page redirect (HTTP 303) or a link. Do not put the page inside an iframe. Create a new checkout for each order; never reuse a link for another order or customer. You never call the payment page's own addresses.
Two ways the page opens
You send accountNumber + walletName | You do not send them |
|---|---|
| The page opens straight on the QR code, for example "Complete your payment using Easypaisa". No wallet choice, no form. | The page opens on the form: the customer picks the wallet, enters the mobile number and the account name, then sees the QR. |
| Field | Allowed values |
|---|---|
accountNumber | Mobile wallet number: 03XXXXXXXXX or +923XXXXXXXXX. Spaces and dashes are ignored. |
walletName | easypaisa, jazzcash, sadapay, nayapay (any letter case) |
- Send both or neither. Only one of them returns
400. - The customer sees the number masked, for example
0312•••4567. The account name is never shown. - If the number is not found at the wallet, the customer gets the form with the wallet preselected and types another number. You do nothing.
- In the postback,
mobileis the number the customer actually paid from. It can differ from youraccountNumber. Match the postback to your order bycheckoutId(ortransactionId), never byreferencealone and never by mobile number.
{
"reference": "ORD-10021",
"amount": 1500,
"paymentMethod": "qr",
"paymentType": "onetime",
"returnUrl": "https://shop.example.com/orders/ORD-10021",
"expiresIn": 15,
"user": {
"customerId": "C-778"
},
"accountNumber": "03121234567",
"walletName": "easypaisa"
}
Amounts: whole rupees only
amount must be whole rupees and at least 300. 1500.00 is accepted; 1500.50 returns 400 with DECIMAL_VALUES_NOT_SUPPORTED, and an amount under 300 returns 400 with AMOUNT_NOT_SUPPORTED. Nothing is created in either case.
What the customer sees
| Screen | When | What it shows |
|---|---|---|
| Enter details | The link is open and no QR exists yet | The order amount; the wallet choice (Easypaisa, JazzCash, NayaPay, SadaPay); the wallet's mobile number and account name. Each link allows 5 submissions. Skipped when you send accountNumber + walletName. |
| Scan and pay | Right after they submit | The QR, "Pay exactly Rs amount", the wallet to pay with, a 10:00 timer and a button to save the QR as an image. |
| Payment received | Paid | The amount, the payment reference and the time in Pakistan time. If the customer watched the payment finish, the page returns them to your returnUrl after about 3 seconds; otherwise it shows a Return button. |
| Payment not completed / expired | Failed, or not paid in time | The amount, the reference and the time, and a Return button. |
| This payment link has expired | The link expired before the customer submitted details | A message to go back and start again, and a Return button. |
The page carries the AntPay name. It never shows the customer's name back to them, and it shows the mobile number only when you sent accountNumber, masked.
Duplicate references
A second POST /api/v1/checkouts with a reference you already used returns 400, and that answer carries no link. Read the existing payment with GET /api/v1/transactions/{reference} and rebuild the link from its checkoutId.
Do not rely on this check to stop a double create. It compares against payments that already exist. Two creates for the same reference that overlap (the usual cause: your first call was still running when you gave up and sent it again) can both be accepted, and you then have two checkouts for one order. Three habits keep that from hurting you:
- Wait at least 80 seconds for the create call before you treat it as failed.
- After a timeout or a
5xx, ask by reference first (next section) and create again only when the API says no payment exists. - Store the
checkoutIdof the checkout you sent the customer to, and accept postbacks and status results only for that id. A result for anothercheckoutIdwith the same reference belongs to a checkout nobody was sent to: it will expire by itself and must not change your order.
When the create call fails or times out
| You got | What it means | What to do |
|---|---|---|
4xx | Nothing was created (except a duplicate, which already exists). | Fix the cause shown in Errors, then send it again. |
5xx, a timeout, no answer | The checkout may or may not exist. | Ask GET /api/v1/transactions/{reference} first. Found: store its checkoutId and use that checkout. 404 with "error": "Transaction not found": nothing was created, create again with the same reference, and store the new checkoutId. Any other answer (another 404, a 5xx, a timeout): you do not know yet; ask again later, and do not create again until you get one of the first two. |