You have developed a PWA for warehouse inventory. An employee brings a phone close to an NTAG215 tag — the asset card loads in a second. But half of the devices throw an error: 'Web NFC is unavailable.' Or your NTAG213 tags cannot fit a JSON with multiple fields. The Web NFC API solves these tasks but requires proper configuration and exception handling. We will help integrate NFC functionality into your site: from simple reading to a full-fledged system for writing and syncing with an API.
How the Web NFC API Works
The Web NFC API provides access to the device's NFC chip from the browser. It works only with NDEF-compatible tags (NFC Data Exchange Format). Support is limited to Chrome for Android 89+ (desktop, iOS, and other browsers are absent). HTTPS and a user gesture (e.g., a button click) are required for the first launch. If there is no support, we implement a fallback to QR codes or manual input. You can check support with one line: const isSupported = 'NDEFReader' in window.
What Data Can Be Stored on NFC Tags?
An NDEF message can contain one or more records. Main types: text — UTF-8 text with language specification; url — a URL; mime — arbitrary data (e.g., JSON); smart-poster — a nested structure with URL, text, and action. In practice, JSON is most often used for transmitting structured data or a URL for quick navigation. The more compact the JSON, the faster the reading: to reduce size, apply tree-shake — remove unnecessary fields.
Comparison of NFC Chips: Which to Choose?
| Chip | Memory | Suitable for | Price (approx) |
|---|---|---|---|
| NTAG213 | 144 bytes | A single URL or short text | Low |
| NTAG215 | 504 bytes | URL + text or a small JSON | Medium |
| NTAG216 | 888 bytes | Large JSON with multiple fields | Above medium |
NTAG215 is 3.5 times more capacious than NTAG213, while only 20% more expensive. For equipment inventory, we recommend NTAG215: it is optimal in terms of capacity/price ratio, allowing you to store JSON with ID, name, location, and check time. One NTAG215 tag costs about 15 rubles, and the savings from automation reach 500,000 rubles per year.
Typical Web NFC API Errors and Their Handling
| Error | Reason | Solution |
|---|---|---|
| NotAllowedError | User denied permission | Show a message asking to allow NFC |
| AbortError | User pressed 'Cancel' | Simply finish the scenario |
| TypeError | Invalid NDEF record format | Check the structure of NDEFMessageInit |
| NotSupportedError | Browser does not support Web NFC | Fallback to QR code or manual input |
Reading NFC Tags: Code Example
To read, create an instance of NDEFReader, call scan(), and subscribe to the reading event. On an access error (NotAllowedError), show the user a clear message.
class NFCReader {
private reader: NDEFReader | null = null
private isReading = false
async startReading(
onRecord: (record: NFCRecord) => void,
onError?: (error: Error) => void
): Promise<void> {
if (!('NDEFReader' in window)) {
throw new Error('Web NFC is not supported in this browser')
}
this.reader = new NDEFReader()
this.isReading = true
try {
await this.reader.scan()
} catch (err) {
if ((err as Error).name === 'NotAllowedError') {
throw new Error('NFC permission not granted')
}
throw err
}
this.reader.addEventListener('reading', (event: NDEFReadingEvent) => {
console.log(`NFC UID: ${event.serialNumber}`)
for (const record of event.message.records) {
onRecord(record)
}
})
this.reader.addEventListener('readingerror', (event) => {
onError?.(new Error('NFC read error'))
})
}
stop() {
this.isReading = false
this.reader = null
}
}
To decode records (text, URL, JSON), use a utility:
interface ParsedNFCRecord {
type: string
data: string | Record<string, unknown>
}
function parseNFCRecord(record: NDEFRecord): ParsedNFCRecord {
switch (record.recordType) {
case 'text': {
const decoder = new TextDecoder(record.encoding ?? 'utf-8')
return {
type: 'text',
data: decoder.decode(record.data),
}
}
case 'url': {
const decoder = new TextDecoder()
return {
type: 'url',
data: decoder.decode(record.data),
}
}
case 'mime': {
if (record.mediaType === 'application/json') {
const decoder = new TextDecoder()
try {
return {
type: 'json',
data: JSON.parse(decoder.decode(record.data)),
}
} catch {
return { type: 'text', data: decoder.decode(record.data) }
}
}
return {
type: record.mediaType ?? 'binary',
data: '[binary data]',
}
}
case 'smart-poster': {
return { type: 'smart-poster', data: 'Smart Poster record' }
}
default:
return { type: record.recordType, data: '[unknown]' }
}
}
Writing to an NFC Tag
For writing, use NDEFReader.write(). You can combine URL, text, and JSON:
class NFCWriter {
async write(data: NFCWriteData): Promise<void> {
const writer = new NDEFReader()
const message: NDEFMessageInit = {
records: this.buildRecords(data),
}
await writer.write(message)
console.log('Write to tag completed')
}
private buildRecords(data: NFCWriteData): NDEFRecordInit[] {
const records: NDEFRecordInit[] = []
if (data.url) {
records.push({ recordType: 'url', data: data.url })
}
if (data.text) {
records.push({
recordType: 'text',
data: data.text,
lang: 'en',
})
}
if (data.json) {
records.push({
recordType: 'mime',
mediaType: 'application/json',
data: new TextEncoder().encode(JSON.stringify(data.json)),
})
}
return records
}
async writeWithPassword(message: NDEFMessageInit, password: string): Promise<void> {
const writer = new NDEFReader()
await writer.write(message, {
overwrite: true,
})
}
}
interface NFCWriteData {
url?: string
text?: string
json?: unknown
}
Real-World Case: Warehouse Inventory
We implemented Web NFC for a logistics client. Each shelf and equipment unit was marked with NTAG215 tags. An employee opens the PWA on Android, presses 'Scan', brings the phone close — and the app automatically loads the asset card from ERP. Previously, the process took 3 minutes of manual input, now it takes 15 seconds. Inventory speed increased 12 times, input errors eliminated. Cost savings for the client exceeded 500,000 rubles per year by reducing downtime and manual labor.
function AssetInventory() {
const [scanned, setScanned] = useState<AssetData | null>(null)
const [isScanning, setIsScanning] = useState(false)
const readerRef = useRef<NFCReader | null>(null)
async function startScan() {
if (!('NDEFReader' in window)) {
alert('Web NFC is not available. Use Chrome on Android.')
return
}
setIsScanning(true)
readerRef.current = new NFCReader()
await readerRef.current.startReading(
(record) => {
const parsed = parseNFCRecord(record)
if (parsed.type === 'json' && isAssetData(parsed.data)) {
setScanned(parsed.data as AssetData)
setIsScanning(false)
readerRef.current?.stop()
}
},
(error) => {
console.error('NFC error:', error)
setIsScanning(false)
}
)
}
function stopScan() {
readerRef.current?.stop()
setIsScanning(false)
}
return (
<div>
{!('NDEFReader' in window) && (
<div className="bg-yellow-50 border border-yellow-200 rounded p-3 text-sm">
Web NFC requires Chrome on Android
</div>
)}
<button
onClick={isScanning ? stopScan : startScan}
className={`w-full py-4 rounded-xl text-lg font-medium ${
isScanning ? 'bg-red-500 text-white' : 'bg-blue-600 text-white'
}`}
>
{isScanning ? '⏹ Stop scanning' : '📡 Scan NFC tag'}
</button>
{isScanning && (
<div className="text-center py-8 text-gray-500">
Bring the tag to the back of the phone...
</div>
)}
{scanned && (
<AssetCard asset={scanned} onClose={() => setScanned(null)} />
)}
</div>
)
}
Tag Writing During Inventory
For initial asset tagging, we use the same writing library:
async function tagAsset(assetId: string, assetData: AssetData) {
const writer = new NFCWriter()
await writer.write({
json: {
id: assetId,
name: assetData.name,
location: assetData.location,
category: assetData.category,
lastCheck: new Date().toISOString(),
},
})
}
What Our Integration Includes
- Requirements analysis and selection of suitable chips (NTAG213/215/216).
- Development of read/write module with handling of all errors (NoPermission, AbortError).
- UI/UX with large buttons and clear instructions for mobile devices.
- Testing on real tags under various conditions (lighting, angle of approach).
- Documentation for integration with your backend (data synchronization).
- Post-launch support (3-month warranty).
Timeline: from 2 to 5 days depending on scope (reading — 1-2 days, reading+writing+synchronization — 3-5 days).
Why Choose Us?
We have been in web development for over 5 years, completed 50+ projects involving hardware API integration (NFC, Bluetooth, Camera). We guarantee the solution works on 95% of Android devices with Chrome. We provide a demo stand for testing before work begins. Order Web NFC integration – contact us for a consultation. We will evaluate your project and propose the optimal technical solution.
For more details on the Web NFC API, read MDN Web NFC API.







