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 message — message 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.
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.
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.
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.
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.
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.
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.
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.
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.