Skip to content

Devices API

All device endpoints require authentication and live under the /api/devices prefix. These manage the lifecycle of e-ink devices: listing, binding, unbinding, sending commands, and reading state.

Device Model

{
  "id": "01912345-6789-7abc-def0-123456789abc",
  "hwId": "a1b2c3d4-5678-9012-abcd-ef0123456789",
  "thingName": "inklet-a1b2c3d4",
  "firmware": "1.2.0",
  "battery": 85,
  "online": true,
  "lastSeenAt": "2026-01-16T08:30:00Z",
  "ownerId": "01912345-0000-7abc-def0-000000000001",
  "boundAt": "2026-01-15T14:00:00Z",
  "claimCode": null,
  "state": "{\"screen\":\"text\",\"lastCmd\":\"abc123\"}",
  "stateUpdatedAt": "2026-01-16T08:25:00Z"
}
Field Type Description
id UUID v7 Database primary key
hwId string Hardware UUID burned into the device at factory
thingName string AWS IoT Core Thing name (assigned during provisioning)
firmware string or null Firmware version reported by the device
battery integer or null Battery percentage (0--100)
online boolean Whether the device is currently connected to IoT Core
lastSeenAt timestamp or null Last heartbeat timestamp
ownerId UUID or null User who owns this device (null if unbound)
boundAt timestamp or null When the device was bound to the current owner
claimCode string or null 6-character pairing code (only set when device is unbound)
state JSON string or null Arbitrary device state reported via MQTT
stateUpdatedAt timestamp or null When the state was last updated

Note

The claimCode is only present on unbound devices. Once a device is bound to a user, the claim code is cleared.

Endpoints


GET /api/devices

:material-lock: Requires authentication

List all devices bound to the authenticated user.

Request Headers:

Authorization: Bearer {accessToken}

Response: 200 OK

[
  {
    "id": "01912345-6789-7abc-def0-123456789abc",
    "hwId": "a1b2c3d4-5678-9012-abcd-ef0123456789",
    "thingName": "inklet-a1b2c3d4",
    "firmware": "1.2.0",
    "battery": 85,
    "online": true,
    "lastSeenAt": "2026-01-16T08:30:00Z",
    "ownerId": "01912345-0000-7abc-def0-000000000001",
    "boundAt": "2026-01-15T14:00:00Z",
    "claimCode": null,
    "state": null,
    "stateUpdatedAt": null
  }
]

Returns an empty array [] if the user has no bound devices.


GET /api/devices/{thing}

:material-lock: Requires authentication --- owner only

Retrieve detailed information for a specific device by its Thing name.

Path Parameters:

Parameter Description
thing The device's thingName (e.g., inklet-a1b2c3d4)

Response: 200 OK

{
  "id": "01912345-6789-7abc-def0-123456789abc",
  "hwId": "a1b2c3d4-5678-9012-abcd-ef0123456789",
  "thingName": "inklet-a1b2c3d4",
  "firmware": "1.2.0",
  "battery": 85,
  "online": true,
  "lastSeenAt": "2026-01-16T08:30:00Z",
  "ownerId": "01912345-0000-7abc-def0-000000000001",
  "boundAt": "2026-01-15T14:00:00Z",
  "claimCode": null,
  "state": "{\"screen\":\"text\",\"lastCmd\":\"abc123\"}",
  "stateUpdatedAt": "2026-01-16T08:25:00Z"
}

Errors:

Code Cause
401 Missing or invalid access token
403 Authenticated user is not the device owner
404 Device not found

GET /api/devices/{thing}/state

:material-lock: Requires authentication --- owner only

Retrieve the raw JSON state reported by the device. This returns the state field parsed as JSON rather than as a string.

Path Parameters:

Parameter Description
thing The device's thingName

Response: 200 OK

{
  "screen": "text",
  "lastCmd": "abc123",
  "brightness": 50
}

Tip

