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.
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.
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.
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.
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 |
Well-Known types use short, registered type names defined by the NFC Forum RTD (Record Type Definition) specifications. The type is encoded as ASCII.
UThe 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
TA 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
SpA 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.
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 |
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.
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.
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.
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 use a dedicated NDEF file within the tag’s application structure:
0xD2760000850101)0xE103)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:
0xA0A1A2A3A4A5)0xE103The NDEF read key for MIFARE Classic is defined by NXP as 0xD3F7D3F7D3F7 (Key A).
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.
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})
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.
ndeflib for pure encoding/decoding; nfcpy for integrated tag access.| ← Chapter 4: MIFARE Ultralight | Table of Contents | Chapter 6: Secure NFC → |