Skip to content

Configuration

The device keeps one persistent configuration document, versioned and validated, covering display behavior, audio, network proxy, the management UI, device-access bridge policy, NFC, Wi-Fi/Bluetooth and more. This page covers how to read and change it; the other guides link back here for the fields specific to their topic.

Reading the current configuration

curl http://10.99.0.1/api/v1/config

Returns the full effective configuration as JSON. A representative (default) document looks like:

{
  "schema": 1,
  "device": { "name": "front-desk-1" },
  "display": {
    "rotation": 0, "forced_mode": null, "resume_on_boot": false,
    "screens": {
      "HDMI-A-1": { "rotation": 0, "forced_mode": null, "url": null, "browser": { "backend": "wpe" } }
    }
  },
  "audio":   { "output": "hdmi", "volume": 80, "mute": false },
  "browser": { "backend": "wpe" },
  "proxy":   { "url": null, "insecure": false },
  "waiting": { "show_status": true },
  "content": { "startup_bundle": "" },
  "ui":      { "enabled": true },
  "splash":  { "boot": { "enabled": true }, "shutdown": { "enabled": true } },
  "devices": {
    "origins": [],
    "hid":    { "enabled": false, "allow": [] },
    "serial": { "enabled": false, "allow": [] },
    "usb":    { "enabled": false, "allow_driver_detach": false, "allow": [] }
  },
  "nfc": {
    "enabled": false, "backend": "auto", "origins": [],
    "i2c": { "bus": 1, "address": 36 }, "spi": { "bus": 0, "cs": 0 },
    "poll": { "interval_ms": 250 }, "feedback": { "on_detect": true }
  },
  "wifi": {
    "enabled": false, "mode": "station", "country": null, "networks": [],
    "ap": { "ssid": "", "psk": "", "band": "2.4", "channel": 0 }
  },
  "bluetooth": { "enabled": false, "name": null, "discoverable": false }
}

Web UI: Config page shows and edits this document.

Changing configuration

Send only the fields you want to change as a partial JSON object to PATCH:

curl -X PATCH http://10.99.0.1/api/v1/config \
  -H 'Content-Type: application/json' \
  -d '{"device": {"name": "checkout-3"}, "audio": {"volume": 60}}'

The response echoes the resulting full configuration plus an applied map, keyed by dotted field path (e.g. "device.name", "audio.volume"), with one of two values per field you touched:

  • live — the change took effect immediately, no reboot needed.
  • reboot_required — the value was persisted and will take effect starting from the next boot; the currently running device is unaffected until then.

This is an honesty guarantee: the device never claims a change is live unless it genuinely is. Fields that are live today: device.name, display.rotation (for the primary/rendered screen), audio.volume/audio.mute, ui.enabled, the devices.* bridge policy, nfc.*, and wifi.*/ bluetooth.* (daemon start/stop/reconfigure happens immediately). Everything else — including display.forced_mode, per-screen rotation on non-primary screens, audio.output, and content.startup_bundle — is reboot_required in the current release; the persisted value is always correct and takes effect at the next boot regardless.

