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.
- Method:
POST(onlyhealthmay also useGET-style checks) - Content-Type:
application/json - Auth:
X-API-Keyheader (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" }
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".
{
"status": "success",
"order_id": "PAM-8F2K9DLA31"
}
{
"status": "error",
"message": "Invalid or inactive API key."
}
HTTP status codes
| Code | Meaning | When it happens |
|---|---|---|
200 | OK | The action succeeded. |
401 | Unauthorized | Missing X-API-Key, or the key is invalid / inactive. |
404 | Not found | The order_id does not exist or belongs to another merchant. |
409 | Conflict | A request with the same Idempotency-Key is still being processed. |
422 | Unprocessable | Validation failed, or an unknown / missing action. |
429 | Too many requests | You exceeded 120 requests per minute for this key. |
500 | Server error | A 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,
createbehaves exactly as before.
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.
| Status | Meaning |
|---|---|
| PENDING | Order created, awaiting payment. |
| UNDER_REVIEW | A candidate payment was seen but needs manual confirmation. |
| VERIFIED | Payment confirmed. verified_at is set and any callback fires. |
| FAILED | Verification was attempted and rejected. |
| CANCELLED | Merchant cancelled the order via the cancel action. |
| EXPIRED | The payment window closed before verification. |
| REFUNDED | Merchant recorded a refund via the refund action. |
Actions
| Action | Description | Auth | Required fields |
|---|---|---|---|
create | Create a new payment order | Yes | amount |
status | Query an order's outcome — the only thing you may fulfil on | Yes | order_id |
verify | Force an immediate verification check | Yes | order_id |
cancel | Cancel a pending order | Yes | order_id |
refund | Mark an order as refunded | Yes | order_id |
invoice | Get the hosted invoice URL | Yes | order_id |
qr | Get the UPI URI + QR (SVG, base64) | Yes | order_id |
health | Gateway health check | No | — |
create — Create a payment order
Creates a new order and returns a hosted pay page URL plus a ready-to-render UPI URI.
| Field | Type | Required | Notes |
|---|---|---|---|
amount | number | Yes | Minimum 1. Rounded to 2 decimals. |
purpose | string | No | Max 64 chars. Normalised to uppercase alphanumerics and used as the UPI transaction note. Auto-generated (unique) if omitted. |
customer_id | string | No | Your own reference for the payer. Max 120 chars. Echoed back in status and webhooks. |
callback_url | string (URL) | No | Max 500 chars. Receives a signed webhook when the order is verified. |
redirect_url | string (URL) | No | Max 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_in | integer | No | Seconds until the order expires. 60–3600. Defaults to the gateway's configured expiry (typically 600). |
extra_data | any | No | Arbitrary 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" }
}
| Field | What it is |
|---|---|
order_status | PENDING, UNDER_REVIEW, VERIFIED, FAILED, CANCELLED, EXPIRED or REFUNDED. |
is_paid | true only when order_status is VERIFIED. Release goods on this and nothing else. |
amount / currency | What you asked for. Compare amount against your own cart total before fulfilling. |
paid_amount | The amount the bank actually reported. null until a payment is matched. |
utr / transaction_id | Bank identifiers for the settled payment, for your receipt and reconciliation. null until matched. |
paid_at | Timestamp the bank recorded for the transfer; verified_at is when this gateway matched it. |
customer_id / extra_data | Exactly 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"
}
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';
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
| Param | Meaning |
|---|---|
pa | Payee UPI address (VPA) |
pn | Payee name |
am | Amount, 2 decimals |
tn | Transaction note = the order's unique purpose |
cu | Currency (INR) |
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.
- Create an order from your server (
action: "create"). You get back anorder_idand a hostedpay_url. Store theorder_idagainst your own cart or invoice. - 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. - The gateway verifies the payment automatically by matching the bank confirmation email (purpose + amount + unique transaction ID / UTR).
- The customer comes back to your one
redirect_urlwith?order_id=appended — whatever happened, success or not. That is all the redirect tells you: this person returned from that order. - Your server asks what happened — call
statuswith thatorder_idand readis_paid. Deliver the goods only then. Nothing the browser hands you is ever proof of payment.
redirect_url, outcome read server-to-serverThis 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 param | Meaning |
|---|---|
order_id | The 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.
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.
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 -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
}
{ "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
statuscall with theorder_id, and release goods only onis_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-V2header on yourcallback_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
$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
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');
});
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'
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
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'])