</> Payments Toolbox

Why your UPI link works in one app and breaks in another

UPI apps disagree about how strictly to parse a upi://pay URI. A literal space, a raw plus or an unescaped ampersand is tolerated by some and truncates the link in others.

A upi://pay link that opens correctly in one app and fails, truncates or shows the wrong payee in another is almost always a percent-encoding problem. UPI apps do not agree on how forgiving to be about a malformed URI, so the same link can work everywhere you tested and break on the one app your customer uses.

What makes it hard to spot is that the fault disappears the moment you look at it. Once a value has been decoded for display, the character that broke the link looks completely ordinary.

The ampersand case

A merchant called Sharma & Sons. Here is the link as a template literal produces it, and the same link encoded properly:

Built by string concatenation
upi://pay?pa=sharmaandsons@okaxis&pn=Sharma & Sons&am=499.00&cu=INR
Correctly encoded
upi://pay?pa=sharmaandsons%40okaxis&pn=Sharma%20%26%20Sons&am=499.00&cu=INR

Run both through a parser and the damage is obvious. The payee name reads Sharma from the first and Sharma & Sons from the second.

The bare & is a parameter separator. Everything after it — Sons, and in stricter parsers the amount and currency too — is read as a new parameter rather than as part of the name. The payer sees a truncated merchant, or a payment sheet with no amount, or nothing at all.

The three characters that do this

  • Space — must be %20. A literal space terminates the URI in strict parsers, so everything after the first space in a payee name is discarded.
  • Ampersand — must be %26. Otherwise it starts a new parameter, as above.
  • Plus — must be %2B. This is the nastiest of the three, because a raw + is valid and simply means different things to different parsers: a space under form encoding, a literal plus under ordinary URI rules. Both readings are defensible, both exist in the wild, and the link is silently wrong in whichever app made the other choice.

A hash (#) deserves a mention too: it starts a fragment, so a note containing one loses everything after it. Encode it as %23.

Building it safely
const params = new URLSearchParams({
  pa: "sharmaandsons@okaxis",
  pn: "Sharma & Sons",
  am: "499.00",
  cu: "INR",
});

// URLSearchParams encodes spaces as "+", which UPI apps read
// inconsistently. Normalise them to %20.
const link = `upi://pay?${params.toString().replace(/\+/g, "%20")}`;

That last detail catches people who did everything right: URLSearchParams is correct URI encoding and still produces + for spaces, which lands you back in the ambiguity above. Replace them.

Checking a link you already have

Paste it into the UPI link and QR tool. It shows each parameter both as it appeared in the link and as it decodes, which is the comparison that makes the fault visible — a value that looks right decoded and wrong raw is exactly the bug described here. It flags unencoded characters rather than silently accepting them.

If the string you are debugging came off a printed QR and does not start with upi://, it is not a UPI link at all. A code beginning with 0002 is an EMVCo merchant payload and belongs in the Bharat QR decoder instead — different format, different rules, and no percent-encoding involved.

Check it against the tool

Frequently asked questions

Why does a plus sign in the payee name behave differently per app?
Because a raw + is ambiguous in a URI query string. Under form encoding it means a space; under ordinary URI rules it is a literal plus. UPI apps have made both choices, so the same link can show two different payee names. Encode it as %2B and the ambiguity disappears.
The link looks correct when I print it. Why is it still broken?
Because printing it decodes it. A payee name containing a literal ampersand truncates the link at that character during parsing, but once the value has been decoded for display the ampersand looks perfectly ordinary and the fault is invisible. Inspect the encoded string, not the rendered one.