PAY ABHRO MODZ
PAY ABHRO MODZ
REST API · v1

API Documentation

Everything you need to integrate PAY ABHRO MODZ — one endpoint, signed webhooks, and copy-paste examples in every language.

Overview

PAY ABHRO MODZ is a self-hosted UPI payment gateway. The entire API is a single endpoint — every operation is selected with the action field in the JSON request body. This keeps integrations tiny: one URL, one HTTP method, one header.

Base endpoint
At a glance
  • Method: POST (only health may also use GET-style checks)
  • Content-Type: application/json
  • Auth: X-API-Key header (64-char key)
  • Rate limit: 120 requests / minute per API key (and 300/min per IP)
  • Currency: INR (paise precision, 2 decimals)

Authentication

Every action except health requires a valid API key. Generate keys from your dashboard under API Keys. Keys are shown once at creation — store them securely.

Send the key in the request header (recommended):

X-API-Key: YOUR_64_CHARACTER_KEY

Or, if your HTTP client cannot set custom headers, pass it in the JSON body instead:

{ "api_key": "YOUR_64_CHARACTER_KEY", "action": "health" }
The same active API key is also used as the secret to sign outgoing webhook callbacks (see Webhooks). Revoking or regenerating a key immediately invalidates old requests and changes the webhook signature.

Request format

All requests are POST to https://wa.abhromodz.in/v1 with a JSON body. The action field determines what happens; all other fields depend on the action.

POST https://wa.abhromodz.in/v1
X-API-Key: YOUR_64_CHARACTER_KEY
Content-Type: application/json

{
  "action": "create",
  "amount": 499
}

Responses & errors

Every response is JSON and always contains a status field, either "success" or "error".

Success
{
  "status": "success",
  "order_id": "PAM-8F2K9DLA31"
}
Error
{
  "status": "error",
  "message": "Invalid or inactive API key."
}

HTTP status codes

CodeMeaningWhen it happens
200OKThe action succeeded.
401UnauthorizedMissing X-API-Key, or the key is invalid / inactive.
404Not foundThe order_id does not exist or belongs to another merchant.
409ConflictA request with the same Idempotency-Key is still being processed.
422UnprocessableValidation failed, or an unknown / missing action.
429Too many requestsYou exceeded 120 requests per minute for this key.
500Server errorA bug on our side. The response carries a reference — quote it when contacting support.

Safe retries (Idempotency-Key)

If your create request times out you cannot tell whether the order was created or not, and retrying blindly can produce two orders for one checkout. Send an Idempotency-Key header and the retry is answered with the original response instead.

POST https://wa.abhromodz.in/v1
X-API-Key: YOUR_64_CHARACTER_KEY
Idempotency-Key: 7f9a2c4e-1b6d-4a58-9c31-0d5e8f2a7b43
Content-Type: application/json

{ "action": "create", "amount": 499, "purpose": "ORDER1024" }
  • Use a new random key per checkout (a UUID is ideal) — never a fixed string.
  • A replayed response carries the header Idempotent-Replay: true.
  • Keys are remembered for 24 hours and are scoped to your merchant account.
  • Reusing a key with different parameters is refused with 422, so a key can never return the wrong order.
  • If the first call is still running you get 409 — wait a moment and retry the same key.
  • The header is optional. Without it, create behaves exactly as before.
Only create needs this. status, invoice and qr are reads, and cancel / refund already do nothing when repeated.

Order lifecycle & statuses

A fresh order starts as PENDING. When a matching bank/UPI confirmation email is parsed, it becomes VERIFIED automatically. Orders that are not paid within the expiry window become EXPIRED.

StatusMeaning
PENDINGOrder created, awaiting payment.
UNDER_REVIEWA candidate payment was seen but needs manual confirmation.
VERIFIEDPayment confirmed. verified_at is set and any callback fires.
FAILEDVerification was attempted and rejected.
CANCELLEDMerchant cancelled the order via the cancel action.
EXPIREDThe payment window closed before verification.
REFUNDEDMerchant recorded a refund via the refund action.

Actions

