davi-nfc-agent

API Reference

The NFC Agent serves both roles from a single server on one port:

Server Port Purpose
Agent Server 9470 Serves both NFC devices (hardware readers, smartphones, browsers) via /ws?mode=device and client applications via /ws
CA Bootstrap 9472 Serves TLS certificates for device setup

The agent server port is configurable via -device-port (default 9470).


Device API

The device endpoint accepts connections from NFC devices that provide tag data.

Connecting

Connect via WebSocket with device mode:

wss://[host]:9470/ws?mode=device

Device Registration

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"]
    }
  }
}

Messages from Device

Tag Scanned

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"
        }
      ]
    }
  }
}

Tag Removed

Send when a tag leaves the reader:

{
  "type": "tagRemoved",
  "payload": {
    "deviceID": "dev_abc123",
    "uid": "04A1B2C3D4E5F6",
    "removedAt": "2024-10-06T12:35:00Z"
  }
}

Device Heartbeat

Keep connection alive:

{
  "type": "deviceHeartbeat",
  "payload": {
    "deviceID": "dev_abc123",
    "timestamp": "2024-10-06T12:35:30Z"
  }
}

Write Response

Respond to a write request from the server:

{
  "type": "deviceWriteResponse",
  "payload": {
    "requestID": "req_xyz789",
    "success": true,
    "error": ""
  }
}

Messages to Device

Write Request

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"
        }
      ]
    }
  }
}

mDNS Discovery

The agent advertises via mDNS/Bonjour:

Devices can discover the agent on the local network without knowing the IP address.


Client API

The agent provides NFC data to client applications on the same port as devices (plain /ws, without the ?mode=device query). This is the agent server port (default 9470, configurable via -device-port).

Connecting

Connect via WebSocket:

const ws = new WebSocket('ws://localhost:9470/ws');

With API secret:

const ws = new WebSocket('ws://localhost:9470/ws?secret=your-secret');

Session Behavior

Messages from Server

Device Status

{
  "type": "deviceStatus",
  "payload": {
    "connected": true,
    "message": "Device connected",
    "cardPresent": false
  }
}

Tag Data

When a card is detected and read:

{
  "type": "tagData",
  "payload": {
    "uid": "04A1B2C3D4E5F6",
    "type": "MIFARE Classic 1K",
    "technology": "ISO14443A",
    "scannedAt": "2024-10-06T12:34:56Z",
    "capabilities": {
      "canRead": true,
      "canWrite": true,
      "canLock": true,
      "maxNdefSize": 716,
      "tagFamily": "MIFARE Classic",
      "supportsNdef": true
    },
    "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
capabilities What the tag supports — see Tag Capabilities
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": [...]
    }
  ]
}

Messages to Server

All client messages support an optional id field for request/response correlation.

Write Request

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 No Record type (see below). Defaults to text.
content string Varies Primary value: text, URI, domain, package name, etc.
language string No ISO language code for text/smartposter (default: en)
mimeType string No Media type for mime records
title string No Display title for smartposter records
payload bytes (base64) No Raw bytes for mime, vcard, external, raw
tnf number No Type Name Format (0–7) for raw records
typeBytes bytes (base64) No NDEF type bytes for raw records
id bytes (base64) No Optional record ID for raw records

Supported type values:

type Fields used Notes
text content, language Default when type omitted
uri / url content Prefix is auto-abbreviated to save tag space
mailto / email, tel, sms, geo content URI shortcut; scheme prepended if absent
smartposter content (URI), title, language “Tap to open title” — URI + label
mime mimeType, payload (or content) Arbitrary MIME media record
vcard content or payload Contact card (text/vcard MIME)
external content (domain:type), payload NFC Forum external type
aar content (package name) Android Application Record (app launch)
empty / erase Empty record — blanks/formats the tag (reversible)
raw tnf, typeBytes, id, payload Fully custom record

WiFi credentials can be written as a mime record with mimeType set to application/vnd.wfa.wsc and a WSC-formatted payload.

Write Response

Success:

{
  "id": "req_1",
  "type": "writeResponse",
  "success": true,
  "payload": {
    "message": "Write operation completed successfully",
    "uid": "04A1B2C3D4E5F6",
    "tagType": "MIFARE Ultralight",
    "bytesWritten": 28,
    "verified": true,
    "attempts": 1
  }
}

The agent confirms every write before reporting success: it checks the encoded message against the tag’s capacity, retries transient failures, and reads the data back to verify it landed.

Success Payload Fields:

