npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@varunsingla/upi-utils

v0.1.0

Published

Correct, dependency-free TypeScript utilities for India's UPI rails — VPA validation, PSP/sponsor-bank resolution, upi:// deep links, and QR payloads.

Readme

upi-utils

CI License: MIT TypeScript Zero dependencies

Correct, dependency-free TypeScript utilities for India's UPI payment rails.

What is UPI? UPI (Unified Payments Interface) is India's real-time bank-to-bank payment network — the rails behind apps like PhonePe, Google Pay, and Paytm, moving billions of transactions a month. You pay someone using a VPA (Virtual Payment Address) that looks like varun@okhdfcbank, or by scanning a QR that encodes a upi://pay?... link.

This library handles the parts that are easy to get subtly wrong unless you've actually shipped on these rails: case-insensitive VPA validation, resolving an @handle to the PSP and sponsor bank behind it, building spec-correct deep links and QR payloads, and Indian-format currency.

npm install @varunsingla/upi-utils
  • Zero runtime dependencies. qrcode is an optional peer dependency, only needed if you want renderQr.
  • TypeScript-first, ships ESM + CJS + types.
  • Designed for the front end (React Native / Expo / web), where deep links and QRs actually live.

Module 1 — VPA validation

A VPA is {identifier}@{handle}, case-insensitive, with a constrained character set. The naive version is a one-line regex; the credible version handles case normalization, length bounds, rejecting leading/trailing/double dots, and treating "syntactically valid" as distinct from "known handle".

import { isValidVpa, normalizeVpa } from '@varunsingla/upi-utils';

isValidVpa('varun@okhdfcbank');  // true
isValidVpa('9876543210@paytm');  // true
isValidVpa('bad vpa@@x');        // false
normalizeVpa('Varun@OKHDFCBANK'); // 'varun@okhdfcbank'

Module 2 — PSP-suffix parsing

The @handle does not map cleanly to a bank. It maps to a PSP (the app/TPAP that issued the address) and a sponsor bank (whose NPCI membership the app rides on). @ybl is PhonePe, not "Yes Bank's app".

import { parseVpa } from '@varunsingla/upi-utils';

parseVpa('merchant@ybl');
// { local: 'merchant', handle: 'ybl', psp: 'PhonePe', sponsorBank: 'Yes Bank', known: true }

parseVpa('user@okaxis').psp;  // 'Google Pay'
parseVpa('shop@apl').psp;     // 'Amazon Pay'
parseVpa('x@paytm').psp;      // 'Paytm'  (sponsorBank: 'Paytm Payments Bank')

⚠️ This mapping is a living dataset, not hardcoded truth. There is no official public NPCI registry of handles; they get added and retired over time. It lives in src/data/handles.json with a lastVerified date and is community-maintained — see CONTRIBUTING.md. A valid VPA on an uncatalogued handle returns known: false rather than failing.

Module 3 — UPI intent / deep links

upi://pay?... is the canonical NPCI deep-link spec. Getting it right means URL-encoding the VPA (@%40) and note, fixing cu=INR, and — the bug a non-payments dev ships — serializing am as a string with exactly two decimals, because some PSP apps choke on a bare integer.

import { buildUpiLink, buildAppUpiLink } from '@varunsingla/upi-utils';

buildUpiLink({
  pa: 'merchant@ybl',  // payee VPA (required)
  pn: 'Chai Point',    // payee name (required)
  am: 149,             // amount → serialized as "149.00"
  tn: 'Order 8821',    // note
  tr: 'TXN20260618093',// txn reference
  mc: '5814',          // merchant category code
});
// upi://pay?pa=merchant%40ybl&pn=Chai%20Point&am=149.00&cu=INR&tn=Order%208821&tr=TXN20260618093&mc=5814

buildAppUpiLink('phonepe', { pa: 'merchant@ybl', pn: 'Chai Point', am: 149 });
// phonepe://pay?pa=merchant%40ybl&pn=Chai%20Point&am=149.00&cu=INR

Android intent-chooser note: a plain upi://pay link opens the system chooser listing every installed UPI app. App-specific schemes (phonepe://, paytmmp://, tez://upi/pay) jump straight to one app — but only if it's installed, so always keep upi:// as the fallback.

Module 4 — QR payloads

A UPI QR just encodes the same upi://pay?... string. The useful distinction is static (no amount — payer types it in) vs dynamic (amount + ref baked in for a single sale).

import { buildQrPayload, qrType, renderQr } from '@varunsingla/upi-utils';

buildQrPayload({ pa: 'merchant@ybl', pn: 'Chai Point' });          // static
buildQrPayload({ pa: 'merchant@ybl', pn: 'Chai Point', am: 149, tr: 'INV42' }); // dynamic
qrType({ pa: 'merchant@ybl', pn: 'Chai Point', am: 149 });         // 'dynamic'

// Optional — requires the `qrcode` peer dependency:
const dataUrl = await renderQr(buildQrPayload({ pa: 'merchant@ybl', pn: 'Chai Point' }));

Advanced tier (not implemented in v1, but on the roadmap):

  • BharatQR — the EMVCo-based QR standard that's interoperable across cards and UPI.
  • Signed QRs — a sign param carrying a merchant signature so a QR can be verified as issued by a registered merchant.

Extras

import { formatInr, maskVpa, maskMobile, generateRrn, generateTxnRef, classifyTransaction } from '@varunsingla/upi-utils';

formatInr(100000);                  // '₹1,00,000'   (Indian lakh/crore grouping)
formatInr(10000000);                // '₹1,00,00,000'
maskVpa('varun@okhdfcbank');        // 'va•••@okhdfcbank'
maskMobile('9876543210');           // '98XXXXXX10'
generateRrn();                      // '402913847562'  (12-digit RRN-shaped ref)
generateTxnRef({ prefix: 'ORD' });  // 'ORD20260619T101530XYZ123'
classifyTransaction({ mc: '5814' });// 'P2M'  (P2P otherwise — mc is the only reliable signal)

Deliberately not built

  • Phone number → VPA inference. There is no privacy-preserving way to map a mobile number to someone's VPA. This is impossible by design, and that's a feature of UPI, not a gap in this library.
  • Hardcoded per-transaction limits. UPI limits shift over time and vary by bank, app, and merchant category (some categories have higher caps). If you need limits, source them into a dated config you keep current — never bake numbers in from memory.

API surface

| Function | Returns | | --- | --- | | isValidVpa(vpa) | boolean | | normalizeVpa(vpa) | string | | splitVpa(vpa) | { local, handle } \| null | | parseVpa(vpa) | { local, handle, psp, sponsorBank, known } \| null | | lookupHandle(handle) | { handle, psp, sponsorBank } \| null | | listHandles() | HandleInfo[] | | buildUpiLink(params) | string | | buildAppUpiLink(app, params) | string | | buildQrPayload(params) | string | | qrType(params) | 'static' \| 'dynamic' | | renderQr(payload, opts?) | Promise<string> (data URL) | | formatInr(amount, opts?) | string | | maskVpa(vpa) / maskMobile(num) | string | | generateRrn() / generateTxnRef(opts?) | string | | classifyTransaction(input) | 'P2P' \| 'P2M' |

Development

npm install
npm test          # vitest
npm run typecheck # tsc --noEmit
npm run build     # tsup → dist/ (esm + cjs + d.ts)

License

MIT © Varun Singla