ActionDescriptionAuthRequired fields
createCreate a new payment orderYesamount
statusQuery an order's outcome — the only thing you may fulfil onYesorder_id
verifyForce an immediate verification checkYesorder_id
cancelCancel a pending orderYesorder_id
refundMark an order as refundedYesorder_id
invoiceGet the hosted invoice URLYesorder_id
qrGet the UPI URI + QR (SVG, base64)Yesorder_id
healthGateway health checkNo

create — Create a payment order

Creates a new order and returns a hosted pay page URL plus a ready-to-render UPI URI.

FieldTypeRequiredNotes
amountnumberYesMinimum 1. Rounded to 2 decimals.
purposestringNoMax 64 chars. Normalised to uppercase alphanumerics and used as the UPI transaction note. Auto-generated (unique) if omitted.
customer_idstringNoYour own reference for the payer. Max 120 chars. Echoed back in status and webhooks.
callback_urlstring (URL)NoMax 500 chars. Receives a signed webhook when the order is verified.
redirect_urlstring (URL)NoMax 500 chars, http/https only. The single return page on your website, used for every outcome — paid, failed, cancelled and expired alike. The checkout page sends the payer here with ?order_id= appended and nothing else. If you omit it the payer stays on this gateway.
expires_inintegerNoSeconds until the order expires. 60–3600. Defaults to the gateway's configured expiry (typically 600).
extra_dataanyNoArbitrary metadata stored with the order and returned in status and webhooks. Keys beginning with an underscore are reserved by the gateway and are dropped.
success_url, failure_url, cancel_url and expire_url no longer exist. Sending any of them returns 422 rather than being quietly ignored, so you cannot end up believing you still have a success-only landing page. There is one redirect_url for all outcomes, and the outcome itself is something you ask for from your server — see Return flow.

Request

curl -X POST https://wa.abhromodz.in/v1 \
  -H "X-API-Key: YOUR_64_CHARACTER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "create",
    "amount": 499,
    "purpose": "ORDER1024",
    "customer_id": "CUST-88",
    "callback_url": "https://yoursite.com/webhooks/pam",
    "redirect_url": "https://yoursite.com/payment/return",
    "expires_in": 900,
    "extra_data": {
      "invoice": "INV-1024",
      "plan": "pro"
    }
  }'

Response 200

{
  "status": "success",
  "order_id": "PAM-8F2K9DLA31",
  "amount": 499,
  "purpose": "ORDER1024",
  "pay_url": "https://wa.abhromodz.in/pay/9f3c...48chars",
  "upi_uri": "upi://pay?pa=7430096522%40fam&pn=PAY%20ABHRO%20MODZ&am=499.00&tn=ORDER1024&cu=INR",
  "qr_png": "https://wa.abhromodz.in/qr/9f3c...48chars.png",
  "qr_svg": "https://wa.abhromodz.in/qr/9f3c...48chars.svg",
  "expires_in": 900
}

qr_png and qr_svg are hosted images of the same UPI code, returned here so one call is enough to show a QR — see action: "qr" below.

status — Query order status

The authoritative outcome of an order, read from the gateway's own record. This is the only thing you may fulfil on. It returns everything fulfilment needs in one call, so you never have to trust anything the customer's browser brought you. Automatically transitions the order to EXPIRED if its window has passed.

Request

curl -X POST https://wa.abhromodz.in/v1 \
  -H "X-API-Key: YOUR_64_CHARACTER_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "action": "status", "order_id": "PAM-8F2K9DLA31" }'

Response 200

{
  "status": "success",
  "order_id": "PAM-8F2K9DLA31",
  "order_status": "VERIFIED",
  "is_paid": true,
  "amount": 499,
  "currency": "INR",
  "purpose": "ORDER1024",
  "customer_id": "CUST-88",
  "utr": "412198765432",
  "transaction_id": "T2607270405120001",
  "paid_amount": 499,
  "paid_at": "2026-07-27T04:05:09+05:30",
  "expires_in": 0,
  "verified_at": "2026-07-27T04:05:12+05:30",
  "extra_data": { "invoice": "INV-1024", "plan": "pro" }
}
FieldWhat it is
order_statusPENDING, UNDER_REVIEW, VERIFIED, FAILED, CANCELLED, EXPIRED or REFUNDED.
is_paidtrue only when order_status is VERIFIED. Release goods on this and nothing else.
amount / currencyWhat you asked for. Compare amount against your own cart total before fulfilling.
paid_amountThe amount the bank actually reported. null until a payment is matched.
utr / transaction_idBank identifiers for the settled payment, for your receipt and reconciliation. null until matched.
paid_atTimestamp the bank recorded for the transfer; verified_at is when this gateway matched it.
customer_id / extra_dataExactly what you sent to create, handed back so this one call is enough to find your own order.

Orders belong to the API key that created them: calling status with another merchant's order_id returns 403, and an unknown one returns 404.

verify — Force a verification check

Immediately runs the verification engine against the order instead of waiting for the background poller. Useful right after the payer confirms they've paid.

Request

curl -X POST https://wa.abhromodz.in/v1 \
  -H "X-API-Key: YOUR_64_CHARACTER_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "action": "verify", "order_id": "PAM-8F2K9DLA31" }'

Response 200

{
  "status": "success",
  "order_id": "PAM-8F2K9DLA31",
  "order_status": "VERIFIED",
  "decision": "VERIFIED",
  "reason": "Matched email: amount, purpose, UTR and txn id all unique."
}

decision is one of VERIFIED, REJECTED or PENDING; reason explains why.

cancel — Cancel a pending order

Cancels an order that is still PENDING. Verified, expired or already-cancelled orders are returned unchanged.

Request

curl -X POST https://wa.abhromodz.in/v1 \
  -H "X-API-Key: YOUR_64_CHARACTER_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "action": "cancel", "order_id": "PAM-8F2K9DLA31" }'

Response 200

{
  "status": "success",
  "order_id": "PAM-8F2K9DLA31",
  "order_status": "CANCELLED"
}

refund — Record a refund

Marks an order as REFUNDED for your records. This is a bookkeeping status — the actual money movement happens in your UPI app / bank.

Request

curl -X POST https://wa.abhromodz.in/v1 \
  -H "X-API-Key: YOUR_64_CHARACTER_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "action": "refund", "order_id": "PAM-8F2K9DLA31" }'

Response 200

{
  "status": "success",
  "order_id": "PAM-8F2K9DLA31",
  "order_status": "REFUNDED"
}

invoice — Get the invoice URL

Returns the hosted, printable invoice URL for an order.

Request

curl -X POST https://wa.abhromodz.in/v1 \
  -H "X-API-Key: YOUR_64_CHARACTER_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "action": "invoice", "order_id": "PAM-8F2K9DLA31" }'

Response 200

{
  "status": "success",
  "order_id": "PAM-8F2K9DLA31",
  "invoice_url": "https://wa.abhromodz.in/invoice/PAM-8F2K9DLA31",
  "amount": 499,
  "purpose": "ORDER1024",
  "order_status": "VERIFIED"
}

qr — Get UPI URI + QR image URLs

Returns the raw UPI URI plus two hosted image URLs for the same code — a PNG and an SVG. They are plain images: drop them in an <img>, load them in an app, or hand the PNG URL straight to a chat API.

Request

curl -X POST https://wa.abhromodz.in/v1 \
  -H "X-API-Key: YOUR_64_CHARACTER_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "action": "qr", "order_id": "PAM-8F2K9DLA31" }'

Response 200

{
  "status": "success",
  "order_id": "PAM-8F2K9DLA31",
  "upi_uri": "upi://pay?pa=7430096522%40fam&pn=PAY%20ABHRO%20MODZ&am=499.00&tn=ORDER1024&cu=INR",
  "pay_url": "https://wa.abhromodz.in/pay/TOKEN",
  "qr_png": "https://wa.abhromodz.in/qr/TOKEN.png",
  "qr_svg": "https://wa.abhromodz.in/qr/TOKEN.svg"
}
Changed: the old qr_svg_base64 field has been removed. Use qr_png or qr_svg. Base64 SVG could not be used by a Telegram bot at all, and every other caller had to decode it before it could be shown.

Website

<img src="https://wa.abhromodz.in/qr/TOKEN.png" alt="Scan to pay" width="320" height="320">

Telegram bot

curl -X POST https://api.telegram.org/bot$TOKEN/sendPhoto \
  -d chat_id=$CHAT_ID \
  -d photo="https://wa.abhromodz.in/qr/TOKEN.png" \
  -d caption="Scan to pay Rs 499"

