Admin API: Plugins
Plugins extend HappyView with WebAssembly modules sourced from the official plugin registry or any URL serving a manifest.json. Most endpoints take a plugin manifest URL and load (or reload) the plugin in place — no restart needed. Encrypted plugin secrets require TOKEN_ENCRYPTION_KEY to be configured.
# All examples assume $TOKEN is an API key (hv_...)
AUTH="Authorization: Bearer $TOKEN"
List installed plugins
GET /admin/plugins
Requires plugins:read. Returns every loaded plugin with its source, required secrets, configuration status, and any pending updates from the official registry cache.
curl http://localhost:3000/admin/plugins -H "$AUTH"
Response: 200 OK
{
"encryption_configured": true,
"plugins": [
{
"id": "steam",
"name": "Steam",
"version": "1.2.0",
"source": "url",
"url": "https://example.com/plugins/steam/manifest.json",
"sha256": null,
"enabled": true,
"auth_type": "openid",
"required_secrets": [
{
"key": "PLUGIN_STEAM_API_KEY",
"name": "Steam Web API Key",
"description": "Get your API key at steamcommunity.com/dev/apikey"
}
],
"secrets_configured": true,
"loaded_at": null,
"update_available": false,
"latest_version": "1.2.0",
"pending_releases": []
}
]
}
secrets_configured is true if the plugin has no required secrets, or if a row exists for it in plugin_configs. update_available and pending_releases are populated from the cached official registry — call POST /admin/plugins/{id}/check-update to refresh them.
Preview a plugin before installing
POST /admin/plugins/preview
Requires plugins:create. Fetches and parses a manifest without installing the plugin, so the dashboard can show what it would register.
curl -X POST http://localhost:3000/admin/plugins/preview \
-H "$AUTH" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/plugins/steam/manifest.json" }'
Response: 200 OK
{
"id": "steam",
"name": "Steam",
"version": "1.2.0",
"description": "Import your Steam game library and playtime data.",
"icon_url": "https://example.com/steam-icon.png",
"auth_type": "openid",
"required_secrets": [
{ "key": "PLUGIN_STEAM_API_KEY", "name": "Steam Web API Key", "description": "..." }
],
"manifest_url": "https://example.com/plugins/steam/manifest.json",
"wasm_url": "https://example.com/plugins/steam/steam.wasm"
}
Returns 400 Bad Request if the manifest can't be fetched or parsed.
Install a plugin
POST /admin/plugins
Requires plugins:create. Fetches the manifest, downloads the WASM, registers the plugin, and persists it.
curl -X POST http://localhost:3000/admin/plugins \
-H "$AUTH" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/plugins/steam/manifest.json",
"sha256": "abc123..."
}'
| Field | Type | Required | Description |
|---|---|---|---|
url | string | yes | URL to the plugin's manifest.json |
sha256 | string | no | Optional sha256 of the WASM binary. If provided, install fails when the downloaded hash mismatches |
Response: 200 OK returning the same PluginSummary shape as the list endpoint. secrets_configured will be false if the plugin requires any secrets — call PUT /admin/plugins/{id}/secrets to configure them before the plugin can run.
List official plugins
GET /admin/plugins/official
Requires plugins:read. Returns the cached catalog of plugins from the official registry. The cache is refreshed periodically by the server; use POST /admin/plugins/{id}/check-update to force-refresh a single entry.
Response: 200 OK
{
"last_refreshed_at": "2026-04-13T11:00:00Z",
"plugins": [
{
"id": "steam",
"name": "Steam",
"description": "Import your Steam game library and playtime data.",
"icon_url": "https://example.com/steam-icon.png",
"latest_version": "1.2.0",
"manifest_url": "https://example.com/plugins/steam/manifest.json"
}
]
}
Remove a plugin
DELETE /admin/plugins/{id}
Requires plugins:delete. Unregisters the plugin from the runtime and deletes its row from the plugins table. Secrets stay in plugin_configs, so they're reused if you reinstall.
Response: 204 No Content. Returns 404 Not Found if no plugin with that id is loaded.
Reload a plugin
POST /admin/plugins/{id}/reload
Requires plugins:create. Re-fetches the plugin from its current source URL and re-registers it. Useful after publishing a new version of a plugin you host yourself.
The body is optional. To point the plugin at a new URL, pass:
{ "url": "https://example.com/plugins/steam/manifest.json" }
When a new URL is provided, the stored sha256 is cleared (the new version has its own hash). File-based plugins cannot be reloaded via this endpoint and return 400 Bad Request.
Response: 200 OK with the refreshed PluginSummary.
Check for plugin updates
POST /admin/plugins/{id}/check-update
Requires plugins:create. Forces a cache refresh for one plugin from the official registry, then returns the updated PluginSummary with update_available, latest_version, and pending_releases reflecting the latest catalog state.
Response: 200 OK with a PluginSummary.
Get plugin secrets
GET /admin/plugins/{id}/secrets
Requires plugins:read. Returns the plugin's configured secrets with values masked (last 4 characters shown for values longer than 8 characters, otherwise fully masked). Requires TOKEN_ENCRYPTION_KEY to be configured.
Response: 200 OK
{
"plugin_id": "steam",
"secrets": {
"PLUGIN_STEAM_API_KEY": "********ABCD"
}
}
Update plugin secrets
PUT /admin/plugins/{id}/secrets
Requires plugins:create. Encrypts the provided secret values with TOKEN_ENCRYPTION_KEY (AES-256-GCM) and upserts them into plugin_configs.
curl -X PUT http://localhost:3000/admin/plugins/steam/secrets \
-H "$AUTH" \
-H "Content-Type: application/json" \
-d '{
"secrets": {
"PLUGIN_STEAM_API_KEY": "your-new-api-key"
}
}'
Special handling:
- Values starting with
********are treated as masked placeholders and the existing encrypted value is preserved (so you canGETthenPUTwithout re-typing every secret). - Empty string values are not stored — use them to clear a secret.
Response: 204 No Content