Skip to content

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

terminal
bash
npm install upiagent

Zero 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:

checkout.ts
typescript
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);
}
That's it. No Gmail credentials, no Google Cloud project, no LLM API keys. You set all of that up once in the dashboard — the SDK just uses your API key.

Payment lifecycle

Every payment has one of five statuses: pending → claimed → verified, plus expired and cancelled.

StatusMeaning
pendingCreated, not paid yet. Expires 20 minutes after creation.
claimedA 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.
verifiedConfirmed 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.
expiredFinal. The pending payment was not paid within 20 minutes.
cancelledFinal. You cancelled the pending payment.
When to deliver. Treat 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.

create-and-wait.ts
typescript
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 button

Next.js integration

API route

app/api/pay/route.ts
typescript
// 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

components/checkout.tsx
tsx
"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

server.ts
typescript
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.

ParamTypeDescription
apiKeystringYour API key from the dashboard
baseUrl?stringOverride 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.

ParamTypeDescription
amountnumberAmount in INR
note?stringNote shown in UPI app
addPaisa?booleanAdd 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.

verify.ts
typescript
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 verified

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

proof.ts
typescript
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?.

list.ts
typescript
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.

OptionTypeDescription
onPaymentCreated?fnCalled with payment after creation (show QR here)
onStatusUpdate?fnCalled on each poll
pollInterval?numberMs between polls (default: 5000)
timeout?numberMs 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 }.

EndpointDescription
POST /api/v1/paymentsCreate a payment. Body { amount: 1..100000, note?, addPaisa? } → 201
GET /api/v1/paymentsList payments, newest first. Query status, since, limit (1–50, default 20), cursor → { payments, hasMore, nextCursor }
GET /api/v1/payments/:idPayment status and details (read-only)
POST /api/v1/payments/:idTrigger Gmail verification now → { verified, status, message?, payment? }. Spends LLM tokens
DELETE /api/v1/payments/:idCancel a pending payment → Payment (409 if not pending)
POST /api/v1/payments/:id/proofSubmit a payment screenshot (png/jpeg/webp, max 3 MB). Spends LLM tokens
GET /api/v1/payments/:id/evidenceVerification attempts, newest first (max 20)
GET /api/v1/usageToday's LLM token usage and daily limit

Create payment response

201 Created
json
{
  "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

terminal
bash
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"}'
response.json
json
{
  "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

GET /api/v1/usage
json
{
  "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).

EventWhen
payment.claimedA screenshot proof passed every check
payment.verifiedBank evidence confirmed the payment
payment.expiredThe payment expired unpaid

Webhook payload

webhook-payload.json
json
{
  "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:

app/api/webhooks/route.ts
typescript
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

HeaderValue
X-UpiAgent-Signaturesha256=<hex HMAC-SHA256 of raw body, keyed with the hex secret>
X-UpiAgent-Eventpayment.claimed | payment.verified | payment.expired
X-UpiAgent-Delivery-IdUnique 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:

mcp.json
json
{
  "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.

Release rule: only release goods when the status is 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.

1

Email source

Is the email from a known bank? Checked against a registry of bank sender patterns (ICICI, HDFC, Axis, SBI, etc.).

2

Amount match

Does the parsed amount match exactly? Zero tolerance by default.

3

Time window

Was the payment within the lookback window? Blocks stale/replayed emails.

4

LLM confidence

AI confidence in parsing the email. Low confidence = verification fails.

5

Deduplication

Has this UPI reference been used before? Prevents double-crediting.

Error handling

errors.ts
typescript
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
  }
}
Verification failures(amount mismatch, no matching email, etc.) don't throw errors. They return { verified: false, message: "..." }. Only infrastructure failures (auth, rate limits) throw.

Limits

WhatLimit
Create payment60/min per merchant
Payment expiry20 minutes after creation
Trigger verification10/min, 100/hr per merchant
Screenshot proof10/min, 120/hr per merchant
Daily LLM token cap20,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:

1.You call createPayment() — we generate a QR code with your UPI ID
2.Customer scans and pays — money goes directly to your bank account
3.Your bank sends a confirmation email to your Gmail
4.You call verify() — our AI reads the bank email and confirms the payment
5.We return the sender name, UPI reference ID, and confidence score
No payment gateway. Money flows directly from customer to your bank via UPI. upiagent only reads emails to confirm it happened. You keep 100% of every payment.

Ready to start?

npm install upiagent

Then grab your API key from the dashboard