MerchantQr.Net 1.1.0

dotnet add package MerchantQr.Net --version 1.1.0
                    
NuGet\Install-Package MerchantQr.Net -Version 1.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="MerchantQr.Net" Version="1.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="MerchantQr.Net" Version="1.1.0" />
                    
Directory.Packages.props
<PackageReference Include="MerchantQr.Net" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add MerchantQr.Net --version 1.1.0
                    
#r "nuget: MerchantQr.Net, 1.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package MerchantQr.Net@1.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=MerchantQr.Net&version=1.1.0
                    
Install as a Cake Addin
#tool nuget:?package=MerchantQr.Net&version=1.1.0
                    
Install as a Cake Tool

MerchantQr.Net

Correct, zero-dependency parser and generator for EMVCo Merchant-Presented Mode (MPM) QR code payloads: the TLV string behind Pix (Brazil), SGQR (Singapore), and many national merchant-QR schemes. CRC validated on parse, always recomputed on build. Deterministic, offline, Native AOT clean.

NuGet   MIT   Zero dependencies   Native AOT clean

Why

Merchant QR is not one standard; it is one wire format wearing many national badges. EMVCo publishes the MPM specification, then each central bank profiles it: Brazil calls it Pix (BR Code), Singapore calls it SGQR, and India, Malaysia, Thailand and others each ship their own scheme on the same skeleton. The skeleton is always the same: a flat list of tag-length-value data objects ending in a CRC-16 checksum.

Almost every team that touches this reimplements the same two things and gets one of them wrong: the TLV framing, and the CRC. The CRC is the classic trap. It is CRC-16/CCITT-FALSE computed over the payload including its own 6304 header, and a single wrong parameter (an XOR here, a reflection there) produces a checksum that looks plausible and is rejected by every real acquirer. MerchantQr.Net is the small, correct primitive that gets both right, with the standard 123456789 check value pinned in the test suite.

Install

dotnet add package MerchantQr.Net

Parse

using MerchantQr;

QrPayload qr = MerchantQrCode.Parse(payload); // throws MerchantQrParseException on a bad CRC or malformed TLV

string? version  = qr.PayloadFormatIndicator; // "01"
string? currency = qr.TransactionCurrency;    // ISO 4217 numeric, e.g. "986"
string? amount   = qr.TransactionAmount;      // raw string, null for a static QR
string? name     = qr.MerchantName;
string? city     = qr.MerchantCity;

// Non-throwing variant
if (MerchantQrCode.TryParse(payload, out QrPayload? parsed))
{
    // parsed.Objects is the ordered top-level TLV list
}

Nested templates (merchant account info 26-51, additional data 62, and the unreserved range) are parsed one level down:

foreach (QrDataObject sub in qr.GetSubObjects("62"))
{
    // e.g. sub.Id == "05" (reference label)
}

Build

Build serializes your data objects and appends a correct CRC, so its output always validates. Any CRC you pass in is discarded and recomputed.

string payload = MerchantQrCode.Build(new[]
{
    new QrDataObject("00", "01"),            // Payload Format Indicator
    new QrDataObject("52", "5411"),          // Merchant Category Code
    new QrDataObject("53", "986"),           // Transaction Currency (BRL)
    new QrDataObject("54", "23.72"),         // Transaction Amount
    new QrDataObject("58", "BR"),            // Country Code
    new QrDataObject("59", "BEST TRANSPORT"),// Merchant Name
    new QrDataObject("60", "SAO PAULO"),     // Merchant City
});

For a nested template, encode the sub-objects first with Encode (a TLV fragment with no CRC), then hand that string to Build:

string additionalData = MerchantQrCode.Encode(new[]
{
    new QrDataObject("05", "REF12345"),      // reference label
});

string payload = MerchantQrCode.Build(new[]
{
    new QrDataObject("00", "01"),
    new QrDataObject("62", additionalData),  // Additional Data template
});

The CRC guarantee

The checksum is CRC-16/CCITT-FALSE: polynomial 0x1021, initial value 0xFFFF, no input reflection, no output reflection, no final XOR. It is computed over the entire payload including the CRC object's own 6304 header, up to but not including the four hex characters of the CRC value, over the Latin-1 bytes of the string.

The implementation is pinned to the industry check value: the CRC of the ASCII string 123456789 is 0x29B1, asserted directly in the test suite. Parse rejects any payload whose CRC does not match; Build always emits a valid one.

Build emits the CRC as four uppercase hex characters, and Parse compares case-insensitively, so a payload carrying a lowercase CRC (some generators emit 6304abcd) still validates.

Non-ASCII payloads

By default the CRC is computed over each character's Latin-1 low byte, which is exact for the ASCII fields that dominate Pix and SGQR. For the rare payload that carries multibyte characters in a field (for example a UTF-8 merchant name), pass an explicit Encoding to Parse and Build; the default keeps the Latin-1 behaviour and is fully backward compatible.

using System.Text;

string payload = MerchantQrCode.Build(objects, Encoding.UTF8); // CRC over UTF-8 bytes
QrPayload qr   = MerchantQrCode.Parse(payload, Encoding.UTF8);  // validated over UTF-8 bytes

Format, in one paragraph

The payload is a flat concatenation of data objects. Each object is a 2-character ID, a 2-character zero-padded decimal length, then a value of exactly that length. 000201 means ID 00, length 02, value 01. Reserved IDs include 00 (Payload Format Indicator), 52 (Merchant Category Code), 53 (Transaction Currency), 54 (Transaction Amount), 58 (Country Code), 59 (Merchant Name), 60 (Merchant City), 62 (Additional Data), and 63 (CRC). IDs 26-51, 62, 64 and 80-99 are nested templates whose value is itself a sequence of sub-objects.

Schemes this covers

Any scheme that profiles EMVCo MPM shares this framing and CRC, so the parser and generator apply directly to Pix / BR Code (Brazil), SGQR (Singapore), and the EMVCo-based merchant-QR standards used across India, Malaysia, Thailand and other markets. Scheme-specific reserved-tag semantics (which merchant account template carries which identifier) sit on top of this primitive; this library gives you the correct wire format underneath.

Notes and limitations

  • The CRC defaults to Latin-1 low bytes, matching payloads whose fields are ASCII (the overwhelmingly common case for Pix and SGQR). Payloads carrying multibyte UTF-8 in a field such as the Merchant Information Language template validate only when the matching Encoding is supplied to Parse and Build (see Non-ASCII payloads).
  • On a duplicate top-level ID the first occurrence wins consistently: both Get and GetSubObjects return the first object's value and sub-objects.
  • Nested templates are parsed one level deep and exposed via GetSubObjects. A template value that is not well-formed sub-TLV is left opaque rather than rejected, so a payload with a valid top-level CRC still parses.
  • Values are surfaced as raw strings. Currency and amount typing, and per-scheme field validation, are the caller's job.

License

MIT. Copyright Israel Iyonsi.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net8.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.0 104 8/21/2026
1.0.0 93 8/19/2026