Telegram fetches the URL itself, so no upload and no base64. Optional ?size= between 128 and 1024 (default 512) sets the pixel width; the image is cached and revalidates with an ETag, and the URL needs no API key — it shows only what the checkout page already shows to the same token holder.

health — Health check (no key)

The only unauthenticated action. Use it for uptime monitoring.

Request

curl -X POST https://wa.abhromodz.in/v1 \
  -H "Content-Type: application/json" \
  -d '{ "action": "health" }'

Response 200

{
  "status": "success",
  "gateway": "PAY ABHRO MODZ",
  "time": "2026-07-27T04:05:12+05:30",
  "version": "v1"
}

Webhooks (callbacks)

If you pass a callback_url when creating an order, PAY ABHRO MODZ sends a server-to-server POST to that URL when the order is verified. The request has a 15-second timeout, and a delivery that does not answer 2xx is retried up to 5 times over roughly 80 minutes. Every attempt is logged in Logs → Callbacks.

Payload

POST https://yoursite.com/webhooks/pam
X-PAM-Signature: 9a1c...hmac-sha256-hex
X-PAM-Signature-V2: 4be7...hmac-sha256-hex
X-PAM-Timestamp: 1785312312
Content-Type: application/json

{
  "order_id": "PAM-8F2K9DLA31",
  "status": "VERIFIED",
  "is_paid": true,
  "amount": 499,
  "currency": "INR",
  "purpose": "ORDER1024",
  "customer_id": "CUST-88",
  "utr": "412198765432",
  "transaction_id": "T2607270405120001",
  "paid_amount": 499,
  "paid_at": "2026-07-27T04:05:09+05:30",
  "verified_at": "2026-07-27T04:05:12+05:30",
  "extra_data": {
    "invoice": "INV-1024",
    "plan": "pro"
  }
}

Same fields as action: "status", so a webhook alone is enough to fulfil once you have verified its signature — you do not need a follow-up call. Still compare amount with what you charged, and keep fulfilment idempotent per order_id: a retry can deliver the same event twice.

Verifying the signature

Both signatures are HMAC-SHA256 keyed with your active API key. Verify X-PAM-Signature-V2 if you can: it covers timestamp + "." + raw body, so you can reject a stale delivery. X-PAM-Signature (the raw body alone) is still sent for existing integrations, but on its own it cannot tell a fresh callback from one captured and replayed a month later.

// PHP
$raw = file_get_contents('php://input');
$ts  = $_SERVER['HTTP_X_PAM_TIMESTAMP']   ?? '';
$sig = $_SERVER['HTTP_X_PAM_SIGNATURE_V2'] ?? '';

// Reject anything older than 5 minutes so a captured callback cannot be replayed.
if (! ctype_digit((string) $ts) || abs(time() - (int) $ts) > 300) {
    http_response_code(400);
    exit('Stale callback');
}

$expected = hash_hmac('sha256', $ts . '.' . $raw, $YOUR_API_KEY);

if (! hash_equals($expected, $sig)) {
    http_response_code(401);
    exit('Invalid signature');
}

$event = json_decode($raw, true);
if ($event['status'] === 'VERIFIED') {
    // mark $event['order_id'] as paid in your database - ignore it if you
    // already have, since a retry can deliver the same event twice
}
http_response_code(200);
echo 'ok';
Respond with HTTP 200 quickly. The signing secret is your currently active API key — keep both sides in sync when you rotate keys. Sign against the raw request body, never a re-encoded copy of the decoded JSON.

UPI URI format

The upi_uri returned by create and qr is a standard UPI deep link. Encode it into a QR code, or use it as an href so mobile users open their UPI app directly.

upi://pay?pa=7430096522%40fam&pn=PAY%20ABHRO%20MODZ&am=499.00&tn=ORDER1024&cu=INR
ParamMeaning
paPayee UPI address (VPA)
pnPayee name
amAmount, 2 decimals
tnTransaction note = the order's unique purpose
cuCurrency (INR)
The tn (purpose) is what links a bank email back to the order during auto-verification. Never reuse a purpose across orders — let the gateway generate it unless you have a reason not to.

Integration in every language

