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
| Param | Meaning | Notes |
|---|---|---|
pa | Payee address (VPA) | Required. name@handle. |
pn | Payee name | Shown in the app. Optional. |
am | Amount | Two decimals. Most apps lock it when present. |
cu | Currency | Always INR. |
tn | Transaction note | Free text, shown to both sides. |
tr | Transaction reference | Merchant 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
- The browser sees a non-http scheme and asks the OS.
- Android shows a chooser of apps registered for
upi://(or opens the default). iOS opens the app registered for the scheme. - The app parses the query, pre-fills the screen, and asks for the UPI PIN.
- 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.
By Arunachalam Kalimuthu, who builds TukdaPay. Found a mistake? Open an issue — this post lives in the repo too.