API reference
A plain REST API for accepting UPI payments on your own website: create a payment, send your customer to a hosted page, and get notified the moment it's paid. This page documents the exact requests and responses the gateway sends — every example below reflects what the API actually returns.
How a payment moves through the API
Three calls cover almost every integration:
- Create payment — send an order ID and amount, get back a hosted payment page URL.
- Send your customer to that URL. They pay with any UPI app.
- Webhook — the gateway calls your server the instant payment is confirmed, so you don't have to keep asking.
- Check status — use any time you want to double-check a specific order yourself.
Authentication
Every request proves it's really you using two values: an API key and an API secret, generated once per website on the API keys page of your dashboard.
Send both as headers on every request:
X-API-Keypk_live_xxxxxxxxxxxxxxxxxxxx — identifies which website is calling.
X-API-Secretsk_xxxxxxxxxxxxxxxxxxxx — proves the request is authorized. Never expose this in browser code.
Example
X-API-Key: pk_live_cbf0c6de9a20e8832f344141aee80dbd7b005d29 X-API-Secret: sk_7f3a1c9e2b6d4508f19a3c7e0b2d5f81 Content-Type: application/json
A request missing either header, or using an invalid or revoked pair, is rejected before anything else is checked — see INVALID_API_KEY.
Base URL & modes
Every website has one base URL. Whether a payment runs in test or live mode is a setting on the website itself, not a separate hostname.
https://pay.bestvletools.in/api/v1
Test mode
New websites start in test mode. Payments created in test mode never touch a real UPI account and always resolve to success or failed on the demo payment page, so you can build your integration end to end before going live. Switch a website to live mode from the dashboard once you're ready to accept real payments.
Create payment
Creates a pending payment and returns a hosted payment page. Redirect your customer there to let them pay with any UPI app.
Request body
| Parameter | Type | Description | |
|---|---|---|---|
| order_id | string | required | Your own order reference, under 100 characters. Calling this endpoint twice with the same order_id returns the original payment instead of creating a second one. |
| amount | number | required | Amount in INR, greater than 0. The customer may be asked to pay a few paise more than this if another pending order happens to be waiting on the exact same amount — the payment page always shows the exact amount due. |
| customer_name | string | optional | Shown on the hosted payment page. |
| customer_email | string | optional | Must be a valid email address if provided. |
| customer_phone | string | optional | Customer's phone number. |
| description | string | optional | Shown to the customer as the payment reason, e.g. "Pro plan renewal". |
| callback_url | string | optional | Where to redirect the customer's browser after they finish paying. See Redirect callback. |
| webhook_url | string | optional | Overrides the website's default webhook URL for this one payment only. |
Example request
curl -X POST https://pay.bestvletools.in/api/v1/payment/create.php \ -H "X-API-Key: pk_live_xxxx" \ -H "X-API-Secret: sk_xxxx" \ -H "Content-Type: application/json" \ -d '{ "order_id": "ORDER-1001", "amount": 99.00, "customer_name": "Rahul Kumar", "customer_email": "rahul@example.com", "callback_url": "https://yoursite.com/payment/callback" }'
<?php $payload = [ 'order_id' => 'ORDER-1001', 'amount' => 99.00, 'customer_name' => 'Rahul Kumar', 'callback_url' => 'https://yoursite.com/payment/callback' ]; $ch = curl_init('https://pay.bestvletools.in/api/v1/payment/create.php'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($payload), CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'X-API-Key: pk_live_xxxx', 'X-API-Secret: sk_xxxx' ] ]); $response = curl_exec($ch); $data = json_decode($response, true); if ($data['success']) { header("Location: " . $data['payment_url']); exit; }
const payload = { order_id: 'ORDER-1001', amount: 99.00, customer_name: 'Rahul Kumar', callback_url: 'https://yoursite.com/payment/callback' }; fetch('https://pay.bestvletools.in/api/v1/payment/create.php', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Key': 'pk_live_xxxx', 'X-API-Secret': 'sk_xxxx' }, body: JSON.stringify(payload) }) .then(res => res.json()) .then(data => { if (data.success) { console.log('Redirect to:', data.payment_url); } });
import requests payload = { "order_id": "ORDER-1001", "amount": 99.00, "customer_name": "Rahul Kumar", "callback_url": "https://yoursite.com/payment/callback" } headers = { "Content-Type": "application/json", "X-API-Key": "pk_live_xxxx", "X-API-Secret": "sk_xxxx" } response = requests.post("https://pay.bestvletools.in/api/v1/payment/create.php", json=payload, headers=headers) data = response.json() if data["success"]: print("Redirect to:", data["payment_url"])
{
"success": true,
"payment_id": "PAY-20260905-00082",
"order_id": "ORDER-1001",
"amount": 99.00,
"status": "pending",
"payment_url": "https://pay.bestvletools.in/pay/PAY-20260905-00082",
"expires_at": "2026-09-05T13:26:00+05:30"
}
Send your customer to payment_url. That page shows a QR code and a UPI deep link — the customer pays with whichever app they already have installed.
Check status
Looks up the current state of a payment by its payment_id. Use this any time you want to confirm an order yourself, instead of — or alongside — waiting for the webhook.
Query parameters
| Parameter | Type | Description | |
|---|---|---|---|
| payment_id | string | required | The payment_id returned when the payment was created. |
Example request
curl "https://pay.bestvletools.in/api/v1/payment/status.php?payment_id=PAY-20260905-00082" \ -H "X-API-Key: pk_live_xxxx" \ -H "X-API-Secret: sk_xxxx"
<?php $ch = curl_init('https://pay.bestvletools.in/api/v1/payment/status.php?payment_id=PAY-20260905-00082'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'X-API-Key: pk_live_xxxx', 'X-API-Secret: sk_xxxx' ] ]); $data = json_decode(curl_exec($ch), true); echo $data['status']; // pending, success, failed, expired…
{
"success": true,
"payment_id": "PAY-20260905-00082",
"order_id": "ORDER-1001",
"amount": 99.00,
"status": "success",
"transaction_id": "442718953201",
"paid_at": "2026-09-05T13:24:41+05:30"
}
Possible status values
pendingWaiting for the customer to pay.
successPayment confirmed and matched.
failedPayment attempt did not go through.
expiredThe payment window closed before payment was received.
refundedMarked as refunded by the admin.
manual_reviewReceived, but needs a human to confirm the match.
Webhooks
Instead of your site repeatedly asking "has this been paid yet?", the gateway sends an HTTP POST to your webhook URL the moment a payment succeeds. Set your webhook URL from the website's settings page in the dashboard.
Payload
{
"event": "payment.success",
"payment_id": "PAY-20260905-00082",
"order_id": "ORDER-1001",
"amount": 99.00,
"currency": "INR",
"status": "success",
"transaction_id": "442718953201",
"payment_method": "UPI",
"paid_at": "2026-09-05T13:24:41+05:30"
}
Verifying the signature
Every webhook request carries an X-Webhook-Signature header — an HMAC-SHA256 of the raw request body, signed with your webhook secret. Check it before trusting the request; anyone can send a POST to a public URL.
<?php $signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? ''; $rawBody = file_get_contents('php://input'); $secret = 'your_webhook_secret'; $expected = hash_hmac('sha256', $rawBody, $secret); if (hash_equals($expected, $signature)) { $data = json_decode($rawBody, true); if ($data['event'] === 'payment.success') { // Mark $data['order_id'] as paid in your own database } http_response_code(200); echo json_encode(['received' => true]); } else { http_response_code(400); }
Respond with a 200 status once you've processed the event. If your server is unreachable or errors out, the gateway retries with backoff before giving up.
Redirect callback
If you passed a callback_url when creating the payment, the customer's browser is redirected there once they finish paying.
https://yoursite.com/payment/callback?status=success&order_id=ORDER-1001&payment_id=PAY-20260905-00082
Full PHP example
A complete, working round trip: create the payment, redirect the customer, then confirm the result securely once they're back.
<?php $apiKey = 'pk_live_xxxx'; $apiSecret = 'sk_xxxx'; function initiatePayment($orderId, $amount, $name) { global $apiKey, $apiSecret; $ch = curl_init('https://pay.bestvletools.in/api/v1/payment/create.php'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'X-API-Key: ' . $apiKey, 'X-API-Secret: ' . $apiSecret ], CURLOPT_POSTFIELDS => json_encode([ 'order_id' => $orderId, 'amount' => $amount, 'customer_name' => $name, 'callback_url' => 'https://' . $_SERVER['HTTP_HOST'] . '/callback.php' ]) ]); $data = json_decode(curl_exec($ch), true); if ($data['success']) { header('Location: ' . $data['payment_url']); exit; } die('Could not start payment: ' . $data['error']['message']); } function confirmPayment($paymentId) { global $apiKey, $apiSecret; // Always re-check with the API — never trust the redirect query string alone. $ch = curl_init('https://pay.bestvletools.in/api/v1/payment/status.php?payment_id=' . urlencode($paymentId)); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'X-API-Key: ' . $apiKey, 'X-API-Secret: ' . $apiSecret ] ]); $data = json_decode(curl_exec($ch), true); if ($data['status'] === 'success') { echo "Paid! UTR: " . $data['transaction_id']; } else { echo "Not confirmed yet — status: " . $data['status']; } }
Errors
A failed request always comes back in the same shape, so your code can check success and show error.message to whoever needs to see it.
{
"success": false,
"error": {
"code": "INVALID_API_KEY",
"message": "Invalid API credentials"
}
}