Field Type Description
message string Human-readable status
uid string UID of the tag that was written
tagType string Detected tag type
bytesWritten number Size of the encoded NDEF message written
verified bool true when the write was confirmed by reading it back
attempts number Number of write attempts before success
locked bool true when the tag was made read-only (see below)

A write that cannot be confirmed (verification mismatch after retries) returns an error response rather than a success — success: true means the data is on the tag. A response with verified: false only occurs if verification was explicitly disabled by the agent.

Tag Capabilities

Every tagData broadcast includes a capabilities object describing what the present tag supports, so a client can gate its UI (show “lock”/”password” only when supported, render a capacity meter, etc.) without a round-trip.

{
  "canRead": true,
  "canWrite": true,
  "canTransceive": false,
  "canLock": true,
  "isReadOnly": false,
  "memorySize": 540,
  "maxNdefSize": 504,
  "technology": "ISO14443A",
  "tagFamily": "NTAG",
  "supportsNdef": true,
  "supportsPassword": true
}
Field Description
canRead / canWrite Whether read / write operations are supported
canTransceive Raw APDU transceive supported
canLock Tag can be made permanently read-only
isReadOnly Tag is already locked (omitted when false)
memorySize Total memory in bytes (omitted when unknown)
maxNdefSize Maximum NDEF message size in bytes (omitted when unknown)
tagFamily MIFARE Classic, DESFire, NTAG, MIFARE Ultralight, Type 4, …
supportsNdef Tag supports NDEF
supportsPassword Tag supports simple password protection (NTAG21x PWD/PACK)

Query on demand — to fetch capabilities without waiting for the next scan, send a capabilitiesRequest:

{
  "id": "req_cap",
  "type": "capabilitiesRequest"
}

Response (type: "capabilitiesResponse"):

{
  "id": "req_cap",
  "type": "capabilitiesResponse",
  "success": true,
  "payload": {
    "capabilities": { "canWrite": true, "canLock": true, "supportsPassword": true, "maxNdefSize": 504 }
  }
}

The query requires exactly one tag to be present; if none (or several) are present, success is false with an error.

Locking Tags (Make Read-Only)

Locking is irreversible — once a tag is made read-only it can never be written again. Only tags that support locking (e.g. NTAG, MIFARE Ultralight) can be locked; others return an error.

Write and lock in one step — add "lock": true to a write request:

{
  "id": "req_1",
  "type": "writeRequest",
  "payload": {
    "lock": true,
    "records": [{ "type": "uri", "content": "https://example.com" }]
  }
}

The write response then includes "locked": true.

Lock an already-written tag — send a lockRequest:

{
  "id": "req_9",
  "type": "lockRequest"
}

Response (type: "lockResponse"):

{
  "id": "req_9",
  "type": "lockResponse",
  "success": true,
  "payload": {
    "message": "Lock operation completed successfully",
    "uid": "04A1B2C3D4E5F6",
    "tagType": "MIFARE Ultralight",
    "locked": true
  }
}

If the present tag does not support locking, success is false with an error.

Password Protection (planned)

Password protection (NTAG PWD/PACK/AUTH0) is not yet available. The per-tag capability is reported (supportsPassword, true for NTAG21x) and the API contract below is fixed, but the destructive configuration writes are gated off pending validation on real hardware — a wrong AUTH0/ACCESS value can permanently lock a tag. Calls currently return a not-supported error.

Planned request shape (subject to change until enabled):

{
  "id": "req_10",
  "type": "passwordRequest",
  "payload": {
    "action": "set",            // "set" or "remove"
    "password": "01020304",     // hex, 4 bytes
    "protectRead": false,        // false = write-protect only
    "startPage": 4               // first protected page (AUTH0)
  }
}

Error:

{
  "id": "req_1",
  "type": "error",
  "success": false,
  "error": "Write failed: card removed",
  "payload": {
    "code": "WRITE_FAILED"
  }
}

Append Pattern

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' }]
  }
}));

REST API

Base URL: http://localhost:9470/api/v1

Health Check

GET /api/v1/health

curl http://localhost:9470/api/v1/health

Response:

{
  "status": "ok",
  "type": "agent"
}

Both /health and /api/v1/health are served on the agent server port and report "type": "agent".


TLS & Certificates

The agent uses auto-generated TLS certificates for secure WebSocket connections.

CA Bootstrap Server

A bootstrap server runs on port 9472 to help devices trust the agent’s certificate:

  1. Open http://[agent-ip]:9472 in a browser
  2. Download the CA certificate
  3. Install on your device

Installing the CA Certificate

iOS:

Android:

Browsers:


Error Codes

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