Loading
← All notes

Encoding EMV QR payloads with TLV, step by step

An EMV QR code is a string of tag-length-value fields with a checksum at the end. Here's how the format works, static and dynamic, and how to build one in C#.

Filed underEMV QR, C# and Payments

When I built a P2P QR payment system at DPL, with both static and dynamic codes, the first requirement was the standard: follow the EMV® QR Code specification, so any compliant scanner can read the code. Under the pixels, the payload is a plain string of TLV fields, and scanning one parses those fields back into a transaction.

Tag, length, value

Every field has three parts: a two-digit ID, a two-digit length, and the value. The ID 59 with the value ANEES STORE (11 characters) becomes 5911ANEES STORE. Some fields are templates that contain TLV fields of their own, like merchant account information.

a dynamic payload, left to right0002010102122638…540825000.005802TZ6304CRC· · ·26380018com.example.wallet0112MERCHANT-00126 = merchant account, a templateIts value is TLV again; 38 is the length of all of it.540825000.0054 = amount08 = eight charactersIDlengthvalue
Fig. 1A dynamic payload, field by field. Every field is an ID, a two-digit length, and a value. A template like 26 carries more TLV fields inside its value, and its length covers all of them.

The fields you'll use most:

  • 00 Payload format indicator, always 01
  • 01 Point of initiation: 11 for static, 12 for dynamic
  • 26-51 Merchant account information (nested TLV)
  • 52 Merchant category code
  • 53 Transaction currency, ISO 4217 numeric (for example 834 for TZS)
  • 54 Transaction amount, used in dynamic codes
  • 58 Country code, 59 merchant name, 60 merchant city
  • 63 CRC, always the last field

Static vs dynamic

A static code (01 = 11) says who to pay; it's generated once and reused, and the payer enters the amount. A dynamic code (01 = 12) is generated for one transaction and also says how much, in field 54.

The checksum

Field 63 holds a CRC-16/CCITT-FALSE checksum (polynomial 0x1021, initial value 0xFFFF). It's calculated over the whole payload, including the 6304 ID and length of the CRC field itself, and written as four uppercase hex characters.

static string Tlv(string id, string value) =>
    $"{id}{value.Length:D2}{value}";

static string Crc16(string payload)
{
    ushort crc = 0xFFFF;
    foreach (var b in Encoding.ASCII.GetBytes(payload))
    {
        crc ^= (ushort)(b << 8);
        for (var i = 0; i < 8; i++)
            crc = (crc & 0x8000) != 0
                ? (ushort)((crc << 1) ^ 0x1021)
                : (ushort)(crc << 1);
    }
    return crc.ToString("X4");
}

var payload =
    Tlv("00", "01") +
    Tlv("01", "12") +
    Tlv("26", Tlv("00", "com.example.wallet") + Tlv("01", "MERCHANT-001")) +
    Tlv("52", "5411") +
    Tlv("53", "834") +
    Tlv("54", "25000.00") +
    Tlv("58", "TZ") +
    Tlv("59", "ANEES STORE") +
    Tlv("60", "DAR ES SALAAM") +
    "6304";

payload += Crc16(payload);

What bites in production

  • Lengths are character counts. Validate names and cities before encoding, and keep them within the spec's limits.
  • Parse, don't split. Read the ID and length, then consume exactly that many characters. Values can contain digits that look like tags.
  • Verify the CRC first when scanning, before trusting any field in the payload.
  • Treat templates as their own TLV strings, with their own lengths.

Once the encoder and parser are solid and well tested, everything built on top of them, from generating a code to starting a transfer from a scan, gets simpler.

the project behind itthe project behind it

Read the case study

PaymentsDPL, 2024 – 2026

Request to Pay over App and USSD

Led delivery of Request to Pay: a merchant requests money from a customer, who can pay from a push notification or a USSD prompt, and can never pay twice.

4,000+merchants adopted Request to Pay

Built with.NET, Microservices, USSD and Push notifications

Read case study
keep readingkeep reading

More notes

keep in touchkeep in touch

New notes go up here first, then on LinkedIn.

Short write-ups on problems I run into while building payment and backend systems. Follow along, or tell me what you'd like me to write about next.

Let's talk