v1 · Stable

API Reference

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

Base URL https://pay.followersindia.net/api/v1
Getting Started

Introduction

The UPI Gateway 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.

Live API authentication

Every Live request must include your Live API key and secret as headers. Find them in Dashboard → API & Webhooks. Credentials select the environment; do not send an environment field.

Never expose your X-Api-Secret in client-side code, mobile apps, or public repositories.

Live request headers
X-Api-Key: key_live_placeholder
X-Api-Secret: sec_live_placeholder
Accept: application/json
Content-Type: application/json
SANDBOX · TEST ONLY

Sandbox API authentication

Generate separate Sandbox credentials under Dashboard → API & Webhooks. Send the same API requests using the test_key_… and test_sec_… values. The credential determines the environment; request parameters cannot switch it.

Sandbox orders use an internal TEST checkout and never contact a payment provider. Their payment URLs begin with /sandbox/pay/, and webhook payloads include "environment": "sandbox" and "test": true.
Sandbox request headers
X-Api-Key: test_key_placeholder
X-Api-Secret: test_sec_placeholder
Accept: application/json
Content-Type: application/json

Testing guide

  1. Generate Sandbox credentials and configure a separate Sandbox webhook URL in Settings.
  2. Create an order with the Sandbox headers. Do not send payment_account_id.
  3. Open the returned TEST payment_url and choose success, failure, pending, expired, timeout, or invalid reference.
  4. Check the order with the same Sandbox credentials and verify the Sandbox webhook with the Sandbox secret.

Each TEST order can be simulated once. Timeout returns HTTP 504; invalid reference and repeat attempts return HTTP 422. Pending remains pending but is still consumed as that order's one simulation.

POST /api/v1/orders

Create payment order

Creates a pending order and returns a hosted payment_url. Live credentials use Live payment processing; Sandbox credentials return the internal TEST checkout. The server ignores client attempts to select an environment.

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 HTTPS URL Max 500 chars. Its host must be in your Settings allowlist.
customer_name optional string Max 100 chars.
customer_email optional email Valid email, max 255 chars.
customer_mobile optional string Max 20 chars.
note optional string Shown in the customer's UPI app. Max 50 chars.
payment_account_id optional integer Live only. Pin to one of your accounts; prohibited in Sandbox.
cURL
curl -X POST https://gateway.example.com/api/v1/orders \
  -H "X-Api-Key: key_live_placeholder" \
  -H "X-Api-Secret: sec_live_placeholder" \
  -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",
    "environment": "live",
    "test": false,
    "utr": null,
    "submitted_utr": null,
    "payment_url": "https://gateway.example.com/pay/ORDAB12CD34EF56GH78",
    "upi_intent": "upi://pay?pa=merchant@upi&am=199.00&tr=ORDAB12CD34EF56GH78",
    "customer": {"name": null, "email": null, "mobile": null},
    "paid_at": null,
    "expires_at": "2026-11-07T12:15:00+05:30",
    "created_at": "2026-11-07T12:00:00+05:30"
  }
}
GET /api/v1/orders/{order_id | client_order_id}

Check order status

Accepts either order_id or client_order_id. Live credentials may check a pending provider payment; Sandbox credentials only read simulated state and never contact a provider. Orders are visible only with credentials for the same merchant and environment. Fulfil only after this endpoint or a verified webhook reports paid.

cURL
curl https://gateway.example.com/api/v1/orders/INV1001 \
  -H "X-Api-Key: key_live_placeholder" \
  -H "X-Api-Secret: sec_live_placeholder" \
  -H "Accept: application/json"
Response · 200 OK
{
  "success": true,
  "data": {
    "order_id": "ORDAB12CD34EF56GH78",
    "client_order_id": "INV1001",
    "amount": "199.00",
    "currency": "INR",
    "status": "paid",
    "environment": "live",
    "test": false,
    "utr": "427123456789",
    "submitted_utr": null,
    "payment_url": "https://gateway.example.com/pay/ORDAB12CD34EF56GH78",
    "upi_intent": "upi://pay?pa=merchant@upi&am=199.00&tr=ORDAB12CD34EF56GH78",
    "customer": {"name": null, "email": null, "mobile": null},
    "paid_at": "2026-11-07T12:10:00+05:30",
    "expires_at": "2026-11-07T12:15:00+05:30",
    "created_at": "2026-11-07T12:00:00+05:30"
  }
}

API response fields

An order moves through these statuses:

Pending

Waiting for the customer to pay until expires_at.

Paid

Payment confirmed. The utr field contains the bank reference.

Under review

A customer-submitted UTR is awaiting merchant review (Live only).

Failed

The TEST payment was simulated as failed (Sandbox only).

Expired

Order was not paid before expires_at.

GET /api/v1/accounts

List UPI accounts

Returns all of your Live UPI accounts and their capacity. Sandbox credentials return environment: sandbox, a null rotation mode, and an empty data array so Live account details never enter the TEST path.

Response · 200 OK
{
  "success": true,
  "environment": "live",
  "rotation_mode": "random",
  "data": [
    {
      "id": 3,
      "label": "Shop 1",
      "upi_id": "merchant@paytm",
      "provider": "paytm_business",
      "is_active": true,
      "is_default": 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 separate Live and Sandbox HTTPS endpoints in Dashboard → API & Webhooks. We send JSON with X-Signature and X-Event headers. Respond with any 2xx status; failed deliveries are retried up to 6 attempts with backoff.

Events
order.paid Payment confirmed. Safe to fulfil the order.
order.expired Order was not paid before it expired.
order.under_review A Live UTR submission needs merchant review.
order.failed Sandbox failure simulation.
order.pending Sandbox pending simulation.
order.timeout Sandbox provider-timeout simulation.

The same event may be delivered more than once. Process it idempotently using order_id plus the event name, then confirm status, environment, amount, and your client_order_id.

Payload · order.paid
{
  "event": "order.paid",
  "data": {
    "order_id": "ORDAB12CD34EF56GH78",
    "client_order_id": "INV1001",
    "amount": "199.00",
    "currency": "INR",
    "status": "paid",
    "environment": "live",
    "test": false,
    "utr": "4271234567890",
    "submitted_utr": null,
    "payment_url": "https://gateway.example.com/pay/ORDAB12CD34EF56GH78",
    "upi_intent": "upi://pay?pa=merchant@upi&am=199.00&tr=ORDAB12CD34EF56GH78",
    "customer": {"name": null, "email": null, "mobile": null},
    "paid_at": "2026-11-07T12:10:00+05:30",
    "expires_at": "2026-11-07T12:15:00+05:30",
    "created_at": "2026-11-07T12:00:00+05:30"
  }
}

Every event uses the complete order object shown above. Sandbox payloads carry environment: sandbox and test: true and are sent only to the Sandbox URL.

Verify signature

Compute the lowercase hexadecimal HMAC-SHA256 of the exact raw request body. Use the Live API secret for the Live endpoint and the Sandbox API secret for the Sandbox endpoint. Compare in constant time before parsing or processing the payload; never trust frontend success or an unsigned body.

PHP
<?php
$body     = file_get_contents("php://input");
$secret   = getenv("PAYMENT_WEBHOOK_SECRET");
$expected = hash_hmac("sha256", $body, $secret);
$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.PAYMENT_WEBHOOK_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.

Validation error · 422
{
  "message": "The amount field must be at least 1.",
  "errors": {
    "amount": ["The amount field must be at least 1."]
  }
}
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).
504 Sandbox checkout simulated a provider timeout.
500 Unexpected server error. Contact support if this persists.

Code examples

Quick-start snippets for common server-side languages.

Node.js (create order)
const response = await fetch("https://gateway.example.com/api/v1/orders", {
  method: "POST",
  headers: {
    "X-Api-Key": process.env.PAYMENT_API_KEY,
    "X-Api-Secret": process.env.PAYMENT_API_SECRET,
    "Accept": "application/json",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ amount: 199, client_order_id: "INV1001" }),
});
if (!response.ok) throw new Error(`API error ${response.status}`);
const { data } = await response.json();
console.log(data.payment_url);
PHP (Guzzle)
<?php
$client = new GuzzleHttp\Client(["base_uri" => "https://gateway.example.com/api/v1"]);

$res = $client->post("/orders", [
    "headers" => [
        "X-Api-Key"    => getenv("PAYMENT_API_KEY"),
        "X-Api-Secret" => getenv("PAYMENT_API_SECRET"),
        "Accept"       => "application/json",
    ],
    "json" => ["amount" => 199, "client_order_id" => "INV1001"],
]);

$data = json_decode($res->getBody(), true);
$paymentUrl = $data["data"]["payment_url"];