Skip to main content

How UPI deep links work (upi://pay): build a payment link and QR code without a payment gateway

The upi://pay URL format, parameter by parameter, with JavaScript and a QR code.

Updated 7 min readMarkdown

If you've ever tapped "Pay" on a website in India and watched your phone jump straight into Google Pay, PhonePe, Paytm or BHIM with the amount already filled in, you've used a UPI deep link. It's just a URL that starts with upi://pay, and you can build one yourself in a few lines, with no payment gateway, no merchant account and no API keys.

This post explains what's in that URL, how to generate it safely in JavaScript, how to turn it into a QR code, and, just as important, what it can't do for you.

The URL format

A UPI payment link looks like this:

upi://pay?pa=yourname@okhdfcbank&am=499.00&tn=Logo%20design%20deposit&cu=INR

upi:// is a URI scheme. When a phone opens a link with that scheme, the operating system hands it to an installed app that has registered for it. NPCI requires every UPI app to listen for these links, so any UPI app on the phone can pick it up. The query parameters come from NPCI's UPI linking specification. The ones you'll actually use:

Parameter Meaning Required?
pa Payee address: the UPI ID (VPA) that receives the money, e.g. name@okicici Yes
pn Payee name, shown to the payer Mandatory in NPCI's spec, though most apps still open a link without it and show the name registered to the VPA
am Amount, in rupees, with up to two decimals No. Leave it out and the payer types the amount
cu Currency. Always INR Recommended
tn Transaction note, e.g. "Invoice 1042" No
tr Transaction reference ID, up to 35 characters (mandatory for merchant and dynamic QR payments) No, for personal links

Two practical notes:

  1. Leaving out am is a feature. For tips, donations or "pay what you owe", an open amount is friendlier than a fixed one.
  2. The payer's app shows who they're paying. Before approving, UPI apps typically display the name registered against the VPA. That's your real trust signal, more than anything you put in the link.

Don't concatenate strings by hand: a note like Design & dev will break your query string. Let URLSearchParams do the encoding:

function buildUpiLink({ upiId, amount, note }) {
  if (!upiId || !/^[\w.\-]{2,}@[a-zA-Z]{2,}$/.test(upiId.trim())) {
    throw new Error('Enter a valid UPI ID, e.g. name@okhdfcbank');
  }
  const params = new URLSearchParams({ pa: upiId.trim(), cu: 'INR' });
  if (amount) {
    const value = Number(amount);
    if (!(value > 0)) throw new Error('Amount must be a positive number');
    params.set('am', value.toFixed(2));
  }
  if (note) params.set('tn', note.trim().slice(0, 50));
  return `upi://pay?${params.toString()}`;
}

console.log(buildUpiLink({ upiId: 'studio@okaxis', amount: 1500, note: 'Website audit' }));
// upi://pay?pa=studio%40okaxis&cu=INR&am=1500.00&tn=Website+audit

Things worth knowing about that output:

  • URLSearchParams encodes @ as %40 and spaces as +. UPI apps generally decode both. If you see a literal + in the note in a particular app, build the query with encodeURIComponent instead, which gives %20 for spaces.
  • The regex above is a sanity check, not validation. The only way to know a VPA is real is to send a small payment to it. Your code can't verify ownership.
  • Keep notes short. Apps truncate long notes differently, so a 50-character cap is a sensible default.

Turning it into a QR code

A QR code is the same link, drawn as a matrix. The payer scans it with their UPI app's scanner (some phone cameras also offer to open a UPI app) and gets the same prefilled screen. With the popular qrcode package:

import QRCode from 'qrcode';

const link = buildUpiLink({ upiId: 'studio@okaxis', amount: 1500, note: 'Website audit' });

// In the browser, draw onto a <canvas id="qr">
await QRCode.toCanvas(document.getElementById('qr'), link, {
  errorCorrectionLevel: 'H', // survives smudges and small logos
  width: 240,
  margin: 2,
});

// Or produce a PNG data URL for an invoice PDF
const pngDataUrl = await QRCode.toDataURL(link, { errorCorrectionLevel: 'H' });

Use high error correction (H) if the code will be printed on a counter card or an invoice that might get folded. Then test it with at least two different UPI apps before you print a hundred copies.

This is the part most tutorials skip:

  • Mobile only. On a desktop browser, upi:// usually does nothing because no app is registered for it. On desktop, show the QR code and keep the link for mobile.
  • In-app browsers vary. Some email clients and social apps' built-in browsers block custom schemes. If a tap does nothing, open the page in Chrome or Safari, or scan the QR code instead.
  • iOS is less predictable than Android. On Android, the system shows a chooser of UPI apps. iOS behaviour depends on which apps are installed and how they handle the scheme. Test on the devices your payers actually use.
  • Apps may add friction for personal VPAs. Some apps show warnings or limits for link-initiated payments to non-merchant UPI IDs. That's the app protecting the payer, and you can't override it from the link.

A upi://pay link starts a payment. It doesn't confirm one. From a web page, a PDF or a chat message there's no callback, no webhook and no "payment succeeded" event. (Native Android apps get an intent response from the UPI app, but even that isn't a substitute for server-side confirmation.) Practically:

  • Reconcile manually. Match incoming credits in your bank or UPI app against the note or amount. A unique note per invoice ("INV-1042") makes this much easier.
  • A prefilled amount isn't a guarantee. NPCI's rules say apps should lock the am value unless you also send a minimum amount (mam), but app behaviour varies, and nothing stops a payer from paying twice. Treat the link as a convenience, not an enforcement mechanism.
  • For order systems, use a gateway. If you need automatic confirmation, refunds, receipts or subscriptions, use a payment gateway (Razorpay, Cashfree, PayU and others). They issue merchant VPAs and send you webhooks. Deep links are for freelancers, small sellers, event collections and invoices, where a human checks the bank statement anyway.

Good uses

  • Freelance invoices: add the link and QR code to the PDF so a client on their phone can pay in two taps.
  • WhatsApp and email follow-ups: "Here's the link for the ₹1,500 deposit" beats sending your UPI ID and asking them to type it.
  • Events and group collections: one QR code on a poster, with an open amount.
  • Tip jars and donation buttons on a personal site, shown as a QR code on desktop.

Shortcut, if you don't want to write code

I built a free generator that builds the same kind of link in the browser: enter a UPI ID, an optional amount and an optional note, and you get the upi://pay link plus a QR code you can save as an image. There's no sign-up and no gateway in between, and it doesn't add a payee name, so the payer's app shows the name linked to your UPI ID. UPI payment link creator on BeginThings. Disclosure: BeginThings is my project.

  1. Test the link and the QR code with two different UPI apps.
  2. Check that the payee name the app shows is the one your payer expects.
  3. Use a unique note per invoice so reconciliation takes seconds.
  4. On desktop pages, show the QR code first and the link second.
  5. Don't promise "instant confirmation". A deep link can't give you one.

If you've hit an app-specific quirk with upi://pay links, I'd love to hear which app and what happened, in the comments.

This article was drafted with AI assistance and checked against the generator's source code and NPCI's published parameter names before publishing.

About the author

  • Building BeginThings: 100+ free browser tools, and BeginRooms, a 3D workspace for client projects. India.

No comments yet. Be the first to comment.