All guides

How a PayNow QR code works: the payload, field by field

Published:

A PayNow QR code looks like a picture, but it holds plain text. A bank app reads that text, shows the payer who they are paying and how much, and sends the money. Nothing is stored in the QR code that you could not print on a name card.

This guide explains that text: what each part means, how the check at the end works, and the small mistakes that make a bank app refuse a code. Every field below was checked against PayNow QR codes made by a Singapore bank app.

The payload is a list of small blocks

The text is a chain of blocks. Each block has three parts:

Part Size Example
ID 2 digits 59 (payee name)
Length 2 digits 14
Value that many characters SUNNY DAY KOPI

So 5914SUNNY DAY KOPI means: field 59, 14 characters long, value "SUNNY DAY KOPI". This format is called TLV (tag, length, value), and it comes from the EMVCo QR code standard that many countries use for payment QR codes.

Two rules follow from this:

  • A value can be at most 99 characters, because the length has only two digits.
  • The length counts characters, so it must be exact. One wrong length breaks every block after it.

Some blocks hold other blocks inside them. Field 26 (who gets paid) and field 62 (extra data) work this way.

Every field in a PayNow QR code

ID Field Value
00 Payload format Always 01
01 Point of initiation 11
26 Who gets paid A block of blocks (see below)
52 Merchant category code 0000
53 Currency 702 (Singapore dollars)
54 Amount Optional, for example 4.50
58 Country SG
59 Payee name Up to 25 characters
60 City Singapore
62 Extra data Optional block; 01 inside it is the reference
63 Check value (CRC) 4 characters, always last

Field 26: who gets paid

ID Meaning Value
00 Scheme SG.PAYNOW
01 Proxy type 0 for a mobile number, 2 for a UEN
02 Proxy a mobile number in the form +65XXXXXXXX, or the UEN
03 Can the payer change the amount? 1 = yes, 0 = no
04 Expiry date Optional, YYYYMMDD

The proxy is the mobile number or UEN that is registered for PayNow. The bank app looks it up and shows the registered name, so the payer can check it before paying.

Field 62: the reference

Inside field 62, sub-field 01 holds a reference such as an invoice number. Bank apps show it to the payer and pass it to the payee, which makes it much easier to match payments.

A worked example

A business with UEN 201912345K wants a code for S$4.50, reference INV-1001, amount fixed, valid until 31 December 2026.

Field 26 first:

Field 26
0009SG.PAYNOW      scheme
01012              proxy type: UEN
0210201912345K     proxy
03010              amount cannot be changed
040820261231       expiry date

Wrapped as field 26 (the inner blocks add up to 49 characters):

Field 26, wrapped
26490009SG.PAYNOW010120210201912345K03010040820261231

The full payload:

Full payload
00020101021126490009SG.PAYNOW010120210201912345K0301004082026123152040000530370254044.505802SG5914SUNNY DAY KOPI6009Singapore62120108INV-10016304204C

The last 4 characters, 204C, are the check value.

The check value at the end

Field 63 is a CRC: a short number made from all the text before it. If one character changes, the CRC changes, and the bank app rejects the code instead of paying the wrong person or amount.

PayNow uses CRC-16/CCITT-FALSE (polynomial 0x1021, start value 0xFFFF). The detail that catches most people: the CRC covers the text 6304 too. You add 6304 (ID 63, length 04), calculate the CRC over everything so far, then append the 4 hex characters in upper case.

PHP
function tlv(string $id, string $value): string
{
    return $id.sprintf('%02d', strlen($value)).$value;
}

function crc16(string $data): string
{
    $crc = 0xFFFF;

    foreach (str_split($data) as $char) {
        $crc ^= ord($char) << 8;

        for ($bit = 0; $bit < 8; $bit++) {
            $crc = ($crc & 0x8000)
                ? (($crc << 1) ^ 0x1021) & 0xFFFF
                : ($crc << 1) & 0xFFFF;
        }
    }

    return sprintf('%04X', $crc);
}

$payload = '...all fields...'.'6304';
$payload .= crc16($payload);

Mistakes that can make a bank app refuse the code

  • A wrong length. Count characters, not bytes you think you typed. A trailing space in the name changes the length.
  • The CRC calculated without 6304. The code looks fine but no bank app accepts it.
  • A lower-case CRC. Use upper-case hex, as the standard does.
  • A mobile number without +65. Use the full +65 form.
  • An amount with a thousands separator or a currency sign. 1250.00 works; 1,250.00 and S$12.50 do not.
  • A name longer than 25 characters. Shorten it.
  • Fields in the wrong order. Keep the IDs in ascending order, with 63 last.

Before you trust any generator, including your own, scan a few results with real bank apps and check the name and amount on the screen. Do not pay to test; stop at the confirm screen.

If you would rather not maintain this yourself

Getting the format right is the easy part. Keeping it right as you add amounts, expiry dates and references, and testing every change against real bank apps, is the part that takes time.

KachingQR has a free API. One request returns JSON with the payload text and the QR code image as base64 (PNG by default, or SVG):

Request (cURL)
curl -sS --max-time 30 -X POST https://kachingqr.com/api/v1/qr \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{"proxy_type":"uen","proxy_value":"201912345K","amount":"10.50","amount_editable":false,"expiry":"2026-11-16","reference":"INV-1001","merchant_name":"Acme Pte Ltd"}'
Response · 200 OK
{
    "data": {
        "payload": "00020101021126490009SG.PAYNOW010120210201912345K03010040820261116520400005303702540510.505802SG5912Acme Pte Ltd6009Singapore62120108INV-10016304E962",
        "proxy_type": "uen",
        "proxy_value": "201912345K",
        "amount": "10.50",
        "amount_editable": false,
        "expiry": "2026-11-16",
        "reference": "INV-1001",
        "merchant_name": "Acme Pte Ltd",
        "image": {
            "format": "png",
            "mime_type": "image/png",
            "base64": "iVBORw0KGgo…"
        }
    }
}

See the API docs for every field, limit and error code, or make a code by hand with the PayNow QR code generator.

KachingQR is an independent tool. It is not part of PayNow or any bank, and it never receives or holds money: payments go straight from the payer's bank to yours.