Multi-Device Hardware API
One REST API for RFID readers, decoders and other networked hardware — no device registration, just name the device in your request.
The API is built around three ideas:
- One shape for every device. The
ur4RFID reader and thejmq-decoderdeactivator are called exactly the same way — you change the device type in the URL, nothing else. New device types appear without the endpoints changing. - Discovery built in.
GET /api/v1/devicesreturns the full catalog: every supported type, what it can do, which parameters each action takes, and the exact endpoints to call. Clients can build UIs from this alone. - Stateless operations. Every device call carries
host+port. The server connects, executes, returns, disconnects. Operate unlimited devices with zero pre-configuration.
192.168.1.213:8888 on 2026-08-19 — catalog, test/info, all three actions (a 4-second TID scan read
12 unique tags / 151 reads), all data endpoints, and every error path. UR4 example responses are real captured output.
Endpoint Map
| Area | Endpoints | Purpose |
|---|---|---|
| Catalog | GET /v1/devices · GET /v1/devices/{type} | Discover device types, capabilities, actions |
| Operations | POST /v1/devices/{type}/test · /info · /actions/{action} | Talk to physical hardware (any type) |
| RFID Data | GET /v1/rfid/stats · /tags · /scans · /sessions · /tid-logs | Query persisted scan history (database only) |
| Legacy | /api/test, /api/scan, … (unversioned) | Deprecated — kept for backward compatibility |
Key Concepts
| Term | Description |
|---|---|
| Device type | Short name for a kind of hardware, e.g. ur4. It goes in the URL. |
| Capability | Something a device can do, e.g. tid_reading, power_control |
| Action | A command you send to a device (scan, set-power, kill, …), each with its own parameters |
| EPC | Electronic Product Code — 24-hex-char unique RFID tag identifier |
| TID | Tag Identifier — factory-programmed chip serial number (24 hex chars) |
| RSSI | Signal strength in dBm (e.g. -6.7); closer to 0 = stronger |
| Session | One timed scan window; all reads share a UUID session_id |
Getting Started
Three things and you are making calls: the base URL, your API key, and a first request.
1 · Base URL
Everything lives under this address — already filled in with the server you are reading these docs from:
http://localhost:8090/api/v12 · Your API key
Every request must carry a key. The one below is live on this server and is pre-filled into every example on this page — details in Authentication.
X-Api-Key: <your-api-key>3 · Your first request
Ask the server which devices it supports. If this returns a list, you are connected and authenticated:
curl http://localhost:8090/api/v1/devices -H "Accept: application/json" \ -H "X-Api-Key: <your-api-key>"
4 · Read some tags
Point at a reader on your network and scan for 5 seconds. Every read is stored automatically, so you can query the history afterwards:
curl -X POST http://localhost:8090/api/v1/devices/ur4/actions/scan \ -H "Content-Type: application/json" -H "Accept: application/json" \ -H "X-Api-Key: <your-api-key>" \ -d '{"host": "192.168.1.213", "port": 8888, "duration": 5, "with_tid": true}'
host and port are the reader's own address on your network — you never register a device anywhere, you just name it in the request.
Conventions
Base URL & Headers
Base URL: http://localhost:8090/api/v1 — send these headers on every request:
Content-Type: application/json Accept: application/json X-Api-Key: <your-api-key>
X-Api-Key header is omitted from the examples below for brevity, but it is always required.Accept: application/json matters: without it, validation failures may come back as a redirect instead of a 422 JSON body.Rate limiting
Every API route is throttled to 120 requests per minute per caller. Responses carry X-RateLimit-Limit and X-RateLimit-Remaining; exceeding the limit returns 429.
Response Envelope
{ "success": true, "data": { ... } }
{ "success": false, "message": "Human-readable description" }Common Device Fields
Every device operation (test, info, actions/*) requires the target's address in the JSON body:
| Field | Type | Required | Description |
|---|---|---|---|
host | string (IPv4) | Yes | IP address of the device |
port | integer (1–65535) | Yes | TCP port — UR4 default is 8888 |
🔐 Authentication
Every endpoint requires an API key. Send it in the X-Api-Key header (or as Authorization: Bearer <key>).
Each key carries a permission level, so a read-only key can never deactivate a tag. Keys are issued by your administrator.
Sending the key
# preferred: dedicated header curl "http://localhost:8090/api/v1/devices" \ -H "Accept: application/json" \ -H "X-Api-Key: rfid_operator_xxxxxxxxxxxxxxxx" # also accepted: bearer token curl "http://localhost:8090/api/v1/devices" \ -H "Authorization: Bearer rfid_operator_xxxxxxxxxxxxxxxx"
Scopes
Each key holds one or more scopes. Every action states the scope it needs, and the catalog publishes it — so you can tell in advance what a key is allowed to do.
| Scope | Grants | Risk |
|---|---|---|
data.read | The device catalog and all /v1/rfid/* history queries | Safe — database only |
device.operate | test, info, and the everyday actions: scan, read, stop, set-power, read-memory | Touches hardware |
tag.write | write-memory, write-epc, lock | Modifies tags |
tag.kill | kill — permanent tag deactivation | Irreversible |
* | Everything above | Full access |
Permission levels
Three levels are available. Ask your administrator for the narrowest one that covers what you need.
| Level | Scopes it holds | Typical use |
|---|---|---|
| Read-only | data.read | Dashboards, reports, inventory views |
| Operator | data.read · device.operate | Scanning stations, day-to-day operation |
| Admin | * — everything | Tag writing and permanent deactivation |
Getting and changing a key
The key shown on this page is the current Admin key, so every example works as-is. In practice:
- Need a key? Ask the administrator — they can hand you one at the level you need.
- Key changed or leaked? The administrator can replace any key in seconds; the old one stops working immediately and this page will show the new one.
- Keys never expire on their own — they stay valid until the administrator changes them.
kill command, use a lower level for anything that only reads or scans.
Rejection responses
401 — missing or invalid key:
{ "success": false, "message": "API key missing. Send it in the X-Api-Key header." }
{ "success": false, "message": "Invalid API key." }403 — valid key, insufficient scope. The response names the scope required and the ones held, so the caller knows which key to use:
{
"success": false,
"message": "This API key is not allowed to perform this action (requires scope 'tag.kill').",
"required_scope": "tag.kill",
"granted_scopes": ["data.read", "device.operate"]
}/api/scan, /api/tags, …) require the same keys — they are not a way around authentication.
Operational notes
- Serve the API over HTTPS — a static key in a header is only as safe as the transport.
- Set
APP_DEBUG=falsein production so errors do not leak stack traces. - Keep the JMQ bridge bound to localhost; it has no authentication of its own by default.
- This page and the home page stay public; only
/api/*is protected.
Device Catalog
Discovery endpoints. The catalog tells clients everything: which device types exist, what each can do, which parameters each action takes, and the exact URLs to call.
A dashboard can render its entire device UI from this response — no hardcoding.
List Device Types
Example
curl http://localhost:8090/api/v1/devices -H "Accept: application/json" \ -H "X-Api-Key: <your-api-key>"
Response 200
{
"success": true,
"data": {
"count": 2,
"devices": [
{
"type": "ur4",
"name": "Chainway UR4 UHF RFID Reader",
"category": "rfid-reader",
"transport": "tcp",
"capabilities": ["inventory", "tid_reading", "user_memory", "power_control", "multi_antenna"],
"actions": {
"scan": {
"description": "Run a blocking inventory scan and persist every tag read (tags, scans, session, TID logs).",
"params": { "duration": "integer|min:1|max:60", "with_tid": "boolean" }
},
"stop": {
"description": "Send a stop-inventory command (e.g. if the reader was left scanning).",
"params": []
},
"set-power": {
"description": "Set RF transmit power in dBm. Higher power = longer read range.",
"params": { "power": "required|integer|min:0|max:33" }
}
},
"endpoints": {
"test": "/api/v1/devices/ur4/test",
"info": "/api/v1/devices/ur4/info",
"action": "/api/v1/devices/ur4/actions/{action}"
}
},
{
"type": "jmq-decoder",
"name": "JMQ400300 UHF Decoder / Deactivator",
"category": "rfid-decoder",
"transport": "http-bridge",
"capabilities": ["tid_reading", "user_memory", "power_control", "tag_write", "tag_kill", "tag_lock"],
"actions": { "read": {…}, "set-power": {…}, "read-memory": {…}, "write-memory": {…}, "write-epc": {…}, "kill": {…}, "lock": {…} },
"endpoints": {
"test": "/api/v1/devices/jmq-decoder/test",
"info": "/api/v1/devices/jmq-decoder/info",
"action": "/api/v1/devices/jmq-decoder/actions/{action}"
}
}
]
}
}Describe One Device Type
Same object as one entry of the list above. Unknown types return 404 with the available types:
{
"success": false,
"message": "Unknown device type 'printer'. Available types: ur4",
"available_types": ["ur4"]
}Device Operations
Generic hardware endpoints — identical for every device type. Replace {type} with any slug from the catalog (ur4 today).
All three require host + port in the body. The server connects, executes, and disconnects — nothing is stored about the device itself.
Test Connection
Verify connectivity and return basic device info.
Example
curl -X POST http://localhost:8090/api/v1/devices/ur4/test \ -H "Content-Type: application/json" -H "Accept: application/json" \ -H "X-Api-Key: <your-api-key>" \ -d '{"host": "192.168.1.213", "port": 8888}'
Response 200
{
"success": true,
"data": {
"type": "ur4",
"connected": true,
"host": "192.168.1.213",
"port": 8888,
"power": null
},
"message": "Connection successful"
}Response 500 — device unreachable
{ "success": false, "message": "Failed to connect to device" }Get Device Info
Live device information. Body identical to test; response identical minus the message.
{
"success": true,
"data": { "type": "ur4", "connected": true, "host": "192.168.1.213", "port": 8888, "power": null }
}power may be null when the reader doesn't answer the GET_POWER query in time; connectivity is still confirmed by connected: true.Execute an Action
The heart of the API. {action} is any action listed for that device in the catalog, and your parameters are checked against the rules shown there. Responses always echo which device and action ran:
{
"success": true,
"data": {
"device": { "type": "ur4", "host": "...", "port": 8888 },
"action": "scan",
// ...action-specific payload
}
}Unknown action → 404 with the valid list
{
"success": false,
"message": "Unknown action 'print' for device type 'ur4'.",
"available_actions": ["scan", "stop", "set-power"]
}UR4 Actions
Device type ur4 — a Chainway UR4 UHF RFID reader, reached directly over your network.
What it supports: bulk tag inventory, reading TIDs and user memory, power control, and up to 4 antennas.
scan
Run a blocking inventory scan for duration seconds, then return every unique tag read. Everything is persisted automatically: tags upserted, raw scans recorded, a session created, TID logs written when with_tid is enabled.
Body
| Field | Type | Required | Default | Constraints |
|---|---|---|---|---|
host / port | standard device fields | |||
duration | integer | No | 3 | 1–60 seconds |
with_tid | boolean | No | false | also read each tag's factory TID |
duration — set your client timeout above duration + ~3s.Example
curl -X POST http://localhost:8090/api/v1/devices/ur4/actions/scan \ -H "Content-Type: application/json" -H "Accept: application/json" \ -H "X-Api-Key: <your-api-key>" \ -d '{"host": "192.168.1.213", "port": 8888, "duration": 4, "with_tid": true}'
Response 200
{
"success": true,
"data": {
"device": { "type": "ur4", "host": "192.168.1.213", "port": 8888 },
"action": "scan",
"session_id": "40abaeed-8d5b-45c1-a624-50e3af47922f",
"unique_tags": 12,
"total_reads": 12,
"tags": [
{
"epc": "E28011606000021268DC33A2",
"tid": "E2801160200063A21B860A4D",
"rssi": -6.32,
"antenna": 1,
"read_count": 14,
"user_data": null,
"timestamp": "2026-08-19T09:07:27.977755Z",
"device_type": "ur4"
}
// ... 11 more tags
]
}
}Tag object fields
| Field | Type | Description |
|---|---|---|
epc | string | 24-hex-char Electronic Product Code |
tid | string | null | Factory chip serial (only when with_tid: true) |
rssi | float | Signal strength in dBm |
antenna | integer | Antenna port (1–4) |
read_count | integer | Times this tag was read during the scan |
user_data | string | null | User memory bank content, if read |
timestamp | string (ISO 8601) | Last read time |
stop
Send a stop-inventory command — useful if a reader was left in inventory mode. No extra parameters.
curl -X POST http://localhost:8090/api/v1/devices/ur4/actions/stop \ -H "Content-Type: application/json" -H "Accept: application/json" \ -H "X-Api-Key: <your-api-key>" \ -d '{"host": "192.168.1.213", "port": 8888}'
{
"success": true,
"data": {
"device": { "type": "ur4", "host": "192.168.1.213", "port": 8888 },
"action": "stop",
"stopped": true
}
}set-power
Set RF transmit power. Higher power = longer read range (and more reflections).
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
host / port | standard device fields | ||
power | integer | Yes | 0–33 (dBm) |
curl -X POST http://localhost:8090/api/v1/devices/ur4/actions/set-power \ -H "Content-Type: application/json" -H "Accept: application/json" \ -H "X-Api-Key: <your-api-key>" \ -d '{"host": "192.168.1.213", "port": 8888, "power": 26}'
{
"success": true,
"data": {
"device": { "type": "ur4", "host": "192.168.1.213", "port": 8888 },
"action": "set-power",
"power": 26
}
}Response 422 — out of range
{
"message": "The power field must not be greater than 33.",
"errors": { "power": ["The power field must not be greater than 33."] }
}Decoder Actions (JMQ400300)
Device type jmq-decoder — a JMQ400300 UHF decoder/deactivator. Beyond reading tags it can write to them, change their EPC, lock them, and permanently deactivate them.
What it supports: reading TIDs and user memory, power control, tag write, tag lock, tag kill.
How you address it
host + port address the decoder service — normally 127.0.0.1 port 9100 — not the decoder hardware itself. The hardware's own address and radio settings are configured once by the administrator, so you always call the same host and port.
Connection errors tell you which part failed
The message separates "the decoder service is down" from "the service is up but the hardware is unreachable":
// bridge running, decoder hardware disconnected: { "success": false, "message": "Failed to connect to JMQ decoder (bridge up at http://127.0.0.1:9100, reader 192.168.0.250:8080 unreachable)" } // bridge itself not running: { "success": false, "message": "Failed to connect to JMQ decoder (bridge unreachable at http://127.0.0.1:9199)" }
Gen2 memory banks (used by memory/lock actions)
| Bank | Name | Contents |
|---|---|---|
0 | Reserved | Kill password (words 0–1) + access password (words 2–3) |
1 | EPC | The tag's EPC |
2 | TID | Factory chip serial (read-only) |
3 | USER | Free user memory |
Passwords are 8 hex digits (4 bytes). On write/kill/lock/read-memory, epc is optional — omitted, the operation targets the single tag in the field; given, a Select filter targets that exact tag.
read
Poll the reader for duration seconds, deduplicating by TID (the bridge embeds each tag's TID during inventory, so tags sharing an EPC are still distinguished). Results are persisted exactly like UR4 scans — tags, scans, a session, and TID logs — keyed by the bridge host:port, and queryable via the RFID Data API.
Body
| Field | Type | Required | Default | Constraints |
|---|---|---|---|---|
host / port | bridge address, e.g. 127.0.0.1 : 9100 | |||
duration | integer | No | 3 | 1–60 seconds (request blocks this long) |
Example
curl -X POST http://localhost:8090/api/v1/devices/jmq-decoder/actions/read \ -H "Content-Type: application/json" -H "Accept: application/json" \ -H "X-Api-Key: <your-api-key>" \ -d '{"host": "127.0.0.1", "port": 9100, "duration": 5}'
Response 200
{
"success": true,
"data": {
"device": { "type": "jmq-decoder", "host": "127.0.0.1", "port": 9100 },
"action": "read",
"session_id": "<uuid>",
"unique_tags": 2,
"total_reads": 2,
"tags": [
{
"epc": "E280116060000212...",
"tid": "E280116020006392...",
"rssi": -41.0,
"antenna": 1,
"read_count": 6,
"device_type": "jmq400300"
}
]
}
}set-power
Set RF transmit power in dBm (power: required integer 0–33). You always send plain dBm; any unit conversion happens for you.
curl -X POST http://localhost:8090/api/v1/devices/jmq-decoder/actions/set-power \ -H "Content-Type: application/json" -H "Accept: application/json" \ -H "X-Api-Key: <your-api-key>" \ -d '{"host": "127.0.0.1", "port": 9100, "power": 25}'
read-memory / write-memory
| Field | Type | Required | Constraints |
|---|---|---|---|
epc | hex string | No | omit → single tag in field |
bank | integer | Yes | 0–3 (see banks table) |
word_address | integer | No | ≥ 0, default 0 |
word_count | integer | Yes | 1–64 (words = 2 bytes) |
access_password | hex string | No | 8 hex digits, default 00000000 |
Returns { "bank": 2, "data_hex": "E28011602000..." }.
Same fields, but data_hex (required hex string) replaces word_count. Returns { "written": true, "bank": …, "bytes": … }.
00000000. Write the Reserved bank (kill pw = words 0–1, access pw = words 2–3) first:
curl -X POST http://localhost:8090/api/v1/devices/jmq-decoder/actions/write-memory \ -H "Content-Type: application/json" \ -d '{"host":"127.0.0.1","port":9100,"epc":"<epc>","bank":0,"word_address":0,"data_hex":"DEADBEEF12345678","access_password":"00000000"}' # DEADBEEF = new kill password · 12345678 = new access password
write-epc
Overwrite a tag's EPC.
| Field | Type | Required | Constraints |
|---|---|---|---|
new_epc_hex | hex string | Yes | the new EPC |
epc | hex string | No | current EPC; omit → single tag in field |
access_password | hex string | No | 8 hex digits |
Returns { "written": true, "new_epc": "..." }.
kill (deactivate)
Permanently deactivates a tag — this is irreversible. Requires a non-zero kill_password previously written to the Reserved bank (see write-memory).
| Field | Type | Required | Constraints |
|---|---|---|---|
kill_password | hex string | Yes | 8 hex digits, non-zero |
epc | hex string | No | omit → single tag in field |
curl -X POST http://localhost:8090/api/v1/devices/jmq-decoder/actions/kill \ -H "Content-Type: application/json" -H "Accept: application/json" \ -H "X-Api-Key: <your-api-key>" \ -d '{"host": "127.0.0.1", "port": 9100, "epc": "<epc>", "kill_password": "DEADBEEF"}'
Returns { "killed": true, "killed_epc": "..." }. SDK errors (e.g. kill password not initialised) come back verbatim in message with status 500.
lock
Lock or unlock a tag memory region. Requires a non-zero access password.
| Field | Type | Required | Allowed values |
|---|---|---|---|
lock_object | string | Yes | kill_password · access_password · epc · tid · user |
lock_type | string | Yes | lock · unlock · perm_lock · perm_unlock |
access_password | hex string | Yes | 8 hex digits, non-zero |
epc | hex string | No | omit → single tag in field |
Returns { "locked": true, "lock_object": "...", "lock_type": "..." }. perm_lock is irreversible.
RFID Data API
Persisted scan history — database queries only. These endpoints never contact hardware and respond instantly.
Data is keyed by host:port, so history stays separated per device. Both UR4 scan and JMQ decoder read persist here (for the decoder, the key is the bridge address).
Device Statistics
Aggregate counters for one reader. Both query parameters are required.
curl "http://localhost:8090/api/v1/rfid/stats?host=192.168.1.213&port=8888" \ -H "Accept: application/json" \ -H "X-Api-Key: <your-api-key>"
{
"success": true,
"data": {
"host": "192.168.1.213",
"port": "8888",
"total_scans": 35,
"unique_tags": 12,
"scans_today": 35,
"sessions_today": 3
}
}Get Tag by EPC
One tag (exact 24-hex-char EPC) with its 100 most recent scan records. Unknown EPC → 404.
curl "http://localhost:8090/api/v1/rfid/tags/E28011606000021268DC3392" \ -H "Accept: application/json" \ -H "X-Api-Key: <your-api-key>"
{
"success": true,
"data": {
"tag": {
"epc": "E28011606000021268DC3392",
"tid": "E2801160200063921B860A4D",
"first_seen_at": "2026-08-19T08:32:29.000000Z",
"last_seen_at": "2026-08-19T09:07:31.000000Z",
"total_read_count": 28
},
"recent_scans": [
{
"host": "192.168.1.213",
"port": 8888,
"rssi": -6.7,
"antenna": 1,
"session_id": "f8d6b214-aec3-4c03-9d7e-683273ad9eb3",
"scanned_at": "2026-08-19T08:32:29.000000Z"
}
]
}
}List Scans
Raw scan records (one row per tag read per session), newest first. Each row embeds its related tag.
| Param | Type | Description |
|---|---|---|
host / port | string / integer | Filter by reader |
session_id | string (UUID) | Filter by scan session |
from / to | datetime | Time range, e.g. 2026-08-19 00:00:00 |
per_page / page | integer | Pagination (default 50) |
curl "http://localhost:8090/api/v1/rfid/scans?host=192.168.1.213&session_id=40abaeed-8d5b-45c1-a624-50e3af47922f" \ -H "Accept: application/json" \ -H "X-Api-Key: <your-api-key>"
{
"success": true,
"data": {
"current_page": 1,
"data": [
{
"host": "192.168.1.213",
"port": 8888,
"epc": "E28011606000021268DC3392",
"tid": "E2801160200063921B860A4D",
"rssi": -6.7,
"antenna": 1,
"session_id": "40abaeed-8d5b-45c1-a624-50e3af47922f",
"scanned_at": "2026-08-19T09:07:31.000000Z",
"tag": { /* related RFIDTag */ }
}
],
"per_page": 50,
"total": 12
}
}List Sessions
Scan sessions, newest first.
| Param | Type | Description |
|---|---|---|
host / port | string / integer | Filter by reader |
status | string | running | completed | failed |
per_page / page | integer | Pagination (default 20) |
{
"success": true,
"data": {
"current_page": 1,
"data": [
{
"session_id": "40abaeed-8d5b-45c1-a624-50e3af47922f",
"host": "192.168.1.213",
"port": 8888,
"started_at": "2026-08-19T09:07:27.000000Z",
"ended_at": "2026-08-19T09:07:31.000000Z",
"total_tags": 12,
"total_reads": 151,
"status": "completed",
"metadata": { "duration": 4, "with_tid": true }
}
],
"per_page": 20,
"total": 3
}
}Get Session
One session (the UUID returned by the scan action) with all of its scan records. Unknown ID → 404.
curl "http://localhost:8090/api/v1/rfid/sessions/40abaeed-8d5b-45c1-a624-50e3af47922f" \ -H "Accept: application/json" \ -H "X-Api-Key: <your-api-key>"
{
"success": true,
"data": {
"session": { /* session object, as in List Sessions */ },
"scans": [ /* every scan row in this session */ ]
}
}List TID Logs
TID (factory chip serial) sighting log — one row per unique TID per reader, with first/last-seen timestamps and cumulative read count. Written whenever a scan runs with with_tid: true.
| Param | Type | Description |
|---|---|---|
search | string | Partial match on tid, epc, or location |
host / port | string / integer | Filter by reader |
per_page / page | integer | Pagination (default 50) |
{
"success": true,
"data": {
"current_page": 1,
"data": [
{
"tid": "E2801160200063921B860A4D",
"epc": "E28011606000021268DC3392",
"host": "192.168.1.213",
"port": 8888,
"session_id": "f8d6b214-aec3-4c03-9d7e-683273ad9eb3",
"rssi": -6.7,
"antenna": 1,
"location": null,
"first_seen_at": "2026-08-19T08:32:29.000000Z",
"last_seen_at": "2026-08-19T09:07:31.000000Z",
"read_count": 2
}
],
"per_page": 50,
"total": 12
}
}Pagination
List endpoints return a paginated object inside data:
| Field | Description |
|---|---|
current_page | Current page number |
data | Array of records for this page |
per_page | Page size |
total | Total records across all pages |
last_page | Number of the final page |
next_page_url / prev_page_url | Ready-made navigation URLs (or null) |
Navigate with ?page=N; change page size with ?per_page=N.
Errors
HTTP Status Codes
| Code | Meaning | When |
|---|---|---|
200 | OK | Request handled successfully |
401 | Unauthorized | API key missing or invalid — see Authentication |
403 | Forbidden | Valid key, but it lacks the scope this operation requires |
404 | Not Found | Unknown device type, unknown action, unknown EPC / session ID |
429 | Too Many Requests | Rate limit exceeded (120 requests/minute) |
422 | Unprocessable Entity | Validation failed (bad IP, missing field, out-of-range value) |
500 | Server Error | Device unreachable, TCP failure, or unexpected exception |
404 — Unknown device type / action
Discovery-friendly: the response tells you what is valid.
// GET /v1/devices/printer { "success": false, "message": "Unknown device type 'printer'. Available types: ur4", "available_types": ["ur4"] } // POST /v1/devices/ur4/actions/print { "success": false, "message": "Unknown action 'print' for device type 'ur4'.", "available_actions": ["scan", "stop", "set-power"] }
422 — Validation error
Device fields (host, port) and the action's own parameters are validated together.
{
"message": "The power field must not be greater than 33.",
"errors": { "power": ["The power field must not be greater than 33."] }
}500 — Device error
{ "success": false, "message": "Failed to connect to device" }404 — Unknown EPC / session
{ "message": "No query results for model [App\\Models\\RFIDTag]." }APP_DEBUG=true (local development), 404/500 responses include a full stack trace. Set APP_DEBUG=false in production.Troubleshooting
"Failed to connect to device"
- Verify raw TCP reachability from the API server:
bash
timeout 5 bash -c 'echo > /dev/tcp/192.168.1.213/8888' && echo OK || echo UNREACHABLE
- Confirm the device's IP and port (UR4 default TCP port is
8888). - Check the device is in server/TCP mode and not held by another client — the UR4 typically serves one TCP client at a time.
- Check firewalls/VLANs between the API server and the device.
Scan returns 0 tags
- Move tags within antenna range, or raise power via the
set-poweraction (max 33). - Increase
duration. - Confirm the antenna port in use is connected.
Scan request times out on the client side
The scan action blocks for the full duration. Set your HTTP client timeout to at least duration + 3 seconds.
power is null in test/info
The reader accepted the connection but didn't answer the GET_POWER query in time. Connectivity is fine (connected: true); it does not affect scanning.
Validation errors come back as HTML/redirect
Add the Accept: application/json header to the request.
Legacy API (deprecated)
The original unversioned UR4-only endpoints still work unchanged (verified live), but new integrations should use /api/v1. Mapping:
| Legacy | v1 replacement |
|---|---|
POST /api/test | POST /api/v1/devices/ur4/test |
POST /api/info | POST /api/v1/devices/ur4/info |
POST /api/scan | POST /api/v1/devices/ur4/actions/scan |
POST /api/stop | POST /api/v1/devices/ur4/actions/stop |
POST /api/power | POST /api/v1/devices/ur4/actions/set-power |
GET /api/stats, /tags, /scans, /sessions, /tid-logs | Same paths under GET /api/v1/rfid/… |
Multi-Device Hardware API v1 · verified against a live UR4 reader on 2026-08-19.