,

Reading EMV Records: How Terminals Get What They Need

ASSI Avatar

I want to share something with you today that brought me a lot of headaches when I was working on my first contactless EMV implementation. If you’re diving into EMV transactions, especially building or testing somethink like a SoftPOS or EMV kernel, you’ll eventually bump into a critical question:

“How does the terminal know what data to request from the card”?

That’s exactly what I asked myself one late night, debugging why some tags were missing in my TLV output. The answer, it turns out, is both simple and nuanced: It’s all about reading EMV records.

Let’s break this down, the way I would explain it to a teammate sitting beside me, sipping 3-in-1 coffee at 2AM while waiting for the next APK build.

Why Does the Terminal Need to Read Records?

When you insert or tap your card, the terminal doesn’t magically know your card number or expiration date. It has to ask the card, politely and properly for the data it needs. This process is defined by the EMV specification and done in a secure and predictable sequence.

The card itself stores data in records, kind of like pages in a little book. The terminal needs to “read” these pages using a command called READ RECORD to get what it needs: PAN, expiration date, issuer scripts, cryptographic keys, and more.

But the trick is: the card doesn’t just hand over its table of contents. The terminal must first ask what records exist and that happens during GPO.

Recap: The Role of GPO and AFL

If you’re not familiar yet with GPO (Get Processing Options) and AFL (Application File Locator), you might want to check my other post: What Happens When You Tap or Insert? A Look at GPO and AFL.

To summarize quickly:

  • After selecting the application (using SELECT AID), the terminal sends a GPO command.
  • The card responds with a structure called the AFL, which tells the terminal which records are available, and how to read them.

The AFL is a set of entries that look like this:

Plaintext
SFI | Start Record | End Record | Number of records to be read offline

Each entry tells the terminal:

  • Which SFI (Short File Identifier) to read from
  • The range of record numbers available
  • Whether to include them for offline data authentication (like CDA, DDA)

This is the terminal’s reading plan.

Sample AFL Breakdown

Let’s look at an actual AFL returned from a real EMV card:

Plaintext
94 0A 08 01 02 01 1C 01 02 02

If we split it:

Plaintext
08 01 02 01
1C 01 02 02

Each group of 4 bytes is one AFL entry:

  • First entry:
    • SFI: 08 -> means file SFI 8 (stored as 08 << 3 = 0x40)
    • Start Record: 01
    • End Record: 02
    • Records for offline auth: 01
  • Second entry:
    • SFI: 1C (SFI 28)
    • Start Record: 01
    • End Record: 02
    • Records for offline auth: 02

So the terminal must now issue READ RECORD commands for:

  • SFI 8, Records 1 and 2
  • SFI 28, Records 1 and 2

The READ RECORD Command

The EMV READ RECORD command is APDU-based, and its format looks like this:

Plaintext
CLA  INS  P1   P2   Lc
00   B2   XX   (SFI<<3 | 4)   00

Explanation:

  • INS = B2 (instruction code for READ RECORD)
  • P1 = record number
  • P2 = (SFI << 3) | 4
  • Le = 00 (terminal expects full response)

Let’s say we’re reading SFI 8, Record 1:

Plaintext
00 B2 01 44 00
  • 01 = record number
  • 0x44 = (SFI 8 << 3) | 4 = 0x40 + 0x04 = 0x44

What Comes Back? TLV, Of Course

The response will contain TLV data like this:

Plaintext
70 1A
  5A 08 4761************
  5F24 03 251231
  5F34 01 02
  ...

The tag 70 means “Record Template”. It contains the actual data elements (like PAN 5A, Expiry 5F24, etc.) that the terminal uses for the transaction.

This is how we build our Card Data Object List (CDOL), Terminal Verification Results (TVR), and even determine how to perform offline data authentication.

Real Code Example (C# Style)

Here’s a simple example from my own project — when we built our own terminal-side card reader module for SoftPOS.

C#
public async Task<List<byte[]>> ReadEMVRecords(List<AFLRecord> aflRecords, ICardReader reader)
{
    var result = new List<byte[]>();

    foreach (var record in aflRecords)
    {
        for (int i = record.StartRecord; i <= record.EndRecord; i++)
        {
            var sfiP2 = (byte)((record.SFI << 3) | 4);
            var apdu = new byte[] { 0x00, 0xB2, (byte)i, sfiP2, 0x00 };

            var response = await reader.TransmitAsync(apdu);

            if (response.IsSuccess)
                result.Add(response.Data);
            else
                Console.WriteLine($"Failed to read SFI {record.SFI}, Record {i}: {response.StatusWord}");
        }
    }

    return result;
}

Nothing too fancy, but it works. And this function eventually feeds data into my TLV parser.

Gotchas and Lessons Learned

Here are a few hard-earned lessons from the trenches:

  1. Not All Records Are Always There. Just because the AFL says to read from Record 1 to 3 doesn’t mean all records will succeed. Cards can be a bit… moody. Handle failures gracefully.
  2. Some Cards Use SFI 1, Some Use SFI 2. Don’t hardcode this. Always parse the AFL. I did that once, bad idea.
  3. Not All Terminals Read All Records. Some terminals stop reading early if they find what they need. But in most cases, for robustness and compliance, it’s better to read everything in the AFL.
  4. TLV Parsers Must Be Smart. I highly suggest writing a flexible TLV parser. Some values are primitive, some are constructed (like 0x70, 0x77). I used a recursive approach that handles nested TLVs.

Where You Might See This In Action

If you’re doing any of the following, this topic is 100% relevant:

  • Building your own SoftPOS or terminal
  • Creating EMV test tools or sniffers
  • Debugging why some tags are missing during transaction
  • Implementing CDA/DDA or Offline Data Authentication
  • Working on EMV L2 certification (fun times…)

References and Credits

Everything in this post is based on what I personally implemented and debugged in real-life projects. But to go deeper or cross-reference specs, I recommend:

  • EMVCo Book 3
  • EMV TLV Utility Tool (by Kevin Smith): very helpful for decoding raw data
  • EMVCo Book 4 for commands/APDU formats
  • CashlessNomad.com for related posts

Final Thoughts

Reading EMV records isn’t just a technical step. It’s the moment when the terminal really “meets” the card. It’s the handshake before the payment happens.

If your terminal reads the wrong records or doesn’t read enough you’ll have a bad time. But once you get it right, it feels like solving a puzzle.

Hope this post helped you connect the dots a bit more clearly.