Use this endpoint when you need to read the device state as a structured JSON object. The GET /api/devices/{thing} endpoint returns the state as an escaped JSON string within the device object.

Errors:

Code Cause
401 Missing or invalid access token
403 Authenticated user is not the device owner
404 Device not found or no state has been reported

POST /api/devices/bind/nfc

:material-lock: Requires authentication

Bind a device to the authenticated user using an NFC payload. The backend verifies the HMAC signature against the factory secret before binding.

Request Body:

{
  "hwId": "a1b2c3d4-5678-9012-abcd-ef0123456789",
  "signature": "3f7a8b2c1d9e0f4a"
}
Field Type Required Description
hwId string Yes Hardware UUID read from the NFC tag
signature string Yes First 16 hex characters of HMAC-SHA256(hwId, FACTORY_SECRET)

Response: 200 OK

Returns the full Device object with the ownerId set to the authenticated user.

Errors:

Code Cause
400 Missing fields or invalid signature
404 No device found with the given hwId
409 Device is already bound to another user

POST /api/devices/bind/code

:material-lock: Requires authentication

Bind a device to the authenticated user using a 6-character claim code displayed on the device screen.

Request Body:

{
  "code": "A3X9K2"
}
Field Type Required Description
code string Yes 6-character claim code (case-insensitive)

Response: 200 OK

Returns the full Device object with the ownerId set to the authenticated user.

Errors:

Code Cause
400 Missing or invalid code format
404 No device found with the given claim code
409 Device is already bound to another user
410 Claim code has expired

Where do claim codes come from?

When a device is unbound and powered on, it publishes a request_claim message via MQTT. The backend generates a 6-character code, stores it in the database, and sends it back via the claim_code command. The device renders it on its e-ink screen.


POST /api/devices/{thing}/unbind

:material-lock: Requires authentication --- owner only

Unbind a device from the authenticated user. The device is returned to an unbound state and will request a new claim code.

Path Parameters:

Parameter Description
thing The device's thingName

Response: 200 OK

{
  "message": "device unbound"
}

After unbinding, the backend sends an unbound command to the device via MQTT. The device clears its screen and re-requests a claim code.

Errors:

Code Cause
401 Missing or invalid access token
403 Authenticated user is not the device owner
404 Device not found

POST /api/devices/{thing}/cmd

:material-lock: Requires authentication --- owner only

Send a command to a device. The command is delivered via MQTT to the device's down/cmd topic.

Path Parameters:

Parameter Description
thing The device's thingName

Request Body:

{
  "kind": "text",
  "text": "Hello from Inklet!"
}
Field Type Required Description
kind string Yes Command type (currently only text is supported for user commands)
text string Conditional Required when kind is text

Response: 200 OK

{
  "id": "01912345-9999-7abc-def0-aaaaaaaaaaaa",
  "deviceId": "01912345-6789-7abc-def0-123456789abc",
  "kind": "text",
  "text": "Hello from Inklet!",
  "createdAt": "2026-01-16T09:00:00Z"
}

The response includes the DeviceCommand record, which tracks command delivery.

Errors:

Code Cause
400 Missing or invalid command fields
401 Missing or invalid access token
403 Authenticated user is not the device owner
404 Device not found

POST /api/devices/{thing}/refresh-code

:material-lock: Requires authentication

Regenerate the claim code for a device. This is used when the current claim code has expired or was lost.

Path Parameters:

Parameter Description
thing The device's thingName

Response: 200 OK

Returns the full Device object with the new claimCode set.

Errors:

Code Cause
401 Missing or invalid access token
404 Device not found

Error Responses

All error responses follow a consistent format:

{
  "message": "device not found"
}
Code Description
400 Bad request --- malformed body, missing required fields, or invalid format
401 Unauthorized --- access token is missing, expired, or invalid
403 Forbidden --- the authenticated user does not own the target device
404 Not found --- the device or resource does not exist
409 Conflict --- the device is already bound to another user
410 Gone --- the claim code has expired and must be regenerated