Skip to content

Instantly share code, notes, and snippets.

@chengkiang
Last active February 28, 2025 14:40
Show Gist options
  • Save chengkiang/7e1c4899768245570cc49c7d23bc394c to your computer and use it in GitHub Desktop.
Save chengkiang/7e1c4899768245570cc49c7d23bc394c to your computer and use it in GitHub Desktop.
SG PayNow QR Code Generator Sample
String.prototype.padLeft = function (n, str) {
if (n < String(this).length) {
return this.toString();
}
else {
return Array(n - String(this).length + 1).join(str || '0') + this;
}
}
function crc16(s) {
var crcTable = [0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5,
0x60c6, 0x70e7, 0x8108, 0x9129, 0xa14a, 0xb16b,
0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, 0x1231, 0x0210,
0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c,
0xf3ff, 0xe3de, 0x2462, 0x3443, 0x0420, 0x1401,
0x64e6, 0x74c7, 0x44a4, 0x5485, 0xa56a, 0xb54b,
0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6,
0x5695, 0x46b4, 0xb75b, 0xa77a, 0x9719, 0x8738,
0xf7df, 0xe7fe, 0xd79d, 0xc7bc, 0x48c4, 0x58e5,
0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,
0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969,
0xa90a, 0xb92b, 0x5af5, 0x4ad4, 0x7ab7, 0x6a96,
0x1a71, 0x0a50, 0x3a33, 0x2a12, 0xdbfd, 0xcbdc,
0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03,
0x0c60, 0x1c41, 0xedae, 0xfd8f, 0xcdec, 0xddcd,
0xad2a, 0xbd0b, 0x8d68, 0x9d49, 0x7e97, 0x6eb6,
0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,
0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a,
0x9f59, 0x8f78, 0x9188, 0x81a9, 0xb1ca, 0xa1eb,
0xd10c, 0xc12d, 0xf14e, 0xe16f, 0x1080, 0x00a1,
0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c,
0xe37f, 0xf35e, 0x02b1, 0x1290, 0x22f3, 0x32d2,
0x4235, 0x5214, 0x6277, 0x7256, 0xb5ea, 0xa5cb,
0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447,
0x5424, 0x4405, 0xa7db, 0xb7fa, 0x8799, 0x97b8,
0xe75f, 0xf77e, 0xc71d, 0xd73c, 0x26d3, 0x36f2,
0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9,
0xb98a, 0xa9ab, 0x5844, 0x4865, 0x7806, 0x6827,
0x18c0, 0x08e1, 0x3882, 0x28a3, 0xcb7d, 0xdb5c,
0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0,
0x2ab3, 0x3a92, 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d,
0xbdaa, 0xad8b, 0x9de8, 0x8dc9, 0x7c26, 0x6c07,
0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba,
0x8fd9, 0x9ff8, 0x6e17, 0x7e36, 0x4e55, 0x5e74,
0x2e93, 0x3eb2, 0x0ed1, 0x1ef0];
var crc = 0xFFFF;
var j, i;
for (i = 0; i < s.length; i++) {
c = s.charCodeAt(i);
if (c > 255) {
throw new RangeError();
}
j = (c ^ (crc >> 8)) & 0xFF;
crc = crcTable[j] ^ (crc << 8);
}
return ((crc ^ 0) & 0xFFFF).toString(16).toUpperCase().padStart(4, '0');
}
function generatePayNowStr( opts ) {
const p = [
{ id: '00', value: '01' }, // ID 00: Payload Format Indicator (Fixed to '01')
{ id: '01', value: '12' }, // ID 01: Point of Initiation Method 11: static, 12: dynamic
{
id: '26', value: // ID 26: Merchant Account Info Template
[{ id: '00', value: 'SG.PAYNOW' },
{ id: '01', value: '2' }, // 0 for mobile, 2 for UEN. 1 is not used.
{ id: '02', value: opts.uen }, // PayNow UEN (Company Unique Entity Number)
{ id: '03', value: opts.editable.toString() }, // 1 = Payment amount is editable, 0 = Not Editable
{ id: '04', value: opts.expiry }] // Expiry date (YYYYMMDD)
},
{ id: '52', value: '0000' }, // ID 52: Merchant Category Code (not used)
{ id: '53', value: '702' }, // ID 53: Currency. SGD is 702
{ id: '54', value: opts.amount.toString() }, // ID 54: Transaction Amount
{ id: '58', value: 'SG' }, // ID 58: 2-letter Country Code (SG)
{ id: '59', value: 'COMPANY NAME' }, // ID 59: Company Name
{ id: '60', value: 'Singapore' }, // ID 60: Merchant City
{
id: '62', value: [{ // ID 62: Additional data fields
id: '01', value: opts.refNumber // ID 01: Bill Number
}]
}
]
let str = p.reduce((final, current) => {
if (Array.isArray(current.value)) { //nest loop
current.value = current.value.reduce((f, c) => {
f += c.id + c.value.length.toString().padLeft(2) + c.value;
return f
}, "")
}
final += current.id + current.value.length.toString().padLeft(2) + current.value;
return final
}, "")
// Here we add "6304" to the previous string
// ID 63 (Checksum) 04 (4 characters)
// Do a CRC16 of the whole string including the "6304"
// then append it to the end.
str += '6304' + crc16(str + '6304');
return str;
}
@starfishpatkhoo
Copy link

May I know can we limit a qr code for one transaction only? For example, once a user has scanned the QR code and paid, the QR code is expired and doesn't allow transaction anymore.

You need bank transaction API access for that.. The QR code has no way to know if it was even scanned at all... :)

@kharsengOrione
Copy link

I see, thank you for the answer.

@skywalkeryin
Copy link

Hi Cheng Kiang, are we able to set the expiry date to 10 minutes after the current time?

