REST API reference

Full /api/v1 HTTP reference for the Hypercolor daemon: the JSON envelope, every route group, and the concurrency model.

The Hypercolor daemon serves a REST API over /api/v1 on port 9420 by default. Every route group below is enumerated from the daemon’s own router (build_router() in crates/hypercolor-daemon/src/api/mod.rs), so this page is the contract, not a curated subset. The same daemon also speaks WebSocket, the CLI, and an MCP server; this page covers HTTP only.

🔗Base URL and surfaces 🎯

http://localhost:9420

Three paths sit outside the /api/v1 tree:

PathPurpose
/healthLiveness check, no auth, returns 200 OK when the daemon is up.
/previewStandalone canvas-preview HTML page.
/mcpMCP server (Streamable HTTP), mounted only when mcp.enabled is true.

Everything else lives under /api/v1. Axum 0.8 path parameters use brace syntax, so a device route is /api/v1/devices/{id}, not :id.

🔗Response envelope

Every JSON response, success or error, carries a meta block. Success responses put the payload under data; errors put it under error. The two keys never both appear.

{
  "data": {},
  "meta": {
    "api_version": "1.0",
    "request_id": "req_019b1f9a-3f4b-7c8d-a2e1-91b4c0d86a25",
    "timestamp": "2026-06-25T18:03:11.482Z"
  }
}

The meta fields are fixed by the daemon:

FieldShapeNotes
api_versionstring "1.0"The literal envelope version. It is unrelated to the v1 URL segment and never reads "v1".
request_idstring req_<uuid-v7>A req_ prefix plus a time-ordered UUID v7. Quote it when filing a bug or correlating logs.
timestampISO 8601 UTCMillisecond precision with a trailing Z.

Error bodies replace data with error:

{
  "error": {
    "code": "validation_error",
    "message": "brightness must be between 0.0 and 1.0",
    "details": null
  },
  "meta": {
    "api_version": "1.0",
    "request_id": "req_019b1f9a-3f4b-7c8d-a2e1-91b4c0d86a25",
    "timestamp": "2026-06-25T18:03:11.482Z"
  }
}

The code is a snake_case string that maps to an HTTP status. The full set:

codeHTTP status
bad_request400
unauthorized401
forbidden403
not_found404
conflict409
payload_too_large413
unsupported_media_type415
validation_error422
rate_limited429
internal_error500

validation_error is 422 Unprocessable Entity, not 400. A well-formed request that fails a business rule (out-of-range brightness, an effect that isn’t runnable) lands here, while a structurally malformed request is bad_request / 400.

🔗Authentication

Loopback clients are exempt from API keys, which is why the local CLI, TUI, and web UI work with no configuration. When you bind the daemon to a non-loopback address or configure a key, send it as a Bearer token:

Authorization: Bearer <your-api-key>

There are two keys: HYPERCOLOR_API_KEY grants control (writes), and HYPERCOLOR_READ_API_KEY grants read-only access. CORS allows loopback origins unconditionally; configured cors_origins are only honored once API auth is enabled. The auth and rate-limiting model is documented in full on the auth and security overview.

🔗Concurrency: revisions and If-Match

Scene-zone structural edits use optimistic concurrency. A GET on a scene’s zones returns a groups_revision and an ETag header carrying the same revision. Send that value back as If-Match on the mutating request. If the revision is stale, the daemon rejects the write with 412 Precondition Failed rather than clobbering a concurrent edit. The Studio zone editor relies on this to stay coherent across multiple clients.


🔗System

GET/health

Liveness check. Returns 200 OK when the daemon is running. No authentication, no envelope. Use this in your reconnect loop and readiness probes.

GET/api/v1/status

Aggregate system status: the running effect, connected device count, audio availability, global brightness, and live render-loop timing.

Response:

