@hussainpithawala/emv-merchant-qr
v1.0.0
Published
EMV® QR Code encoder/decoder for Merchant-Presented Mode v1.0 — Bharat QR / NPCI compatible. Zero dependencies.
Maintainers
Readme
@hussainpithawala/emv-merchant-qr
TypeScript encoder/decoder for EMV® QR Code payloads — Merchant-Presented Mode v1.0.
Fully compatible with Bharat QR / NPCI payment infrastructure (UPI, RuPay).
This is the TypeScript port of
emv-merchant-qr-lib(Go).
Features
- 🔍 Decode any EMV/Bharat QR string into a typed
Payloadobject - ✍️ Encode a structured payload back to a valid QR Code string
- 🔐 CRC16-CCITT validation on decode; automatic computation on encode
- 💳 Multi-network — primitive (IDs
02–25) and template (IDs26–51) MAIs - 🇮🇳 Bharat QR / NPCI — UPI VPA + RuPay credentials in a single QR
- 💰 Tip & convenience fees — fixed, percentage, and consumer-prompted tip
- 🗂️ Additional Data Fields — bill number, loyalty, store label, and more
- 🌐 Alternate language template for Hindi and other regional display names
- 🔧 Unreserved Templates (IDs
80–99) for NPCI proprietary data - Zero runtime dependencies — pure TypeScript, ships as CJS + ESM
Installation
npm install @hussainpithawala/emv-merchant-qr
# or
yarn add @hussainpithawala/emv-merchant-qrRequires Node.js 18+.
Quick Start
import { newPayload, encode, decode, PROMPT_VALUE } from '@hussainpithawala/emv-merchant-qr';
// Build a Bharat QR payload for a Mumbai kirana store
const p = newPayload();
p.addTemplateMerchantAccount('26', 'A000000524', { id: '01', value: 'sharma.kirana@icici' });
p.merchantCategoryCode = '5411'; // Grocery Stores
p.transactionCurrency = '356'; // INR
p.countryCode = 'IN';
p.merchantName = 'Sharma Kirana Store';
p.merchantCity = 'Mumbai';
const raw = encode(p); // → "0002010226..."
const decoded = decode(raw); // throws EMVQRCRCMismatchError if corruptedUsage Examples
Static Bharat QR (printed sticker)
No amount embedded — the consumer enters it in their UPI app.
const p = newPayload();
p.addTemplateMerchantAccount('26', 'A000000524', { id: '01', value: 'merchant@upi' });
p.merchantCategoryCode = '5411';
p.transactionCurrency = '356';
p.countryCode = 'IN';
p.merchantName = 'My Store';
p.merchantCity = 'Mumbai';
const qr = encode(p); // print or display this string as a QR codeDynamic QR (per-transaction, amount locked)
p.transactionAmount = '450.00'; // consumer cannot alter this
const qr = encode(p);Full Bharat QR — UPI + RuPay
p.addTemplateMerchantAccount('26', 'A000000524', { id: '01', value: 'merchant@icici' }); // UPI
p.addTemplateMerchantAccount('27', 'A000000524', { id: '01', value: '4403847000746908' }); // RuPay
const d = decode(encode(p));
console.log(d.hasMultipleNetworks()); // true — consumer app offers network choiceFixed Convenience Fee (restaurant packaging)
p.transactionAmount = '800';
p.setFixedConvenienceFee('25'); // ₹25 packaging fee
const d = decode(encode(p));
console.log(d.totalAmount()); // 825Percentage Convenience Fee (electricity board)
p.transactionAmount = '5000';
p.setPercentageConvenienceFee('1.50'); // 1.50% surcharge
const d = decode(encode(p));
console.log(d.totalAmount()); // 5075Consumer-Prompted Tip
p.transactionAmount = '1200';
p.setPromptForTip(); // consumer app shows "Add a tip?" — must allow 0 tip
const d = decode(encode(p));
console.log(d.tipOrConvenienceIndicator); // "01"Additional Data Fields (loyalty, bill number, etc.)
p.setAdditionalData((adf) => {
adf.billNumber = 'INV2024-001'; // GST invoice number
adf.loyaltyNumber = PROMPT_VALUE; // "***" → consumer app prompts for loyalty card
adf.storeLabel = 'Connaught Place'; // branch label
});
const d = decode(encode(p));
console.log(d.loyaltyNumberRequired()); // trueHindi Alternate Language Name
p.merchantName = 'Sharma Kirana Store'; // English (required)
p.setLanguageTemplate('hi', 'Sharma Kiraane', ''); // Hindi name
const d = decode(encode(p));
console.log(d.preferredMerchantName('hi')); // "Sharma Kiraane"
console.log(d.preferredMerchantName('en')); // "Sharma Kirana Store"Error Handling
import {
decode,
EMVQRCRCMismatchError,
EMVQRInvalidTLVError,
EMVQRMissingRequiredError,
} from '@hussainpithawala/emv-merchant-qr';
try {
const p = decode(scannedString);
} catch (err) {
if (err instanceof EMVQRCRCMismatchError) {
// QR code is corrupted or tampered
} else if (err instanceof EMVQRInvalidTLVError) {
// Malformed TLV structure
} else if (err instanceof EMVQRMissingRequiredError) {
// Required field missing (encode-time)
}
}API Reference
Top-level functions
| Function | Description |
|---|---|
| decode(raw) | Parse a QR string; validates CRC. Throws on error. |
| decodeWithOptions(raw, opts) | Parse with { skipCRCValidation: true } option. |
| encode(payload) | Serialise and append computed CRC. Throws on error. |
| encodeWithOptions(payload, opts) | Serialise with override options. |
| newPayload() | Create a Payload with payloadFormatIndicator = "01". |
Payload class
Data properties
| Property | Type | Notes |
|---|---|---|
| payloadFormatIndicator | string | Always "01" |
| merchantAccountInfos | MerchantAccountInfo[] | Primitive and template MAIs |
| merchantCategoryCode | string | ISO 18245 |
| transactionCurrency | string | ISO 4217 numeric ("356" = INR) |
| transactionAmount | string | Empty = consumer enters amount |
| tipOrConvenienceIndicator | string | "" / "01" / "02" / "03" |
| valueConvenienceFeeFixed | string | Set when indicator = "02" |
| valueConvenienceFeePercent | string | Set when indicator = "03" |
| countryCode | string | ISO 3166-1 alpha-2 |
| merchantName | string | Max 25 chars |
| merchantCity | string | Max 15 chars |
| postalCode | string | Optional |
| additionalData | AdditionalDataField \| null | |
| languageTemplate | LanguageTemplate \| null | |
| unreservedTemplates | UnreservedTemplate[] | IDs "80"–"99" |
Methods
| Method | Description |
|---|---|
| addPrimitiveMerchantAccount(id, value) | Add primitive MAI (IDs "02"–"25"). Pass "" for auto-ID. |
| addTemplateMerchantAccount(id, guid, ...extra) | Add template MAI (IDs "26"–"51"). Pass "" for auto-ID. |
| setFixedConvenienceFee(amount) | Fixed fee added automatically by consumer app. |
| setPercentageConvenienceFee(percent) | Percentage fee, e.g. "1.50" for 1.50%. |
| setPromptForTip() | Consumer app prompts for tip (must allow 0). |
| setAdditionalData(fn) | Set ADF sub-fields via callback. |
| setLanguageTemplate(lang, name, city) | Set alternate language display name. |
| totalAmount() | Base + fee. Throws if no transactionAmount. |
| loyaltyNumberRequired() | true when loyaltyNumber === "***". |
| mobileNumberRequired() | true when mobileNumber === "***". |
| preferredMerchantName(lang) | Language-aware name with English fallback. |
| preferredMerchantCity(lang) | Language-aware city with English fallback. |
| hasMultipleNetworks() | true when > 1 MAI entry. |
MerchantAccountInfo class
| Method | Description |
|---|---|
| globallyUniqueID() | Returns sub-field "00" value (template MAIs). |
| subField(id) | Returns the value of sub-field id, or "". |
| isTemplate() | true for IDs "26"–"51". |
Error classes
| Class | When thrown |
|---|---|
| EMVQRError | Base class for all EMV QR errors |
| EMVQRInvalidLengthError | Payload string too short |
| EMVQRInvalidTLVError | Malformed TLV structure |
| EMVQRCRCMismatchError | CRC does not match |
| EMVQRMissingRequiredError | Required field absent (encode) |
| EMVQRParseError | Field-specific parse failure (has .fieldId) |
Constants
// Tip indicators
TIP_INDICATOR_PROMPT_CONSUMER // "01"
TIP_INDICATOR_FIXED_CONVENIENCE_FEE // "02"
TIP_INDICATOR_PERCENTAGE_FEE // "03"
// Sentinel — triggers consumer app prompt for input
PROMPT_VALUE // "***"
// Indian context
// Currency: "356" (INR) Country: "IN" NPCI GUID: "A000000524"Bharat QR Reference
NPCI mandated (Sept 2017) that all Bharat QR codes must include at least:
| MAI Slot | Network | Notes |
|---|---|---|
| Primitive 02–08 | Visa, MC, Amex, etc. | Acquirer-assigned 16-char merchant IDs |
| Template 26 | UPI | GUID = A000000524, sub-field 01 = VPA |
| Template 27 | RuPay | GUID = A000000524, sub-field 01 = acquirer cred |
Currency : 356 (INR)
Country : INContributing
See CONTRIBUTING.md.
License
MIT — see LICENSE.
Disclaimer
EMV® is a registered trademark of EMVCo, LLC. This library is an independent open-source implementation and is not affiliated with or endorsed by EMVCo or NPCI.
