v1 · Stable

API Reference

Integrate UPI payments into your app in minutes. Create orders, redirect customers, and receive real-time webhooks.

Base URL https://www.upi.bhumi.marketing/api/v1
Getting Started

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.

🔒
Secure

HMAC-SHA256 signed webhooks. Credentials encrypted at rest.

⚡
Real-time

Instant payment confirmation via polling or webhooks.

🔄
Auto-rotation

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.

Required headers
X-Api-Key: key_xxxxxxxxxxxxxxxx
X-Api-Secret: sec_xxxxxxxxxxxxxxxx
Accept: application/json
Content-Type: application/json
POST /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.

Request body
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 email 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
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"}'
Response · 201 Created
{
  "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"
  }
}
GET /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
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"
Response · 200 OK
{
  "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:

Pending

Waiting for the customer to pay. Expires after 15 minutes.

Paid

Payment confirmed. The utr field contains the bank reference.

Expired

Order was not paid before expires_at.

GET /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.

Response · 200 OK
{
  "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:

🎲
Random rotation (default)

A random account among those with remaining daily capacity.

⭐
Default account first

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.

Events
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.

Payload · order.paid
{
  "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"
  }
}
Payload · order.expired
{
  "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
<?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);
Node.js
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.

Python (requests)
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 (Guzzle)
<?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"];