{
  "data": {
    "running": true,
    "version": "0.1.0",
    "device_count": 3,
    "effect_count": 59,
    "active_effect": "borealis",
    "global_brightness": 85,
    "audio_available": true,
    "render_loop": {
      "state": "running",
      "target_fps": 60,
      "capacity_fps": 60.0,
      "delivered_fps": 59.8,
      "actual_fps": 60.0
    }
  },
  "meta": {
    "api_version": "1.0",
    "request_id": "req_019b1f9a-3f4b-7c8d-a2e1-91b4c0d86a25",
    "timestamp": "2026-06-25T18:03:11.482Z"
  }
}

effect_count reflects whatever the registry holds at request time (native built-ins plus discovered HTML effects); treat it as live, not a fixed product number.

GET/api/v1/server

Stable server identity: instance ID, instance name, and version. This is the same identity advertised over discovery.

GET/api/v1/system/sensors

Latest hardware sensor snapshot: CPU temperature, GPU load, RAM usage, and raw component readings. These feed sensor-bound effect controls.

GET/api/v1/system/sensors/{label}

A single named sensor reading. Common labels: cpu_temp, gpu_load, ram_used.

🔗Effects

Browsing the effect catalog in the web UI

GET/api/v1/effects

List the effect catalog. Returns data.items (effect summaries) plus data.pagination. Supports the standard offset / limit query params.

Response:

{
  "data": {
    "items": [
      {
        "id": "borealis",
        "name": "Borealis",
        "description": "Aurora borealis with domain-warped fBm noise",
        "author": "Hypercolor",
        "category": "ambient",
        "source": "html",
        "runnable": true,
        "tags": ["ambient", "shader"],
        "version": "1.0.0",
        "audio_reactive": false
      }
    ],
    "pagination": {
      "offset": 0,
      "limit": 50,
      "total": 59,
      "has_more": false
    }
  },
  "meta": {
    "api_version": "1.0",
    "request_id": "req_019b1f9a-3f4b-7c8d-a2e1-91b4c0d86a25",
    "timestamp": "2026-06-25T18:03:11.482Z"
  }
}

The catalog combines around a dozen native Rust built-ins with the HTML/GLSL effects discovered on disk. Don’t hardcode the count; read pagination.total.

GET/api/v1/effects/{id}

Full detail for one effect, including its control definitions (types, ranges, defaults). The controls array is what a UI renders into sliders, color pickers, and dropdowns.

POST/api/v1/effects/{id}/apply

Apply an effect to the active output. Optionally override control defaults.

Request body (optional):

{
  "controls": {
    "speed": 7,
    "palette": "SilkCircuit"
  }
}

Response: the applied effect, the resolved control values, any layout binding, the resolved transition (cut, 0 today), and a warnings array.

GET/api/v1/effects/active

The currently active effect and its live control values.

PATCH/api/v1/effects/current/controls

Patch controls on the running effect. Changes take effect on the next frame. Note the path segment is current, not active.

Request body:

{
  "controls": {
    "speed": 3,
    "intensity": 90
  }
}
PUT/api/v1/effects/current/controls/{name}/binding

Bind one named control on the running effect to an input source (audio band, sensor reading, etc.) so it modulates live instead of holding a fixed value.

POST/api/v1/effects/current/reset

Reset every control on the running effect back to its default.

PATCH/api/v1/effects/{id}/controls

Patch controls on a specific effect by ID, whether or not it is the active one.

POST/api/v1/effects/stop

Stop the running effect. Output goes dark.

POST/api/v1/effects/rescan

Rescan the effects directory and pick up newly built effects without restarting the daemon. Call this after shipping an effect from the SDK.

POST/api/v1/effects/install

Install an effect from an uploaded file via multipart form upload, so a freshly built HTML bundle reaches the library without a manual file copy.

GET/api/v1/effects/{id}/cover

Cover image for one effect.

GET/api/v1/effects/active/cover

Cover image for the active effect.

GET/api/v1/effects/{id}/layout

Get the layout bound to an effect.

PUT/api/v1/effects/{id}/layout

Bind an effect to a spatial layout.

