Chapter 5: NDEF Format — Records, Types & Encoding

NDEF (NFC Data Exchange Format) is the application-layer format that makes NFC tags interoperable. It is defined by the NFC Forum and sits entirely above the physical and protocol layers — you can store NDEF on an NTAG213, a MIFARE Classic card (via MAD), an ISO 15693 tag, or even in a file on a host card emulator.

When a smartphone reads an NFC tag and automatically opens a URL or contact card, it is parsing NDEF. Understanding NDEF is necessary for writing useful tags and for building applications that process tag data.


5.1 Where NDEF Sits in the Stack

The relationship between the layers:

+-------------------------------+
|   Application (URL, vCard...) |
+-------------------------------+
|        NDEF Records           |   ← NFC Forum spec
+-------------------------------+
|        NDEF Message           |   ← NFC Forum spec
+-------------------------------+
|   NFC Forum Tag Type 2/4/5    |   ← NFC Forum Type spec
+-------------------------------+
|   ISO 14443 / ISO 15693       |   ← Physical / protocol layer
+-------------------------------+
|        13.56 MHz RF           |   ← Physical medium
+-------------------------------+

NDEF does not know or care about CRYPTO1 keys or ISO 14443 anticollision. Once you have authenticated (if required) and the tag is readable, NDEF parsing is purely a byte-stream problem.


5.2 NDEF Message and Records

An NDEF Message is a sequence of one or more NDEF Records. A message has no header of its own — the structure is entirely expressed through the MB/ME flags of the records it contains.

Record Structure

Each NDEF record has the following binary layout:

+-------+-------+-------+-------+-------+-------+-------+-------+
|  MB   |  ME   |  CF   |  SR   |  IL   |      TNF (3 bits)     |
+-------+-------+-------+-------+-------+-----------------------+
|                    TYPE LENGTH (1 byte)                        |
+---------------------------------------------------------------+
|           PAYLOAD LENGTH (1 or 4 bytes, SR flag)               |
+---------------------------------------------------------------+
|              ID LENGTH (1 byte, IL flag)                       |
+---------------------------------------------------------------+
|                       TYPE (0–255 bytes)                       |
+---------------------------------------------------------------+
|                       ID   (0–255 bytes)                       |
+---------------------------------------------------------------+
|                      PAYLOAD (variable)                        |
+---------------------------------------------------------------+

First byte — flags + TNF:

Bit Flag Meaning
7 MB (Message Begin) Set on the first record of a message
6 ME (Message End) Set on the last record of a message
5 CF (Chunk Flag) Set if this record is a chunk (partial payload)
4 SR (Short Record) If set, Payload Length is 1 byte; otherwise 4 bytes
3 IL (ID Length present) If set, the ID Length field is present
2–0 TNF (Type Name Format) 3-bit field — see table below

A single-record message has both MB and ME set. An empty message (a special case) is one record with MB=1, ME=1, TNF=0 (Empty), and all length fields = 0.


5.3 TNF — Type Name Format

The TNF field tells the parser how to interpret the TYPE field:

TNF value Name TYPE field interpretation
0x00 Empty No type; empty record. No type/ID/payload.
0x01 Well-Known NFC Forum RTD (Record Type Definition) short name, e.g. U for URI
0x02 MIME MIME media type string, e.g. text/plain
0x03 Absolute URI Absolute URI string as the type
0x04 External NFC Forum External Type, format domain:type
0x05 Unknown Unknown type; type length must be 0
0x06 Unchanged Used in chunked records; type of first chunk applies
0x07 Reserved Reserved for future use

5.4 Well-Known Record Types (TNF = 0x01)

Well-Known types use short, registered type names defined by the NFC Forum RTD (Record Type Definition) specifications. The type is encoded as ASCII.

URI Record — type U

The URI record is the most common NDEF record. It stores a URI (URL, tel:, mailto:, etc.) with a 1-byte prefix code to save space:

Payload:
  Byte 0:  URI identifier code (see table)
  Bytes 1–N: URI string (UTF-8, without the prefix)

URI identifier codes:

Code Prefix
0x00 (none — full URI follows)
0x01 http://www.
0x02 https://www.
0x03 http://
0x04 https://
0x05 tel:
0x06 mailto:
0x07 ftp://anonymous:anonymous@
0x08 ftp://ftp.
0x09 ftps://
0x0D urn:epc:id:

So the URL https://example.com is encoded as \x04example.com (13 bytes instead of 19).

def encode_uri_record(uri):
    prefixes = {
        'https://www.': 0x02, 'http://www.': 0x01,
        'https://': 0x04, 'http://': 0x03,
        'tel:': 0x05, 'mailto:': 0x06,
    }
    code, rest = 0x00, uri
    for prefix, val in prefixes.items():
        if uri.startswith(prefix):
            code, rest = val, uri[len(prefix):]
            break
    payload = bytes([code]) + rest.encode('utf-8')
    return payload

Text Record — type T

A Text record stores a human-readable string with a specified language code and encoding:

Payload:
  Byte 0:  Status byte
    bits 7: UTF encoding — 0 = UTF-8, 1 = UTF-16
    bits 6: RFU (reserved, must be 0)
    bits 5–0: Language code length (number of bytes)
  Bytes 1–N: Language code (ISO 639, e.g. "en", "fr", "de")
  Bytes N+1–: Text string (UTF-8 or UTF-16)

Example: English UTF-8 text “Hello” → \x02en\x48\x65\x6C\x6C\x6F

Smart Poster Record — type Sp

A Smart Poster is a container record: its payload is itself an NDEF message containing child records. Mandatory child: a URI record. Optional children: Text records (title in various languages), Action record (open URL / save for later / open application), Size record, Type record (MIME type of the content at the URI).

Smart Posters are used in NFC marketing tags and information kiosks to provide multi-language title + URL combinations.


5.5 MIME Type Records (TNF = 0x02)

MIME records store arbitrary typed content. The TYPE field is a MIME type string, and the PAYLOAD is the content of that type.

Common examples:

MIME type Use
text/plain Plain text
text/vcard vCard contact data
application/json JSON data
application/vnd.android.package-archive Android APK
application/vnd.bluetooth.ep.oob Bluetooth out-of-band pairing
application/vnd.wfa.wsc Wi-Fi Simple Config (WPS) credential

Wi-Fi Credentials Example

The Wi-Fi WPS MIME type is used to provision a device to a Wi-Fi network by tapping an NFC tag. The payload is a TLV-encoded Wi-Fi Simple Config credential. Smartphones (Android 8.1+, iOS 13+) handle this natively — tapping the tag displays a “Join network?” prompt.


5.6 External Type Records (TNF = 0x04)

External types allow applications to define their own record types, using a reverse-domain-name convention similar to Android Intent URIs:

TYPE format: domain.com:typename

The NFC Forum defines the domain nfcdevice.com for device-specific types. Application developers use their own domain. Example: mycompany.com:productinfo.


5.7 How NDEF Maps to Physical Tag Memory

NDEF is a byte stream, but it needs to be stored in the tag’s memory structure. The NFC Forum defines how this mapping works for each tag type.

Type 2 Tags (NTAG21x, Ultralight)

The NDEF TLV (Tag-Value-Length) container format is used:

CC (Capability Container) at page 3:
  E1 10 [size_code] [access]

User memory starts at page 4:
  [03] [NDEF_length] [NDEF_data...] [FE]
   ↑         ↑              ↑          ↑
   T        L (1 or 3 bytes) V         Terminator TLV

TLV types used:

Type Meaning
0x00 NULL (padding / ignored)
0x01 Lock Control TLV (for dynamic lock pages)
0x02 Memory Control TLV (reserved memory)
0x03 NDEF Message TLV (the actual NDEF message)
0xFE Terminator TLV (marks end of TLVs)

The L field uses a 3-byte form if length > 254: 0xFF [high byte] [low byte].

Example — NDEF message on NTAG213 storing https://example.com:

Page 3: E1 10 12 00 (CC: magic, version 1.0, 144 bytes, R/W)

User memory from page 4:

03 0F           → NDEF TLV, length 15
D1 01 0B 55     → NDEF record: MB+ME+SR, TNF=01 (Well-Known),
                   Type length 1, Payload length 11, Type = 'U'
04              → URI code for https://
65 78 61 6D 70 6C 65 2E 63 6F 6D
                → "example.com"
FE              → Terminator TLV

Type 4 Tags (DESFire, JCOP)

Type 4 tags use a dedicated NDEF file within the tag’s application structure:

  1. Select the NFC Forum application (AID 0xD2760000850101)
  2. Select the CC file (file ID 0xE103)
  3. Read the CC file to find the NDEF file ID and size
  4. Select and read the NDEF file

MIFARE Classic — MAD (MIFARE Application Directory)

MIFARE Classic is not an NFC Forum tag type, but NXP defined an application layer — MAD (MIFARE Application Directory) — that allows NDEF to be stored on it.

The MAD occupies sector 0, blocks 1–2 (for MAD version 1) or sectors 0 and 10 (for MAD version 2 on 4K cards). It is a lookup table mapping sector numbers to Application Identifiers (AIDs).

The NFC Forum AID is 0xE103 (little-endian: 03 E1). To find NDEF on a MIFARE Classic card:

  1. Authenticate to sector 0 with its MAD Key (0xA0A1A2A3A4A5)
  2. Read blocks 1–2 (MAD data)
  3. Parse the AID table to find which sectors have AID 0xE103
  4. Authenticate to those sectors with the NDEF key
  5. Read those sectors and concatenate the NDEF TLV data

The NDEF read key for MIFARE Classic is defined by NXP as 0xD3F7D3F7D3F7 (Key A).


5.8 Multi-Record Messages

An NDEF message can contain multiple records. Each record except the last has ME=0; the last has ME=1:

import ndef  # ndeflib

records = [
    ndef.UriRecord("https://example.com"),
    ndef.TextRecord("Visit our website", language="en"),
]
message = b"".join(ndef.message_encoder(records))

A common multi-record use case is the Smart Poster: a URI record + one or more Text records providing titles in different languages.


5.9 Encoding and Decoding NDEF in Python

The ndeflib library provides clean, Pythonic NDEF encoding and decoding:

import ndef

# Encode a URI record
records = [ndef.UriRecord("https://example.com")]
encoded = b"".join(ndef.message_encoder(records))
print(encoded.hex())

# Decode NDEF bytes
for record in ndef.message_decoder(encoded):
    print(type(record).__name__, record)

The nfcpy library integrates NDEF directly into its tag objects:

import nfc

def on_connect(tag):
    if tag.ndef:
        for record in tag.ndef.records:
            print(record)

with nfc.ContactlessFrontend('usb') as clf:
    clf.connect(rdwr={'on-connect': on_connect})

5.10 Practical Encoding Rules and Gotchas

Always write the CC first. Before writing NDEF to an NTAG, write the CC at page 3 (E1 10 [size_code] 00). Without a valid CC, smartphones will not attempt NDEF read. Size codes: NTAG213 = 0x12 (18 × 8 = 144 bytes), NTAG215 = 0x3E (62 × 8 = 496 bytes), NTAG216 = 0x6D (109 × 8 = 872 bytes).

Terminate the TLV stream. Always write 0xFE after the last TLV. Some readers are tolerant of missing terminators, but relying on this is a bug.

SR flag usage. Use SR=1 (1-byte payload length) for payloads under 256 bytes. This saves 3 bytes per record — significant when the total tag memory is 48 bytes.

Chunking. The CF/Chunk flag allows splitting a large payload across multiple records. In practice, chunking is rare in NFC tags because tag memories are small. Most implementations do not support it.

NDEF is write-once by convention. Once an NDEF message is written to a tag, it is conventional to either overwrite the entire TLV region (re-writing TLV type 0x03 with new length and new NDEF bytes followed by 0xFE) or to lock the tag. Partial updates are error-prone.


Summary


← Chapter 4: MIFARE Ultralight Table of Contents Chapter 6: Secure NFC →