Documentation
Get started with upiagent
Accept UPI payments directly into your bank account. No payment gateway, no fees, no merchant onboarding. Just install the package and go.
Install
npm install upiagentThat's it. The package has zero native dependencies and works in Node.js 18+.
Quick start
The full flow in 20 lines. Generate a QR, customer pays, you verify.
import { createPayment, fetchAndVerifyPayment } from "upiagent";
// Step 1 — Generate a payment QR code
const payment = await createPayment(
{ upiId: "yourshop@ybl", name: "Your Shop" },
{ amount: 499, note: "Order #123" }
);
// Show payment.qrDataUrl to the customer (it's a base64 PNG)
// Save payment.transactionId to your database
// Step 2 — After the customer pays, verify it
const result = await fetchAndVerifyPayment({
gmail: {
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
refreshToken: process.env.GMAIL_REFRESH_TOKEN,
},
llm: {
provider: "gemini",
model: "gemini-2.0-flash",
apiKey: process.env.GEMINI_API_KEY,
},
expected: { amount: 499 },
});
if (result.verified) {
console.log("Payment confirmed!", result.payment);
}Generate QR
createPayment() generates a UPI intent URL and encodes it as a QR code. Customers scan this with any UPI app — GPay, PhonePe, Paytm, CRED, etc.
import { createPayment } from "upiagent";
const payment = await createPayment(
{ upiId: "shop@ybl", name: "My Shop" },
{
amount: 299,
note: "Order #456",
addPaisa: true, // adds random paisa for unique amount matching
}
);
// payment.qrDataUrl → base64 PNG, use in <img src={...} />
// payment.intentUrl → upi://pay?pa=shop@ybl&am=299.42&...
// payment.transactionId → "txn_a1b2c3d4"
// payment.amount → 299.42 (with random paisa added)Why addPaisa?
When addPaisa: true, a random amount between 0.01 and 0.99 is added. So a 299 payment becomes 299.42. This makes every QR unique — even if two customers pay the same item at the same time, the amounts are different, so verification can match exactly.
SVG variant
import { createPaymentSvg } from "upiagent";
const payment = await createPaymentSvg(
{ upiId: "shop@ybl", name: "My Shop" },
{ amount: 299 }
);
// payment.qrDataUrl is now an SVG stringVerify payment
There are two ways to verify. Use fetchAndVerifyPayment() to do everything in one call, or use verifyPayment() if you already have the email.
Option A: Fetch + verify (recommended)
Connects to Gmail, fetches recent bank alert emails, and checks each one against your expected payment.
import { fetchAndVerifyPayment } from "upiagent";
const result = await fetchAndVerifyPayment({
gmail: {
clientId: "your-client-id",
clientSecret: "your-client-secret",
refreshToken: "your-refresh-token",
},
llm: {
provider: "gemini",
model: "gemini-2.0-flash",
apiKey: "your-gemini-key",
},
expected: {
amount: 499, // exact amount to match
timeWindowMinutes: 30, // how far back to look (default: 30)
},
lookbackMinutes: 30, // Gmail search window
maxEmails: 10, // max emails to check
});
if (result.verified) {
console.log(result.payment.senderName); // "John Doe"
console.log(result.payment.amount); // 499
console.log(result.payment.upiReferenceId); // "412345678901"
console.log(result.confidence); // 0.95
}Option B: Verify a single email
If you already have the email (e.g., from your own Gmail polling), pass it directly.
import { verifyPayment } from "upiagent";
const result = await verifyPayment(email, {
llm: {
provider: "gemini",
model: "gemini-2.0-flash",
apiKey: "your-gemini-key",
},
expected: { amount: 499 },
});What's in the result?
// VerificationResult
{
verified: true, // did it pass all 5 layers?
payment: {
amount: 499,
senderName: "John Doe",
senderUpiId: "john@ybl",
upiReferenceId: "412345678901",
bankName: "HDFC Bank",
receivedAt: "2025-01-15T10:30:00Z",
},
confidence: 0.95, // LLM confidence (0-1)
failureReason: null, // or "AMOUNT_MISMATCH", "DUPLICATE", etc.
layerResults: [ // step-by-step breakdown
{ layer: "email_source", passed: true },
{ layer: "amount_match", passed: true },
{ layer: "time_window", passed: true },
{ layer: "llm_confidence", passed: true },
{ layer: "dedup", passed: true },
],
}Security layers
Every payment goes through 5 validation layers, in order. If any layer fails, verification stops immediately (fail-fast).
Email source
Is the email from a known bank? We check against a registry of bank email patterns (ICICI, HDFC, Axis, SBI, etc.). Blocks spoofed emails.
Amount match
Does the parsed amount match exactly? Default tolerance is 0% — even Re.1 off and it fails. You can set amountTolerancePercent if needed, but we recommend exact matching.
Time window
Was the payment within the lookback window? Default is 30 minutes. UPI settles in seconds, but bank emails can be delayed. This blocks old/stale emails from being replayed.
LLM confidence
How confident is the AI in its parsing? If the email was garbled or ambiguous, the confidence score drops and verification fails.
Deduplication
Has this exact transaction been verified before? Prevents double-crediting. Uses in-memory store by default, or Postgres for production.
Postgres dedup (production)
The default in-memory dedup store resets when your server restarts. For production, use the Postgres store:
import { fetchAndVerifyPayment, PostgresDedupStore } from "upiagent";
const dedup = new PostgresDedupStore(process.env.DATABASE_URL);
const result = await fetchAndVerifyPayment({
// ... gmail, llm, expected
dedup,
});Webhooks
Send HMAC-signed webhook notifications when payments are verified or expire. Includes automatic retries with exponential backoff.
Sending webhooks
import { WebhookSender } from "upiagent";
const sender = new WebhookSender({
url: "https://yourapp.com/webhooks/payment",
secret: "whsec_your_secret_here",
});
// Send a payment.verified event
const delivery = await sender.send({
event: "payment.verified",
data: {
paymentId: "txn_123",
amount: 499,
currency: "INR",
status: "verified",
upiReferenceId: "412345678901",
senderName: "John Doe",
confidence: 0.95,
},
});
// delivery.delivered → true/false
// delivery.attempts → number of attempts (retries on failure)Receiving webhooks
Verify the HMAC signature before processing:
import { verifyWebhookSignature } from "upiagent";
// In your webhook handler (Express, Next.js, etc.)
const signature = req.headers["x-upiagent-signature"];
const event = req.headers["x-upiagent-event"];
const body = await req.text();
const isValid = verifyWebhookSignature(body, signature, "whsec_your_secret");
if (!isValid) {
return new Response("Invalid signature", { status: 401 });
}
// Safe to process
const payload = JSON.parse(body);
if (payload.event === "payment.verified") {
// credit the customer
}Webhook headers
| Header | Value |
|---|---|
| X-UpiAgent-Signature | HMAC-SHA256 hex digest |
| X-UpiAgent-Event | payment.verified or payment.expired |
| X-UpiAgent-Delivery-Id | Unique delivery ID for idempotency |
Gmail setup
upiagent reads bank alert emails from your Gmail to detect incoming payments. You need a Google Cloud project with the Gmail API enabled.
Option A: CLI setup wizard
The fastest way. Run this and follow the prompts:
npx upiagent setupOption B: Manual setup
LLM providers
upiagent uses AI to extract payment details (amount, sender, reference ID) from bank alert emails. Three providers are supported:
| Provider | Model | Cost | Config |
|---|---|---|---|
| Google Gemini | gemini-2.0-flash | Free | GEMINI_API_KEY |
| OpenAI | gpt-4o-mini | Paid | OPENAI_API_KEY |
| Anthropic | claude-3-haiku | Paid | ANTHROPIC_API_KEY |
We default to Gemini because it's free and fast enough for parsing bank emails. You or your merchants can bring their own keys for any provider.
// Gemini (free, default)
llm: { provider: "gemini", model: "gemini-2.0-flash", apiKey: "..." }
// OpenAI
llm: { provider: "openai", model: "gpt-4o-mini", apiKey: "..." }
// Anthropic
llm: { provider: "anthropic", model: "claude-3-haiku-20240307", apiKey: "..." }API reference
createPayment(merchant, options)
Generate a UPI QR code for a payment.
| Param | Type | Description |
|---|---|---|
| merchant.upiId | string | Your UPI ID (e.g., "shop@ybl") |
| merchant.name | string | Display name in UPI app |
| options.amount | number | Amount in INR |
| options.addPaisa | boolean? | Add random paisa for unique matching (default: false) |
| options.transactionId | string? | Your reference ID (auto-generated if omitted) |
| options.note | string? | Note shown in UPI app |
Returns a PaymentRequest with qrDataUrl, intentUrl, transactionId, and amount.
fetchAndVerifyPayment(options)
Fetch bank alert emails from Gmail and verify against expected payment.
| Param | Type | Description |
|---|---|---|
| gmail | GmailCredentials | clientId, clientSecret, refreshToken |
| llm | LlmConfig | provider, model, apiKey |
| expected.amount | number | Amount to match |
| expected.timeWindowMinutes | number? | Max age of payment (default: 30) |
| lookbackMinutes | number? | Gmail search window (default: 30) |
| dedup | DedupStore? | InMemoryDedupStore or PostgresDedupStore |
Returns a VerificationResult— see the "Verify payment" section above for the full shape.
WebhookSender
Create with new WebhookSender({ url, secret }). Call .send(payload) to deliver. Retries automatically (3 attempts with 1s, 5s, 25s delays).
verifyWebhookSignature(body, signature, secret)
Returns true if the HMAC-SHA256 signature matches. Use this in your webhook handler to verify the request is from upiagent.
Error handling
upiagent throws typed errors you can catch and handle specifically:
import {
GmailAuthError,
GmailRateLimitError,
LlmError,
LlmRateLimitError,
LlmBudgetExceededError,
ConfigError,
} from "upiagent";
try {
const result = await fetchAndVerifyPayment({ ... });
} catch (err) {
if (err instanceof GmailAuthError) {
// Gmail OAuth token expired — re-authenticate
}
if (err instanceof GmailRateLimitError) {
// Gmail API rate limit — back off and retry
}
if (err instanceof LlmError) {
// LLM API call failed — check your API key
}
if (err instanceof LlmRateLimitError) {
// LLM rate limit — use the built-in LlmRateLimiter
}
if (err instanceof LlmBudgetExceededError) {
// Token budget exceeded — use CostTracker to set limits
}
}VerificationResult with verified: false and a failureReason code. Only infrastructure failures (auth, rate limits, network) throw errors.Examples
Next.js API route
// app/api/create-payment/route.ts
import { createPayment } from "upiagent";
export async function POST(req: Request) {
const { amount, note } = await req.json();
const payment = await createPayment(
{ upiId: process.env.UPI_ID!, name: "My Store" },
{ amount, note, addPaisa: true }
);
return Response.json({
qrDataUrl: payment.qrDataUrl,
transactionId: payment.transactionId,
amount: payment.amount,
});
}Express server
import express from "express";
import { createPayment, fetchAndVerifyPayment } from "upiagent";
const app = express();
app.use(express.json());
app.post("/pay", async (req, res) => {
const payment = await createPayment(
{ upiId: "shop@ybl", name: "My Shop" },
{ amount: req.body.amount, addPaisa: true }
);
res.json(payment);
});
app.post("/verify", async (req, res) => {
const result = await fetchAndVerifyPayment({
gmail: { /* credentials */ },
llm: { provider: "gemini", model: "gemini-2.0-flash", apiKey: "..." },
expected: { amount: req.body.amount },
});
res.json({ verified: result.verified, payment: result.payment });
});
app.listen(3000);Webhook receiver (Next.js)
// app/api/webhooks/payment/route.ts
import { verifyWebhookSignature } from "upiagent";
export async function POST(req: Request) {
const body = await req.text();
const signature = req.headers.get("x-upiagent-signature")!;
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") {
// Update order status, send confirmation email, etc.
console.log("Payment verified:", payload.data.paymentId);
}
return new Response("ok");
}Ready to start?
npm install upiagent