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",
  "nickname": "Living Room Display",
  "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",
  "tags": ["undeclared"],
  "latestPushId": "01912345-aaaa-7abc-def0-bbbbbbbbbbbb",
  "latestPushAt": "2026-01-16T09:00: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)
nickname string or null User-defined display name (if not set, clients should fall back to hwId)
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
tags string array User-defined tags for the device (default: ["undeclared"])
latestPushId UUID or null ID of the most recently published push
latestPushAt timestamp or null When the latest push was published to the device

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

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",
    "nickname": "Living Room Display",
    "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",
    "tags": ["undeclared"],
    "latestPushId": "01912345-aaaa-7abc-def0-bbbbbbbbbbbb",
    "latestPushAt": "2026-01-16T09:00:00Z"
  }
]

Returns an empty array [] if the user has no bound devices. Optional fields (nickname, latestPushId, latestPushAt, etc.) are omitted when null (omitempty).


GET /api/devices/{id}

Requires authentication --- owner only

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

Path Parameters:

Parameter Description
id The device's UUID (e.g., 01912345-6789-7abc-def0-123456789abc)

Response: 200 OK

{
  "id": "01912345-6789-7abc-def0-123456789abc",
  "hwId": "a1b2c3d4-5678-9012-abcd-ef0123456789",
  "thingName": "inklet-a1b2c3d4",
  "nickname": "Living Room Display",
  "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",
  "state": "{\"screen\":\"text\",\"lastCmd\":\"abc123\"}",
  "stateUpdatedAt": "2026-01-16T08:25:00Z",
  "tags": ["undeclared"],
  "latestPushId": "01912345-aaaa-7abc-def0-bbbbbbbbbbbb",
  "latestPushAt": "2026-01-16T09:00: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/{id}/state

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
id The device's UUID

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/{id} 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

GET /api/devices/{id}/nickname

Requires authentication --- owner only

Get the device's display name. Returns the user-defined nickname if set, otherwise falls back to the device's hwId.

Path Parameters:

Parameter Description
id The device's UUID

Response: 200 OK

{
  "nickname": "Living Room Display"
}

Errors:

Code Cause
403 Authenticated user is not the device owner
404 Device not found

PUT /api/devices/{id}/nickname

Requires authentication --- owner only

Set or clear the device's display name. Send an empty string to clear the nickname (subsequent GET will return hwId).

Path Parameters:

Parameter Description
id The device's UUID

Request Body:

{
  "nickname": "Living Room Display"
}
Field Type Required Description
nickname string Yes Display name to set. Empty string "" clears the nickname

Response: 200 OK

{
  "message": "nickname updated"
}

Errors:

Code Cause
400 Invalid request body
403 Authenticated user is not the device owner
404 Device not found

GET /api/devices/{id}/health

Requires authentication --- owner only

Get a real-time health snapshot of the device including connectivity, battery, firmware, and push status.

Path Parameters:

Parameter Description
id The device's UUID

Response: 200 OK

{
  "id": "01912345-6789-7abc-def0-123456789abc",
  "hwId": "a1b2c3d4-5678-9012-abcd-ef0123456789",
  "nickname": "Living Room Display",
  "online": true,
  "state": "{\"screen\":\"text\"}",
  "battery": 85,
  "firmware": "1.2.0",
  "lastSeenAt": "2026-01-16T08:30:00Z",
  "stateUpdatedAt": "2026-01-16T08:25:00Z",
  "latestPushId": "01912345-aaaa-7abc-def0-bbbbbbbbbbbb",
  "latestPushAt": "2026-01-16T09:00:00Z"
}
Field Type Description
id UUID Device ID
hwId string Hardware UUID
nickname string Display name (falls back to hwId if not set)
online boolean Whether the device is currently connected
state JSON string or null Device state reported via MQTT
battery integer or null Battery percentage
firmware string or null Firmware version
lastSeenAt timestamp or null Last heartbeat
stateUpdatedAt timestamp or null Last state update
latestPushId UUID or null Current push ID
latestPushAt timestamp or null When the latest push was published

Errors:

Code Cause
403 Authenticated user is not the device owner
404 Device not found

POST /api/devices/bind/nfc

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

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/{id}/unbind

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
id The device's UUID

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/{id}/cmd

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
id The device's UUID

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",
  "thingName": "inklet-a1b2c3d4",
  "kind": "text",
  "payload": "{\"text\":\"Hello from Inklet!\"}",
  "status": "delivered",
  "createdAt": "2026-01-16T09:00:00Z"
}

The response is a DeviceCommand record for tracking command delivery. payload is a JSON string containing the full command parameters; status is delivered on success.

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/{id}/refresh-code

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
id The device's UUID

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

GET /api/devices/{id}/push

Requires authentication --- owner only

