Skip to content

Instantly share code, notes, and snippets.

@ariunbayar
Created June 9, 2026 13:21
Show Gist options
  • Select an option

  • Save ariunbayar/b1b7e333fb89239b4b48fd17b381889e to your computer and use it in GitHub Desktop.

Select an option

Save ariunbayar/b1b7e333fb89239b4b48fd17b381889e to your computer and use it in GitHub Desktop.
EMVCo Merchant-Presented Mode (MPM) QR payload generator
/**
* EMVCo Merchant-Presented Mode (MPM) QR payload generator.
* Spec: "EMV QR Code Specification for Payment Systems — Merchant-Presented Mode".
*
* Output is the data string you encode into a QR image (use any QR lib,
* e.g. `qrcode`). The string is plain TLV + a trailing CRC.
*/
/** One TLV element: 2-digit ID + 2-digit length + value. */
function tlv(id: string, value: string): string {
if (value.length > 99) throw new Error(`Value too long for field ${id} (${value.length})`);
return id + String(value.length).padStart(2, "0") + value;
}
/** CRC-16/CCITT-FALSE: poly 0x1021, init 0xFFFF, no reflection, no final XOR. */
function crc16(data: string): string {
let crc = 0xffff;
for (let i = 0; i < data.length; i++) {
crc ^= data.charCodeAt(i) << 8;
for (let b = 0; b < 8; b++) {
crc = crc & 0x8000 ? ((crc << 1) ^ 0x1021) & 0xffff : (crc << 1) & 0xffff;
}
}
return crc.toString(16).toUpperCase().padStart(4, "0");
}
export interface MpmParams {
/** "static" => reusable code (no fixed amount), "dynamic" => one-time. Field 01. */
mode?: "static" | "dynamic";
/**
* Merchant Account Information templates, IDs "02"–"51".
* The inner value is scheme-specific (usually sub-tag "00" = GUID/AID,
* plus network-defined sub-tags). Build it with `tlv` of the sub-fields.
* e.g. { "29": tlv("00","D156...") + tlv("01","123456") }
*/
merchantAccounts: Record<string, string>;
merchantCategoryCode: string; // 52, 4-digit MCC ("0000" if unknown)
currency: string; // 53, ISO 4217 numeric (MNT = "496", USD = "840")
amount?: string; // 54, e.g. "100.00" — omit for static codes
countryCode: string; // 58, ISO 3166-1 alpha-2 (e.g. "MN")
merchantName: string; // 59, max 25 chars
merchantCity: string; // 60, max 15 chars
postalCode?: string; // 61
additionalData?: { // 62 sub-template
billNumber?: string; // 01
mobileNumber?: string; // 02
storeLabel?: string; // 03
loyaltyNumber?: string; // 04
referenceLabel?: string; // 05
customerLabel?: string; // 06
terminalLabel?: string; // 07
purpose?: string; // 08
};
}
export function buildEmvMpm(p: MpmParams): string {
const parts: string[] = [];
parts.push(tlv("00", "01")); // Payload Format Indicator
parts.push(tlv("01", p.mode === "dynamic" ? "12" : "11")); // Point of Initiation Method
// Merchant Account Information templates (02–51), in ascending ID order.
for (const id of Object.keys(p.merchantAccounts).sort()) {
parts.push(tlv(id, p.merchantAccounts[id]));
}
parts.push(tlv("52", p.merchantCategoryCode));
parts.push(tlv("53", p.currency));
if (p.amount) parts.push(tlv("54", p.amount));
parts.push(tlv("58", p.countryCode));
parts.push(tlv("59", p.merchantName));
parts.push(tlv("60", p.merchantCity));
if (p.postalCode) parts.push(tlv("61", p.postalCode));
if (p.additionalData) {
const a = p.additionalData;
const sub =
(a.billNumber ? tlv("01", a.billNumber) : "") +
(a.mobileNumber ? tlv("02", a.mobileNumber) : "") +
(a.storeLabel ? tlv("03", a.storeLabel) : "") +
(a.loyaltyNumber ? tlv("04", a.loyaltyNumber) : "") +
(a.referenceLabel ? tlv("05", a.referenceLabel) : "") +
(a.customerLabel ? tlv("06", a.customerLabel) : "") +
(a.terminalLabel ? tlv("07", a.terminalLabel) : "") +
(a.purpose ? tlv("08", a.purpose) : "");
if (sub) parts.push(tlv("62", sub));
}
// CRC (63) is computed over the whole string INCLUDING the "6304" prefix.
const payload = parts.join("") + "6304";
return payload + crc16(payload);
}
/* ---- Example (Mongolia, dynamic, MNT 5000) ----
const data = buildEmvMpm({
mode: "dynamic",
merchantAccounts: { "29": tlv("00", "A0000007681010") + tlv("01", "MERCHANT-ID") },
merchantCategoryCode: "0000",
currency: "496", // MNT
amount: "5000",
countryCode: "MN",
merchantName: "MY SHOP",
merchantCity: "ULAANBAATAR",
additionalData: { billNumber: "INV-2026-001", referenceLabel: "ORDER42" },
});
// Render with the `qrcode` package:
// import QRCode from "qrcode";
// await QRCode.toDataURL(data, { errorCorrectionLevel: "M" });
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment