TLV Format in EMV QR Payloads
EMVCo MPM payloads are not JSON. They are a tight Tag-Length-Value stream. If you mis-count length, every later field is garbage even when CRC was computed on that garbage.
The three fields
Tag: two digits (00–99 at the root for MPM). Length: two digits, the count of characters in the value (00–99). Value: exactly that many characters, which may themselves be a TLV list for templates.
Worked Tag 54 (amount)
Take the characters 5406125.00. Tag = 54, Length = 06, Value = 125.00.
The decoder reads two digits of tag, two digits of length, then exactly six characters of value. If length were wrongly set to 05, the next tag’s digits would be swallowed into the amount and every later field would shift.
Round-trip check in EMV QR Hub: generate a billed QR in the EMV studio, open Decode, confirm Tag 54 value matches the amount you entered and that CRC validates.
Worked Tag 54
5406125.00 means tag 54, length 06, value 125.00. If you write 5405125.00 the parser eats 125.0 and then treats the leftover 0 as the start of the next tag — a classic desync.
Nested templates
Tag 26’s value is another TLV stream (GUID, account ids). The outer length must equal the inner stream length. Students often set outer length to “about 36” from a sample and then change an inner id.
Why generators exist
Hand-concatenating TLV is how samples rot. Use the EMV studio, then inspect with Decode. Specification details: EMVCo QRCPS. We implement the encoding; we do not publish the spec.
Maximum 99
A two-digit length cannot describe a 100-character value. Long additional data belongs in a template that is itself chunked, or it does not belong in the QR. Do not invent three-digit lengths.
Round-trip test
Parse a payload to a tree, serialize again, compare strings. If they differ only by CRC hex case, normalize. If tags move, your serializer is not stable enough for signatures or logs.
Human dump
Our decoder pretty-prints tag meaning next to values so you do not have to memorize 52 versus 53. Your library should offer the same dump in tests.
Worked nested Tag 26 (illustrative)
Suppose a root object reads 26360014A00000067701011101130066000000000 — that is Tag 26, length 36, then 36 characters of inner TLV. Inner 00 length 14 might be a GUID prefix such as A000000677010111, then inner 01 length 13 holds an account proxy. If you change the proxy to 15 characters and leave outer length 36, the next root tag is eaten. Decode will show a nonsense merchant name; the fix is the outer length, not Tag 59.
Students often copy length 36 from a blog sample forever. Length is not a magic constant. It is the character count of the current inner stream. Recalculate it every time an inner field changes, the same way you recompute CRC every time any earlier field changes.
Desync symptoms you will actually see
Classic desync: Tag 54 written 5405125.00 instead of 5406125.00. The parser consumes 125.0, then the leftover 0 starts a phantom tag. Downstream country and name become fragments. Another classic: UTF-8 byte counts used as length while the payload is ASCII digits plus a dotted amount. A third: stripping leading zeros from Tag 00 so the stream starts 0201… which is not MPM.
When Decode shows CRC invalid and a truncated tree, fix the first bad length. Recomputing CRC on a desynced string just checksums garbage. The round-trip test (parse → serialize → compare) catches serializer bugs that unit tests on a single golden string miss.
Templates versus primitive values
Primitive tags (53, 58, 59, 54, 63, …) have values that are not TLV. Template tags (26–51, 62, 64, some unreserved) have values that are TLV. If you pretty-print a template as a single string, you will miss inner defects. If you try to parse a primitive as TLV, you will invent tags from the digits of an amount or a city.
Our decoder labels templates as trees and primitives as scalars so you can see the difference. When you write tests, assert both. EMVCo’s QR spec is the authority for which IDs are templates; we mirror that in software for the fields the studio actually emits.
Generator discipline
Hand-concatenating TLV is how cookbook samples rot. Use the EMV studio, copy the payload, inspect in Decode, then store the exact string next to the print job. If you must assemble TLV in code, centralize tag/length padding in one function and fuzz it. Two-digit zero-padding is not optional.
This academy will keep showing small worked fragments rather than pasting EMVCo PDFs. For normative field lists, download the current QR specification set from emvco.com. We are independent of EMVCo, LLC.
Teaching TLV with a highlighter
Print a payload in monospace, draw boxes around tag/length/value triples, and colour nested MAI. One page of highlighting beats ten slides. Then enter the same string in Decode and confirm your boxes match the UI rows.
Capacity planning
Before adding verbose Tag 62, estimate total string length and check QR version capacity at your print size. TLV correctness plus ISO symbol capacity are joint constraints.
Order matters
Wallets read left to right. Tag 00 opens, Tag 63 closes. Optional tags sit in between in the order the spec allows. Reordering randomly can still CRC-pass and still fail a cert suite.
Identity block
After POI you need a payable destination (26–51), who the shop is (59, 60), where (58), what MCC (52), and which currency (53). Amount (54) only when the ticket is priced in the QR.
Worked static string
00020101021126360014A000000677010111011300660000000005204581253033565802IN5910EMVQRHUB6007DELHI6304A13F is format, static POI, one MAI, MCC, INR, IN, name, city, CRC. Use it as a parse drill, never as a live merchant.
Optional additional data
Tag 62 holds bill number, mobile, store label, and similar sub-tags. Each sub-tag is TLV inside 62. If you do not need reconciliation, omit 62 rather than sending empty shells.
Generate then inspect
Build in the EMV studio, paste into Decode, compare this map. Payload decoding and the payload cookbook cover parser and samples without duplicating this structure page.
Minimum viable static sticker
00, 01=11, one MAI, 52, 53, 58, 59, 60, 63. That is enough for many micro-merchants. Adding 54 without switching to 12 is how you create amount bugs.
Minimum viable billed QR
Same as static plus 01=12 and 54. Add 62 bill number if the kitchen ticket id must survive into the acquirer report.
What not to duplicate
Do not put the same account id in two MAI templates “for luck.” Duplicate destinations are undefined. Do not put currency in Tag 59. Do not put city in Tag 58.
Byte budget
QR versions have capacity limits. Verbose merchant names plus huge 62 blobs plus logos in the image are separate problems: the payload can be fine while the pretty QR will not scan at 30 cm. Test print size.
Root order as a reading convention
A typical MPM string reads left to right as format, POI, MAI template(s), MCC, currency, (amount), country, name, city, (postal), (additional data), CRC. Wallets are stricter about presence than about whether you permutated two optional tags, but CRC depends on exact bytes, so treat order as stable. If your serializer reorders tags on every save, logs and signatures become useless.
Tag 00 first and Tag 63 last are the constraints you should treat as hard. Anything after CRC is unspecified. Anything before 00 is not MPM. Decode will show you the order it parsed; compare that to what you thought you emitted.
Minimum viable static sticker versus billed QR
Static: 00, 01=11, one MAI, 52, 53, 58, 59, 60, 63. No Tag 54. That is enough for many micro-merchants. Billed: same plus 01=12 and 54, often 62 with a bill number so kitchen tickets survive into the acquirer report. Adding 54 without switching to 12 is how you create amount bugs.
Do not put currency in Tag 59, city in Tag 58, or the account id in the merchant name. Those fields look “helpful” in a demo and become undefined for parsers. Do not duplicate the same account in two MAI templates.
Byte budget and the symbol layer
QR versions have capacity limits. Verbose names, huge 62 blobs, and a heavy logo in the image are separate problems: the payload can be legal while a 2 cm print will not scan at 30 cm. Test print size with the cameras your customers have. Error correction at the ISO layer is not a substitute for a short payload.
If you are out of budget, shorten Tag 59, drop unused 62 sub-tags, then consider QR version. Do not invent three-digit TLV lengths. See the TLV article for why 99 is the cap per value.
Worked static reading (illustrative)
000201010211…52…5303356…5802IN5910EMVQRHUB6007DELHI6304… reads as format 01, static 11, then MCC, INR, India, name EMVQRHUB, city DELHI, CRC. There is no 54, which is correct for a reusable sticker. Replace name, city, MAI, and MCC before you print; recompute CRC after every edit.
Generate in the studio, inspect in Decode, store the exact string with the print job. For the normative layout, use EMVCo QRCPS on emvco.com. We are not affiliated; we show original examples from this toolkit.
Round-trip from form to string
A form UI should serialize to TLV in one function and never let operators edit length digits by hand. If your CMS stores “merchant name” separately from “Tag 59 length,” you will eventually desync. The studio on this site keeps caps on name and city so length digits track the visible fields. Export the string, decode it, diff against the form. That round-trip is the cheapest integration test before you wire print or POS.
Logging payloads safely
Store hashes and tag summaries in logs, not necessarily full MAI in plaintext, if your scheme treats proxies as sensitive. Always store enough to reproduce CRC failures. When support pastes a string into Decode on this site, it stays local — prefer that over emailing production payloads.
POS integration boundary
Your POS should call one function: buildMpmPayload(order) → string → QR symbol. It should not let cashiers edit TLV. Tag 62 bill number should come from the order id field you already trust. If the POS cannot produce order ids, stay on static 11 until it can.
Export for print vendors
Send print vendors the ASCII string and a Decode screenshot, not only a PNG. Vendors who re-type payloads introduce length bugs. Prefer API integration that passes the string verbatim.
