Files
LEDMatrix/docs/REST_API_REFERENCE.md
T
ChuckandClaude Opus 5.5 3849be9669 docs: drop the skin system and src/base_classes from the docs
Deletes docs/SKIN_SYSTEM.md and docs/CREATING_SKINS.md and every link to
them (docs/README.md, README.md, PLUGIN_DEVELOPMENT_GUIDE.md, the /skins
section of REST_API_REFERENCE.md), the skin section of CLAUDE.md and the
term in PRODUCT.md. SPORTS_UNIFICATION.md now says src/base_classes was
removed and shared code lives in src/common, in the Layering section and
the view-model-contract rule. Other docs stop pointing at the removed
package. CHANGELOG records both removals under Unreleased.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 10:01:14 -04:00

2101 lines
46 KiB
Markdown

# LEDMatrix REST API Reference
Reference for the REST API served by the LEDMatrix web interface.
**Base URL**: `http://your-pi-ip:5000/api/v3`
Most endpoints answer JSON in this envelope:
```json
{
"status": "success" | "error",
"data": { ... },
"message": "Optional message"
}
```
Not every endpoint follows it exactly. Where a response puts fields at the
top level instead of under `data` (install-from-url, registry-from-url, the
auth endpoints, upload endpoints, `system/git-info`, `system/check-update`),
the entry below says so.
## Table of Contents
- [Configuration](#configuration)
- [Display Control](#display-control)
- [Plugins](#plugins)
- [Plugin Store](#plugin-store)
- [System](#system)
- [Backup and Restore](#backup-and-restore)
- [Fonts](#fonts)
- [Cache](#cache)
- [WiFi](#wifi)
- [Streams](#streams)
- [Logs](#logs)
- [Error tracking](#error-tracking)
- [Health and Status](#health-and-status)
- [Schedule (dim/power)](#schedule-dimpower)
- [Integrations](#integrations)
- [Plugin-specific endpoints](#plugin-specific-endpoints)
- [Starlark Apps](#starlark-apps)
> The API blueprint is the `api_v3` package in
> `web_interface/blueprints/api_v3/` (one module per area: `config.py`,
> `display.py`, `plugins.py`, `system.py`, `backup.py`, `fonts.py`,
> `misc.py`, `wifi.py`, `starlark.py`). `web_interface/app.py` registers it
> at `/api/v3` (`app.register_blueprint(api_v3, url_prefix='/api/v3')`).
> The three SSE endpoints (`/api/v3/stream/*`) are defined directly on the
> Flask app in `app.py` (`stream_stats`, `stream_display`, `stream_logs`).
> `test/fixtures/api_v3_url_map.json` is the canonical list of blueprint
> routes (116 URL rules); a test fails if the code and that fixture differ.
---
## Configuration
### Get Main Configuration
**GET** `/api/v3/config/main`
Return `config/config.json`. Fields whose names look like credentials
(`api_key`, `token`, `password`, `secret`, ...) are blanked to `""` in the
response; `config/config_secrets.json` values are never included.
**Response**:
```json
{
"status": "success",
"data": {
"timezone": "America/New_York",
"location": {
"city": "New York",
"state": "NY",
"country": "US"
},
"display": { ... },
"plugin_system": { ... }
}
}
```
### Save Main Configuration
**POST** `/api/v3/config/main`
Update the main configuration. Accepts JSON (`Content-Type: application/json`)
or form data. The body uses the web UI's flat field names, which the handler
maps into the nested config:
| Fields | Stored at |
|--------|-----------|
| `timezone`, `city`, `state`, `country` | `timezone`, `location.*` |
| `web_display_autostart`, `auto_update_enabled` | `web_display_autostart`, `auto_update.enabled` |
| `plugins_directory` (and the unused legacy flags `auto_discover`, `auto_load_enabled`, `development_mode`, stored only when sent) | `plugin_system.*` |
| `target_fps` (30-200) | `target_fps` |
| `rows`, `cols`, `chain_length`, `parallel`, `brightness`, `hardware_mapping`, `pwm_bits`, `led_rgb_sequence`, `panel_type`, `pixel_mapper_config`, `disable_hardware_pulsing`, `inverse_colors`, `show_refresh_rate`, ... | `display.hardware.*` |
| `gpio_slowdown`, `rp1_rio` | `display.runtime.*` |
| `use_short_date_format` | `display.use_short_date_format` |
| `max_dynamic_duration_seconds` | `display.dynamic_duration.max_duration_seconds` |
| `double_sided_*`, `vegas_*`, `sync_*` | `display.double_sided`, `display.vegas_scroll`, `sync` |
| `<name>_duration`, `default_duration`, `duration__<mode>` | `display.display_durations.*` |
| `plugin_rotation_order` (list of plugin ids) | `display.plugin_rotation_order` |
Any other top-level key is deep-merged into the config as given.
A JSON body changes only the keys it contains; everything else keeps its
stored value. (Form posts from the web UI send every field of a tab, and
there an unchecked checkbox — which the browser omits — is saved as
`false`.)
**Request Body** (JSON):
```json
{
"timezone": "America/New_York",
"city": "New York",
"brightness": 90
}
```
**Response**:
```json
{
"status": "success",
"message": "Configuration saved successfully"
}
```
Invalid values (e.g. an out-of-range `target_fps`, a hardware option the
Raspberry Pi 5 driver cannot use) are rejected with `400` and nothing is
saved.
### Get Schedule Configuration
**GET** `/api/v3/config/schedule`
Retrieve the current on/off schedule.
**Response**:
```json
{
"status": "success",
"data": {
"enabled": true,
"mode": "global",
"start_time": "07:00",
"end_time": "23:00"
}
}
```
**Per-day mode response**:
```json
{
"status": "success",
"data": {
"enabled": true,
"mode": "per-day",
"days": {
"monday": {
"enabled": true,
"start_time": "07:00",
"end_time": "23:00"
},
"tuesday": { ... }
}
}
}
```
### Save Schedule Configuration
**POST** `/api/v3/config/schedule`
Replace the schedule configuration.
**Request Body** (Global mode):
```json
{
"enabled": true,
"mode": "global",
"start_time": "07:00",
"end_time": "23:00"
}
```
**Request Body** (Per-day mode, flat form-field names):
```json
{
"enabled": true,
"mode": "per-day",
"monday_enabled": true,
"monday_start": "07:00",
"monday_end": "23:00",
"tuesday_enabled": true,
"tuesday_start": "08:00",
"tuesday_end": "22:00"
}
```
A day whose `<day>_enabled` key is absent counts as enabled, with default
times `07:00`-`23:00`. At least one day must be enabled.
**Response**:
```json
{
"status": "success",
"message": "Schedule configuration saved successfully"
}
```
### Get Secrets Configuration
**GET** `/api/v3/config/secrets`
Retrieve `config/config_secrets.json` with every set value replaced by eight
bullet characters (`"••••••••"`). Empty values and `YOUR_*` placeholders
are returned as-is, so a client can tell "set" from "not set".
**Response**:
```json
{
"status": "success",
"data": {
"ledmatrix-weather": {
"api_key": "••••••••"
}
}
}
```
### Save Raw Configuration
**POST** `/api/v3/config/raw/main`
Replace `config/config.json` with the JSON body (advanced use only).
**POST** `/api/v3/config/raw/secrets`
Save the secrets file (advanced use only). Masked values (`"••••••••"`)
and blank strings in the body are dropped, and the rest is merged onto the
stored secrets, so posting back the GET response unchanged changes nothing.
A secret cannot be cleared by blanking it here.
---
## Display Control
### Get Current Display
**GET** `/api/v3/display/current`
Get the latest display snapshot as a base64 PNG (`image` is `null` when no
snapshot is available).
**Response**:
```json
{
"status": "success",
"data": {
"timestamp": 1234567890.123,
"width": 128,
"height": 32,
"image": "base64_encoded_image_data"
}
}
```
### Get Current Display Status
**GET** `/api/v3/display/current-status`
The mode and plugin the display service is currently showing, as published
by the display process (stale after 120 seconds).
**Response**:
```json
{
"status": "success",
"data": {
"mode": "nfl_live",
"plugin_id": "football-scoreboard",
"last_updated": 1234567890.123
}
}
```
When nothing has been published, every field is `null`.
### List Display Modes
**GET** `/api/v3/display/modes`
Every display mode that can be requested on-demand, with the plugin that owns
it. This is the list the force-display dialog offers.
Send the reported `plugin_id` alongside `mode` when starting an on-demand
display: `/display/on-demand/start` falls back to `find_plugin_for_mode` when
`plugin_id` is omitted, and that lookup only sees modes declared in a static
manifest — a plugin whose modes are generated (each installed Starlark app is
one) returns 404 there.
Triggers plugin discovery, which is otherwise lazy — so a caller that never
opens the dashboard still gets the full list.
**Query Parameters**:
- `include_disabled` (optional): `1` to include modes belonging to disabled
plugins. They are still valid on-demand targets — the controller enables the
plugin for the duration of the request — and are reported with
`"enabled": false`.
**Response**:
```json
{
"status": "success",
"data": {
"modes": [
{
"mode": "nfl_live",
"plugin_id": "football-scoreboard",
"plugin_name": "Football Scoreboard",
"name": "nfl_live",
"enabled": true
},
{
"mode": "clock-simple",
"plugin_id": "clock-simple",
"plugin_name": "Simple Clock",
"name": "Simple Clock",
"enabled": true
}
]
}
}
```
`name` is a label for a dropdown: a single-mode plugin's own name, or the raw
mode string for a multi-mode plugin, since there is no per-mode name anywhere.
### On-Demand Display Status
**GET** `/api/v3/display/on-demand/status`
Get the current on-demand display state.
**Response**:
```json
{
"status": "success",
"data": {
"state": {
"active": true,
"plugin_id": "football-scoreboard",
"mode": "nfl_live",
"duration": 45,
"pinned": true,
"status": "running",
"last_updated": 1234567890.123
},
"service": {
"active": true,
"returncode": 0,
"stdout": "active",
"stderr": ""
}
}
}
```
With no on-demand request, `state` is
`{"active": false, "status": "idle", "last_updated": null}`.
### Start On-Demand Display
**POST** `/api/v3/display/on-demand/start`
Request a specific plugin to display on-demand.
**Request Body**:
```json
{
"plugin_id": "football-scoreboard",
"mode": "nfl_live",
"duration": 45,
"pinned": true,
"start_service": true
}
```
**Parameters** (at least one of `plugin_id` and `mode` is required):
- `plugin_id` (string, optional): Plugin identifier
- `mode` (string, optional): Display mode name (plugin_id inferred if not provided)
- `duration` (number, optional): Duration in seconds (0 = until stopped)
- `pinned` (boolean, optional): Pin display (pause rotation)
- `start_service` (boolean, optional): (Re)start the display service so it picks the request up (default: true)
**Response**:
```json
{
"status": "success",
"data": {
"request_id": "uuid-here",
"plugin_id": "football-scoreboard",
"mode": "nfl_live",
"duration": 45,
"pinned": true,
"service": { "active": true, "returncode": 0, "stdout": "", "stderr": "" }
}
}
```
`service` is `null` when `start_service` is false.
### Stop On-Demand Display
**POST** `/api/v3/display/on-demand/stop`
Stop the current on-demand display.
**Request Body**:
```json
{
"stop_service": false
}
```
**Parameters**:
- `stop_service` (boolean, optional): Also stop the display service (default: false)
**Response**:
```json
{
"status": "success",
"data": {
"request_id": "uuid-here",
"service": null
}
}
```
---
## Plugins
### Get Installed Plugins
**GET** `/api/v3/plugins/installed`
List all installed plugins with their status and metadata.
**Response**:
```json
{
"status": "success",
"data": {
"plugins": [
{
"id": "football-scoreboard",
"name": "Football Scoreboard",
"version": "1.2.3",
"latest_version": "1.2.4",
"update_available": true,
"author": "ChuckBuilds",
"category": "Sports",
"description": "NFL and NCAA Football scores",
"tags": ["sports", "football", "nfl"],
"enabled": true,
"verified": true,
"loaded": true,
"state": "loaded",
"error_info": null,
"last_updated": "2025-01-15T10:30:00Z",
"last_commit": "abc1234",
"last_commit_message": "feat: Add live game updates",
"branch": "main",
"web_ui_actions": [],
"vegas_mode": null,
"vegas_content_type": null
}
]
}
}
```
### Get Plugin Configuration
**GET** `/api/v3/plugins/config?plugin_id=<plugin_id>`
Get a plugin's configuration, with schema defaults filled in for keys that
are not stored. `data` is the configuration object itself.
**Query Parameters**:
- `plugin_id` (required): Plugin identifier
**Response**:
```json
{
"status": "success",
"data": {
"enabled": true,
"display_duration": 30,
"favorite_teams": ["TB", "DAL"]
}
}
```
### Save Plugin Configuration
**POST** `/api/v3/plugins/config`
Update a plugin's configuration. With a JSON body, the keys in `config` are
merged onto the plugin's stored configuration: keys you do not send keep
their stored values. Fields the schema marks `"x-secret": true` are written
to `config/config_secrets.json` instead of `config.json`. The web UI posts
form data instead (`?plugin_id=` in the query string, fields as form fields).
**Request Body**:
```json
{
"plugin_id": "football-scoreboard",
"config": {
"display_duration": 30,
"favorite_teams": ["TB", "DAL"]
}
}
```
**Response**:
```json
{
"status": "success",
"message": "Plugin football-scoreboard configuration saved successfully"
}
```
A config that fails schema validation is rejected with `400` and nothing is
saved.
### Get Plugin Schema
**GET** `/api/v3/plugins/schema?plugin_id=<plugin_id>`
Get the JSON schema for a plugin's configuration. A plugin without a
`config_schema.json` gets a minimal default schema.
**Query Parameters**:
- `plugin_id` (required): Plugin identifier
**Response**:
```json
{
"status": "success",
"data": {
"schema": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean",
"default": true
},
"display_duration": {
"type": "number",
"minimum": 1,
"maximum": 300
}
}
}
}
}
```
### Reset Plugin Configuration
**POST** `/api/v3/plugins/config/reset`
Reset a plugin's configuration to its schema defaults.
**Request Body**:
```json
{
"plugin_id": "football-scoreboard",
"preserve_secrets": true
}
```
**Response**:
```json
{
"status": "success",
"message": "Plugin football-scoreboard configuration reset to defaults",
"data": { "config": { ... } }
}
```
### Toggle Plugin
**POST** `/api/v3/plugins/toggle`
Enable or disable a plugin. A `plugin_id` of the form `starlark:<app_id>`
toggles a Starlark app.
**Request Body**:
```json
{
"plugin_id": "football-scoreboard",
"enabled": true
}
```
**Response**:
```json
{
"status": "success",
"message": "Plugin football-scoreboard enabled successfully"
}
```
### Install Plugin
**POST** `/api/v3/plugins/install`
Install a plugin from the plugin store.
**Request Body**:
```json
{
"plugin_id": "football-scoreboard",
"branch": "main"
}
```
`branch` is optional.
**Response** (queued; poll `/plugins/operation/<operation_id>`):
```json
{
"status": "success",
"data": { "operation_id": "uuid-here" },
"message": "Plugin football-scoreboard installation queued"
}
```
When the operation queue is unavailable the install runs synchronously and
the response has only a `message`.
### Uninstall Plugin
**POST** `/api/v3/plugins/uninstall`
Remove an installed plugin.
**Request Body**:
```json
{
"plugin_id": "football-scoreboard",
"preserve_config": false
}
```
**Response** (queued):
```json
{
"status": "success",
"data": { "operation_id": "uuid-here" },
"message": "Plugin uninstallation queued"
}
```
### Update Plugin
**POST** `/api/v3/plugins/update`
Update a plugin to the latest version. Runs synchronously.
**Request Body**:
```json
{
"plugin_id": "football-scoreboard"
}
```
**Response**:
```json
{
"status": "success",
"message": "Plugin football-scoreboard updated ...",
"data": {
"last_updated": "2025-01-15T10:30:00Z",
"commit": "abc1234..."
}
}
```
### Install Plugin from URL
**POST** `/api/v3/plugins/install-from-url`
Install a plugin directly from a GitHub repository URL. Runs synchronously.
**Request Body**:
```json
{
"repo_url": "https://github.com/user/ledmatrix-my-plugin",
"branch": "main",
"plugin_path": null
}
```
**Parameters**:
- `repo_url` (required): GitHub repository URL
- `branch` (optional): Branch name (default: `main`, then `master`)
- `plugin_path` (optional): Path within the repository, for monorepo plugins
- `plugin_id` (optional): Plugin id, for monorepo installations
**Response** (fields at the top level):
```json
{
"status": "success",
"message": "Plugin my-plugin installed successfully",
"plugin_id": "my-plugin",
"name": "My Plugin",
"branch": "main"
}
```
### Load Registry from URL
**POST** `/api/v3/plugins/registry-from-url`
Load a `plugins.json` registry from a GitHub repository URL.
**Request Body**:
```json
{
"repo_url": "https://github.com/user/ledmatrix-plugins"
}
```
**Response** (fields at the top level):
```json
{
"status": "success",
"plugins": [
{
"id": "plugin-1",
"name": "Plugin One",
"description": "..."
}
],
"registry_url": "https://github.com/user/ledmatrix-plugins"
}
```
### Get Plugin Health
**GET** `/api/v3/plugins/health`
Get health state for all installed plugins, keyed by plugin id.
**Response**:
```json
{
"status": "success",
"data": {
"football-scoreboard": {
"plugin_id": "football-scoreboard",
"circuit_state": "closed",
"consecutive_failures": 0,
"total_failures": 2,
"total_successes": 1500,
"success_rate": 99.87,
"last_success_time": 1234567890.123,
"last_failure_time": 1234560000.0,
"last_error": null,
"is_healthy": true,
"degraded": false,
"degraded_reason": null,
"circuit_opened_time": null,
"half_open_start_time": null
}
}
}
```
### Get Plugin Health (Single)
**GET** `/api/v3/plugins/health/<plugin_id>`
Health state for one plugin; `data` has the same fields as one entry above.
Answers `503` when health tracking is unavailable.
### Reset Plugin Health
**POST** `/api/v3/plugins/health/<plugin_id>/reset`
Reset health state for a plugin (manual recovery).
**Response**:
```json
{
"status": "success",
"message": "Health state reset for plugin football-scoreboard"
}
```
### Get Plugin Metrics
**GET** `/api/v3/plugins/metrics`
Get resource usage metrics for all installed plugins, keyed by plugin id.
**Response**:
```json
{
"status": "success",
"data": {
"football-scoreboard": {
"plugin_id": "football-scoreboard",
"memory_mb": 24.5,
"cpu_percent": 3.2,
"execution_time": 0.12,
"avg_execution_time": 0.1,
"min_execution_time": 0.05,
"max_execution_time": 0.9,
"call_count": 500,
"last_update_time": 1234567890.123,
"limits": {
"max_memory_mb": 50,
"max_cpu_percent": 50,
"max_execution_time": 5.0,
"warning_threshold": 0.8
}
}
}
}
```
`limits` (and usage percentages derived from it) appear only when limits
are configured for the plugin.
### Get Plugin Metrics (Single)
**GET** `/api/v3/plugins/metrics/<plugin_id>`
Metrics for one plugin; `data` has the same fields as one entry above.
### Reset Plugin Metrics
**POST** `/api/v3/plugins/metrics/<plugin_id>/reset`
Reset metrics for a plugin.
### Get/Set Plugin Limits
**GET** `/api/v3/plugins/limits/<plugin_id>`
Get a plugin's resource limits. `data` is `null` when none are configured.
**Response**:
```json
{
"status": "success",
"data": {
"max_memory_mb": 50,
"max_cpu_percent": 50,
"max_execution_time": 5.0,
"warning_threshold": 0.8
}
}
```
**POST** `/api/v3/plugins/limits/<plugin_id>`
Set a plugin's resource limits. The body replaces all four limits: a key you
omit is stored as no limit (`warning_threshold` defaults to `0.8`).
**Request Body**:
```json
{
"max_memory_mb": 50,
"max_cpu_percent": 50,
"max_execution_time": 5.0,
"warning_threshold": 0.8
}
```
### Get Plugin State
**GET** `/api/v3/plugins/state`
Get the state manager's record for every plugin, keyed by plugin id. Pass
`?plugin_id=<id>` for one plugin (`data` is then that record).
**Response**:
```json
{
"status": "success",
"data": {
"football-scoreboard": {
"plugin_id": "football-scoreboard",
"status": "loaded",
"enabled": true,
"version": "1.2.3",
"installed_at": "2025-01-15T10:30:00",
"last_updated": "2025-01-15T10:30:00",
"config_version": 1,
"metadata": {}
}
}
}
```
### Reconcile Plugin State
**POST** `/api/v3/plugins/state/reconcile`
Reconcile plugin state across config, disk and the state manager.
**Request Body** (optional):
```json
{
"force": false
}
```
**Response**:
```json
{
"status": "success",
"message": "...",
"data": {
"inconsistencies_found": 1,
"inconsistencies_fixed": 1,
"inconsistencies_manual": 0,
"inconsistencies": [
{"plugin_id": "...", "type": "...", "description": "...", "fix_action": "..."}
],
"fixed": [ ... ],
"manual_fix_required": [ ... ]
}
}
```
### Get Reconciliation Status
**GET** `/api/v3/plugins/reconciliation-status`
Result of the last startup reconciliation, as written by the display service.
**Response**:
```json
{
"status": "success",
"data": {
"done": true,
"unresolved": []
}
}
```
Before a run has finished, `data` is `{"done": false, "unresolved": []}`.
### Get Plugin Operation
**GET** `/api/v3/plugins/operation/<operation_id>`
Get status of a queued plugin operation (install, uninstall).
**Response**:
```json
{
"status": "success",
"data": {
"operation_id": "uuid-here",
"operation_type": "install",
"plugin_id": "football-scoreboard",
"parameters": {},
"status": "completed",
"progress": 100,
"message": "Installation completed successfully",
"error": null,
"result": { ... },
"created_at": "2025-01-15T10:30:00",
"started_at": "2025-01-15T10:30:01",
"completed_at": "2025-01-15T10:30:20"
}
}
```
### Get Operation History
**GET** `/api/v3/plugins/operation/history?limit=50`
Get the plugin operation audit log. `data` is a list.
**Query Parameters**:
- `limit` (optional): Maximum number of records (default: 50)
- `plugin_id` (optional): Only records for this plugin
- `operation_type` (optional): Only records of this type (`install`, `update`, `enable`, ...)
**Response**:
```json
{
"status": "success",
"data": [
{
"operation_id": "uuid-here",
"operation_type": "install",
"plugin_id": "football-scoreboard",
"timestamp": "2025-01-15T10:30:00",
"status": "success",
"user": null,
"details": null,
"error": null
}
]
}
```
### Clear Operation History
**DELETE** `/api/v3/plugins/operation/history`
Clear the operation audit log.
### Execute Plugin Action
**POST** `/api/v3/plugins/action`
Execute an action declared in the plugin manifest's `web_ui_actions`. See
[PLUGIN_WEB_UI_ACTIONS.md](PLUGIN_WEB_UI_ACTIONS.md).
**Request Body**:
```json
{
"plugin_id": "football-scoreboard",
"action_id": "refresh_games",
"params": {}
}
```
**Response** (fields at the top level; a script that prints JSON can return
its own object instead):
```json
{
"status": "success",
"message": "Action completed successfully",
"output": "script stdout"
}
```
### Upload Plugin Assets
**POST** `/api/v3/plugins/assets/upload`
Upload images for a plugin. Stored under
`assets/plugins/<plugin_id>/uploads/`.
**Request**: Multipart form data
- `plugin_id` (required): Plugin identifier
- `files` (required, repeatable, up to 10): PNG, JPEG, BMP or GIF images, 5 MB each, 50 MB total per plugin
**Response** (fields at the top level):
```json
{
"status": "success",
"uploaded_files": [
{
"id": "uuid-here",
"filename": "image_1700000000_abcd1234.png",
"path": "assets/plugins/football-scoreboard/uploads/image_1700000000_abcd1234.png",
"size": 1024,
"uploaded_at": "2025-01-15T10:30:00Z"
}
],
"total_files": 3
}
```
### Delete Plugin Asset
**POST** `/api/v3/plugins/assets/delete`
Delete an uploaded plugin image by its id (the `id` from upload or list).
**Request Body**:
```json
{
"plugin_id": "football-scoreboard",
"image_id": "uuid-here"
}
```
### List Plugin Assets
**GET** `/api/v3/plugins/assets/list?plugin_id=<plugin_id>`
List uploaded images for a plugin.
**Query Parameters**:
- `plugin_id` (required): Plugin identifier
**Response**:
```json
{
"status": "success",
"data": {
"assets": [
{
"id": "uuid-here",
"filename": "image_1700000000_abcd1234.png",
"path": "assets/plugins/football-scoreboard/uploads/image_1700000000_abcd1234.png",
"size": 1024,
"uploaded_at": "2025-01-15T10:30:00Z",
"original_filename": "logo.png"
}
]
}
}
```
### Authenticate Spotify
**POST** `/api/v3/plugins/authenticate/spotify`
Spotify OAuth for the music plugin (`ledmatrix-music`; the plugin is fixed,
not taken from the body). Two steps: call with an empty body to get the
authorization URL, then call again with the URL Spotify redirected to.
**Request Body** (step 2):
```json
{
"redirect_url": "http://127.0.0.1:8888/callback?code=..."
}
```
**Response** (step 1, fields at the top level):
```json
{
"status": "success",
"message": "Authorization URL generated",
"auth_url": "https://accounts.spotify.com/authorize?..."
}
```
Step 2 returns `status`, `message` and the script's `output`.
### Authenticate YouTube Music
**POST** `/api/v3/plugins/authenticate/ytm`
Run the music plugin's YouTube Music authentication script. No body. Returns
`status`, `message` and the script's `output`.
### Upload Calendar Credentials
**POST** `/api/v3/plugins/calendar/upload-credentials`
Upload the Google OAuth client file for the calendar plugin.
**Request**: Multipart form data
- `file` (required): `credentials.json` (JSON, max 1 MB)
### Authenticate Calendar
**POST** `/api/v3/plugins/calendar/authenticate`
Google OAuth for the calendar plugin, in two steps. Step 1 (no body) returns
the consent URL. Step 2 posts the URL Google redirected to (it fails to load
in the browser, but its address carries the authorization code):
```json
{
"redirect_url": "http://localhost/?code=..."
}
```
Requires `credentials.json` to have been uploaded first (`400` otherwise).
---
## Plugin Store
### List / Search Store Plugins
**GET** `/api/v3/plugins/store/list`
List plugins from the registry and saved repositories. The same endpoint
searches.
**Query Parameters**:
- `query` (optional): Text search over name, description, id
- `category` (optional): Category filter
- `tags` (optional, repeatable): Tag filter
- `fetch_commit_info` (optional): `false` to skip fetching commit metadata from GitHub (default: fetched)
**Response**:
```json
{
"status": "success",
"data": {
"plugins": [
{
"id": "football-scoreboard",
"name": "Football Scoreboard",
"author": "ChuckBuilds",
"category": "Sports",
"description": "NFL and NCAA Football scores",
"tags": ["sports"],
"stars": 0,
"verified": true,
"repo": "https://github.com/ChuckBuilds/ledmatrix-plugins",
"last_updated": "2025-01-15",
"last_updated_iso": "2025-01-15T10:30:00Z",
"last_commit": "abc1234",
"last_commit_message": "...",
"last_commit_author": "...",
"version": "1.2.3",
"branch": "main",
"default_branch": "main",
"plugin_path": "plugins/football-scoreboard"
}
]
}
}
```
### Get GitHub Status
**GET** `/api/v3/plugins/store/github-status`
Whether a GitHub token is configured and valid.
**Response**:
```json
{
"status": "success",
"data": {
"token_status": "valid",
"authenticated": true,
"rate_limit": 5000,
"message": "GitHub API authenticated",
"error": null
}
}
```
`token_status` is `none`, `valid` or `invalid`; `rate_limit` is the nominal
hourly limit (60 unauthenticated), not a live count.
### Refresh Plugin Store
**POST** `/api/v3/plugins/store/refresh`
Force refresh of the registry cache.
**Response**:
```json
{
"status": "success",
"message": "Plugin store refreshed",
"plugin_count": 42
}
```
### Get Saved Repositories
**GET** `/api/v3/plugins/saved-repositories`
Get the list of saved custom plugin repositories.
**Response**:
```json
{
"status": "success",
"data": {
"repositories": [
{
"url": "https://github.com/user/ledmatrix-plugins",
"name": "ledmatrix-plugins",
"type": "registry"
}
]
}
}
```
### Save Repository
**POST** `/api/v3/plugins/saved-repositories`
Save a custom plugin repository. Returns the updated list in
`data.repositories`.
**Request Body**:
```json
{
"repo_url": "https://github.com/user/ledmatrix-plugins",
"name": "Custom Plugins"
}
```
### Delete Saved Repository
**DELETE** `/api/v3/plugins/saved-repositories`
Remove a saved repository. Returns the updated list in `data.repositories`.
**Request Body**:
```json
{
"repo_url": "https://github.com/user/ledmatrix-plugins"
}
```
---
## System
### Get System Status
**GET** `/api/v3/system/status`
Get system status and metrics (cached for 10 seconds).
**Response**:
```json
{
"status": "success",
"data": {
"timestamp": 1234567890.123,
"uptime": "3d 4h",
"uptime_seconds": 273600,
"service_active": true,
"cpu_percent": 25.5,
"memory_used_percent": 45.2,
"memory_total_mb": 3794.0,
"memory_used_mb": 1715.0,
"memory_available_mb": 1900.0,
"cpu_temp": 45.0,
"disk_used_percent": 60.0,
"disk_total_gb": 29.0,
"disk_used_gb": 17.4
}
}
```
### Get System Version
**GET** `/api/v3/system/version`
Get LEDMatrix repository version.
**Response**:
```json
{
"status": "success",
"data": {
"version": "v2.4-10-g1234567"
}
}
```
### Check for Update
**GET** `/api/v3/system/check-update`
Whether `origin/main` has commits the checkout lacks. Cached briefly.
Fields at the top level (no envelope):
```json
{
"update_available": true,
"remote_sha": "abc123...",
"commits_behind": 3
}
```
When git cannot run the check, the response also carries
`"check_failed": true` and an `error` explaining why.
### Automatic Update Status
**GET** `/api/v3/system/auto-update`
Weekly automatic-update status for the General tab and the Overview banner:
`last_run`, `summary`, `status`, `next_due`, `alert`, `alert_id`,
`verifier_installed`, `setup_status`, `setup_message`, `verifying` (in
`data`).
**POST** `/api/v3/system/auto-update/dismiss`
Hide the current automatic-update alert until a new one replaces it.
```json
{
"alert_id": "..."
}
```
### Git Info
**GET** `/api/v3/system/git-info`
Branch, dirty state, recent commits and remote for the Tools tab. Fields at
the top level: `branch`, `dirty`, `status`, `recent_commits`, `remote_url`
(credentials scrubbed), `upstream`, `can_pull`.
### Git Branches
**GET** `/api/v3/system/git-branches`
Fetches `origin` and lists branches to switch to: `current`, `upstream`,
`local` (list), `remote_only` (list), at the top level.
### Execute System Action
**POST** `/api/v3/system/action`
Execute system-level actions. JSON or form data.
**Request Body**:
```json
{
"action": "restart_display_service"
}
```
**Available Actions**:
- `start_display`: Start the display service
- `stop_display`: Stop the display service
- `restart_display_service`: Restart the display service
- `restart_web_service`: Restart the web interface service
- `enable_autostart`: Enable display service autostart
- `disable_autostart`: Disable display service autostart
- `reboot_system`: Reboot the Raspberry Pi
- `shutdown_system`: Power off the Raspberry Pi
- `git_pull`: Update LEDMatrix from git (the Update button)
- `checkout_branch`: Switch branch; takes `branch` and optional `stash`
- `force_git_reset`: `git reset --hard origin/main`
- `install_base_requirements`: pip install `requirements.txt` and `web_interface/requirements.txt`
- `install_plugin_requirements`: pip install every plugin's `requirements.txt`
- `clear_pycache`: Delete `__pycache__` directories
**Response** (service actions):
```json
{
"status": "success",
"message": "Action completed"
}
```
A failed service action returns `"status": "error"` with `returncode` and
`stderr`. `git_pull` returns `message`, `restart_required` and
`dependency_failures`; the install actions return `output` or `details`.
---
## Backup and Restore
Backups are ZIP files kept in the backup export directory.
### Preview
**GET** `/api/v3/backup/preview`
Summary of what a new backup would include.
### List
**GET** `/api/v3/backup/list`
Stored backups, newest first. `data` is a list of
`{"filename", "size", "created_at"}`.
### Export
**POST** `/api/v3/backup/export`
Create a backup. Returns `{"status": "success", "filename": "..."}`.
### Validate
**POST** `/api/v3/backup/validate`
Check an uploaded backup and return its manifest in `data`.
**Request**: Multipart form data
- `backup_file` (required): the ZIP
### Restore
**POST** `/api/v3/backup/restore`
Restore an uploaded backup.
**Request**: Multipart form data
- `backup_file` (required): the ZIP
- `options` (optional): JSON object; keys `restore_config`, `restore_secrets`,
`restore_wifi`, `restore_fonts`, `restore_plugin_uploads`,
`reinstall_plugins` (each defaults to `true`; unknown keys are rejected)
A partial restore answers `500` with `"status": "error"`, a message listing
what did and didn't restore, and the result in `data`.
### Download
**GET** `/api/v3/backup/download/<filename>`
Download a stored backup.
### Delete
**DELETE** `/api/v3/backup/<filename>`
Delete a stored backup.
---
## Fonts
### Get Font Catalog
**GET** `/api/v3/fonts/catalog`
Fonts in `assets/fonts/`, keyed by file name without extension.
**Response**:
```json
{
"status": "success",
"data": {
"catalog": {
"press_start": {
"filename": "press_start.ttf",
"family_name": "Press Start 2P",
"display_name": "Press Start 2P",
"path": "assets/fonts/press_start.ttf",
"type": "ttf",
"is_system": true,
"scalable": true,
"native_size": null,
"metadata": { ... }
}
}
}
}
```
### Get Font Tokens
**GET** `/api/v3/fonts/tokens`
Get font size token definitions.
**Response**:
```json
{
"status": "success",
"data": {
"tokens": {
"xs": 6,
"sm": 8,
"md": 10,
"lg": 12,
"xl": 14,
"xxl": 16
}
}
}
```
### Upload Font
**POST** `/api/v3/fonts/upload`
Upload a custom font file. It is saved as `assets/fonts/<font_family><ext>`.
**Request**: Multipart form data
- `font_file` (required): `.ttf`, `.otf` or `.bdf`, max 10 MB
- `font_family` (required): name for the font (letters, numbers, `_`, `-`)
**Response** (fields at the top level):
```json
{
"status": "success",
"message": "Font custom_font uploaded successfully",
"font_family": "custom_font",
"filename": "custom_font.ttf",
"path": "assets/fonts/custom_font.ttf"
}
```
### Delete Font
**DELETE** `/api/v3/fonts/<font_family>`
Delete an uploaded font (`<font_family>` is the file name without
extension). System fonts answer `403`.
### Font Preview
**GET** `/api/v3/fonts/preview?font=<filename>&text=<sample>&size=12`
Render text in a font, for the web UI font picker. BDF fonts are not
previewed (`400`).
**Query Parameters**:
- `font` (required): font file name in `assets/fonts/` (e.g. `press_start.ttf`)
- `text` (optional): up to 100 characters (default `Sample Text 123`)
- `size` (optional): 4-72 (default 12)
- `bg`, `fg` (optional): hex colours without `#` (default `000000` / `ffffff`)
**Response**:
```json
{
"status": "success",
"data": {
"image": "data:image/png;base64,...",
"width": 140,
"height": 32
}
}
```
> Font overrides (`/api/v3/fonts/overrides`) were removed. Per-plugin font
> choices are made in each plugin's own settings.
---
## Cache
### List Cache Entries
**GET** `/api/v3/cache/list`
List cache files.
**Response**:
```json
{
"status": "success",
"data": {
"cache_files": [ ... ],
"cache_dir": "/var/cache/ledmatrix",
"total_files": 12
}
}
```
### Delete Cache Entry
**POST** `/api/v3/cache/delete`
Delete one cache entry by key. There is no clear-all option here; use
`scripts/utils/clear_cache.py --clear-all` on the Pi for that.
**Request Body**:
```json
{
"key": "weather_current_12345"
}
```
---
## WiFi
### Get WiFi Status
**GET** `/api/v3/wifi/status`
Get current WiFi connection status.
**Response**:
```json
{
"status": "success",
"data": {
"connected": true,
"ssid": "MyNetwork",
"ip_address": "192.168.1.100",
"signal": 70,
"ap_mode_active": false,
"auto_enable_ap_mode": true,
"last_connect_attempt": null
}
}
```
### Scan WiFi Networks
**GET** `/api/v3/wifi/scan`
Scan for available WiFi networks. `data` is a list. If AP mode is active it is
turned off for the scan and back on afterwards, and `message` says so.
**Response**:
```json
{
"status": "success",
"data": [
{
"ssid": "MyNetwork",
"signal": 70,
"security": "WPA2",
"frequency": 2437
}
]
}
```
### Connect to WiFi
**POST** `/api/v3/wifi/connect`
Connect to a WiFi network.
**Request Body**:
```json
{
"ssid": "MyNetwork",
"password": "mypassword"
}
```
**Response**: `"status": "success"` when connected; `"status": "pending"`
(with `data.ssid`) when the connection continues in the background — poll
`/wifi/status` and read `last_connect_attempt`. A wrong password answers
`400` with `"error_type": "wrong_password"`.
### Disconnect from WiFi
**POST** `/api/v3/wifi/disconnect`
Disconnect from current WiFi network.
### Enable Access Point Mode
**POST** `/api/v3/wifi/ap/enable`
Enable WiFi access point mode. Optional body `{"force": true}`.
### Disable Access Point Mode
**POST** `/api/v3/wifi/ap/disable`
Disable WiFi access point mode.
### Get Auto-Enable AP Setting
**GET** `/api/v3/wifi/ap/auto-enable`
**Response**:
```json
{
"status": "success",
"data": {
"auto_enable_ap_mode": true
}
}
```
### Set Auto-Enable AP
**POST** `/api/v3/wifi/ap/auto-enable`
**Request Body**:
```json
{
"auto_enable_ap_mode": true
}
```
### WiFi Radio
**GET** `/api/v3/wifi/radio`
Radio state: `data.enabled` (`null` if unknown), `data.ethernet_connected`,
`data.available`.
**POST** `/api/v3/wifi/radio`
Turn the WiFi radio on or off. Turning it off is refused unless Ethernet is
connected or `force` is true, so you don't cut off your own connection.
```json
{
"enabled": false,
"force": false
}
```
---
## Streams
Server-Sent Events, defined in `web_interface/app.py`. Each event is one
`data: <json>` line; idle connections get `: heartbeat` comments.
### System Statistics Stream
**GET** `/api/v3/stream/stats`
```
data: {"timestamp": 1234567890.1, "uptime": "Running", "service_active": true, "cpu_percent": 25.5, "memory_used_percent": 45.2, "memory_available_mb": 1900.0, "cpu_temp": 45.0, "disk_used_percent": 60.0, "power": {...}}
```
### Display Preview Stream
**GET** `/api/v3/stream/display`
```
data: {"timestamp": 1234567890.123, "width": 128, "height": 32, "image": "base64_data_here"}
```
### Service Logs Stream
**GET** `/api/v3/stream/logs`
Each event carries the latest journal lines for `ledmatrix` and
`ledmatrix-web` as one text block:
```
data: {"timestamp": 1234567890.123, "logs": "2025-01-15T10:30:00+0000 host python[123]: ..."}
```
---
## Logs
### Get Logs
**GET** `/api/v3/logs`
The last 100 journal lines for `ledmatrix.service` and
`ledmatrix-web.service`, as one text block. Takes no parameters.
**Response**:
```json
{
"status": "success",
"data": {
"logs": "2025-01-15T10:30:00+0000 host python[123]: Plugin loaded: football-scoreboard\n..."
}
}
```
---
## Error tracking
### Get Error Summary
**GET** `/api/v3/errors/summary`
Aggregated counts, detected patterns and recent errors across plugins and
core components.
### Get Plugin Errors
**GET** `/api/v3/errors/plugin/<plugin_id>`
Error health and statistics for one plugin.
### Clear Errors
**POST** `/api/v3/errors/clear`
Clear error records older than `max_age_hours` (default 24, 1-8760).
Returns `data.cleared_count`.
```json
{
"max_age_hours": 24
}
```
---
## Health and Status
### Health Check
**GET** `/api/v3/health`
Health of the web interface, display service, config file, plugin system and
display snapshot. `data.status` is `healthy` or `degraded`, with
`data.services` and `data.checks`.
### Hardware Status
**GET** `/api/v3/hardware/status`
LED matrix initialization result written by the display service at startup.
Before the service has written it, `data` is
`{"ok": null, "error": "Display service not yet started"}`.
### Sync Status
**GET** `/api/v3/sync/status`
Live multi-display sync status from the display process; before it has
written one, `data` is `{"role", "port", "state": "starting"}` from config.
---
## Schedule (dim/power)
### Get Dim Schedule
**GET** `/api/v3/config/dim-schedule`
Read the schedule that lowers brightness at configured times.
**Response**:
```json
{
"status": "success",
"data": {
"enabled": true,
"dim_brightness": 30,
"mode": "per-day",
"days": {
"monday": { "enabled": true, "start_time": "20:00", "end_time": "07:00" },
"tuesday": { ... }
}
}
}
```
In `global` mode, `start_time` and `end_time` sit at the top level instead
of `days`.
### Update Dim Schedule
**POST** `/api/v3/config/dim-schedule`
Replace the dim schedule. `dim_brightness` is 0-100 (default 30). In
`per-day` mode the days can be sent either as the `days` object that GET
returns, or as the web form's flat fields (`monday_enabled`,
`monday_start`, `monday_end`, ...). A day that is not sent counts as
enabled with default times `20:00`-`07:00`; at least one day must be
enabled.
---
## Integrations
### MQTT Bridge
**GET** `/api/v3/integrations/mqtt-bridge`
Home Assistant MQTT bridge service state and settings: `data.service`,
`data.config_exists`, `data.config_path`, `data.config` (password
omitted), `data.password_set`, `data.env_override_prefix`.
**PUT** `/api/v3/integrations/mqtt-bridge/config`
Write `integrations/mqtt_bridge/bridge_config.json`. Only the keys you send
change. The password is write-only: omit `mqtt_password` to keep it, send a
value to replace it, or send `"clear_password": true`. A password with
`mqtt_tls` off is refused unless `allow_insecure_mqtt` is true. Returns
`data.password_set` and `data.restart_required` (the bridge must be
restarted to pick up changes). See
[integrations/mqtt_bridge/README.md](../integrations/mqtt_bridge/README.md).
---
## Plugin-specific endpoints
A handful of endpoints belong to individual plugins.
### Calendar
**GET** `/api/v3/plugins/calendar/list-calendars`
List the calendars on the authenticated Google account. Used by the calendar
plugin's config UI. Returns `calendars` at the top level. The upload and
authenticate endpoints are under [Plugins](#upload-calendar-credentials).
### Of The Day
**POST** `/api/v3/plugins/of-the-day/json/upload`
Upload JSON data files (multipart field `files`) as Of-The-Day categories.
Returns `uploaded_files` and `total_files` at the top level.
**POST** `/api/v3/plugins/of-the-day/json/delete`
Delete an uploaded data file.
```json
{
"file_id": "category_name"
}
```
### Plugin Static Assets
**GET** `/api/v3/plugins/<plugin_id>/static/<path:file_path>`
Serve a static file from a plugin's directory. Used internally by the web UI
to render plugin previews and icons.
---
## Starlark Apps
The Starlark plugin lets you run [Tronbyt](https://github.com/tronbyt/apps)
Starlark apps on the matrix. These endpoints expose its UI.
### Status
**GET** `/api/v3/starlark/status`
Returns whether the Pixlet binary is installed and the Starlark plugin
is operational.
### Install Pixlet
**POST** `/api/v3/starlark/install-pixlet`
Download and install the Pixlet binary on the Pi.
### Apps
**GET** `/api/v3/starlark/apps` — list installed Starlark apps
**GET** `/api/v3/starlark/apps/<app_id>` — get app details
**DELETE** `/api/v3/starlark/apps/<app_id>` — uninstall an app
**GET** `/api/v3/starlark/apps/<app_id>/config` — get app config schema
**PUT** `/api/v3/starlark/apps/<app_id>/config` — update app config
**POST** `/api/v3/starlark/apps/<app_id>/render` — render app to a frame
**POST** `/api/v3/starlark/apps/<app_id>/toggle` — enable/disable app (`{"enabled": bool}`; omit to flip)
### Repository (Tronbyt community apps)
**GET** `/api/v3/starlark/repository/categories` — list categories
**GET** `/api/v3/starlark/repository/browse` — every app with metadata (filtering happens client-side; cached for 2 hours)
**POST** `/api/v3/starlark/repository/install` — install an app: `{"app_id": "...", "render_interval": 300, "display_duration": 15}`
### Upload custom app
**POST** `/api/v3/starlark/upload`
Upload a custom Starlark `.star` file as a new app. Multipart fields: `file`
(required, max 5 MB), `name`, `app_id`, `render_interval`,
`display_duration`.
### Editor
A Pixlet editing session for one app. The display is stopped while a session
runs.
**GET** `/api/v3/starlark/editor/apps` — apps the editor can open (`data.apps`, `data.apps_dir`, `data.pixlet_available`)
**GET** `/api/v3/starlark/editor/status` — `data.running`, plus `app_id`, `port`, `pid`, `started_at`, `timeout`, `seconds_remaining`, `host_bound` while running
**POST** `/api/v3/starlark/editor/start` — `{"app_id": "...", "timeout": 1800, "port": 8080}` (`timeout` and `port` optional)
**POST** `/api/v3/starlark/editor/stop` — end the session and restart the display
---
## Error Responses
Errors use one of two shapes. Most endpoints answer:
```json
{
"status": "error",
"message": "Error description",
"details": "Additional error details (optional)"
}
```
Endpoints built on the structured error helper add a code and category:
```json
{
"status": "error",
"error_code": "CONFIG_SAVE_FAILED",
"error_category": "configuration",
"message": "Error description",
"details": "optional",
"context": { },
"suggested_fixes": [ ]
}
```
**Common HTTP Status Codes**:
- `200`: Success
- `400`: Bad Request (invalid parameters)
- `403`: Forbidden (e.g. deleting a system font)
- `404`: Not Found (resource doesn't exist)
- `408`: Timed out (plugin actions, auth scripts)
- `500`: Internal Server Error
- `503`: Service Unavailable (feature not available)
---
## See Also
- [Plugin API Reference](PLUGIN_API_REFERENCE.md) - API for plugin developers
- [Plugin Development Guide](PLUGIN_DEVELOPMENT_GUIDE.md) - Complete plugin development guide
- [Web Interface README](../web_interface/README.md) - Web interface documentation