Get the current push for a device. Returns a JSON object containing a CloudFront signed URL for the rendered bitmap. If the device already has a latest push, that one is returned (changed: false). Otherwise the backend promotes the highest-priority push in QUEUE status, marks it PUBLISHED, and sets it as the device's latest push (changed: true).

Path Parameters:

Parameter Description
id The device's UUID

Query Parameters:

Parameter Type Default Description
format string png Bitmap format the returned url points to. One of png, raw2, raw4 (see below). Unknown values fall back to png.

Bitmap formats:

Format File Description
png image.png 1-bit Floyd--Steinberg dithered PNG (800×480)
raw2 image2.raw 1 bpp packed bitmap (48000 bytes), black/white
raw4 image4.raw 2 bpp packed bitmap (96000 bytes), 4 grayscale levels

Response: 200 OK

{
  "url": "https://cdn.iminklet.com/render/{userId}/{deviceId}/{pushId}/image.png?Expires=...&Signature=...&Key-Pair-Id=...",
  "pushId": "01912345-aaaa-7abc-def0-bbbbbbbbbbbb",
  "status": "PUBLISHED",
  "changed": false
}
Field Type Description
url string CloudFront signed URL for the bitmap (expires in 15 minutes)
pushId UUID ID of the push record
status string Push status (PUBLISHED)
changed boolean true if a new push was promoted from the queue; false if the existing latest push was returned

Errors:

Code Cause
400 Invalid device id
401 Missing or invalid access token
403 Authenticated user is not the device owner
404 Device not found, or no push is available (neither latest nor queued)

POST /api/devices/{id}/push/refresh

Requires authentication --- owner only

Rotate to the next push in the queue. The backend finds the next highest-priority push in QUEUE status, marks the device's previous latest push as EXPIRED, marks the new push as PUBLISHED, sets it as the device's latest push, and returns its signed URL. If no queued push is available, it returns the current latest push instead (changed: false).

Path Parameters:

Parameter Description
id The device's UUID

Query Parameters:

Parameter Type Default Description
format string png Bitmap format the returned url points to. One of png, raw2, raw4. Unknown values fall back to png.

Response: 200 OK

{
  "url": "https://cdn.iminklet.com/render/{userId}/{deviceId}/{pushId}/image.png?Expires=...&Signature=...&Key-Pair-Id=...",
  "pushId": "01912345-cccc-7abc-def0-dddddddddddd",
  "status": "PUBLISHED",
  "changed": true
}
Field Type Description
url string CloudFront signed URL for the new bitmap
pushId UUID ID of the push record
status string Push status (PUBLISHED)
changed boolean true if a new push was promoted from the queue; false if no new push was available and the current one was returned

Behavior:

  1. Finds the next highest-priority push in QUEUE status for this device
  2. If found: marks the previous latestPushId as EXPIRED, marks the new push PUBLISHED, updates latestPushId, returns the new push (changed: true)
  3. If no queued push is available: returns the current latest push (changed: false)

Errors:

Code Cause
400 Invalid device id
401 Missing or invalid access token
403 Authenticated user is not the device owner
404 Device not found, or nothing is available (no current push and no queued push)

GET /api/devices/{id}/push/{pushId}

Requires authentication --- owner only

Get a specific push by its push ID. Works like GET /api/devices/{id}/push, but instead of resolving the current/queued push it returns the signed URL for the exact pushId — useful for re-displaying a push from history. Does not change any push state.

Path Parameters:

Parameter Description
id The device's UUID
pushId The push's UUID. Must belong to this device.

Query Parameters:

Parameter Type Default Description
format string png Bitmap format the returned url points to. One of png, raw2, raw4. Unknown values fall back to png.

Response: 200 OK

{
  "url": "https://cdn.iminklet.com/render/{userId}/{deviceId}/{pushId}/image.png?Expires=...&Signature=...&Key-Pair-Id=...",
  "pushId": "01912345-aaaa-7abc-def0-bbbbbbbbbbbb",
  "status": "CONFIRMED",
  "changed": false
}

Errors:

Code Cause
400 Invalid device id or push id
401 Missing or invalid access token
403 Authenticated user is not the device owner
404 Device not found, or push not found for this device, or push has no rendered file

GET /api/devices/{id}/pushes

Requires authentication --- owner only

List all of the device's pushes, newest first. Uses cursor (keyset) pagination — well suited to infinite-scroll / lazy loading — and is filterable by status and created-at range.

Path Parameters:

Parameter Description
id The device's UUID

Query Parameters:

Parameter Type Default Description
status string Filter by push status (PREPARE, QUEUE, PUBLISHED, CONFIRMED, EXPIRED). Omit for all.
from RFC3339 timestamp Only pushes created at or after this time
to RFC3339 timestamp Only pushes created at or before this time
cursor string Opaque cursor from a previous response's nextCursor. Omit for the first page.
limit integer 20 Page size (max 50)

