How to Parse NDEF Messages — NFC Data Exchange Format Walkthrough

NDEF (NFC Data Exchange Format) is the standard binary format for data stored on NFC tags. Every NFC tag read returns an NDEF message — a sequence of records, each with a type, payload, and control flags. This guide walks through parsing NDEF from raw bytes, covering record header flags, TNF type name formats, payload length encoding, chunked records, and common record types (Text, URI, Smart Poster, MIME).

1. NDEF Message Structure Overview

An NDEF message is a sequence of one or more NDEF records. Each record has:

2. Parsing the Record Header Byte

The first byte of every NDEF record encodes 5 flags + 3-bit TNF:

Byte 0: [MB] [ME] [CF] [0] [SR] [IL] [TNF (3 bits)]
           b7   b6   b5  b4   b3   b2   b1-b0

// Python parsing example
def parse_header(byte):
    mb  = (byte >> 7) & 1   # Message Begin
    me  = (byte >> 6) & 1   # Message End
    cf  = (byte >> 5) & 1   # Chunk Flag
    sr  = (byte >> 3) & 1   # Short Record
    il  = (byte >> 2) & 1   # ID Length present
    tnf = byte & 0x07       # Type Name Format
    return mb, me, cf, sr, il, tnf

3. TNF — Type Name Format Values

TNFValueMeaningExample
Empty0x00No type or payloadUsed as first chunk of chunked record
Well-Known0x01NFC Forum RTD type"T" (Text), "U" (URI), "Sp" (Smart Poster)
MIME0x02Internet media type"text/html", "application/json", "image/png"
Absolute URI0x03Full URI type name"urn:nfc:sn:mytype"
External0x04Vendor-specific"android.com:pkg", "nfcpy:ndef"
Unknown0x05Opaque dataProprietary payload, no type info
Unchanged0x06Chunked middle/finalUsed in chunked record sequence

4. Decoding a Well-Known Text Record

Text records (TNF=0x01, type="T") are the most common. The payload starts with a status byte encoding UTF-8/UTF-16 + language code length, followed by the language code and text.

# Text Record payload structure:
# [Status Byte] [Language Code (2-5 bytes)] [Text (UTF-8/UTF-16)]

def parse_text_payload(payload):
    status = payload[0]
    utf16 = (status >> 7) & 1       # 0=UTF-8, 1=UTF-16
    lang_len = status & 0x3F         # language code length
    lang = payload[1:1+lang_len].decode('ascii')
    text_bytes = payload[1+lang_len:]
    encoding = 'utf-16-be' if utf16 else 'utf-8'
    text = text_bytes.decode(encoding)
    return lang, text

# Example: "Hello NFC" in English
# Hex: 02 65 6E 48 65 6C 6C 6F 20 4E 46 43
#        │  └─en─┘ └── Hello NFC ──────────┘
#   status=0x02 (UTF-8, lang_len=2)

5. Decoding a URI Record

URI records (TNF=0x01, type="U") use a clever abbreviation: the first payload byte is a prefix code (0x00-0x24) that maps to common URI schemes like "http://www.", "https://", "tel:", "mailto:". The remaining bytes are the suffix.

URI_PREFIXES = {
    0x00: "", 0x01: "http://www.", 0x02: "https://www.",
    0x03: "http://", 0x04: "https://", 0x05: "tel:",
    0x06: "mailto:", 0x07: "ftp://anonymous:anonymous@",
    # ... 0x08-0x24 for more prefixes
}

def parse_uri_payload(payload):
    prefix_code = payload[0]
    prefix = URI_PREFIXES.get(prefix_code, "")
    suffix = payload[1:].decode('utf-8')
    return prefix + suffix

# Example: 0x04 + "cupass.com" → "https://cupass.com"

6. Parsing Chunked Records

When a record payload is too large for a single NDEF record (common on tags with small memory), the payload is split across multiple records using the Chunk Flag (CF):

def reassemble_chunks(records):
    """Combine chunked records into a single payload."""
    payload = bytearray()
    record_type = None
    tnf = None
    for rec in records:
        if rec.cf:
            if rec.mb:  # First chunk
                tnf = rec.tnf
                record_type = rec.type
            payload.extend(rec.payload)
        else:
            payload.extend(rec.payload)  # Last chunk
    return tnf, record_type, bytes(payload)
Test this yourself: Use our NDEF Message Parser to paste any NDEF hex and see all records decoded instantly — Text, URI, Smart Poster, MIME, and chunked records.

7. Smart Poster Record Parsing

A Smart Poster (type "Sp") contains nested NDEF records: a URI record (mandatory), plus optional Title, Icon, Type, Size, and Action records. It's an NDEF message embedded inside a single record's payload.

def parse_smart_poster(payload):
    """Smart Poster payload is itself an NDEF message."""
    records = parse_ndef_message(payload)
    uri = None
    titles = {}
    for rec in records:
        if rec.type == b'U':
            uri = parse_uri_payload(rec.payload)
        elif rec.type == b'T':
            lang, text = parse_text_payload(rec.payload)
            titles[lang] = text
    return {'uri': uri, 'titles': titles}

8. Common NDEF Pitfalls

IssueCauseFix
MB flag missing on first recordCorrupted tag writeAssume MB=1 for first record in raw dump
SR flag wrongPayload > 255 bytes but SR=1Use 4-byte payload length (SR=0) for >255 bytes
Text encoding mismatchUTF-16 flag set but data is ASCIIAlways check status byte bit 7 before decoding
Chunk signature failureMismatched TNF across chunksVerify first chunk TNF, remaining have TNF=0x06

Related Tools

NDEF Parser Tool — Paste hex, see all records decoded | NDEF Writer Simulator — Build NDEF messages interactively | NFC Capacity Calculator — Check if your payload fits | Android NFC NDEF Guide — Read/write NDEF on Android