Based on the specifications that I used for this (and it's few years old now), it can only support down to the day. There might be a new version of the specifications, but I am not sure where to find it.
Regards CK

Thanks for the information. Since I'm using this to generate dynamic QR code for a form, can I check if I can set the expiry date to current date + 1 day? Eg: Current date is 20230814, I would like the qr code to expire on 20230815

It turns out that you can simply change the expiry date format from YYYYMMDD to YYYYMMDDHHmmss (24-hour format), allowing you to include a specific time in the expiry date.
For example:
const opts = {
uen: '+6512345678', // Change to a valid mobile number
editable: 0,
expiry: '20250227185500', // Expire in 2025 Feb 27 18:55:00
amount: 12.34,
refNumber: 'ABC123'
};

const qr = generatePayNowStr(opts);

@chengkiang
Copy link
Author

Hi Cheng Kiang, are we able to set the expiry date to 10 minutes after the current time?

Based on the specifications that I used for this (and it's few years old now), it can only support down to the day. There might be a new version of the specifications, but I am not sure where to find it.
Regards CK

Thanks for the information. Since I'm using this to generate dynamic QR code for a form, can I check if I can set the expiry date to current date + 1 day? Eg: Current date is 20230814, I would like the qr code to expire on 20230815

It turns out that you can simply change the expiry date format from YYYYMMDD to YYYYMMDDHHmmss (24-hour format), allowing you to include a specific time in the expiry date. For example: const opts = { uen: '+6512345678', // Change to a valid mobile number editable: 0, expiry: '20250227185500', // Expire in 2025 Feb 27 18:55:00 amount: 12.34, refNumber: 'ABC123' };

const qr = generatePayNowStr(opts);

Great! Thanks for the solution!

@cstoneskc
Copy link

cstoneskc commented Dec 19, 2024

Hi I like to query about difference between 010211 (static) and 010212 (dynamic). I have added amount and Bill-reference which will be different for different client. No expiration date. I have tried generating a sample QRcode based on static as well as dynamic with other data remaining the same - both can work . Anyone can tell me whether there is any difference between static and dynamic that I may have missed out?
Another query: I added a reference under ID62. It is OK. But I find that after it's scanned by bank apps, this field cannot be amended. If I want to allow it to be amended - can it be done? Just want this reference to be default only. Thanks for any answer.

@chengkiang
Copy link
Author

Hi I like to query about difference between 010211 (static) and 010212 (dynamic). I have added amount and Bill-reference which will be different for different client. No expiration date. I have tried generating a sample QRcode based on static as well as dynamic with other data remaining the same - both can work . Anyone can tell me whether there is any difference between static and dynamic that I may have missed out? Another query: I added a reference under ID62. It is OK. But I find that after it's scanned by bank apps, this field cannot be amended. If I want to allow it to be amended - can it be done? Just want this reference to be default only. Thanks for any answer.

According to the EMV specifications,

Identifies the communication technology (here QR Code) and whether the
data is static or dynamic.

The Point of Initiation Method has a value of "11" for static QR Codes and
a value of "12" for dynamic QR Codes.

The value of "11" is used when the same QR Code is shown for more than
one transaction.

The value of "12" is used when a new QR Code is shown for each
transaction.

ID62 (refNumber) cannot be edited if you provide it.

@cstoneskc
Copy link

Hi I like to query about difference between 010211 (static) and 010212 (dynamic). I have added amount and Bill-reference which will be different for different client. No expiration date. I have tried generating a sample QRcode based on static as well as dynamic with other data remaining the same - both can work . Anyone can tell me whether there is any difference between static and dynamic that I may have missed out? Another query: I added a reference under ID62. It is OK. But I find that after it's scanned by bank apps, this field cannot be amended. If I want to allow it to be amended - can it be done? Just want this reference to be default only. Thanks for any answer.

According to the EMV specifications,

Identifies the communication technology (here QR Code) and whether the
data is static or dynamic.

The Point of Initiation Method has a value of "11" for static QR Codes and
a value of "12" for dynamic QR Codes.

The value of "11" is used when the same QR Code is shown for more than
one transaction.

The value of "12" is used when a new QR Code is shown for each
transaction.

ID62 (refNumber) cannot be edited if you provide it.

The intention is to put individualised QRcode on invoices sent to each client. The dynamic sounds like the client can scan/pay only once? But when I tested it - I find that both the static and dynamic QR codes, I can pay twice successfully.

@chengkiang
Copy link
Author

Hi I like to query about difference between 010211 (static) and 010212 (dynamic). I have added amount and Bill-reference which will be different for different client. No expiration date. I have tried generating a sample QRcode based on static as well as dynamic with other data remaining the same - both can work . Anyone can tell me whether there is any difference between static and dynamic that I may have missed out? Another query: I added a reference under ID62. It is OK. But I find that after it's scanned by bank apps, this field cannot be amended. If I want to allow it to be amended - can it be done? Just want this reference to be default only. Thanks for any answer.

According to the EMV specifications,

Identifies the communication technology (here QR Code) and whether the
data is static or dynamic.

The Point of Initiation Method has a value of "11" for static QR Codes and
a value of "12" for dynamic QR Codes.

The value of "11" is used when the same QR Code is shown for more than
one transaction.

The value of "12" is used when a new QR Code is shown for each
transaction.

ID62 (refNumber) cannot be edited if you provide it.

The intention is to put individualised QRcode on invoices sent to each client. The dynamic sounds like the client can scan/pay only once? But when I tested it - I find that both the static and dynamic QR codes, I can pay twice successfully.

It's probably not implemented by the payment apps, though the setting is there. It's mentioned in the EMV specs, but not in the SG PayNow specs. There might have been a newer specification, but I have not researched this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment