Skip to content

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

terminal
bash
npm install upiagent

That'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.

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

create-payment.ts
typescript
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

create-svg.ts
typescript
import { createPaymentSvg } from "upiagent";

const payment = await createPaymentSvg(
  { upiId: "shop@ybl", name: "My Shop" },
  { amount: 299 }
);
// payment.qrDataUrl is now an SVG string

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

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

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

result-shape.ts
typescript
// 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).

1

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.

2

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.

3

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.

4

LLM confidence

How confident is the AI in its parsing? If the email was garbled or ambiguous, the confidence score drops and verification fails.

5

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:

postgres-dedup.ts
typescript
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

send-webhook.ts
typescript
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:

receive-webhook.ts
typescript
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

HeaderValue
X-UpiAgent-SignatureHMAC-SHA256 hex digest
X-UpiAgent-Eventpayment.verified or payment.expired
X-UpiAgent-Delivery-IdUnique 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:

terminal
bash
npx upiagent setup

Option B: Manual setup

1.Go to Google Cloud Console and create a new project
2.Enable the Gmail API (search for "Gmail API" in the API library)
3.Create OAuth 2.0 credentials (Desktop app type)
4.Add your Gmail as a test user under the OAuth consent screen
5.Set the redirect URI to "http://localhost:3000/auth/callback"
6.Use the client ID and secret in your code
Tip:Your Gmail must be the one that receives bank alerts. This is the Gmail connected to your bank account (the one that gets "You have received Rs.499 from..." emails).

LLM providers

upiagent uses AI to extract payment details (amount, sender, reference ID) from bank alert emails. Three providers are supported:

ProviderModelCostConfig
Google Geminigemini-2.0-flashFreeGEMINI_API_KEY
OpenAIgpt-4o-miniPaidOPENAI_API_KEY
Anthropicclaude-3-haikuPaidANTHROPIC_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.

llm-config.ts
typescript
// 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.

ParamTypeDescription
merchant.upiIdstringYour UPI ID (e.g., "shop@ybl")
merchant.namestringDisplay name in UPI app
options.amountnumberAmount in INR
options.addPaisaboolean?Add random paisa for unique matching (default: false)
options.transactionIdstring?Your reference ID (auto-generated if omitted)
options.notestring?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.

ParamTypeDescription
gmailGmailCredentialsclientId, clientSecret, refreshToken
llmLlmConfigprovider, model, apiKey
expected.amountnumberAmount to match
expected.timeWindowMinutesnumber?Max age of payment (default: 30)
lookbackMinutesnumber?Gmail search window (default: 30)
dedupDedupStore?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:

errors.ts
typescript
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
  }
}
Non-error failures(amount mismatch, duplicate, etc.) don't throw. They return a 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
typescript
// 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

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