VVLE Payment Tools/Docs
Getting started

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.
No SDK required. Every endpoint is a plain JSON POST or GET over HTTPS — call it from PHP, Node, Python, or anything that can make an HTTP request.
Getting started

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-Key

pk_live_xxxxxxxxxxxxxxxxxxxx — identifies which website is calling.

X-API-Secret

sk_xxxxxxxxxxxxxxxxxxxx — proves the request is authorized. Never expose this in browser code.

Keep your secret server-side. Treat X-API-Secret like a password — call these endpoints from your backend, never from JavaScript running in the customer's browser.

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.

Getting started

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.

Base endpoint URL
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.

REST API

Create payment

Creates a pending payment and returns a hosted payment page. Redirect your customer there to let them pay with any UPI app.

POST /api/v1/payment/create.php

Request body

ParameterTypeDescription
order_idstringrequiredYour 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.
amountnumberrequiredAmount 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_namestringoptionalShown on the hosted payment page.
customer_emailstringoptionalMust be a valid email address if provided.
customer_phonestringoptionalCustomer's phone number.
descriptionstringoptionalShown to the customer as the payment reason, e.g. "Pro plan renewal".
callback_urlstringoptionalWhere to redirect the customer's browser after they finish paying. See Redirect callback.
webhook_urlstringoptionalOverrides 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"])
Response200 OK
{
  "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.

REST API

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.

GET /api/v1/payment/status.php?payment_id=…

Query parameters

ParameterTypeDescription
payment_idstringrequiredThe 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…
Response200 OK
{
  "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

pending

Waiting for the customer to pay.

success

Payment confirmed and matched.

failed

Payment attempt did not go through.

expired

The payment window closed before payment was received.

refunded

Marked as refunded by the admin.

manual_review

Received, but needs a human to confirm the match.

Integrations

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

POST to your webhook URL
{
  "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.

Never mark an order paid without checking this signature. A request that doesn't match should be ignored, not processed.
<?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.

Integrations

Redirect callback

If you passed a callback_url when creating the payment, the customer's browser is redirected there once they finish paying.

Example redirect
https://yoursite.com/payment/callback?status=success&order_id=ORDER-1001&payment_id=PAY-20260905-00082
Don't trust the redirect alone. A browser URL can be edited by anyone. Use this redirect only to show the customer a "checking your payment…" page, then confirm the real result with Check status or wait for the webhook before delivering anything.
Integrations

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'];
    }
}
Reference

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.

Error response shape4xx / 5xx
{
  "success": false,
  "error": {
    "code": "INVALID_API_KEY",
    "message": "Invalid API credentials"
  }
}

Error codes

401 · INVALID_API_KEYMissing, incorrect, or revoked X-API-Key / X-API-Secret.
403 · WEBSITE_DISABLEDThis website has been disabled from the dashboard.
404 · NOT_FOUNDNo payment exists with the given payment_id for this website.
405 · METHOD_NOT_ALLOWEDWrong HTTP method — create.php only accepts POST, status.php only accepts GET.
422 · VALIDATION_ERRORA required field is missing or malformed — check error.message for which one.Common causes: missing order_id, non-positive amount, invalid customer_email, or a callback_url / webhook_url that isn't a valid URL.
429 · RATE_LIMITEDToo many requests from this website in a short window. Wait and retry.
500 · UPI_NOT_CONFIGUREDNo UPI ID is configured on the gateway yet. Contact the administrator.