DELETE/api/v1/effects/{id}/layout

Clear an effect’s layout binding.

Effect screenshots are served statically under /api/v1/effects/screenshots/... from the bundled screenshot root.

🔗Devices

The devices panel in the web UI

GET/api/v1/devices

List discovered and connected devices. Returns data.items plus data.pagination.

Response:

{
  "data": {
    "items": [
      {
        "id": "razer-blackwidow-v4-001",
        "layout_device_id": "razer-blackwidow-v4-001",
        "name": "Razer BlackWidow V4",
        "status": "connected",
        "brightness": 100,
        "total_leds": 126,
        "zones": []
      }
    ],
    "pagination": {
      "offset": 0,
      "limit": 50,
      "total": 1,
      "has_more": false
    }
  },
  "meta": {
    "api_version": "1.0",
    "request_id": "req_019b1f9a-3f4b-7c8d-a2e1-91b4c0d86a25",
    "timestamp": "2026-06-25T18:03:11.482Z"
  }
}
GET/api/v1/devices/{id}

Full detail for one device: zones, LED layout, firmware version, attachment configuration.

PUT/api/v1/devices/{id}

Update device settings (name, brightness, zone assignments).

DELETE/api/v1/devices/{id}

Remove a device from tracking.

POST/api/v1/devices/discover

Trigger a discovery scan across every backend. Returns newly found devices.

POST/api/v1/devices/{id}/pair

Initiate pairing for a device that requires authentication (Hue link button, Nanoleaf hold-to-pair token). This is the credential path for network devices; see the per-vendor hardware guides for the timed pairing windows.

DELETE/api/v1/devices/{id}/pair

Forget a device’s stored pairing credentials.

POST/api/v1/devices/{id}/identify

Flash a device’s LEDs so you can spot it physically.

POST/api/v1/devices/{id}/zones/{zone_id}/identify

Flash one zone on a device to identify it.

POST/api/v1/devices/{id}/attachments/{slot_id}/identify

Flash one attachment slot’s LEDs to identify it.

GET/api/v1/devices/{id}/controls

Control surface for a device: fields, types, and current values.

GET/api/v1/devices/{id}/attachments

Attachment configuration for a device.

PUT/api/v1/devices/{id}/attachments

Update a device’s attachment configuration.

DELETE/api/v1/devices/{id}/attachments

Clear a device’s attachment configuration.

POST/api/v1/devices/{id}/attachments/preview

Preview attachment placement without persisting it.

GET/api/v1/devices/{id}/logical-devices

List logical-device segments carved out of one physical device.

POST/api/v1/devices/{id}/logical-devices

Create a logical-device segment on a physical device.

GET/api/v1/devices/metrics

Per-device output telemetry snapshot: frame counts, errors, latency.

The router also exposes /api/v1/devices/debug/queues and /api/v1/devices/debug/routing for inspecting output queue and routing state while debugging.

🔗Logical devices

Logical devices are user-defined LED-range segments carved out of a physical device so one strip can act as several addressable units.

GET/api/v1/logical-devices

List every logical-device segment across all physical devices.

GET/api/v1/logical-devices/{id}

Get one logical-device segment.

PUT/api/v1/logical-devices/{id}

Update a logical-device segment.

DELETE/api/v1/logical-devices/{id}

Delete a logical-device segment.

🔗Drivers

GET/api/v1/drivers

List registered driver modules with their ID, name, and connection state.

GET/api/v1/drivers/{id}/config

Configuration for one driver module.

GET/api/v1/drivers/{id}/controls

Control surface for one driver module: fields, types, current values.

🔗Displays and faces

Display devices are physical screens (AIO LCD modules, Ableton Push 2) that show full-screen HTML faces. See display faces for the authoring contract.

GET/api/v1/displays

List connected display devices.

GET/api/v1/displays/{id}/preview.jpg

A JPEG preview frame from a display device. Live frame streaming runs over the display_preview WebSocket channel.

GET/api/v1/displays/{id}/face

The active face configuration on a display device.

PUT/api/v1/displays/{id}/face

Set the face effect on a display device. Binds an HTML effect to the device in the active scene.

DELETE/api/v1/displays/{id}/face

Remove the face assignment from a display device.

PATCH/api/v1/displays/{id}/face/controls

Patch control values on a display’s active face.

PATCH/api/v1/displays/{id}/face/composition

Patch composition parameters (blend mode, z-order, opacity) for a face render group.

🔗Simulators

Virtual display simulators let you build and test face effects with no physical display attached.

GET/api/v1/simulators/displays

List simulated displays.

POST/api/v1/simulators/displays

Create a simulated display.

GET/api/v1/simulators/displays/{id}

Get one simulated display.

PATCH/api/v1/simulators/displays/{id}

Update a simulated display’s configuration.

DELETE/api/v1/simulators/displays/{id}

Delete a simulated display.

GET/api/v1/simulators/displays/{id}/frame

The latest composited frame from a simulated display.

🔗Attachments

Attachment templates describe physical accessories (keycaps, case panels, stands) that clip onto device slots and carry their own LED zones.

GET/api/v1/attachments/templates

List attachment templates (built-in and user-defined).

POST/api/v1/attachments/templates

Create a user-defined attachment template.

GET/api/v1/attachments/templates/{id}

Get one attachment template.

PUT/api/v1/attachments/templates/{id}

Update a user-defined attachment template.

DELETE/api/v1/attachments/templates/{id}

Delete a user-defined attachment template.

GET/api/v1/attachments/categories

List attachment categories (keycap-set, case-panel, stand, etc.).

GET/api/v1/attachments/vendors

List attachment vendors that have templates available.

🔗Control surfaces

Control surfaces expose typed fields and actions for dynamic device or driver configuration (WLED protocol selection, Hue bridge IP, and the like). The web UI reads these to render device-specific settings panels.

GET/api/v1/control-surfaces

List every registered control surface across devices and drivers.

GET/api/v1/control-surfaces/{surface_id}

Get one control surface with its current field values.

PATCH/api/v1/control-surfaces/{surface_id}/values

Apply typed field values to a control surface.

Request body:

{
  "fields": {
    "protocol": { "type": "enum", "value": "ddp" },
    "ip_address": { "type": "ip", "value": "10.0.0.50" }
  }
}
POST/api/v1/control-surfaces/{surface_id}/actions/{action_id}

Invoke a typed control-surface action (Discover, Sync, Reset, and so on).

🔗Scenes

Scenes are whole-rig configurations: the effects, zones, and assignments that define how your entire setup lights up. Switching scenes swaps the whole rig.

The scenes panel in the web UI

GET/api/v1/scenes

List defined scenes.

POST/api/v1/scenes

Create a named scene. New scenes are born with a default Primary zone, live mutation mode, and the engine’s default scene transition.

Request body:

{
  "name": "Late Night",
  "description": "Dim amber for late sessions",
  "enabled": true,
  "mutation_mode": "live"
}
GET/api/v1/scenes/active

The currently active scene.

GET/api/v1/scenes/{id}

One scene’s configuration.

PUT/api/v1/scenes/{id}

Update a scene.

DELETE/api/v1/scenes/{id}

Delete a scene.

POST/api/v1/scenes/{id}/activate

Activate a scene, applying its effects and controls with the configured transition.

POST/api/v1/scenes/deactivate

Deactivate the current scene, returning to the default free-running state.

🔗Scene zones

Zones are flexible partitions of the scene’s canvas. Each zone owns a set of device outputs and renders its own effect. Zones live under a scene; there is no top-level /zones collection.

Building zones in Studio

GET/api/v1/scenes/{id}/zones

List a scene’s zones. The response includes groups_revision and an ETag header carrying the same revision for optimistic concurrency.

POST/api/v1/scenes/{id}/zones

