Back to blog

Article

Decode Southeast Asian Payment QR Codes: QRIS, PayNow, DuitNow & PromptPay

7/1/20266 min readby xlocale Team
payment-qremvcoqrissoutheast-asiafintechxlocale

You scanned a merchant QR, the payment failed, and all you have to debug with is a 300-character string of digits. Somewhere in there is a merchant ID, a currency code, an amount, and a checksum — but nothing tells you which byte is which. If you work on payments anywhere in Southeast Asia, this is a Tuesday. The good news: almost every scheme in the region speaks the same underlying format, and once you can read it, the string stops being a black box.

One spec behind all of them

QRIS in Indonesia, PayNow in Singapore, DuitNow in Malaysia, PromptPay in Thailand, and VietQR in Vietnam look like five different systems. Under the hood they are one: the EMVCo Merchant-Presented Mode (MPM) QR Code Specification. Each national scheme adds its own merchant-account templates and rules, but the envelope — how fields are laid out, how they're delimited, how the checksum is computed — is shared.

That shared envelope is a flat Tag-Length-Value (TLV) structure. There are no line breaks, no JSON, no separators you can see. Every field is:

  • Tag — 2 ASCII digits identifying the field (e.g. 52)
  • Length — 2 ASCII digits giving the value's byte count (e.g. 04)
  • Value — exactly that many characters

So 52045812 reads as: tag 52, length 04, value 5812. The next field starts immediately after. Parse one field, advance the cursor, repeat until the string ends. That's the whole algorithm.

The key fields you'll actually read

A minimal static QRIS or PromptPay payload has a predictable spine. These are the tags worth memorising:

| Tag | Field | Example | Meaning | |---|---|---|---| | 00 | Payload Format Indicator | 01 | Always 01 for EMVCo QR | | 01 | Point of Initiation | 11 or 12 | 11 = static (reusable), 12 = dynamic (one-time) | | 2651 | Merchant Account Information | (nested TLV) | Scheme-specific; identifies the payee | | 52 | Merchant Category Code | 5812 | ISO 18245 MCC — here, "eating places" | | 53 | Transaction Currency | 360 | ISO 4217 numeric — 360 = IDR, 702 = SGD, 764 = THB | | 54 | Transaction Amount | 50000 | Omitted on static QRs where the payer types the amount | | 58 | Country Code | ID | ISO 3166-1 alpha-2 | | 59 | Merchant Name | KOPI SUSU | Displayed to the payer | | 60 | Merchant City | JAKARTA | | | 63 | CRC | A13F | Checksum over everything before it |

The point-of-initiation field (01) matters more than people expect. A 12 means the QR is dynamic — it encodes a specific amount for a specific transaction and should not be reused. A 11 is static: the same sticker on the counter every day, with the amount entered by the customer. Reconciliation logic that ignores this distinction will happily double-count.

Merchant account templates are nested TLV

Tags 26 through 51 are reserved for Merchant Account Information templates, and this is where the national schemes diverge. The value of one of these tags is itself a TLV blob. Inside, a Globally Unique Identifier (GUID) at sub-tag 00 declares the scheme — for example ID.CO.QRIS.WWW for QRIS or SG.PAYNOW for PayNow — followed by scheme-specific sub-fields like the acquirer ID, merchant PAN, or a proxy value (a phone number or national ID for PayNow/PromptPay person-to-person transfers).

So decoding is recursive: parse the top-level TLV, and when you hit a merchant-account tag, parse its value as TLV again. A decoder that stops at the top level will show you a merchant template as one opaque string; a good one drills in.

The CRC is not optional

Tag 63 is always last, always 4 hex characters, and is a CRC-16/CCITT-FALSE checksum computed over the entire payload including the 6304 tag-and-length prefix but excluding the 4 checksum digits themselves. Get the range wrong and every QR looks corrupt.

Payload (trimmed):  00020101021126...5303360540550000...6304
                    └──────────── CRC input ──────────────┘
                    (includes "6304", excludes the 4 hex digits after it)

CRC-16/CCITT-FALSE  poly 0x1021, init 0xFFFF, no reflection, xorout 0x0000
Result:             A13F  ->  appended as ...6304A13F

If you're generating QRs, computing this last and appending it is the final step. If you're debugging a QR that "won't scan," a mismatched CRC is the single most common cause — a copy-paste that dropped a character silently breaks it.

Why bother decoding by hand

Reading the raw TLV pays off in three recurring situations:

  • Integration work. When you're building a payment acceptance flow and the provider's QR isn't behaving, seeing the exact currency (53), amount (54), and merchant template tells you whether the bug is yours or theirs.
  • Reconciliation. Matching settlement reports back to transactions is easier when you can extract the merchant PAN and the static/dynamic flag straight from the QR you stored.
  • Debugging failed scans. A wrong length byte, a truncated value, or a bad CRC is invisible to the eye but obvious once the string is parsed field by field.

This is the same discipline as inspecting a webhook signature or a raw HTTP envelope — you stop trusting the wrapper and read the bytes. If you also handle callbacks from these payment providers, the webhook signature verifier applies the same "verify, don't assume" mindset to the notification side.

Key takeaways

  • QRIS, PayNow, DuitNow, PromptPay, and VietQR all share the EMVCo MPM envelope — learn it once, read them all.
  • The format is flat Tag-Length-Value: 2-digit tag, 2-digit length, then the value. Merchant-account templates (2651) are nested TLV, so parsing is recursive.
  • Memorise the spine: 00 format, 01 static/dynamic, 52 MCC, 53 currency, 54 amount, 58 country, 59 name, 63 CRC.
  • The point-of-initiation flag (11 vs 12) decides whether a QR is reusable — critical for correct reconciliation.
  • Tag 63 is a CRC-16/CCITT-FALSE over the whole payload including 6304; a wrong checksum is the top cause of unscannable QRs.

Stop squinting at raw digit strings. Paste any SEA merchant QR into the payment QR decoder and it breaks the TLV down field by field — resolving currency and country codes, drilling into merchant templates, and validating the CRC — so you can see exactly what's inside before it ships.

End of article