How to Read NFC Tag NDEF Data (2026)
NFC tags are everywhere — in business cards, smart posters, product packaging, access control cards, and home automation triggers. But how do you actually read the data stored on an NFC tag programmatically? This guide covers the NDEF data format, Android NFC API code, Python examples, tag type detection, and debugging tips.
What is NDEF?
NDEF (NFC Data Exchange Format) is the standard data format defined by the NFC Forum for storing application data on NFC tags. NDEF provides a lightweight, binary container format that any NFC device can read, regardless of the underlying tag hardware (NTAG, MIFARE, FeliCa, Topaz).
An NDEF message is a sequence of one or more NDEF records. Each record carries a typed payload (text string, URL, MIME-type data, or custom application data). The first record is the header record, and the last record has the "ME" (Message End) flag set.
NDEF Record Structure
Every NDEF record starts with a 3-byte header:
Byte 0: Flags
bit 7: MB (Message Begin)
bit 6: ME (Message End)
bit 5: CF (Chunk Flag)
bit 4: SR (Short Record)
bit 3: IL (ID Length present)
bits 2-0: TNF (Type Name Format)
Byte 1: Type Length (number of bytes in Type field)
Byte 2: Payload Length (1 byte if SR=1, 4 bytes if SR=0)
[Optional] ID Length (if IL=1)
[Optional] Type field (Type Length bytes)
[Optional] ID field (ID Length bytes)
Payload (Payload Length bytes)
NDEF Record Types (TNF)
The TNF (Type Name Format) field in the record header tells you what kind of data is in the payload:
| TNF | Value | Type Field | Common Use |
|---|---|---|---|
| NFC Forum Well-Known Type | 0x01 | "T" (Text), "U" (URI) | Plain text, URLs |
| MIME media-type | 0x02 | e.g., "text/plain", "image/png" | vCard, images, custom MIME |
| Absolute URI | 0x03 | Full URI string | Custom app deep links |
| NFC Forum External Type | 0x04 | e.g., "album:example.com" | Custom app-specific data |
| Unknown | 0x05 | (empty) | Opaque binary data |
| Unchanged (chunked) | 0x06 | (empty) | Continuation of a chunked record |
| Empty | 0x00 | (empty) | Empty record (no payload) |
Reading NDEF on Android (Kotlin)
// 1. Register for NDEF discovery in AndroidManifest.xml:
// <intent-filter>
// <action android:name="android.nfc.action.NDEF_DISCOVERED"/>
// <data android:scheme="https" android:host="example.com"/>
// </intent-filter>
// 2. Handle the NDEF intent in your Activity:
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
if (intent.action == NfcAdapter.ACTION_NDEF_DISCOVERED) {
val rawMessages = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)
rawMessages?.forEach { parcelable ->
val ndefMessage = parcelable as NdefMessage
ndefMessage.records.forEach { record ->
when (record.tnf) {
NdefRecord.TNF_WELL_KNOWN -> {
if (record.types contentEquals NdefRecord.RTD_TEXT) {
val text = parseTextRecord(record)
Log.d("NFC", "Text: $text")
} else if (record.types contentEquals NdefRecord.RTD_URI) {
val uri = record.toUri()
Log.d("NFC", "URI: $uri")
}
}
NdefRecord.TNF_MIME_MEDIA -> {
val mimeType = String(record.type)
Log.d("NFC", "MIME: $mimeType, size=${record.payload.size}")
}
}
}
}
}
}
fun parseTextRecord(record: NdefRecord): String {
val payload = record.payload
val status = payload[0].toInt()
val languageCodeLength = status and 0x3F
val encoding = if ((status and 0x80) == 0) Charsets.UTF_8 else Charsets.UTF_16
return String(payload, 1 + languageCodeLength,
payload.size - 1 - languageCodeLength, encoding)
}
Reading NDEF with Python (pyscard + ACR122U)
from smartcard.System import readers
from smartcard.util import toHexString, toBytes
# Connect to ACR122U reader
reader = readers()[0]
connection = reader.createConnection()
connection.connect()
# Select NDEF application (Type 4 Tag)
SELECT_NDEF = [0x00, 0xA4, 0x04, 0x00, 0x07,
0xD2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01, 0x00]
_, sw1, sw2 = connection.transmit(SELECT_NDEF)
assert (sw1, sw2) == (0x90, 0x00), f"Select failed: {sw1:02X}{sw2:02X}"
# Select NDEF Capability Container (file 0xE103)
SELECT_CC = [0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x03]
_, sw1, sw2 = connection.transmit(SELECT_CC)
assert (sw1, sw2) == (0x90, 0x00)
# Read CC to find NDEF file size
READ_BINARY = [0x00, 0xB0, 0x00, 0x00, 0x0F]
data, sw1, sw2 = connection.transmit(READ_BINARY)
ndef_file_size = (data[7] << 8) | data[8]
# Select NDEF file (0xE104)
SELECT_NDEF_FILE = [0x00, 0xA4, 0x00, 0x0C, 0x02, 0xE1, 0x04]
_, sw1, sw2 = connection.transmit(SELECT_NDEF_FILE)
# Read NDEF message
READ_NDEF = [0x00, 0xB0, 0x00, 0x00, 0x02] # First 2 bytes: NDEF length
ndef_len_data, _, _ = connection.transmit(READ_NDEF)
ndef_len = (ndef_len_data[0] << 8) | ndef_len_data[1]
READ_FULL = [0x00, 0xB0, 0x00, 0x00, min(ndef_len, 0xFF)]
ndef_data, _, _ = connection.transmit(READ_FULL)
# NDEF data starts at byte 2 (after length field)
print(f"NDEF data ({ndef_len} bytes): {toHexString(ndef_data[2:])}")
Tag Type Detection
Different NFC tag types (Type 1-5) store NDEF data differently. You need to know the tag type to read NDEF correctly:
| Tag Type | Standard | Common Chips | NDEF Storage |
|---|---|---|---|
| Type 1 | ISO 14443-3A | Topaz, Innovision | TLV in memory blocks |
| Type 2 | ISO 14443-3A | NTAG213/215/216, MIFARE Ultralight | TLV (0x03) in user memory |
| Type 3 | JIS X 6319-4 | FeliCa | Service-based read |
| Type 4 | ISO 14443-4 | MIFARE DESFire, SmartMX | NDEF file (0xE104) in application |
| Type 5 | ISO 15693 | ICODE SLI, Tag-it | TLV in block memory |
For Type 2 tags (most common for NTAG), NDEF data is stored as a TLV block in the user memory area, starting at the 0x03 tag byte. For Type 4 tags (DESFire), NDEF is stored in a dedicated file (FID 0xE104) within the NDEF application (AID D2760000850101).
Debugging Tips
- Always check the NDEF TLV first — for Type 2 tags, scan for the
0x03byte in user memory; the next 1-3 bytes give the NDEF message length - Check for terminator TLV —
0xFEmarks the end of TLV blocks - Verify CC file — the Capability Container (0xE103) tells you if NDEF is supported and the max message size
- Handle chunked records — if CF=1 in the record header, the payload is split across multiple records
- Use the right reader — ACR122U/ACR1252U support all tag types; some cheaper readers only support Type 2
- Watch for MIFARE Classic — not NFC Forum compliant; needs authentication keys and does not use standard NDEF TLV
Frequently Asked Questions
How do I read an NFC tag with my phone?
On Android: enable NFC in Settings, then tap the phone against the tag. If the tag has a URI record, the phone auto-opens the URL. For NDEF inspection, use an NFC tool app (NFC Tools, Trigger). On iPhone (XS and later): Background Tag Reading works automatically — just tap the tag while the screen is on. Third-party apps can read NDEF via CoreNFC framework.
What is the difference between NDEF and raw tag data?
NDEF is a high-level application data format. Raw tag data includes hardware-level data (UID, ATQA, SAK, ATS) and memory blocks. NDEF sits inside the tag's user memory in a TLV (Type-Length-Value) structure. To read NDEF, you locate the NDEF Message TLV (tag byte 0x03) in the tag memory, read the length, then parse the NDEF message bytes.
Can all NFC tags store NDEF data?
Most NFC Forum-compliant tags can, but MIFARE Classic tags are not fully NFC Forum compliant — they store NDEF in a proprietary format that requires authentication keys. NTAG213/215/216, MIFARE Ultralight, and MIFARE DESFire all support standard NDEF. Use the NFC Tag Type Detector to identify your tag type from ATR/ATQA/SAK values.
How much NDEF data can I store on a tag?
It depends on the tag chip's user memory. NTAG213: 144 bytes (usable ~130 bytes NDEF). NTAG215: 504 bytes (Amiibo compatible). NTAG216: 888 bytes (vCard contacts). MIFARE DESFire EV2: up to 4KB per file. Use the NFC Tag Capacity Calculator to calculate exact NDEF capacity including overhead.