Skip to the tool
MoveAheadPayments Toolbox

EMV tag decoder — BER-TLV and ISO 8583 field 55

Decode EMV chip data into named tags, with the TVR, AIP and cryptogram broken out bit by bit.

Runs in your browser — nothing you paste is sent to a server

Hex, with or without spaces. Nothing you paste leaves your browser.

How EMV encodes a tag and a length

Every object is a tag, then a length, then that many bytes of value. Both parts carry rules that a generic viewer gets wrong, and both are where a dump stops decoding.

9F 26 08 1234567890ABCDEF
│  │  │  └── value, exactly 8 bytes
│  │  └───── length, short form (below 0x80)
│  └──────── second tag byte; bit 8 clear, so the tag ends here
└─────────── first tag byte: low five bits all set (1F), so the tag continues
             bit 6 clear, so this holds a value rather than nested objects

77 81 A4 …
   │  └── one further length byte: 164 bytes, not 129
   └───── 0x81 means "one length byte follows"

Bit 6 of the first tag byte is what says a tag is a template holding more objects inside it — 77 and A5 have it set, 9F26 does not. This decoder reads nesting from that bit rather than from a list of templates it knows, which is why it descends correctly into a proprietary template it has never seen before.

The three fields worth decoding bit by bit

  • 95 — Terminal Verification Results. Five bytes recording what the terminal checked and what it found. It is the first thing to read on a declined chip transaction, and the last thing anyone wants to decode by hand. Note that it records the terminal's findings only: an issuer decline arrives in tag 8A and leaves the TVR clear.
  • 82 — Application Interchange Profile. What the card says it supports: SDA, DDA or CDA, whether cardholder verification happens, and whether the issuer can be authenticated.
  • 9B — Transaction Status Information. What actually got performed, as opposed to what was supported. Reading 82 and 9B together answers “was this even attempted?”, which is usually the real question.

A reserved bit that is set is reported rather than ignored. In real data that almost always means the value is offset by a byte, and a decoder that silently skips it hides the actual fault.

Scope and limits

  • Amounts are shown assuming two decimal places, and the reading says so. The exponent lives in a different tag that is often absent from the same dump, so a currency like JPY would be read wrong — stating the assumption is better than guessing silently.
  • Tag 9F10, Issuer Application Data, is shown as raw hex. Its layout is scheme-proprietary and differs between Visa, Mastercard and RuPay; naming a byte here would be inventing a specification rather than reading one.
  • Template 80, the format 1 GENERATE AC response, is a bare concatenation with no tags inside it. Splitting it requires the CDOL that requested it, which is not in the response, so its bytes are shown whole.
  • Unknown tags still decode. BER-TLV carries its own structure, so a proprietary 9Fxx or BFxx tag is parsed correctly and simply reported as not being in the dictionary.

Worked example

A GENERATE AC response from a terminal, as it would appear inside field 55 of an ISO 8583 authorisation. The card asked to go online and the terminal recorded two findings — which is the situation worth being able to read at a glance.

77 32 9F 27 01 80 9F 36 02 00 A5 9F 26 08 12 34 56 78 90 AB CD EF 95 05 00 80 00 80 00 9A 03 26 08 18 9F 02 06 00 00 00 14 99 00 5F 2A 02 03 56 82 02 39 00

What it says

Card's decision (9F27):
ARQC — go online for authorisation.
Amount (9F02):
1499.00 (assuming 2 decimal places)
Terminal Verification Results (95):
0080008000
  • byte 2, bit 8 ICC and terminal have different application versions
  • byte 4, bit 8 Transaction exceeds floor limit

Two bits set in five bytes of hex, and reading them by hand off a specification table is how the wrong one gets read. That is the whole job of this tool.

Frequently asked questions

What is the difference between this and the Bharat QR decoder?
They read two different formats that are both called TLV. An EMVCo QR payload is ASCII: two digits of tag, two digits of length, then that many characters, and you can read it with your eyes. EMV chip data is binary BER-TLV: the tag is one or two bytes and declares in its own bits whether it holds nested objects, and the length is either a single byte or a count byte announcing how many length bytes follow. If your data starts with 0002 it belongs in the QR decoder; if it is a hex dump full of 9F tags it belongs here.
Why is my TVR all zeros when the transaction declined?
The Terminal Verification Results record what the terminal checked, not what the issuer decided. An issuer decline arrives in the authorisation response code, tag 8A, and leaves the TVR untouched. A TVR of all zeros with a decline means the terminal was happy and the issuer was not, which points your investigation at the host rather than at the card or the reader.
How do I tell an ARQC from a TC?
Not from tag 9F26, which is the cryptogram itself and looks identical either way. It is the top two bits of tag 9F27, the Cryptogram Information Data: 10 is an ARQC meaning go online, 01 is a TC meaning approved offline, and 00 is an AAC meaning declined offline. This decoder reads those bits and says which one the card produced.
Why did my dump stop decoding partway through?
A length byte was wrong, and in BER-TLV one wrong length reinterprets everything after it. This decoder stops where it lost alignment and names the tag and the byte offset it was reading, which is where the fault is rather than where the symptom appeared. The usual causes are a dump truncated by a log line limit, and long-form lengths misread as short — a first length byte of 0x81 means one more length byte follows, not a length of 129.
Is the card number safe to paste here?
Nothing you paste leaves your browser — the decoding runs on your device, and there is no request carrying it anywhere. Tags that carry cardholder data are also masked in the output by default: the PAN in tag 5A, track 2 in tag 57, and the cardholder name in 5F20. You can turn masking off when you need the full value, and the raw hex is always shown beside the reading either way.

Use it in your own code

This page is a front end for a function you can install. It is the same implementation — not a port — so it answers exactly what you see above.

npm i pymnt-tools

import { parseEmvTlv } from "pymnt-tools/emv-tlv";

Prefer HTTP? The same functions are a free keyed API, or browse the package on npm.