Pick your language and copy a complete create request. Every snippet sends the POST to https://wa.abhromodz.in/v1 and prints the JSON Response shown at the bottom. The endpoint below is your live gateway URL, filled in automatically — just paste your API key. The same one-function pattern works for status, verify, cancel, refund, invoice, qr and health: only the JSON body changes.

The end-to-end flow
  1. Create an order from your server (action: "create"). You get back an order_id and a hosted pay_url. Store the order_id against your own cart or invoice.
  2. Send the customer to pay_url — redirect the browser, or open it in a WebView on mobile. They pay over UPI on the gateway's checkout page.
  3. The gateway verifies the payment automatically by matching the bank confirmation email (purpose + amount + unique transaction ID / UTR).
  4. The customer comes back to your one redirect_url with ?order_id= appended — whatever happened, success or not. That is all the redirect tells you: this person returned from that order.
  5. Your server asks what happened — call status with that order_id and read is_paid. Deliver the goods only then. Nothing the browser hands you is ever proof of payment.
Return flow — one redirect_url, outcome read server-to-server

This works the way Cashfree and Razorpay work: one return URL, and the real answer comes from an API call your server makes. Add redirect_url to your create request (in any language below — it goes straight into the JSON body). When the order finishes — paid, failed, cancelled or expired — the hosted checkout page sends the customer to exactly that URL, with one parameter appended:

https://yoursite.com/payment/return?order_id=PAM-8F2K9DLA31
Query paramMeaning
order_idThe order the customer just came back from. Look it up with action: "status".

There is no status, no ts and no sig in the return URL, on purpose. The customer's own browser is what carries that URL, so anything it claimed about the outcome would be a claim the customer controls — and a link that said “status=VERIFIED” would be worth exactly the price of your goods. Signing it does not fix that: the customer holds the signed link before your server does, so they can replay it, share it, or simply not pay and press back. Removing the claim removes the whole problem. The URL says who came back; your server says what happened.

Do not treat the redirect as the trigger for fulfilment either. Customers close tabs, lose signal and land on your page before the bank email has arrived. Handle the redirect_url hit as “show this customer where their order stands”, and let the signed callback_url webhook (or a short poll of status) be what actually releases goods. If status still says PENDING, show a “we're confirming your payment” page — do not show a failure.
One rule covers every bypass attempt: release goods only on is_paid: true from an action: "status" call your own server made, or on a webhook whose signature you verified. Also check that the order_id is one you created, that its amount matches your cart, and that you have not already fulfilled it. Full copy-paste examples are in the verify-on-your-server section below.
cURL
curl -X POST https://yourgateway.com/v1 \
  -H "X-API-Key: YOUR_64_CHARACTER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "create",
    "amount": 499,
    "purpose": "ORDER1024",
    "customer_id": "CUST-88",
    "callback_url": "https://yoursite.com/webhooks/pam",
    "redirect_url": "https://yoursite.com/payment/return"
  }'

Response 200 — same JSON for every language

{
  "status": "success",
  "order_id": "PAM-8F2K9DLA31",
  "amount": 499,
  "purpose": "ORDER1024",
  "pay_url": "https://wa.abhromodz.in/pay/9f3c...48chars",
  "upi_uri": "upi://pay?pa=7430096522%40fam&pn=PAY%20ABHRO%20MODZ&am=499.00&tn=ORDER1024&cu=INR",
  "qr_png": "https://wa.abhromodz.in/qr/9f3c...48chars.png",
  "qr_svg": "https://wa.abhromodz.in/qr/9f3c...48chars.svg",
  "expires_in": 900
}
To run any other operation, keep the same request code and just change the body, e.g. { "action": "status", "order_id": "PAM-8F2K9DLA31" }. See Actions for the full list and each action's response.

The redirect is navigation — the outcome comes from your server

The customer arrives at your redirect_url with nothing but ?order_id=. That is deliberate: anyone can type a URL, so a parameter claiming VERIFIED — signed or not — would be a bypass waiting to happen. Treat the redirect as “this person is back, go look up their order”, and get the answer one of these two ways:

  • Ask the gateway directly — a server-to-server status call with the order_id, and release goods only on is_paid: true. Your API key never leaves your server, so the customer cannot make this call or alter its answer.
  • Or let the webhook drive it — verify the X-PAM-Signature-V2 header on your callback_url (see Webhooks) and fulfil there. Recommended for anything valuable, because it does not depend on the customer's browser coming back at all.

