TukdaPay

Blog

UPI deep links: upi://pay format and parameters

Every “Pay” button in TukdaPay’s UPI payment splitter is a plain link. This is what’s in it and why a web page can build one but can’t see what happens next.

The URL

upi://pay?pa=shop@okaxis&pn=Sri%20Tea%20Stall&am=1999.00&cu=INR&tn=Part%201%2F3
ParamMeaningNotes
paPayee address (VPA)Required. name@handle.
pnPayee nameShown in the app. Optional.
amAmountTwo decimals. Most apps lock it when present.
cuCurrencyAlways INR.
tnTransaction noteFree text, shown to both sides.
trTransaction referenceMerchant reference id. Optional.

Values are URL-encoded. The scheme is defined in NPCI’s UPI Linking Specification; the parameters above are the ones TukdaPay uses.

On Android the app opens through an intent, which is why developers also call this a UPI intent link.

What happens on tap

  1. The browser sees a non-http scheme and asks the OS.
  2. Android shows a chooser of apps registered for upi:// (or opens the default). iOS opens the app registered for the scheme.
  3. The app parses the query, pre-fills the screen, and asks for the UPI PIN.
  4. The app talks to the bank via NPCI. The browser is not involved.

Some apps won’t accept a payment opened from a link, for example to a personal UPI ID. The payer can scan the payee’s QR in their UPI app and type the amount instead.

What the page can’t know

Step 4 never reports back to the page. There is no callback, no return URL, no event. That’s why TukdaPay’s Paid is a checkbox the user ticks, not a status it detects. Only a merchant integration, through a payment gateway or the merchant’s bank, gets confirmations, and that requires being the payee.

QR codes are the same link

A UPI QR sticker is just this URL encoded as a QR. TukdaPay renders one per part on desktop using the qrcode library, so a phone can scan what it can’t tap. To see these links from the payer’s side, read how to split a UPI payment above ₹2,000.

Building one

export interface UpiParams {
  pa: string;
  pn?: string;
  am: number;
  tn?: string;
}

export function buildUpiUrl({ pa, pn, am, tn }: UpiParams): string {
  const params: [string, string][] = [['pa', pa]];
  if (pn && pn.trim()) params.push(['pn', pn.trim()]);
  params.push(['am', am.toFixed(2)], ['cu', 'INR']);
  if (tn && tn.trim()) params.push(['tn', tn.trim()]);

  return `upi://pay?${params.map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&')}`;
}

That’s the whole of lib/upi.ts, minus validation. The tests are in the repository.

Read the source

By , who builds TukdaPay. Found a mistake? Open an issue — this post lives in the repo too.