Documentation
Get started with upiagent
Accept UPI payments directly into your bank account. No payment gateway, no fees, no merchant onboarding. Install the package, add your API key, done.
Install
npm install upiagentZero native dependencies. Works in Node.js 18+, Next.js, Express, or any server-side JS.
Quick start
Sign up at beta-dashboard.upiagent.live, connect your Gmail and UPI ID, grab your API key. Then it's 5 lines:
import { UpiAgent } from "upiagent/client";
const upi = new UpiAgent({ apiKey: process.env.UPIAGENT_API_KEY });
// 1. Create payment — returns a QR code
const payment = await upi.createPayment({
amount: 499,
note: "Order #123",
addPaisa: true, // ₹499 → ₹499.37 (unique amount for matching)
});
// 2. Show QR to customer
// payment.qrDataUrl → base64 PNG for <img src={...} />
// payment.intentUrl → upi://pay?... for mobile deep link
// 3. Verify — checks your Gmail for bank alert automatically
const result = await upi.verify(payment.id);
if (result.verified) {
console.log("Paid!", result.payment.senderName, result.payment.upiReferenceId);
}Payment lifecycle
Every payment has one of five statuses: pending → claimed → verified, plus expired and cancelled.
| Status | Meaning |
|---|---|
| pending | Created, not paid yet. Expires 20 minutes after creation. |
| claimed | A payment screenshot passed every check (success status, exact amount, paid to your UPI ID, paid after the request was created, unused 12-digit UTR, image never submitted before). Optimistic — fine for low-value goods. |
| verified | Confirmed by bank evidence (Gmail bank alert or the Android notification app). Fine for anything. A claimed payment is upgraded to verified when bank evidence arrives. |
| expired | Final. The pending payment was not paid within 20 minutes. |
| cancelled | Final. You cancelled the pending payment. |
verified as paid for anything. For low-value orders you can also treat claimed as paid. expired and cancelled are final — stop polling.Create & wait (one call)
For the simplest integration, use createAndWaitForPayment() — it creates the payment, polls for verification, and returns when done. It keeps polling while the payment is claimed and stops on verified, expired, or cancelled.
import { UpiAgent } from "upiagent/client";
const upi = new UpiAgent({ apiKey: process.env.UPIAGENT_API_KEY });
const payment = await upi.createAndWaitForPayment(
{ amount: 499, note: "Order #123", addPaisa: true },
{
onPaymentCreated: (p) => {
// Show QR to customer
console.log("Scan to pay:", p.qrDataUrl);
},
onStatusUpdate: (p) => {
console.log("Status:", p.status);
},
pollInterval: 5000, // check every 5s
timeout: 180_000, // give up after 3 min
}
);
if (payment.status === "verified") {
console.log("Payment confirmed!", payment.senderName);
}
// "expired" | "cancelled" → final, show a retry buttonNext.js integration
API route
// app/api/pay/route.ts
import { UpiAgent } from "upiagent/client";
const upi = new UpiAgent({ apiKey: process.env.UPIAGENT_API_KEY! });
export async function POST(req: Request) {
const { amount, orderId } = await req.json();
const payment = await upi.createPayment({
amount,
note: `Order ${orderId}`,
addPaisa: true,
});
return Response.json({
id: payment.id,
qrDataUrl: payment.qrDataUrl,
intentUrl: payment.intentUrl,
amount: payment.amount,
});
}
// app/api/pay/[id]/route.ts
export async function GET(
_req: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
const status = await upi.getStatus(id);
return Response.json(status);
}React component
"use client";
import { useState } from "react";
// Your webhook handler marks the order paid. The page checks once when the
// customer says they've paid — no polling.
export function Checkout({ amount }: { amount: number }) {
const [payment, setPayment] = useState<any>(null);
const [status, setStatus] = useState("idle");
async function pay() {
const res = await fetch("/api/pay", {
method: "POST",
body: JSON.stringify({ amount, orderId: "ORD_123" }),
});
setPayment(await res.json());
setStatus("pending");
}
async function check() {
const r = await fetch(`/api/pay/${payment.id}`);
setStatus((await r.json()).status); // claimed = screenshot OK, verified = bank-confirmed
}
if (status === "idle") return <button onClick={pay}>Pay ₹{amount}</button>;
if (status === "pending") return (
<div>
<img src={payment.qrDataUrl} alt="Scan to pay" />
<a href={payment.intentUrl}>Open UPI App</a>
<button onClick={check}>I've paid</button>
</div>
);
if (status === "claimed") return <p>Received — confirming with the bank… <button onClick={check}>Refresh</button></p>;
if (status === "verified") return <p>Payment confirmed!</p>;
return <p>Payment {status}. Try again.</p>;
}Express integration
import express from "express";
import { UpiAgent } from "upiagent/client";
const app = express();
app.use(express.json());
const upi = new UpiAgent({ apiKey: process.env.UPIAGENT_API_KEY! });
app.post("/pay", async (req, res) => {
const payment = await upi.createPayment({
amount: req.body.amount,
note: req.body.note,
addPaisa: true,
});
res.json(payment);
});
app.get("/pay/:id", async (req, res) => {
const status = await upi.getStatus(req.params.id);
res.json(status);
});
app.post("/pay/:id/verify", async (req, res) => {
const result = await upi.verify(req.params.id);
res.json(result);
});
app.listen(3000);SDK reference
UpiAgent(config)
Create a client instance. All you need is your API key.
| Param | Type | Description |
|---|---|---|
| apiKey | string | Your API key from the dashboard |
| baseUrl? | string | Override API URL (default: https://beta.upiagent.live) |
upi.createPayment(params)
Create a payment and get a QR code. Customers scan with any UPI app — GPay, PhonePe, Paytm, CRED.
| Param | Type | Description |
|---|---|---|
| amount | number | Amount in INR |
| note? | string | Note shown in UPI app |
| addPaisa? | boolean | Add random paisa for unique matching (recommended) |
Returns a Payment with qrDataUrl (base64 PNG), intentUrl (UPI deep link), id, amount, and status.
upi.verify(paymentId)
Trigger verification — scans Gmail for a matching bank alert. Call after the customer has paid.
const result = await upi.verify(payment.id);
// result.verified → true if payment matched
// result.payment → { amount, upiReferenceId, senderName, bankName, confidence }
// result.status → "pending" | "claimed" | "verified" | "expired" | "cancelled"
// result.message → failure reason if not verifiedVerification spends LLM tokens and counts toward your daily token cap.
upi.getStatus(paymentId)
Read-only status check — no verification triggered.
upi.submitProof(paymentId, { image, mediaType? })
Submit the customer's payment screenshot (base64 or data: URL; png, jpeg or webp; max 3 MB). If every check passes, the payment moves to claimed and a payment.claimed webhook is sent.
const proof = await upi.submitProof(payment.id, {
image: screenshotBase64, // or "data:image/png;base64,..."
mediaType: "image/png",
});
// proof.accepted → true if the screenshot passed every check
// proof.status → payment status after the check (e.g. "claimed")
// proof.utr → 12-digit UTR read from the screenshot, or null
// proof.confidence → 0..1
// proof.reasons → why it was rejected (string[])
if (!proof.accepted) {
// Do NOT deliver the goods
}accepted: false means do not deliver the goods. Each proof is one vision LLM call and counts toward your daily token cap.upi.cancel(paymentId)
Cancel a pending payment. Returns the updated Payment. Only pending payments can be cancelled (409 otherwise).
upi.listPayments(params?)
List payments, newest first. Filters: status?, since? (ISO 8601), limit? (1–50, default 20), cursor?.
const { payments, hasMore, nextCursor } = await upi.listPayments({
status: "claimed",
limit: 20,
});
if (hasMore) {
const next = await upi.listPayments({ status: "claimed", cursor: nextCursor! });
}upi.getEvidence(paymentId)
Verification attempts for a payment, newest first (max 20). Each entry has source (gmail | notification | screenshot), status (match | no_match | error), confidence, and createdAt.
upi.getUsage()
Today's LLM token usage: tokensToday, dailyLimit (default 20,000), remaining, tier (platform | own_key), and resetsAt (00:00 UTC).
upi.createAndWaitForPayment(params, options)
Create + poll in one call. Keeps polling while the payment is claimed; returns when it is verified, expired, or cancelled, or on timeout.
| Option | Type | Description |
|---|---|---|
| onPaymentCreated? | fn | Called with payment after creation (show QR here) |
| onStatusUpdate? | fn | Called on each poll |
| pollInterval? | number | Ms between polls (default: 5000) |
| timeout? | number | Ms before giving up (default: 180000) |
REST API
Not using Node? Call the API directly. Every request needs Authorization: Bearer upi_ak_.... Errors are JSON { "error": string }.
| Endpoint | Description |
|---|---|
| POST /api/v1/payments | Create a payment. Body { amount: 1..100000, note?, addPaisa? } → 201 |
| GET /api/v1/payments | List payments, newest first. Query status, since, limit (1–50, default 20), cursor → { payments, hasMore, nextCursor } |
| GET /api/v1/payments/:id | Payment status and details (read-only) |
| POST /api/v1/payments/:id | Trigger Gmail verification now → { verified, status, message?, payment? }. Spends LLM tokens |
| DELETE /api/v1/payments/:id | Cancel a pending payment → Payment (409 if not pending) |
| POST /api/v1/payments/:id/proof | Submit a payment screenshot (png/jpeg/webp, max 3 MB). Spends LLM tokens |
| GET /api/v1/payments/:id/evidence | Verification attempts, newest first (max 20) |
| GET /api/v1/usage | Today's LLM token usage and daily limit |
Create payment response
{
"id": "uuid",
"transactionId": "TXN_m4x7k2_a1b2c3",
"amount": 499.37,
"intentUrl": "upi://pay?pa=...&am=499.37&...",
"qrDataUrl": "data:image/png;base64,...",
"status": "pending",
"expiresAt": "2026-05-03T10:50:00Z",
"createdAt": "2026-05-03T10:30:00Z"
}Submit screenshot proof
curl -X POST https://beta.upiagent.live/api/v1/payments/$ID/proof \
-H "Authorization: Bearer $UPIAGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"image": "data:image/png;base64,iVBORw0KGgo...", "mediaType": "image/png"}'{
"accepted": true,
"status": "claimed",
"utr": "003538060093",
"confidence": 0.96,
"reasons": []
}accepted: false means do not deliver the goods. Check reasons for why the screenshot was rejected.Usage response
{
"tokensToday": 4210,
"dailyLimit": 20000,
"remaining": 15790,
"tier": "platform",
"resetsAt": "2026-05-04T00:00:00Z"
}Webhooks
Set your webhook URL in the dashboard settings. We POST to it when a payment is claimed, verified, or expired. HTTPS only; private/internal addresses are rejected. Failed deliveries are retried 3 times (after 1s, 5s, 25s).
| Event | When |
|---|---|
| payment.claimed | A screenshot proof passed every check |
| payment.verified | Bank evidence confirmed the payment |
| payment.expired | The payment expired unpaid |
Webhook payload
{
"event": "payment.verified",
"timestamp": "2026-05-03T10:30:01Z",
"deliveryId": "d_...",
"data": {
"paymentId": "uuid",
"amount": 499.37,
"currency": "INR",
"status": "verified",
"upiReferenceId": "003538060093",
"senderName": "JOHN DOE",
"confidence": 0.95,
"verifiedAt": "2026-05-03T10:30:00Z"
}
}data.status is claimed, verified, or expired. Sender fields are optional.
Verify signature
X-UpiAgent-Signature is sha256= followed by the hex HMAC-SHA256 of the raw request body, keyed with your webhook secret (a hex string, decoded to bytes). Use verifyWebhookSignature from upiagent:
import { verifyWebhookSignature } from "upiagent";
export async function POST(req: Request) {
const body = await req.text();
const signature = req.headers.get("x-upiagent-signature")!;
// WEBHOOK_SECRET = the hex secret from Dashboard → Settings
if (!verifyWebhookSignature(body, signature, process.env.WEBHOOK_SECRET!)) {
return new Response("Invalid signature", { status: 401 });
}
const payload = JSON.parse(body);
if (payload.event === "payment.verified") {
// Bank-confirmed: fulfill the order
} else if (payload.event === "payment.claimed") {
// Screenshot passed all checks: OK for low-value goods
} else if (payload.event === "payment.expired") {
// Final: release any reserved stock
}
return new Response("ok");
}Headers
| Header | Value |
|---|---|
| X-UpiAgent-Signature | sha256=<hex HMAC-SHA256 of raw body, keyed with the hex secret> |
| X-UpiAgent-Event | payment.claimed | payment.verified | payment.expired |
| X-UpiAgent-Delivery-Id | Unique ID for idempotency |
AI agents (MCP)
Let an AI agent create payments and check proofs over the Model Context Protocol. Use the hosted server at https://beta.upiagent.live/api/mcp (Streamable HTTP) with Authorization: Bearer upi_ak_..., or run it locally:
{
"mcpServers": {
"upiagent": {
"command": "npx",
"args": ["-y", "upiagent", "mcp"],
"env": { "UPIAGENT_API_KEY": "upi_ak_..." }
}
}
}UPIAGENT_BASE_URL is optional. Tools: upiagent_create_payment, upiagent_submit_payment_proof, upiagent_get_payment_status, upiagent_list_payments, upiagent_cancel_payment, upiagent_get_usage.
verified (or claimed for low-value orders) — never when a proof comes back accepted: false.Security layers
Every payment goes through 5 validation layers. If any layer fails, verification stops immediately.
Email source
Is the email from a known bank? Checked against a registry of bank sender patterns (ICICI, HDFC, Axis, SBI, etc.).
Amount match
Does the parsed amount match exactly? Zero tolerance by default.
Time window
Was the payment within the lookback window? Blocks stale/replayed emails.
LLM confidence
AI confidence in parsing the email. Low confidence = verification fails.
Deduplication
Has this UPI reference been used before? Prevents double-crediting.
Error handling
import { UpiAgent, UpiAgentApiError } from "upiagent/client";
const upi = new UpiAgent({ apiKey: process.env.UPIAGENT_API_KEY! });
try {
const payment = await upi.createPayment({ amount: 499, addPaisa: true });
} catch (err) {
if (err instanceof UpiAgentApiError) {
console.error(err.status, err.message);
// 401 → invalid API key
// 429 → rate limit or daily token cap
// 404 → payment not found
// 409 → cancel on a payment that isn't pending
}
}{ verified: false, message: "..." }. Only infrastructure failures (auth, rate limits) throw.Limits
| What | Limit |
|---|---|
| Create payment | 60/min per merchant |
| Payment expiry | 20 minutes after creation |
| Trigger verification | 10/min, 100/hr per merchant |
| Screenshot proof | 10/min, 120/hr per merchant |
| Daily LLM token cap | 20,000 tokens/day by default (429 when reached) |
Only verification and screenshot proofs spend tokens — everything else is free. Check your usage with upi.getUsage().
How it works
When you sign up, you connect your Gmail (the one that receives bank alerts like "You have received Rs.499 from...") and your UPI ID. upiagent handles the rest:
Ready to start?
npm install upiagentThen grab your API key from the dashboard