The TigerTag playground is one page, tools/playground.html, byte-for-byte identical in
the JavaScript SDK (TigerTag-SDK-JS) and the
Python SDK (TigerTag-SDK-Python).
Each repository ships a small server that implements the contract below with its own SDK:
| SDK | Start | Reader support |
|---|---|---|
| JavaScript | node tools/server.js [port] |
npm install ws nfc-pcsc |
| Python | python3 tools/server.py [port] |
pip install "tigertag[nfc]" (pyscard) |
Then open http://localhost:<port>/tools/playground.html (default port 7432).
TIGERTAG_PLAYGROUND_NO_NFC=1 starts either server without touching the readers.
All request and response bodies are JSON with snake_case keys, except the NFC message
fields reqId and pagesWritten (kept for compatibility). Hex strings are lowercase in
responses and accepted in any case in requests. Servers may add extra keys; the page only
relies on the keys listed here. Errors are { "error": "<message>" } with a 4xx / 5xx status.
The page must never contain SDK-specific code: everything that differs comes from
GET /api/version.
{ "version": "1.2.0", "sdk": "javascript", "label": "JS SDK",
"repo": "https://github.com/TigerTag-Project/TigerTag-SDK-JS",
"install": "npm install tigertag", "server": "node tools/server.js", "style": "camel" }| Key | JavaScript | Python |
|---|---|---|
sdk |
javascript |
python |
label |
JS SDK |
Python SDK |
repo |
…/TigerTag-SDK-JS |
…/TigerTag-SDK-Python |
install |
npm install tigertag |
pip install tigertag |
server |
node tools/server.js |
python3 tools/server.py |
style |
camel (toRawDict(), create({ tagCount })) |
snake (to_raw_dict(), create(tag_count=…)) |
The page uses it for the header link and badge, the SDK highlighted in the GitHub list, the
create() code shown in SDK Input, the method names in the SDK Output tabs and every hint
that names the SDK or its server.
Request: { "uid": "<14 hex>"?, "payload": "<hex: 80, 144 or 180 bytes>" } — without a UID the
payload is parsed as a dump (fromDump() / from_dump()).
Response:
| Key | Content |
|---|---|
pretty, describe |
pretty() / describe() text |
verify |
signature result { ok, status, detail } |
raw_dict, dict |
toRawDict() / to_raw_dict(), toDict() / to_dict() |
validate |
list of warnings |
tag_info, tag_count, tag_index |
byte +39 and its two nibbles (null when 0) |
Request: the keyword arguments of TigerTag.create() in snake_case — product_id,
id_material, id_aspect_1, id_aspect_2, id_type, id_diameter, id_brand,
color1_r/g/b/a, color2_r/g/b, color3_r/g/b, measure, id_unit, nozzle_temp_min/max,
dry_temp, dry_time, bed_temp_min/max, timestamp, custom_message, td_raw,
tag_count, tag_index — plus uid (hex) and measure_available (when it differs from
measure).
Response: { "payload": "<80-byte hex>", "validate": [...], "tag_info", "tag_count", "tag_index" }.
Both SDKs produce the same bytes for the same fields.
Request: { "uid"?, "payload" }. Response:
{ "api_data": <raw_api() JSON or null>, "diffs": [{ "field", "chip_value", "api_value" }], "in_sync": bool, "error": "<message or null>" }.
The catalogue copy in use: { "downloaded", "source": "bundled" | "downloaded", "count", "fetched_at", "checked_at", "url", "etag", "last_modified", "path", "bundled": { "path", "date" } }.
A ready-to-burn TigerTag+:
{ "entry": { id, title, brand, sku, barcode, img_src, material, measure, product_type, color, color_info }, "rfid_data", "fields": <create() keyword arguments, snake_case>, "raw_dict", "payload": "<80-byte hex>", "pretty", "describe", "catalog": <catalog info> }.
Errors: 400 (not a number), 404 (not in the catalogue), 422 (no RFID data), 503 (no copy at all).
Checks for a new catalogue now (conditional download). Response: the catalog info;
503 { error, ...info } when offline.
{ "custom", "db_path", "data_dir", "offline", "auto_update", "max_age" (seconds), "last_check", "last_error", "tables": { "id_material.json": { "source": "bundled" | "downloaded" | "custom", "path", "timestamp" }, … }, "catalog": <catalog info> }.
Request: { "force"?: bool, "catalog"?: bool }. Response: { "changed": [files], "info": <db info> };
503 { error, info } when offline or unreachable.
The table in use (e.g. id_material.json), as served to the page; header
X-TigerTag-Source: bundled | downloaded | custom. 404 for an unknown file.
Each event is data: <JSON>\n\n; : keep-alive comments every 15 s. On connect the server
sends readers:status, then one card:detected per chip already on a reader.
type |
Fields |
|---|---|
readers:status |
readers: [{ id, name, connected, hasCard, uid }], nfc (reader support installed), hint (how to install it, or null), error |
reader:connected / reader:disconnected |
reader: { id, name } |
card:detected |
reader, uid, payload (hex, 144 bytes, 80 on small chips) and the /api/parse keys (pretty, describe, verify, raw_dict, dict, validate, tag_info, tag_count, tag_index) |
card:removed |
reader |
error |
reader, message |
Request: { "reqId"?, "reader"? } (reader id / name; all readers holding a chip when omitted).
Response: { "messages": [ { "type": "read:result", reqId, reader, uid, payload, bytes, ok, error? }…, { "type": "read:done", reqId } ] }.
Request: { "reqId"?, "reader"?, "payload": "<hex, 80 or 144 bytes>" }. Writes pages 0x04–0x27
(36 pages, one UPDATE BINARY per page) of every reader holding a chip, or only reader.
The signature pages 0x18–0x27 are always written as 00 — a playground never writes a
signature (only a certified manufacturer can issue one, and a copied signature is invalid since
it covers the chip UID). Pages 0–3 and 0x28+ are never touched.
Response: { "messages": [ { "type": "burn:result", reqId, reader, uid, ok, pagesWritten: 36, signature_dropped, error? }…, { "type": "burn:done", reqId } ] }; 400 for a bad length.
Dry run, no reader touched. Request: { "payload" }. Response:
{ "signature_dropped": bool, "pages": 36, "apdus": ["FF D6 00 04 04 BC 0F CB 97", …] }.
tools/playground.html must stay identical in both repositories.
scripts/check_playground_sync.js (JavaScript) and scripts/check_playground_sync.py
(Python) compare it with the sibling repository's copy — the local checkout next to this one
when present, otherwise the other repository's main on GitHub — and fail on any difference.
Both test suites run the check and skip it when neither copy is reachable.