Whichever you use, do these three checks before releasing anything: the order_id is one you created (look it up in your own database, do not trust it as an identity), the returned amount matches what you charged, and you have not already fulfilled that order_id. And remember PENDING is normal on arrival — the bank email may still be in flight, so show “confirming your payment” rather than a failure.

PHP — handle the return, then ask the gateway
<?php
$key = 'YOUR_64_CHARACTER_KEY';   // server-side only, never in HTML or JS

// 1) The redirect brings one thing: which order the customer came back from.
$orderId = $_GET['order_id'] ?? '';

// 2) It must be an order YOU created. This is also what stops someone
//    replaying a stranger's order_id to unlock your goods.
$cart = my_find_cart_by_order_id($orderId);   // your own database
if (! $cart) {
    http_response_code(404);
    exit('Unknown order.');
}
if ($cart['fulfilled']) {
    exit('Already delivered.');                // idempotent by order_id
}

// 3) Authoritative answer, server-to-server. The customer cannot touch this.
$ch = curl_init('https://yourgateway.com/v1');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['X-API-Key: ' . $key, 'Content-Type: application/json'],
    CURLOPT_POSTFIELDS     => json_encode(['action' => 'status', 'order_id' => $orderId]),
]);
$order = json_decode(curl_exec($ch), true);
curl_close($ch);

// 4) Paid, and paid the right amount.
$paid = ($order['is_paid'] ?? false) === true
     && (float) ($order['amount'] ?? 0) === (float) $cart['total'];

if ($paid) {
    my_fulfil($cart, $order['utr'] ?? null);   // deliver the goods
    exit('Payment confirmed.');
}

if (($order['order_status'] ?? '') === 'PENDING') {
    exit('We are confirming your payment - this page will update shortly.');
}

exit('Payment not completed.');                // deliver nothing
Node.js (Express) — handle the return, then ask the gateway
const KEY = 'YOUR_64_CHARACTER_KEY';   // server-side only

