Skip to content

Error codes

Every non-2xx response — on REST, WebSocket, and serial NDJSON alike — uses one error envelope:

{ "error": { "code": "content_not_found", "message": "no bundle named 'promo'", "details": {} } }
  • code is a stable, machine-readable string from the ErrorCode enum below. Switch your error handling on code, not on messagemessage is human-readable English meant for logs/UI, and its exact wording can change between releases; code values never change meaning (new codes may be added in a minor release, existing ones don't).
  • message explains the failure in plain English.
  • details is optional structured context (may be absent or {}).

On serial NDJSON, the same envelope is carried as the body of a response frame, with status set to the matching HTTP-equivalent code — see Serial API. A line that fails to parse entirely (not a valid API error, a transport-level problem) gets its own bad_frame code instead, described at the bottom of this page.

Generic codes

Code HTTP status When you get it
bad_request 400 Malformed or invalid request payload — a field fails validation (enum membership, range, pattern, required field missing), an unknown field was sent to PATCH /config, or a value conflicts with the board profile (e.g. wifi.ap.band: "5" on a Zero 2 W).
not_found 404 The requested resource doesn't exist — generic case; several endpoints use a more specific *_not_found code instead (see below) where one exists.
conflict 409 The request conflicts with the device's current state (e.g. deleting a bundle that's on screen, scanning Wi-Fi while AP mode is active).
internal 500 Unexpected failure inside displayd. If you see this reliably for a given call, it's worth filing an issue with the request and the device logs (GET /device/logs).
not_implemented 501 The endpoint exists in the contract but isn't wired up in this firmware build yet (temporary — incremental rollout during early milestones).
bad_frame — (serial only) A line on the serial link failed to parse as NDJSON. The stream stays usable; just resend a well-formed line.

Content bundles

Code HTTP status When you get it
content_not_found 404 POST /content/{name}/check or an implicit lookup references a bundle name that has never been uploaded.
bundle_in_use 409 DELETE /content/{name} on a bundle that's currently displayed — switch the display away first.
bundle_invalid 400 PUT /content/{name} upload failed validation: not a zip (wrong magic bytes), a CRC-32 mismatch on some entry, an absolute or ..-traversing entry path, a symlink entry, or no index.html at the archive root. Nothing is stored; any existing bundle of that name is left untouched.

Display

Code HTTP status When you get it
screen_not_found 400 The ?screen= selector (or display.screens.<id> config key) doesn't match a present output. v1 renders a single screen — omit the parameter, or use exactly the primary screen's id from GET /display/screens.
unavailable 503 A capability is implemented but its backend is currently absent/unusable — e.g. POST /display/screen on a display without HDMI-CEC support.

Page channel

Code HTTP status When you get it
page_channel_closed 409 POST /page/message while the currently displayed page never opened a page channel (e.g. a plain remote page that doesn't load display-client.js). Check GET /page/status first if you're unsure.

Operations

Code HTTP status When you get it
operation_not_found 404 GET /operations/{id} with an unknown or expired operation id.

Update (OTA)

Code HTTP status When you get it
update_verify_failed — (surfaced via the Operation.error field, state: failed) The RAUC bundle's signature or content verification failed — the bundle is rejected, the previously installed slot is untouched.
update_in_progress 409 POST /update/apply (or a new PUT /update/bundle) while an install/apply is already running.

Device lifecycle

Code HTTP status When you get it
confirm_mismatch 400 POST /device/factory-reset with a confirm value that doesn't equal the device's serial number. This is a safety check, not a permission check — get the serial from GET /device.

NFC (spec 10)

Code HTTP status When you get it
nfc_disabled 503 Returned by every /nfc/* endpoint while config.nfc.enabled is false (the default). There is no read-only degraded mode — check GET /config before assuming the surface is live.
invalid_state 409 A tag_handle used in transceive/ndef/write is no longer valid — the tag left the reader's field, or the reader itself was unplugged, since the nfc_tag event that gave you the handle.
nfc_tag_unsupported 409 The present tag can't perform the requested operation: not ISO-DEP for transceive, or not NDEF-writable / capacity exceeded for ndef/write.

404 not_found is also used under the nfc tag, for an unknown reader_id or tag_handle that was never issued.

Wi-Fi & Bluetooth (spec 12)

Code HTTP status When you get it
wifi_unsupported 503 Returned by every /wifi/* endpoint on a board with no integrated Wi-Fi radio (GET /device's capabilities.wifi: false — the virt/generic model profiles).
wifi_disabled 503 Returned by every /wifi/* endpoint when the board has the radio but config.wifi.enabled is false.
bluetooth_unsupported 503 Returned by every /bluetooth/* endpoint on a board with no integrated Bluetooth radio (capabilities.bluetooth: false).
bluetooth_disabled 503 Returned by every /bluetooth/* endpoint when the board has the radio but config.bluetooth.enabled is false.

Both surfaces are double-gated (capability, then config) with no in-between state — see the Wireless guide. not_found (404) is also used for an unknown ssid in POST /wifi/connect or an unknown address in POST /bluetooth/pair/remove; conflict (409) is used for e.g. scanning Wi-Fi while AP mode is active, or Bluetooth operations while the adapter is powered off.

Debugging tip

If a code you're hitting isn't self-explanatory from message alone, GET /device/logs?limit=50 (or subscribe to the log topic — see Events) usually has the underlying reason logged at the point of failure.