Create a zone in a scene. Send If-Match with the last seen groups_revision; a stale revision returns 412 Precondition Failed.

Request body:

{
  "name": "Desk",
  "color": "#80ffea"
}
GET/api/v1/scenes/{id}/zones/{zone_id}

Get one scene zone.

PATCH/api/v1/scenes/{id}/zones/{zone_id}

Update zone metadata. Set make_primary to make a zone the default output zone; that structural edit should carry If-Match.

Request body:

{
  "name": "Desk halo",
  "description": "Ambient strips behind the monitor",
  "brightness": 0.8,
  "enabled": true
}
DELETE/api/v1/scenes/{id}/zones/{zone_id}

Delete a zone. The default and display zones cannot be deleted through this route.

POST/api/v1/scenes/{id}/zones/{zone_id}/devices

Assign device outputs to a zone. Each item may reference an existing device-output ID or carry a full payload.

Request body:

{
  "device_zones": [
    { "id": "keyboard-left" }
  ]
}
DELETE/api/v1/scenes/{id}/zones/{zone_id}/devices/{device_zone_id}

Unassign one device output from a zone.

PUT/api/v1/scenes/{id}/zones/{zone_id}/layout

Update one zone’s spatial layout. The route takes a SpatialLayout payload and preserves the zone’s existing output roster, so it is for placement edits only; add or remove outputs through the device routes above. Structural edits should carry If-Match.

Request body:

{
  "id": "default-zone-layout",
  "name": "Default zone",
  "canvas_width": 640,
  "canvas_height": 480,
  "zones": [],
  "default_sampling_mode": { "type": "bilinear" },
  "default_edge_behavior": "clamp",
  "spaces": null,
  "version": 1
}
PATCH/api/v1/scenes/{id}/unassigned-behavior

Set how outputs not claimed by any zone should render.

Request body:

{
  "unassigned_behavior": "off"
}

Values are "off", "hold", or { "fallback": "<zone_uuid>" }.

🔗Scene layers

Each zone (render group) stacks layers: effects, faces, and media composited with a blend mode and opacity.

GET/api/v1/scenes/{id}/groups/{group_id}/layers

List the layers in a zone.

POST/api/v1/scenes/{id}/groups/{group_id}/layers

Add a layer to a zone.

PATCH/api/v1/scenes/{id}/groups/{group_id}/layers/order

Reorder the layers in a zone.

PUT/api/v1/scenes/{id}/groups/{group_id}/layers/{layer_id}

Update one layer (blend mode, opacity, transform, color, source binding).

DELETE/api/v1/scenes/{id}/groups/{group_id}/layers/{layer_id}

Delete a layer.

PATCH/api/v1/scenes/{id}/groups/{group_id}/layers/{layer_id}/controls

Patch the control values on one layer’s source effect.

POST/api/v1/scenes/{id}/layers/broadcast-media

Broadcast a media layer across the scene’s zones in one call.

🔗Profiles

Profiles save a full state snapshot (effect, controls, brightness, assignments) that you can restore later.

GET/api/v1/profiles

List saved profiles.

POST/api/v1/profiles

Create a profile from the current state.

Request body:

{
  "name": "Gaming"
}
GET/api/v1/profiles/{id}

Get one profile’s saved state.

PUT/api/v1/profiles/{id}

Update a profile.

DELETE/api/v1/profiles/{id}

Delete a profile.

POST/api/v1/profiles/{id}/apply

Apply a profile, restoring its effect, controls, and assignments.

🔗Layouts

Layouts define how the effect canvas maps onto physical LED positions, in normalized [0.0, 1.0] coordinates so effects stay resolution-independent.

GET/api/v1/layouts

List spatial layouts.

POST/api/v1/layouts

Create a spatial layout.

GET/api/v1/layouts/active

The active layout.

PUT/api/v1/layouts/active/preview

Preview a layout without applying it. Returns the zone-to-LED mapping that would result, so a UI can render it visually.

