The QR code taped to a shop counter isn't a picture of a payment. It's a short line of text in a standard, public format used across the region. You can read the whole thing with a generic QR scanner and a text editor.
This is what one contains, field by field, and how to check that it has not been tampered with.
The whole payload is one flat string
Merchant-presented payment QR codes in Southeast Asia — Singapore's PayNow, Malaysia's DuitNow, Indonesia's QRIS, Thailand's PromptPay, Vietnam's VietQR — all encode their contents using EMVCo's merchant-presented mode. The encoding is deliberately dull: every field is a two-digit tag, then a two-digit length, then that many characters of value. Read the length, take that many characters, move on. Some fields contain more fields in the same shape.
The example below is a published test payload from an open-source library that parses these codes. It is deliberately not a real merchant's QR. A live one carries a real business's identifiers, and reproducing those in an article would be publishing somebody's payment details for no reason.
00020101021126490009SG.PAYNOW010120210202012345X030110408202112315182 0007SG.SGQR0113202012345X123020701.00010306828671040201050312306040000 0708201912315204000053037025802SG5911RATINAN LEE6009SINGAPORE61068287 6162140110987654321X630429FD
A field-by-field breakdown looks like this.
| Tag | Length | Value | What it is |
|---|---|---|---|
| 00 | 02 | 01 | Payload format indicator |
| 01 | 02 | 11 | Point of initiation method |
| 26 | 49 | … | Merchant account — PayNow |
| 51 | 82 | … | Merchant account — SGQR |
| 52 | 04 | 0000 | Merchant category code |
| 53 | 03 | 702 | Transaction currency |
| 58 | 02 | SG | Country |
| 59 | 11 | RATINAN LEE | Merchant name |
| 60 | 09 | SINGAPORE | Merchant city |
| 61 | 06 | 828761 | Postal code |
| 62 | 14 | … | Additional data |
| 63 | 04 | 29FD | Checksum |
Nothing is encrypted or signed. The merchant's name, city, postal code and account identifier are all in plain text for anyone to read. This is by design, since every banking app in the country needs to parse it, but it's a detail to remember before you print one on a public poster.
Two account records, not one
Tags 26 through 51 are reserved for merchant account information, and each one contains its own nested set of fields. This payload uses two of them.
Tag 26 opens with the identifier SG.PAYNOW and carries the proxy type, the proxy value, whether the amount may be edited, and an expiry date. Tag 51 opens with SG.SGQR and carries a different set again — an identification number, a version, a postal code, a level number.
A Singapore code therefore carries two records: the scheme-specific one that moves the money and an umbrella record that identifies the QR. An app that reads only tag 26 will make a working payment but miss half the data in the payload.
The code that tells you nobody typed the amount
Tag 01 is two characters and decides how the code behaves. A value of 11 is static: the code is printed once and stuck to the counter, and the payer types the amount into their own banking app. A value of 12 is dynamic: the code is generated for one transaction, usually on a screen, and carries the amount with it.
This payload is 11, and the rest of it agrees — there is no tag 54 anywhere in the string. Tag 54 is the transaction amount, and a static code has nothing to put in it.
As a payer, the main takeaway is this: if the code is a sticker, not a screen, the amount isn't in the QR. You type it yourself, and nothing in the code will catch a typo.
Checking the last four characters yourself
The final field, tag 63, is a checksum over everything that came before it. The algorithm is CRC-16 in the CCITT-FALSE parameterisation, which uses a 0x1021 polynomial with an initial value of 0xFFFF, no reflection of input or output, and no final XOR. It is computed over the entire payload up to and including the literal characters 6304 — the checksum's own tag and length are inside the checksummed region, which catches a truncated payload.
Before trusting any implementation of this, run it against the standard conformance input. The agreed check value for CRC-16/CCITT-FALSE is the string 123456789, which must produce 0x29B1. An implementation that gets that wrong will still produce four plausible hex characters for a payment payload, and you will have no way to tell.
Ours produces 0x29B1 on the check string. Run over the payload above, it produces 29FD, which is what tag 63 states. The code is internally consistent. The same check runs in the site's payment QR decoder, which recomputes the checksum of any payload pasted into it.
A matching checksum proves only that the string has not been garbled in transmission or truncated by a bad scan. It is not a signature. Anyone can alter a payload and recompute a valid checksum in a few lines of code, and the result will verify perfectly. The checksum protects against accident, not against intent.
Where a factor of a hundred goes missing
Tag 53 carries the currency as a three-digit number rather than as letters, following the numeric column of ISO 4217. In this payload it is 702, which is the Singapore dollar.
The numeric codes for the region, taken from the list the ISO maintenance agency itself publishes:
| Currency | Numeric code | Minor units |
|---|---|---|
| Indonesian rupiah | 360 | 2 |
| Malaysian ringgit | 458 | 2 |
| Singapore dollar | 702 | 2 |
| Vietnamese dong | 704 | 0 |
| Thai baht | 764 | 2 |
The last column is the one that bites, though not where it first appears to. Tag 54 carries the amount as an ordinary decimal in whole units — EMVCo's own worked example writes 23.72. The minor-units column says how many digits may follow the decimal point: two for four of these currencies, and none for the dong, so a correct Vietnamese amount has no fractional part at all.
The factor of a hundred goes missing one step later. Payment systems usually store money as a whole number of the smallest unit — cents rather than dollars — and turning the decimal in tag 54 into that integer means multiplying by ten to the power of the minor units. For the rupiah, the ringgit, the Singapore dollar and the baht that is a hundred. For the dong it is one. Code that multiplies by a hundred everywhere is right four times out of five in this table, which is the worst possible frequency: often enough to look correct, rare enough to survive testing.
Practical Checks
If you print a static code for a business, read your own payload. Your registered merchant name, city, and postal code are in plain text. A code generated from a hastily filled form may be publishing details you would rather keep private.
If you are writing anything that consumes these codes, verify the checksum before parsing the fields, and test your checksum against 123456789 before verifying anything. Treat a valid checksum as evidence of a clean scan and nothing more.
And if you are paying from a sticker rather than a screen, the amount is yours to type. The QR has no opinion about it.
Where this came from
The field numbering and the checksum parameters are EMVCo's published merchant-presented mode specification. The sample payload is a test fixture from an open-source parser for these codes, chosen deliberately over a real merchant's QR. The currency codes and minor units are from the list published by the ISO 4217 maintenance agency, in its edition of 17 September 2026 — not from a mirror, because mirrors of that table go stale quietly. The decode and both checksum computations in this article were run rather than quoted, on 20 September 2026.
This article covers only the merchant-presented format, where the shop shows a code and the customer scans it. There is a second mode in which the customer presents the code and the merchant scans it, and its field numbering is different. Nothing above applies to it.