Response: 200 OK

{
  "items": [
    {
      "pushId": "01912345-aaaa-7abc-def0-bbbbbbbbbbbb",
      "title": "Daily Summary",
      "summary": "Today's highlights include Rust memory safety and a WebAssembly article.",
      "status": "CONFIRMED",
      "priority": 10,
      "createdAt": "2026-05-26T09:00:00Z"
    }
  ],
  "nextCursor": "MjAyNi0wNS0yNlQwOTowMDowMFo...",
  "hasMore": true
}
Field Type Description
items[].pushId UUID ID of the push record
items[].title string Short headline (may be empty)
items[].summary string 1--2 sentence description (may be empty)
items[].status string Push status
items[].priority integer Push priority
items[].createdAt timestamp When the push was created
nextCursor string Cursor for the next page; omitted when hasMore is false
hasMore boolean true if more pushes remain after this page

Infinite scroll

Load the first page without cursor. When the user scrolls near the bottom, if hasMore is true, request the next page with ?cursor={nextCursor}. Stop when hasMore is false. Cursor pagination is anchored to (createdAt, id), so newly created or rotated pushes don't shift already-loaded pages (no duplicates/skips) and deep pages stay fast.

Combine with GET /api/devices/{id}/push/{pushId} to fetch the bitmap URL for any entry, or POST /api/devices/{id}/current-push to make one the device's current push.

Errors:

Code Cause
400 Invalid device id, or malformed cursor
401 Missing or invalid access token
403 Authenticated user is not the device owner
404 Device not found

POST /api/devices/{id}/current-push

Requires authentication --- owner only

Force the device's current push to a specific push. Marks the device's previous latest push back to QUEUE (un-pushed), marks the target push PUBLISHED, sets it as the device's latest, and sends an MQTT new_push signal so the device fetches it immediately.

Path Parameters:

Parameter Description
id The device's UUID

Request Body:

{ "pushId": "01912345-aaaa-7abc-def0-bbbbbbbbbbbb" }
Field Type Required Description
pushId UUID Yes The push to make current. Must belong to this device and have a rendered file.

Response: 200 OK

{ "message": "current push set" }

Errors:

Code Cause
400 Invalid device id or push id
401 Missing or invalid access token
403 Authenticated user is not the device owner
404 Device not found, or push not found for this device, or push not yet rendered

POST /api/devices/{id}/custom-push/upload

Requires authentication --- owner only

Step 1 of uploading a custom image to display. Returns a presigned S3 upload ticket (image only, ≤10 MB, 15-minute expiry). The frontend uploads the image directly to S3, then calls the confirm endpoint.

Path Parameters:

Parameter Description
id The device's UUID

Request Body:

{ "contentType": "image/png", "sizeBytes": 204800 }
Field Type Required Description
contentType string Yes MIME type, must be image/*
sizeBytes integer No Declared size; rejected if over 10 MB

Response: 200 OK

{
  "fileId": "01912345-2b44-7c2b-8e36-d367ecab52b7",
  "url": "https://inklet-dev.s3.us-east-1.amazonaws.com",
  "fields": { "key": "user/.../custom.png", "...": "..." },
  "expiresAt": "2026-05-30T13:47:09Z"
}

Upload to S3 as a multipart/form-data POST (all fields first, then the file field), as with content uploads.

Errors:

Code Cause
400 Invalid device id, or content type not an image
401 Missing or invalid access token
403 Authenticated user is not the device owner
413 File exceeds the 10 MB limit

POST /api/devices/{id}/custom-push/confirm

Requires authentication --- owner only

Step 2: confirm the uploaded image and create a push for it. The backend verifies the S3 upload, creates a push + render task pointing at the internal __custom_image__ template, and enqueues rendering. The render worker downloads the image, resizes to 800×480, and produces the dithered/grayscale bitmaps.

The returned push starts in PREPARE, then becomes QUEUE once rendered. Poll GET /api/devices/{id}/pushes (or GET /push/{pushId}) for status, then call POST /api/devices/{id}/current-push to display it.

Path Parameters:

Parameter Description
id The device's UUID

Request Body:

{ "fileId": "01912345-2b44-7c2b-8e36-d367ecab52b7", "title": "Vacation photo" }
Field Type Required Description
fileId UUID Yes The fileId from the upload step
title string No Optional headline shown in push lists

Response: 200 OK

{ "pushId": "01912345-cccc-7abc-def0-dddddddddddd" }

Errors:

Code Cause
400 Invalid device id or file id
401 Missing or invalid access token
403 Authenticated user is not the device owner
404 Device or file not found
409 File not in a confirmable state

Error Responses

All error responses follow a consistent format:

{
  "error": "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