The NFC Agent uses a two-server architecture:
| Server | Port | Purpose |
|---|---|---|
| Device Server | 9470 | Connects NFC devices (hardware readers, smartphones, browsers) |
| Client Server | 9471 | Serves client applications consuming NFC data |
| CA Bootstrap | 9472 | Serves TLS certificates for device setup |
The Device Server accepts connections from NFC devices that provide tag data.
Connect via WebSocket with device mode:
wss://[host]:9470/ws?mode=device
After connecting, register the device:
{
"type": "registerDevice",
"payload": {
"deviceName": "My Device",
"platform": "ios",
"appVersion": "1.0.0",
"capabilities": {
"canRead": true,
"canWrite": false,
"nfcType": "corenfc"
},
"metadata": {
"userAgent": "..."
}
}
}
Registration Response:
{
"type": "registerDeviceResponse",
"success": true,
"payload": {
"deviceID": "dev_abc123",
"serverInfo": {
"version": "1.0.0",
"supportedNFC": ["ndef", "mifare"]
}
}
}
Send when a tag is detected:
{
"type": "tagScanned",
"payload": {
"deviceID": "dev_abc123",
"uid": "04A1B2C3D4E5F6",
"technology": "ISO14443A",
"type": "MIFARE Classic 1K",
"scannedAt": "2024-10-06T12:34:56Z",
"ndefMessage": {
"records": [
{
"recordType": "text",
"content": "Hello, NFC!",
"language": "en"
}
]
}
}
}
Send when a tag leaves the reader:
{
"type": "tagRemoved",
"payload": {
"deviceID": "dev_abc123",
"uid": "04A1B2C3D4E5F6",
"removedAt": "2024-10-06T12:35:00Z"
}
}
Keep connection alive:
{
"type": "deviceHeartbeat",
"payload": {
"deviceID": "dev_abc123",
"timestamp": "2024-10-06T12:35:30Z"
}
}
Respond to a write request from the server:
{
"type": "deviceWriteResponse",
"payload": {
"requestID": "req_xyz789",
"success": true,
"error": ""
}
}
Server requests the device to write data to a tag:
{
"type": "deviceWriteRequest",
"payload": {
"requestID": "req_xyz789",
"deviceID": "dev_abc123",
"ndefMessage": {
"records": [
{
"type": "text",
"content": "Hello!",
"language": "en"
}
]
}
}
}
The Device Server advertises via mDNS/Bonjour:
_nfc-device._tcplocal.Devices can discover the agent on the local network without knowing the IP address.
The Client Server provides NFC data to client applications.
Connect via WebSocket:
const ws = new WebSocket('ws://localhost:9471/ws');
With API secret:
const ws = new WebSocket('ws://localhost:9471/ws?secret=your-secret');
409 Conflict until first disconnects{
"type": "deviceStatus",
"payload": {
"connected": true,
"message": "Device connected",
"cardPresent": false
}
}
When a card is detected and read:
{
"type": "tagData",
"payload": {
"uid": "04A1B2C3D4E5F6",
"type": "MIFARE Classic 1K",
"technology": "ISO14443A",
"scannedAt": "2024-10-06T12:34:56Z",
"message": {
"type": "ndef",
"records": [
{
"tnf": 1,
"type": "T",
"text": "Hello, NFC!",
"payload": [72, 101, 108, 108, 111]
}
]
},
"text": "Hello, NFC!",
"err": null
}
}
Payload Fields:
| Field | Description |
|---|---|
uid |
Card unique identifier (hex string) |
type |
Card type: MIFARE Classic 1K, MIFARE Classic 4K, MIFARE DESFire, MIFARE Ultralight, ISO14443-4 Type 4A (experimental) |
technology |
NFC technology standard (ISO14443A, ISO14443B, etc.) |
scannedAt |
ISO 8601 timestamp |
message |
Structured NDEF message data |
text |
Quick access to first text record |
err |
Error message or null on success |
NDEF Message Structure:
{
"type": "ndef",
"records": [
{
"tnf": 1,
"type": "T",
"text": "Decoded text",
"language": "en",
"payload": [...]
}
]
}
tnf: Type Name Format (0x01 = Well Known)type: Record type (T = Text, U = URI)text: Decoded text (for Text records)uri: Decoded URI (for URI records)All client messages support an optional id field for request/response correlation.
Write NDEF data to a card (complete overwrite):
{
"id": "req_1",
"type": "writeRequest",
"payload": {
"records": [
{
"type": "text",
"content": "Hello, NFC!",
"language": "en"
}
]
}
}
Multiple records:
{
"id": "req_2",
"type": "writeRequest",
"payload": {
"records": [
{
"type": "text",
"content": "Hello, NFC!",
"language": "en"
},
{
"type": "uri",
"content": "https://example.com"
}
]
}
}
Record Fields:
| Field | Type | Required | Description |
|---|---|---|---|
type |
string | Yes | text or uri |
content |
string | Yes | Text or URI content |
language |
string | No | ISO language code (default: en) |
Success:
{
"id": "req_1",
"type": "writeResponse",
"success": true,
"payload": {
"message": "Write operation completed successfully"
}
}
Error:
{
"id": "req_1",
"type": "error",
"success": false,
"error": "Write failed: card removed",
"payload": {
"code": "WRITE_FAILED"
}
}
To append records, use read-modify-write:
// 1. Read current tag data
const currentData = await client.getLastTag();
// 2. Extract existing records
const existingRecords = currentData.message.records.map(r => ({
type: r.type === 'T' ? 'text' : 'uri',
content: r.text || r.uri,
language: r.language || 'en'
}));
// 3. Write back with new record appended
socket.send(JSON.stringify({
type: 'writeRequest',
payload: {
records: [...existingRecords, { type: 'text', content: 'New record' }]
}
}));
Base URL: http://localhost:9471/api/v1
GET /api/v1/health
curl http://localhost:9471/api/v1/health
Response:
{
"status": "ok"
}
The agent uses auto-generated TLS certificates for secure WebSocket connections.
A bootstrap server runs on port 9472 to help devices trust the agent’s certificate:
http://[agent-ip]:9472 in a browseriOS:
Android:
Browsers:
| Code | Description |
|---|---|
WRITE_FAILED |
Write operation failed |
NO_CARD |
No card present on reader |
READ_FAILED |
Failed to read card data |
SESSION_LOCKED |
Another client holds the session |
INVALID_REQUEST |
Malformed request |