NFC¶
The device can poll an NFC reader mounted under (or near) the screen and surface tag taps — UID, NDEF content, or raw APDU exchanges for closed-loop token schemes — to both the displayed page and the host, over the same two-transport API used for everything else.
What this is not. NFC support here is for loyalty cards, closed-loop tokens (a private/integrator-controlled scheme, e.g. local currency or points), and reading tag UID/NDEF content. It is not regulated contactless bank-card (EMV) payment — that requires certified payment-terminal hardware and is entirely out of scope.
Supported hardware¶
The only reader backend currently implemented is the ACR122U (USB), which requires a free
USB host port — Pi 4 or Pi 5 only (the Zero 2 W's single USB port is used by the gadget
link to the host). PN532 readers over I2C/SPI (which would work on the Zero 2 W too, since they
use GPIO rather than a USB host port) are planned but not yet available; nfc.backend already
reserves the pn532-i2c/pn532-spi values for when they land, so a config written today will
keep working.
Supported tag technologies: UID (any tag), NDEF read/write on Type 2 (NTAG/Ultralight) and NDEF read on Type 4 (DESFire in NDEF mode) tags, and raw ISO-DEP (ISO 14443-4) APDU exchange for your own application-level protocol. Mifare Classic crypto1 authentication, FeliCa and ISO 15693 are not supported.
Enable NFC¶
NFC is disabled by default. Enable it via configuration:
curl -X PATCH http://10.99.0.1/api/v1/config \
-H 'Content-Type: application/json' \
-d '{"nfc": {"enabled": true, "origins": ["https://pos.example.com"]}}'
This applies live — the reader manager starts immediately, no reboot needed. While
nfc.enabled is false (the default), every /api/v1/nfc/* call answers 503 with error
code nfc_disabled, and no NFC events are emitted — this is a hard, unambiguous gate, not a
degraded "empty reader list" response.
nfc.origins is a page-origin allow-list: only pages loaded from an origin in this list can call
the NFC API or receive NFC events from JavaScript (see "Reacting to taps from the displayed
page" below). It's empty (nobody allowed) by default even once NFC itself is enabled — set it
explicitly. This does not restrict the host's own API access, which is always allowed (the
trusted USB link).
Other nfc.* fields (poll interval, backend selection, bus parameters for a future PN532 reader,
auto-feedback on detect) are documented in the
configuration reference; the poll interval and feedback
toggle apply live, while switching backend/bus reinitializes the reader.
Web UI: NFC page — enable toggle, origin allow-list, reader status.
List readers¶
Returns each detected reader's id, backend, whether it's currently present, and its
capabilities (supported techs, LED/buzzer presence). An empty list here (with NFC enabled) means
no reader is plugged in — plug the ACR122U into a USB-A port on the Pi and it appears without a
reboot (hotplug is supported).
Web UI: NFC page — reader list.
React to a tag tap¶
Tag arrivals and departures come as events, not as a poll — subscribe on the event stream (see Events):
{ "type": "event", "topic": "nfc_tag", "payload": {
"reader_id": "acr122u-0", "tag_handle": "h-17",
"uid": "04a1b2c3", "atqa": "0044", "sak": "00",
"techs": ["ndef"], "ndef": { "records": [ /* … */ ] }
} }
tag_handle is a short-lived, opaque handle — it's only valid while that specific tag stays in
the field. Use it as the target of the calls below; a stale handle (tag already removed, reader
unplugged) gets 409 invalid_state.
Both nfc_reader_connected/nfc_reader_disconnected (USB hotplug) and nfc_tag/nfc_tag_removed
arrive on both transports (network WebSocket and serial NDJSON), so a host wired up over
serial only sees exactly the same events as one on the network link.
Read/write NDEF content¶
An NDEF-formatted tag's parsed content already arrives in the nfc_tag event's ndef field —
you don't need a separate read call for the common case. To write:
curl -X POST http://10.99.0.1/api/v1/nfc/ndef/write \
-H 'Content-Type: application/json' \
-d '{"reader_id": "acr122u-0", "tag_handle": "h-17", "message": { "records": [ {"recordType": "text", "data": "..."} ] } }'
A tag that isn't NDEF-writable (wrong type, over capacity) gets 409 nfc_tag_unsupported.
Raw APDU exchange (closed-loop token schemes)¶
curl -X POST http://10.99.0.1/api/v1/nfc/transceive \
-H 'Content-Type: application/json' \
-d '{"reader_id": "acr122u-0", "tag_handle": "h-17", "apdu": "<base64 APDU bytes>"}'
Returns {"response": "<base64>", "sw": "9000"} — the raw response payload (status word
stripped) plus the ISO 7816 status word as 4 hex digits. This is the building block for your own
application-specific token/loyalty protocol against an ISO-DEP tag; the device does not interpret
the exchange in any way. Requires the tag to support ISO-DEP (409 nfc_tag_unsupported
otherwise).
Drive reader LED/buzzer feedback¶
curl -X POST http://10.99.0.1/api/v1/nfc/reader/feedback \
-H 'Content-Type: application/json' \
-d '{"reader_id": "acr122u-0", "led": "green", "buzzer_ms": 200}'
led is one of off | red | green | orange | blink-red | blink-green | blink-orange; buzzer_ms
is a pulse duration (rounded to 100 ms steps on the ACR122U, capped at 2500 ms). Omit whichever
field you don't want to change. If nfc.feedback.on_detect is enabled (the default), the device
already auto-pulses on every successful tap, so you may not need to call this explicitly for the
common "beep on tap" behavior.
Web UI: NFC page — manual LED/buzzer test controls.
Reacting to taps from the displayed page¶
A page shown by the device can subscribe to NFC directly, without going through the host, as long
as its origin is in nfc.origins — this uses the same page-channel mechanism as any other
host↔page or page↔device messaging (see REST and WebSocket API). A
future release adds a navigator.nfc (Web NFC-shaped) polyfill for this; until then, page-side
NFC access goes through the same low-level page-channel primitives as any other custom
integration.
See also¶
- Configuration — general
PATCH /configsemantics. - Configuration reference — the full
nfc.*field list. - Events — subscribing to
nfc_tag,nfc_tag_removed,nfc_reader_connected,nfc_reader_disconnected.