$timestamp, 'X-Signature' => hash_hmac('sha256', $canonical, ANTPAY_SIGNING_SECRET), // secret used as the 64-character STRING ]; } // [/sign] // [call] function antpay_call(string $method, string $path, ?array $payload = null): array { $body = $payload === null ? '' : json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); // serialise ONCE $headers = ['X-Api-Key' => ANTPAY_API_KEY]; if ($body !== '') $headers['Content-Type'] = 'application/json'; if (ANTPAY_SIGNING_SECRET !== '') $headers += antpay_sign_request($method, $path, $body); $lines = []; foreach ($headers as $k => $v) $lines[] = $k . ': ' . $v; $ch = curl_init(ANTPAY_API_BASE . $path); curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $lines, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => ANTPAY_TIMEOUT_S, ]); if ($body !== '') curl_setopt($ch, CURLOPT_POSTFIELDS, $body); // send $body verbatim: do not re-encode $text = curl_exec($ch); $status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch); return [$status, is_string($text) ? json_decode($text, true) : null]; } // [/call] // [create] // 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)); } // [/create] // [status] // Ask for the payment by YOUR reference. Use it after an unclear create, and as a fallback to the postback. // Returns null ONLY when the API itself says there is no such payment. Any other 404 means the address was refused // (a wrong path, or a reference with characters that cannot travel in a URL): that is an error, not "no payment". function antpay_get_by_reference(string $reference): ?array { [$status, $data] = antpay_call('GET', '/api/v1/transactions/' . rawurlencode($reference)); if ($status === 200 && is_array($data) && !empty($data['success'])) return $data['transaction']; // ['status'] is pending | completed | failed if ($status === 404 && is_array($data) && ($data['error'] ?? null) === 'Transaction not found') return null; throw new RuntimeException("status call failed (HTTP $status, " . json_encode($data['error'] ?? null) . ')'); } function antpay_get_checkout(string $checkoutId): ?array { [$status, $data] = antpay_call('GET', '/api/v1/checkouts/' . rawurlencode($checkoutId)); if ($status === 200 && is_array($data) && !empty($data['success'])) return $data['checkout']; if ($status === 404 && is_array($data) && ($data['error'] ?? null) === 'Checkout session not found') return null; throw new RuntimeException("status call failed (HTTP $status, " . json_encode($data['error'] ?? null) . ')'); } // [/status] // [verify] // A postback is genuine when X-Mint-Signature equals "sha256=" + HMAC-SHA256(raw body, your postback secret). // $rawBody must be the bytes exactly as received (file_get_contents('php://input')), not a re-encoded array. function antpay_verify_postback(string $rawBody, ?string $signatureHeader, ?string $secret = null): bool { $secret = $secret ?? ANTPAY_POSTBACK_SECRET; if ($secret === '' || !is_string($signatureHeader)) return false; $expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret); return hash_equals($expected, $signatureHeader); } // [/verify] // [postback] // Step 4: receive the postback (this is the body of your postback URL, e.g. postback.php). // Verify, record once, answer 2xx within 10 seconds. function antpay_handle_postback(callable $onResult): void { $raw = file_get_contents('php://input'); if (!antpay_verify_postback($raw, $_SERVER['HTTP_X_MINT_SIGNATURE'] ?? null)) { http_response_code(401); // not from AntPay, or the body was changed return; } $p = json_decode($raw, true); try { // $p['status'] is "completed" or "failed" (never "pending"). Find your order by $p['checkoutId']: the id you // stored when you created the checkout. Do not match on the reference alone. // $onResult must be safe to call twice with the same transactionId + status. $onResult($p); http_response_code(200); echo 'ok'; } catch (Throwable $e) { http_response_code(500); // we try again: 3 attempts in total, 5 s apart } } // [/postback]