Skip to main content

NFC Agent Python SDK

Python SDK for interacting with NFC readers via the nfc-agent local server.

Installation

pip install nfc-agent

Requirements

Quick Start

REST API (Simple Operations)

import asyncio
from nfc_agent import NFCClient

async def main():
    async with NFCClient() as client:
        # List readers
        readers = await client.get_readers()
        print(f"Found {len(readers)} reader(s)")

        # Read card
        try:
            card = await client.read_card(0)
            print(f"Card UID: {card.uid}")
            print(f"Card Type: {card.type}")
            if card.data:
                print(f"Data: {card.data}")
        except Exception as e:
            print(f"No card present: {e}")

asyncio.run(main())

Synchronous Usage

from nfc_agent import NFCClient

with NFCClient() as client:
    readers = client.get_readers_sync()
    card = client.read_card_sync(0)
    print(f"Card UID: {card.uid}")

WebSocket (Real-time Events)

import asyncio
from nfc_agent import NFCWebSocket

async def main():
    async with NFCWebSocket() as ws:
        # Subscribe to reader events
        await ws.subscribe(0)

        @ws.on_card_detected
        def handle_card(event):
            print(f"Card detected: {event.card.uid}")

        @ws.on_card_removed
        def handle_removed(event):
            print(f"Card removed from reader {event.reader}")

        # Keep running
        await asyncio.sleep(60)

asyncio.run(main())

Card Polling

import asyncio
from nfc_agent import NFCClient

async def main():
    async with NFCClient() as client:
        poller = client.poll_card(0, interval=0.5)

        @poller.on_card
        def handle_card(card):
            print(f"Card: {card.uid}")

        @poller.on_removed
        def handle_removed():
            print("Card removed")

        await poller.start()
        await asyncio.sleep(30)
        poller.stop()

asyncio.run(main())

API Reference

NFCClient

REST API client for simple request/response operations.

from nfc_agent import NFCClient

# With context manager (recommended)
async with NFCClient(base_url="http://127.0.0.1:32145", timeout=5.0) as client:
    ...

# Or sync
with NFCClient() as client:
    ...

Methods

Method Description
get_readers() List available NFC readers
read_card(reader_index, *, refresh=False) Read card metadata + NDEF. Fast — use for detection/polling. Pass refresh=True to bypass cache.
write_card(reader_index, *, data, data_type, url) Write data to a card
get_version() Get agent version information
is_connected() Check if agent is running
poll_card(reader_index, *, interval) Create a card poller
MIFARE Classic
Method Description
read_mifare_block(reader_index, block, *, key, key_type) Read 16-byte block
write_mifare_block(reader_index, block, *, data, key, key_type) Write 16-byte block
write_mifare_blocks(reader_index, blocks, *, key, key_type) Batch write blocks
derive_uid_key_aes(reader_index, aes_key) Derive key from UID
aes_encrypt_and_write_block(...) AES encrypt and write
write_mifare_sector_trailer(...) Write sector trailer
MIFARE Ultralight / NTAG
Method Description
read_ultralight_page(reader_index, page, *, password) Read 4-byte page
write_ultralight_page(reader_index, page, *, data, password) Write 4-byte page
write_ultralight_pages(reader_index, pages, *, password) Batch write pages

NFCWebSocket

WebSocket client for real-time communication and events.

from nfc_agent import NFCWebSocket

async with NFCWebSocket(
    url="ws://127.0.0.1:32145/v1/ws",
    timeout=5.0,
    auto_reconnect=True,
    reconnect_interval=3.0,
    secure=False  # Use wss:// for HTTPS pages
) as ws:
    ...

Methods

All methods from NFCClient, plus:

Method Description
subscribe(reader_index, *, include_raw=False) Subscribe to card events. Pass include_raw=True to also receive on_card_data events with full memory dump.
unsubscribe(reader_index) Unsubscribe from events
read_card_full(reader_index) Unified read — metadata + NDEF + full raw memory dump. Slow — call once on demand, not in a poll loop.
dump_card(reader_index) Raw memory dump only (pages for NTAG, blocks for MIFARE Classic; no NDEF metadata)
erase_card(reader_index) Erase NDEF data
lock_card(reader_index) Permanently lock card
set_password(reader_index, password) Set NTAG password
remove_password(reader_index, password) Remove NTAG password
write_records(reader_index, records) Write multiple NDEF records
health() Health check
DESFire Transparent Session
Method Description
open_desfire_session(reader_index) Open a transparent DESFire APDU session (holds the card connection open)
desfire_transmit(reader_index, *, apdu) Transmit one APDU (hex), returns {response, sw1, sw2}
desfire_transmit_batch(reader_index, *, apdus) Transmit multiple APDUs in order, returns {responses: [...]}
close_desfire_session(reader_index) Close the DESFire session

Events

@ws.on_card_detected
def handle_card(event):
    print(event.card.uid)

@ws.on_card_data
def handle_data(event):
    # event.pages (NTAG) or event.blocks (MIFARE Classic)
    print(event.pages)

@ws.on_card_removed
def handle_removed(event):
    print(f"Removed from reader {event.reader}")

@ws.on_readers_changed
def handle_readers(event):
    # Fired when readers are plugged in/removed, or pcscd becomes available
    # after startup. event.readers is the full current reader list.
    print(f"Readers now connected: {event.readers}")

@ws.on_connected
def handle_connected():
    print("Connected")

@ws.on_disconnected
def handle_disconnected():
    print("Disconnected")

@ws.on_error
def handle_error(error):
    print(f"Error: {error}")

CardPoller

Polls a reader for card presence.