app.get('/payment/return', async (req, res) => {
  // 1) All the redirect carries.
  const { order_id } = req.query;

  // 2) It must be an order you created, and not one already delivered.
  const cart = await findCartByOrderId(order_id);
  if (!cart) return res.status(404).send('Unknown order');
  if (cart.fulfilled) return res.send('Already delivered');

  // 3) Authoritative answer, server-to-server.
  const r = await fetch('https://yourgateway.com/v1', {
    method: 'POST',
    headers: { 'X-API-Key': KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({ action: 'status', order_id }),
  });
  const order = await r.json();

  // 4) Paid, and for the right amount.
  if (order.is_paid === true && Number(order.amount) === Number(cart.total)) {
    await fulfil(cart, order.utr);
    return res.send('Payment confirmed');
  }

  if (order.order_status === 'PENDING') {
    return res.send('We are confirming your payment...');
  }

  res.send('Payment not completed');
});
Python (Flask) — handle the return, then ask the gateway
import requests
from flask import request, abort

KEY = 'YOUR_64_CHARACTER_KEY'   # server-side only

@app.get('/payment/return')
def payment_return():
    # 1) All the redirect carries.
    order_id = request.args.get('order_id', '')

    # 2) It must be an order you created, and not one already delivered.
    cart = find_cart_by_order_id(order_id)
    if not cart:
        abort(404)
    if cart['fulfilled']:
        return 'Already delivered'

    # 3) Authoritative answer, server-to-server.
    r = requests.post('https://yourgateway.com/v1',
        headers={'X-API-Key': KEY, 'Content-Type': 'application/json'},
        json={'action': 'status', 'order_id': order_id}, timeout=15)
    order = r.json()

    # 4) Paid, and for the right amount.
    if order.get('is_paid') is True and float(order['amount']) == float(cart['total']):
        fulfil(cart, order.get('utr'))
        return 'Payment confirmed'

    if order.get('order_status') == 'PENDING':
        return 'We are confirming your payment...'

    return 'Payment not completed'
Anti-bypass checklist: deliver only on is_paid: true from a server-side status call or a signature-verified webhook · look the order_id up in your own records, never trust it as identity · compare the returned amount with what you charged · make fulfilment idempotent per order_id · keep your API key server-side only · never let anything in the query string, the page, or a JavaScript variable decide that a payment succeeded.

Full integration examples

All-in-one example Node.js 18+

One copy-paste script that runs the whole flow: create an order, fetch its QR, poll until the payment is verified, and verify an incoming webhook. The endpoint below is your live gateway URL, detected automatically — just paste in your API key.

// PAY ABHRO MODZ — all-in-one integration (Node.js 18+, global fetch)
const crypto = require('node:crypto');

const BASE = 'https://wa.abhromodz.in/v1';          // auto-detected gateway endpoint
const KEY  = 'YOUR_64_CHARACTER_KEY';   // from Dashboard -> API Keys

// Single helper — every action goes through this one function.
async function pam(body) {
  const res = await fetch(BASE, {
    method: 'POST',
    headers: { 'X-API-Key': KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  });
  const data = await res.json();
  if (data.status !== 'success') throw new Error(data.message || 'API error');
  return data;
}

async function main() {
  // 1) Create an order
  const order = await pam({
    action: 'create',
    amount: 499,
    customer_id: 'CUST-88',
    callback_url: 'https://yoursite.com/webhooks/pam',
    expires_in: 900,
  });
  console.log('Pay page :', order.pay_url);
  console.log('UPI URI  :', order.upi_uri);
  console.log('QR image :', order.qr_png);   // <img src> / sendPhoto - no decoding

  // 2) The same URLs are also available on their own at any time
  const qr = await pam({ action: 'qr', order_id: order.order_id });
  console.log('QR PNG   :', qr.qr_png, '  QR SVG :', qr.qr_svg);

  // 3) Poll every 4s until verified or the window closes
  let status;
  do {
    await new Promise(r => setTimeout(r, 4000));
    status = await pam({ action: 'status', order_id: order.order_id });
    console.log('Status   :', status.order_status);
  } while (status.order_status === 'PENDING');

  console.log('Final    :', status.order_status);   // VERIFIED / EXPIRED / ...
}

// Webhook receiver — verify the signature before trusting the payload.
function verifyWebhook(rawBody, signatureHeader) {
  const expected = crypto.createHmac('sha256', KEY).update(rawBody).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader || ''));
}

main().catch(err => console.error('Error:', err.message));

Replace https://yourgateway.com/v1 with the endpoint shown at the top of this page, and YOUR_64_CHARACTER_KEY with your key.

Node.js (fetch)

const BASE = 'https://yourgateway.com/v1';
const KEY  = 'YOUR_64_CHARACTER_KEY';

async function call(body) {
  const res = await fetch(BASE, {
    method: 'POST',
    headers: {
      'X-API-Key': KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(body),
  });
  return res.json();
}

// 1) Create an order
const order = await call({
  action: 'create',
  amount: 499,
  customer_id: 'CUST-88',
  callback_url: 'https://yoursite.com/webhooks/pam',
});
console.log(order.pay_url, order.upi_uri);

// 2) Poll for status
const status = await call({ action: 'status', order_id: order.order_id });
console.log(status.order_status); // PENDING -> VERIFIED

PHP (cURL)

function pam($body) {
    $ch = curl_init('https://yourgateway.com/v1');
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => [
            'X-API-Key: YOUR_64_CHARACTER_KEY',
            'Content-Type: application/json',
        ],
        CURLOPT_POSTFIELDS => json_encode($body),
    ]);
    $out = curl_exec($ch);
    curl_close($ch);
    return json_decode($out, true);
}

$order = pam([
    'action' => 'create',
    'amount' => 499,
    'customer_id' => 'CUST-88',
]);
echo $order['pay_url'];

Python (requests)

import requests

BASE = 'https://yourgateway.com/v1'
HEADERS = {'X-API-Key': 'YOUR_64_CHARACTER_KEY'}

def call(body):
    return requests.post(BASE, json=body, headers=HEADERS, timeout=15).json()

order = call({'action': 'create', 'amount': 499, 'customer_id': 'CUST-88'})
print(order['pay_url'], order['upi_uri'])

status = call({'action': 'status', 'order_id': order['order_id']})
print(status['order_status'])