GET/api/v1/layouts/{id}

One layout’s configuration: device zones, positions, LED mappings.

PUT/api/v1/layouts/{id}

Update a layout.

DELETE/api/v1/layouts/{id}

Delete a layout.

POST/api/v1/layouts/{id}/apply

Apply a layout as the active spatial mapping.

🔗Library

The library holds favorites, presets, and playlists.

🔗Favorites

GET/api/v1/library/favorites

List favorited effects.

POST/api/v1/library/favorites

Add an effect to favorites.

Request body:

{
  "effect_id": "borealis"
}
DELETE/api/v1/library/favorites/{effect}

Remove an effect from favorites. The path key is the effect ID, not a favorite ID.

🔗Presets

GET/api/v1/library/presets

List saved presets (effect plus control-value combinations).

POST/api/v1/library/presets

Save the current effect and controls as a named preset.

GET/api/v1/library/presets/{id}

Get one preset.

PUT/api/v1/library/presets/{id}

Update a preset.

DELETE/api/v1/library/presets/{id}

Delete a preset.

POST/api/v1/library/presets/{id}/apply

Apply a preset.

🔗Playlists

GET/api/v1/library/playlists

List playlists.

POST/api/v1/library/playlists

Create a playlist of effects with transition timing.

GET/api/v1/library/playlists/active

The currently running playlist, if any.

GET/api/v1/library/playlists/{id}

Get one playlist.

PUT/api/v1/library/playlists/{id}

Update a playlist.

DELETE/api/v1/library/playlists/{id}

Delete a playlist.

POST/api/v1/library/playlists/{id}/activate

Start a playlist. Effects cycle on the playlist’s timing.

POST/api/v1/library/playlists/stop

Stop the running playlist.

🔗Settings and audio

GET/api/v1/settings/brightness

The current global brightness level.

PUT/api/v1/settings/brightness

Set global brightness, 0.0 to 1.0.

Request body:

{
  "brightness": 0.8
}
GET/api/v1/audio/devices

List available audio capture devices for reactive effects. Pick the monitor of your output, not a microphone, if you want lights to follow what’s playing.

🔗Screen capture

POST/api/v1/capture/source/pick

Open the platform picker so the user can choose a screen or window capture source for screen-reactive effects.

🔗Configuration

GET/api/v1/config

Show the full current configuration.

GET/api/v1/config/get?key=path.to.key

Get one configuration value by dotted key path.

POST/api/v1/config/set

Set a configuration value.

Request body:

{
  "key": "audio.enabled",
  "value": true
}
POST/api/v1/config/reset

Reset a configuration value to its default.

Request body:

{
  "key": "audio.device_name"
}

🔗Diagnostics

POST/api/v1/diagnose

Run system diagnostics: device connectivity, audio capture, effect-engine health, and configuration validity. This is the same check the diagnose CLI command and MCP tool run.

POST/api/v1/diagnose/memory

A memory diagnostics snapshot: daemon RSS (which includes the in-process Servo renderer), canvas buffer size, and allocation counters. Useful when chasing slow memory growth.

🔗Assets

User media (images, video) used by media layers.

GET/api/v1/assets

List media assets.

POST/api/v1/assets

Upload a media asset.

GET/api/v1/assets/{id}

Get asset metadata.

PUT/api/v1/assets/{id}

Update asset metadata.

DELETE/api/v1/assets/{id}

Delete an asset.

GET/api/v1/assets/{id}/blob

Fetch the raw asset bytes.

GET/api/v1/assets/{id}/thumbnail

Fetch the asset thumbnail.


🔗Where to go next

For the streaming side of the daemon (live frames, spectrum, preview canvases, and REST-over-WebSocket), see the WebSocket protocol. To drive the same surface from a shell or an agent, see the CLI reference and the Agents and MCP guide. The request and response body shapes for the devices, effects, scenes, and zones domains are defined once in hypercolor-types::api and shared by the daemon and both UIs.