poller = client.poll_card(reader_index, interval=1.0)

@poller.on_card
def handle_card(card):
    print(card.uid)

@poller.on_removed
def handle_removed():
    print("Removed")

@poller.on_error
def handle_error(e):
    print(f"Error: {e}")

await poller.start()
# ...
poller.stop()

Types

from nfc_agent import (
    Reader,
    Card,
    CardDataType,
    VersionInfo,
    HealthInfo,
    CardDetectedEvent,
    CardRemovedEvent,
    MifareKeyType,
    MifareBlockData,
    UltralightPageData,
    NDEFRecord,
)

Exceptions

from nfc_agent import (
    NFCAgentError,    # Base exception
    ConnectionError,  # Connection failed
    CardError,        # Card operation failed
    DesfireError,     # DESFire session failed (subclass of CardError, optional .status_code)
    APIError,         # API returned error
    TimeoutError,     # Request timed out
    ReaderError,      # Reader issue
)

Examples

Write URL to Card

async with NFCClient() as client:
    await client.write_card(0, data="https://example.com", data_type="url")

Write Text to Card

async with NFCClient() as client:
    await client.write_card(0, data="Hello World", data_type="text")

Write JSON to Card

import json

async with NFCClient() as client:
    data = json.dumps({"user_id": 123, "name": "Alice"})
    await client.write_card(0, data=data, data_type="json")

Read MIFARE Classic Block

from nfc_agent import MifareKeyType

async with NFCClient() as client:
    block = await client.read_mifare_block(
        0,
        block=4,
        key="FFFFFFFFFFFF",
        key_type=MifareKeyType.A
    )
    print(f"Block {block.block}: {block.data}")

DESFire Transparent Session

For DESFire EV2/EV3 cards, the SDK exposes a transparent APDU session (WebSocket only — there is no REST equivalent). The agent holds the card connection open across messages so an external party — typically the SimplyPrint backend, which keeps the DESFire keys in its HSM — can drive an interactive AuthenticateEV2First handshake (a 3-pass challenge/response that can't be pre-computed) and the secure-messaging commands that follow.

The agent performs no DESFire cryptography and holds no keys. Your code sends raw APDU bytes and gets the card's raw response (including the status word) back; all session secrets stay on your side. Send native DESFire APDUs (CLA=0x90) — the real commands come from the backend. Don't subscribe a reader while a session is open on it (unsubscribe first), and note sessions are dropped automatically on disconnect. Recommended readers are the ACR1252U / ACR1552U class.

from nfc_agent import NFCWebSocket, DesfireError

async with NFCWebSocket() as ws:
    # Open the session — agent keeps the card connection open across calls
    session = await ws.open_desfire_session(0)
    print(f"UID: {session.uid}  ATR: {session.atr}")

    try:
        # Transmit one APDU. Use single transmits for the interactive auth handshake.
        # (Here: the get-UID pseudo-APDU, answered by any ISO 14443-A card.)
        resp = await ws.desfire_transmit(0, apdu="ffca000000")
        print(resp.response, hex(resp.sw1), hex(resp.sw2))  # full reply incl. status word

        # Batch transmit for non-interactive stretches
        batch = await ws.desfire_transmit_batch(0, apdus=["ffca000000", "ffca000000"])
        for r in batch.responses:
            print(r.response, hex(r.sw1), hex(r.sw2))
    except DesfireError as e:
        print(f"DESFire error: {e}  status={e.status_code}")
    finally:
        await ws.close_desfire_session(0)

apdu and response are hex strings; response includes the trailing status word. Failures raise DesfireError (a subclass of CardError) with an optional status_code. See scripts/test_desfire_session.py for a card-agnostic smoke test.

Monitor Multiple Readers

async with NFCWebSocket() as ws:
    # Subscribe to all readers
    readers = await ws.get_readers()
    for i, reader in enumerate(readers):
        await ws.subscribe(i)
        print(f"Subscribed to {reader.name}")

    @ws.on_card_detected
    def handle(event):
        print(f"Reader {event.reader}: {event.card.uid}")

    await asyncio.sleep(300)

Development

# Install dev dependencies
pip install -e ".[dev]"

# Run tests
pytest

# Lint
ruff check src tests

# Type check
mypy src

License

MIT

Metadata

Release files for nfc-agent 0.6.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for nfc-agent 0.6.0
File Size Uploaded
nfc_agent-0.6.0.tar.gz 18.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for nfc-agent 0.6.0
File Interpreter ABI Platform
nfc_agent-0.6.0-py3-none-any.whl Python 3 none any Details

Total release size: 40.5 kB

Release files / nfc_agent-0.6.0.tar.gz

Download URL nfc_agent-0.6.0.tar.gz
Size 18.6 kB
Tags Source
SHA-256 checksum
How to use checksums
e5318f5e688c715a5f57d78812cbf1edb09e955a7fb7de736d2f7ceb301c2f5f
BLAKE2b-256 checksum
How to use checksums
ef2ea2c739d98ac343962c56f4444ffb6a77ea2eeb4bbe977d2f21be0991bba5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jun 18, 2026.

Transparency log

Release files / nfc_agent-0.6.0-py3-none-any.whl

Download URL nfc_agent-0.6.0-py3-none-any.whl
Size 21.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2f89a6de4a4ac32dfbcbe4b6bbfca112ba0fd52156c8d2189b9945a8aae7d9e5
BLAKE2b-256 checksum
How to use checksums
8948f942051ef97a95b3dc37870880dc10703b1917799a62ecdde6940fbe0397
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jun 18, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.6.0 This release

2 release files

0.5.2

2 release files

0.5.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page