Skip to content

Instantly share code, notes, and snippets.

@chengkiang
Last active April 3, 2024 04:24
Show Gist options
  • Star 34 You must be signed in to star a gist
  • Fork 13 You must be signed in to fork a gist
  • 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;
}
@natsu90
Copy link

natsu90 commented Aug 2, 2023

any idea how can we setup webhook upon payment received?

i feel like we should be able to pass an additional parameter, says {id: '63', value: 'https://url-webhook'}

@starfishpatkhoo
Copy link

any idea how can we setup webhook upon payment received?

i feel like we should be able to pass an additional parameter, says {id: '63', value: 'https://url-webhook'}

You need to subscribe to the service from your bank. Or your payment processor. Because ultimately, someone needs to call that webhook, and that someone wants to be paid for calling the webhook.

@chengkiang
Copy link
Author

any idea how can we setup webhook upon payment received?

i feel like we should be able to pass an additional parameter, says {id: '63', value: 'https://url-webhook'}

If I am not wrong, this functionality has to be setup with the bank for business accounts. I noticed in some shops, when you pay via PayNow QR code, they can get a notification in their POS or mobile device.

Regards
CK

@chrislee275
Copy link

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

@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

@chrislee275
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

@Comgen21
Copy link

Comgen21 commented Mar 5, 2024

Hi how to add logo center in QR Code

@chengkiang
Copy link
Author

Hi how to add QR code color

This code only generates a string which you can pass to a QR Code generator to generate the QR code. To have colours, logos, etc., use a QR code generator which supports these features.

@johnaa123
Copy link

why it can't generate dynamic QR code any more?

@johnaa123
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

Do your dynamic QR code still works? How can't get dynamic QR code now with default config { id: '01', value: '12' } ?

@chengkiang
Copy link
Author

chengkiang commented Apr 1, 2024

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

Do your dynamic QR code still works? How can't get dynamic QR code now with default config { id: '01', value: '12' } ?

I just tested and it works with the following:

    const opts = {
      uen: '+6512345678', // Change to a valid mobile number
      editable: 0,
      expiry: '20240402',    // Expire on 2 Apr 2024
      amount: 12.34,
      refNumber: 'ABC123'
    };

    const qr = generatePayNowStr(opts);

If you are using a mobile number instead of a company UEN, you need to change line #80 of the gist to:

{ id: '01', value: '0' },

and also make sure it's a valid mobile number. If you put in a fake number, it won't work.

By the way, I noticed in your comment that your date is in the year 2023.

@kharsengOrione
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.

@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.

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