Disclosure: As an Amazon Associate, CardWise earns from qualifying purchases at no additional cost to you. This does not affect our recommendations.
How to Write NDEF to NFC Tags — Complete Encoding & Writing Guide
Writing NDEF data to an NFC tag involves two steps: encoding your data as NDEF binary records, then writing those bytes to the tag using platform-specific APIs. This guide covers everything: building NDEF record headers, encoding Text/URI/Smart Poster/WiFi/Bluetooth records, and writing to tags via Android, iOS, Web NFC, and PC/SC readers.
1. Encoding a Single NDEF Record to Binary
def encode_ndef_record(tnf, record_type, payload, id=None):
"""Build one NDEF record as bytes."""
flags = tnf & 0x07 # TNF in lower 3 bits
flags |= 0x80 # MB = 1
flags |= 0x40 # ME = 1
sr = len(payload) < 256
if sr:
flags |= 0x10 # SR = 1, 1-byte payload length
if id is not None:
flags |= 0x04 # IL = 1
header = bytes([flags, len(record_type)])
if sr:
header += bytes([len(payload)])
else:
header += bytes([0x00, 0x00, (len(payload) >> 8) & 0xFF, len(payload) & 0xFF])
if id is not None:
header += bytes([len(id)])
header += record_type.encode() if isinstance(record_type, str) else record_type
if id is not None:
header += id
header += payload
return header
2. Encoding Common NDEF Record Types
Text Record ("T")
def encode_text_record(text, lang='en', utf16=False):
"""Encode NDEF Text Record."""
status = (0x80 if utf16 else 0x00) | (len(lang) & 0x3F)
encoding = 'utf-16-be' if utf16 else 'utf-8'
payload = bytes([status]) + lang.encode('ascii') + text.encode(encoding)
return encode_ndef_record(0x01, b'T', payload) # TNF=Well-Known, type="T"
URI Record ("U")
URI_PREFIXES = {
'http://www.': 0x01, 'https://www.': 0x02,
'http://': 0x03, 'https://': 0x04,
'tel:': 0x05, 'mailto:': 0x06,
}
def encode_uri_record(uri):
"""Encode NDEF URI Record with abbreviation prefix."""
for prefix, code in URI_PREFIXES.items():
if uri.startswith(prefix):
payload = bytes([code]) + uri[len(prefix):].encode('utf-8')
return encode_ndef_record(0x01, b'U', payload)
# No prefix match: use code 0x00 (no abbreviation)
return encode_ndef_record(0x01, b'U', bytes([0x00]) + uri.encode('utf-8'))
# encode_uri_record('https://cupass.com') → prefix=0x04 + 'cupass.com'
WiFi Credential Record ("application/vnd.wfa.wsc")
def encode_wifi_record(ssid, password, auth_type='WPA2-Personal'):
"""Encode NDEF WiFi Peer-to-Peer record."""
# Build WSC (WiFi Simple Configuration) TLV
# This is a MIME record with type "application/vnd.wfa.wsc"
payload = bytearray()
# Credential TLV (0x100E)
cred = bytearray()
cred += bytes([0x10, 0x45, 0x00, len(ssid)]) + ssid.encode() # SSID
cred += bytes([0x10, 0x27, 0x00, len(password)]) + password.encode() # Network Key
cred += bytes([0x10, 0x03, 0x00, 0x02]) # Auth Type = WPA2-Personal
payload = cred
return encode_ndef_record(0x02, b'application/vnd.wfa.wsc', bytes(payload))
3. Writing NDEF — Android (Java/Kotlin)
// Android: Write NDEF using Ndef or NdefFormatable
Ndef ndef = Ndef.get(tag);
if (ndef != null) {
try {
ndef.connect();
if (!ndef.isWritable()) {
// Tag is read-only
return;
}
// Check capacity
NdefMessage msg = ndef.getCachedNdefMessage();
if (msg != null && ndef.getMaxSize() < msg.toByteArray().length) {
// NDEF too large
return;
}
ndef.writeNdefMessage(ndefMessage);
ndef.close();
} catch (FormatException e) {
// Tag not NDEF formatted — use NdefFormatable
NdefFormatable formatable = NdefFormatable.get(tag);
if (formatable != null) {
formatable.connect();
formatable.format(ndefMessage);
formatable.close();
}
}
}
4. Writing NDEF — Web NFC API
// Web NFC API (Chrome Android only, requires HTTPS)
async function writeNDEF() {
try {
const ndef = new NDEFReader();
await ndef.write({
records: [
{ recordType: "url", data: "https://cupass.com" }
]
});
console.log("NDEF written successfully");
} catch (err) {
console.error("Write failed:", err);
}
}
// Writing multiple records:
await ndef.write({
records: [
{ recordType: "text", data: "Hello from CardWise" },
{ recordType: "url", data: "https://cupass.com" }
]
});
5. Writing NDEF — PC/SC with pyscard (ACR122U)
# Requires pyscard installed
from smartcard.System import readers
from smartcard.util import toBytes
def write_ndef_to_mifare(ndef_bytes):
"""Write NDEF message to NTAG via ACR122U."""
r = readers()
conn = r[0].createConnection()
conn.connect()
# Step 1: Authenticate (for MIFARE Classic; NTAG needs no auth)
# Step 2: Write NDEF TLV: [0x03] [length] [NDEF message] [0xFE]
tlv = bytes([0x03, len(ndef_bytes)]) + ndef_bytes + bytes([0xFE])
# Step 3: Write page by page (NTAG: 4 bytes per page, starting page 4)
for i in range(0, len(tlv), 4):
page = 4 + i // 4
data = tlv[i:i+4].ljust(4, b'\x00')
# WRITE command: [0xFF, 0xD6, 0x00, page, 0x04, data]
apdu = [0xFF, 0xD6, 0x00, page, 0x04] + list(data)
conn.transmit(apdu)
Ready to write real tags? The NTAG215 sticker pack (50 tags for ~$7) is the most affordable way to practice NDEF writing — Check Price on Amazon. For a more professional look, the NTAG215 Business Cards 20-pack comes pre-formatted for contact sharing — Check Price on Amazon. Writing on metal surfaces? The Anti-Metal NTAG215 20-pack includes a ferrite layer so the tag works on laptops and cabinets — Check Price on Amazon.
6. Tag Formatting and Write Protection
| Operation | NTAG | MIFARE Classic | DESFire |
|---|---|---|---|
| NDEF Pre-formatted | Yes (factory) | No (must format) | No (ISO file system) |
| Set Read-Only | Lock dynamic lock bytes | Set access bits C=010 | Set file access rights |
| Password Protect | PWD_AUTH + PACK | Key A or B per sector | AES auth per file |
| Irreversible Lock | Write 0x00 to CFG lock bytes | Set trailer access bits to deny write | Change key + forget it |
Related Tools
NDEF Writer Simulator — Build NDEF messages in-browser | NDEF Parser — Verify your encoded messages | NFC Capacity Calculator — Check if it fits | Android NFC NDEF Guide