An invalid patch (unknown field, bad enum value, value out of range, a schema that doesn't match the current one) is rejected wholesale with 400 bad_request — nothing from that request is applied, so you never end up with a half-written config.

Every successful PATCH also emits a config_changed event ({paths, config}) on the event stream (see Events), so other subscribed hosts, the waiting page, and the management UI all stay in sync without polling.

Web UI: Config page → edit a field → Save; the page shows whether each change applied live or needs a reboot.

Device name

curl -X PATCH http://10.99.0.1/api/v1/config -d '{"device": {"name": "checkout-3"}}'

Applies live (sets the device's hostname immediately) and is what shows up in GET /device, the waiting page's status strip, and the management UI header.

Display settings

display.rotation0 | 90 | 180 | 270, degrees clockwise. Applies live for the primary screen (pushed to the browser without a restart) but the change is only guaranteed to survive future reboots once persisted — which PATCH already does. For 90°/270°, content on the rotated screen must be authored for the swapped logical viewport (window.innerWidth/innerHeight do not auto-swap; only the visual output is rotated).

display.forced_mode — force a specific video mode instead of the display's preferred EDID mode, as "<connector>:<WxH@R>" (e.g. "HDMI-A-1:1920x1080@60"). reboot_required — the mode is applied at boot before the browser starts.

display.resume_on_boot — if true, the device restores whatever was on screen (URL or bundle) across a reboot instead of returning to its normally-configured target. reboot_required.

display.screens.<connector-id> — per-screen overrides (rotation, forced mode, startup URL, browser backend) for boards with more than one output. In the current release the device still renders on a single (primary) screen only, so per-screen settings for a secondary connector are accepted and persisted but not yet rendered; use GET /display/screens to see the connector ids the device recognizes on your board.

See Displaying content → change rotation for a worked example.

Startup target

content.startup_bundle — name of a content bundle to show at boot, default "" (none). Only consulted at boot time; reboot_required; doesn't navigate the currently running display (POST /display/bundle does that live).

Boot target resolution, in order (each step falls through to the next on failure):

  1. display.resume_on_boot is true and a saved state exists → restore it.
  2. content.startup_bundle is set, and that bundle exists and validates → show it.
  3. The primary screen's configured startup url (display.screens.<primary>.url) → navigate there.
  4. Otherwise → the waiting page.

With everything at defaults, the device always boots to the waiting page.

Audio

See the dedicated Audio guide for audio.volume/audio.mute/audio.output.

Proxy

curl -X PATCH http://10.99.0.1/api/v1/config \
  -d '{"proxy": {"url": "http://10.99.0.2:3128", "insecure": false}}'

proxy.url is used by the kiosk browser (and the device's own outbound requests) for all network access — useful when the device only has USB connectivity and the host shares its own network access via a local proxy. proxy.insecure: true disables TLS certificate verification through the proxy; the supported alternative to that is uploading a trusted CA chain (PUT /api/v1/files/certificates/ca) so certificates validate normally.

Management UI toggle

curl -X PATCH http://10.99.0.1/api/v1/config -d '{"ui": {"enabled": false}}'

ui.enabled (default true) controls whether the device serves the built-in management UI at http://10.99.0.1/. Applies live — flipping it off immediately makes / return 404 while /api/v1/* keeps working; flipping it back on restores the UI without a restart.

Device-access bridge (HID/USB/serial passthrough)

devices.origins and the per-domain devices.hid/devices.serial/devices.usb blocks control which page origins, and which specific peripherals plugged into the Pi's USB-A host ports, a displayed page may reach through the navigator.hid/navigator.usb/navigator.serial polyfills. All three domains are disabled by default. This is a Pi 4/Pi 5-only capability (no free USB host port on the Zero 2 W); the block is accepted but inert there. Example enabling a specific USB-serial scanner for a trusted page origin:

curl -X PATCH http://10.99.0.1/api/v1/config -d '{
  "devices": {
    "origins": ["https://pos.example.com"],
    "serial": {"enabled": true, "allow": [{"usbVendorId": 1027, "usbProductId": 24577}]}
  }
}'

Applies live (re-evaluated on the page's next requestDevice()/open() call). This is a specialized capability outside the scope of this page; the fields themselves are documented in the configuration reference.

NFC, Wi-Fi and Bluetooth

These have their own dedicated guides, since enabling them involves more than a single config field: NFC, Wireless (Wi-Fi/Bluetooth).

Splash images

See Boot and shutdown splash for the splash.boot.enabled/splash.shutdown.enabled toggles and how to upload the images themselves.

Odoo IoT Box emulation

See Odoo IoT Box emulation for the odoo block.

Schema versioning

The configuration document carries a "schema" integer. You don't need to set it when patching — the device rejects a patch whose schema doesn't match the current version, which exists so older tooling fails loudly instead of writing a config it doesn't fully understand, rather than silently corrupting fields it doesn't recognize.

See also