Introduction
The Bhumi Marketing UPI API lets you create UPI payment orders, redirect customers to a hosted checkout, and receive signed webhooks when payments complete. All API calls are server-to-server — never expose your secret in a browser or mobile app.
HMAC-SHA256 signed webhooks. Credentials encrypted at rest.
Instant payment confirmation via polling or webhooks.
Distribute load across multiple UPI accounts automatically.
Authentication
Every request must include your API key and secret as headers. Find them in Dashboard → API & Webhooks.
Never expose your X-Api-Secret in client-side code, mobile apps, or public repositories.
X-Api-Key: key_xxxxxxxxxxxxxxxx
X-Api-Secret: sec_xxxxxxxxxxxxxxxx
Accept: application/json
Content-Type: application/json
/api/v1/orders
Create order
Creates a pending order and returns a payment_url. Redirect your customer there. For standard UPI accounts, a upi_intent deep-link is also returned so you can render your own QR.
| Parameter | Type | Description |
|---|---|---|
amount
required
|
number | Amount in INR (₹1 – ₹1,00,000). Up to 2 decimal places. |
client_order_id
required
|
string | Your unique order ID. Allowed: A–Z, 0–9, _ and -. Max 64 chars. |
redirect_url
optional
|
url | Customer is redirected here after payment with order_id, client_order_id and status as query params. |
customer_name
optional
|
string | Shown on the checkout page. |
customer_email
optional
|
Customer email address. | |
customer_mobile
optional
|
string | Customer mobile number. |
note
optional
|
string | Shown in the customer's UPI app. Max 50 chars. |
payment_account_id
optional
|
integer | Pin to a specific UPI account. Falls back to rotation if the account is full or inactive. |
curl -X POST https://www.upi.bhumi.marketing/api/v1/orders \
-H "X-Api-Key: key_xxx" \
-H "X-Api-Secret: sec_xxx" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"amount": 199, "client_order_id": "INV1001", "redirect_url": "https://yoursite.com/thanks"}'
{
"success": true,
"data": {
"order_id": "ORDAB12CD34EF56GH78",
"client_order_id": "INV1001",
"amount": "199.00",
"currency": "INR",
"status": "pending",
"payment_url": "https://www.upi.bhumi.marketing/pay/ORDAB12CD34EF56GH78",
"upi_intent": "upi://pay?pa=merchant@upi&am=199.00&tr=ORDAB12CD34EF56GH78",
"expires_at": "2026-01-01T12:15:00+05:30"
}
}
/api/v1/orders/{order_id | client_order_id}
Get order status
Returns the latest order state, checking the payment provider if the order is still pending. Always confirm with this endpoint or a verified webhook before fulfilling an order — never trust the redirect URL alone.
curl https://www.upi.bhumi.marketing/api/v1/orders/INV1001 \
-H "X-Api-Key: key_xxx" \
-H "X-Api-Secret: sec_xxx" \
-H "Accept: application/json"
{
"success": true,
"data": {
"order_id": "ORDAB12CD34EF56GH78",
"client_order_id": "INV1001",
"amount": "199.00",
"currency": "INR",
"status": "paid",
"utr": "427123456789",
"paid_at": "2026-01-01T12:10:00+05:30"
}
}
Order object
An order moves through these statuses:
Waiting for the customer to pay. Expires after 15 minutes.
Payment confirmed. The utr field contains the bank reference.
Order was not paid before expires_at.
/api/v1/accounts
List UPI accounts
Returns your active UPI accounts with their IDs, daily limits, and remaining capacity. Use the id as payment_account_id when creating an order to pin it to a specific account.
{
"success": true,
"rotation_mode": "random",
"data": [
{
"id": 3,
"label": "Shop 1",
"upi_id": "merchant@paytm",
"provider": "paytm_business",
"is_active": true,
"daily_limit": "100000.00",
"used_today": "42500.00",
"remaining_today": "57500.00"
}
]
}
Rotation & daily limits
When you omit payment_account_id, the gateway automatically picks an account based on your rotation mode:
A random account among those with remaining daily capacity.
Your default account is used until it hits its daily limit, then falls back to others.
Each account has a daily limit (₹1,00,000 by default). Usage includes paid orders from today plus open pending orders. If no account has capacity, the API returns 422 with an amount error.
Webhook events
Configure your endpoint in Dashboard → API & Webhooks. We send a signed POST request for each event. Respond with any 2xx status; failed deliveries are retried up to 6 times with exponential backoff.
order.paid |
Payment confirmed. Safe to fulfil the order. |
order.expired |
Order was not paid before it expired. |
The same event may be delivered more than once. Always handle webhooks idempotently using client_order_id.
{
"event": "order.paid",
"data": {
"order_id": "ORDAB12CD34EF56GH78",
"client_order_id": "INV1001",
"amount": "199.00",
"currency": "INR",
"status": "paid",
"utr": "4271234567890",
"paid_at": "2026-01-01T12:10:00+05:30"
}
}
{
"event": "order.expired",
"data": {
"order_id": "ORDAB12CD34EF56GH78",
"client_order_id": "INV1001",
"amount": "199.00",
"currency": "INR",
"status": "expired",
"expires_at": "2026-01-01T12:15:00+05:30"
}
}
Verify signature
Every webhook includes an X-Signature header containing the HMAC-SHA256 of the raw request body, keyed with your API secret. Always verify this before processing.
<?php
$body = file_get_contents("php://input");
$expected = hash_hmac("sha256", $body, "sec_xxx");
$given = $_SERVER["HTTP_X_SIGNATURE"] ?? "";
if (!hash_equals($expected, $given)) {
http_response_code(401);
exit;
}
$event = json_decode($body, true);
if ($event["event"] === "order.paid") {
// Fulfil $event["data"]["client_order_id"] (idempotently)
}
http_response_code(200);
const crypto = require("crypto");
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const expected = crypto
.createHmac("sha256", process.env.UPI_SECRET)
.update(req.body)
.digest("hex");
const given = req.get("X-Signature") || "";
if (given.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(given))) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body);
if (event.event === "order.paid") {
// Fulfil event.data.client_order_id (idempotently)
}
res.sendStatus(200);
});
Error codes
All errors return a JSON body with a message field. Validation errors (422) also include an errors object keyed by field name.
| Status | Meaning |
|---|---|
401 |
Missing or invalid API key / secret. |
403 |
Merchant account is disabled. |
404 |
Order not found. |
422 |
Validation failed — see the errors object for field-level details. |
429 |
Rate limit exceeded (120 requests / minute). |
500 |
Unexpected server error. Contact support if this persists. |
Code examples
Quick-start snippets for common server-side languages.
import requests, hmac, hashlib
headers = {
'X-Api-Key': 'key_xxx',
'X-Api-Secret': 'sec_xxx',
'Accept': 'application/json',
'Content-Type': 'application/json',
}
res = requests.post(
'https://www.upi.bhumi.marketing/api/v1/orders',
json={'amount': 199, 'client_order_id': 'INV1001'},
headers=headers,
timeout=10,
)
data = res.json()
payment_url = data['data']['payment_url']
<?php
$client = new GuzzleHttp\Client(["base_uri" => "https://www.upi.bhumi.marketing/api/v1"]);
$res = $client->post("/orders", [
"headers" => [
"X-Api-Key" => "key_xxx",
"X-Api-Secret" => "sec_xxx",
"Accept" => "application/json",
],
"json" => ["amount" => 199, "client_order_id" => "INV1001"],
]);
$data = json_decode($res->getBody(), true);
$paymentUrl = $data["data"]["payment_url"];