REST API Reference
All REST endpoints are served by Hono route handlers under apps/web/server/routes/. Unless noted, request and response bodies are JSON.
System
GET /api/status
Server health and system overview.
Response:
{
"status": "ok",
"uptime": 3600,
"connectedDevices": 2,
"mediaStats": {
"totalFiles": 150,
"totalSize": 5368709120,
"byType": { "video": 40, "audio": 80, "image": 25, "document": 5 }
}
}
GET /api/ollama
Ollama connectivity check.
Response:
{ "status": "ok", "models": ["granite4:350m", "llava:latest"] }
Chat
POST /api/chat
Send a chat message and receive a streaming response.
Request:
{
"message": "Hello!",
"conversationId": "uuid",
"deviceId": "web-device"
}
Both conversationId and deviceId are optional. If conversationId is omitted, a new conversation is created.
Response: text/event-stream
The first line is metadata JSON, followed by streamed content:
{"conversationId":"uuid","messageId":"uuid"}
Hello! I'm your AI assistant. How can I help you today?
GET /api/chat/conversations
List conversations. Optional query: ?deviceId=uuid
Response:
[
{
"id": "uuid",
"deviceId": "uuid",
"title": "Weather discussion",
"createdAt": "2024-01-15T10:00:00Z",
"updatedAt": "2024-01-15T10:05:00Z"
}
]
GET /api/chat/conversations/[id]
Get a conversation with its messages.
POST /api/chat/conversations/[id]
Update conversation (e.g., rename).
Paired Devices
Phones, tablets, and TVs that have been paired with Open Genie. See also the IoT Devices section below for ESP32s and smart plugs.
GET /api/paired-devices
List all paired devices with connection status.
Response:
[
{
"id": "uuid",
"name": "iPhone 15",
"type": "phone",
"userId": "uuid",
"isConnected": true,
"lastSeen": "2024-01-15T10:00:00Z"
}
]
POST /api/paired-devices/pair
Generate a pairing code.
Response:
{
"code": "ABC123",
"expiresAt": "2024-01-15T10:10:00Z"
}
POST /api/paired-devices/verify
Verify a pairing code and register a device.
Request:
{
"code": "ABC123",
"deviceId": "uuid",
"deviceName": "My iPhone",
"deviceType": "phone"
}
Response:
{ "token": "eyJhbGciOiJIUzI1NiJ9..." }
GET /api/paired-devices/token
Generate a token directly (dev/admin use).
Query: ?deviceId=uuid&userId=uuid&deviceType=phone
IoT Devices
Hardware integrations — ESP32 music boxes, Tuya smart plugs, and Bluetooth audio sinks. See IoT Devices for architecture details.
GET /api/iot/devices
List all IoT devices with live state (connection status, plug state, BT sink, …).
Response:
[
{
"id": "uuid",
"name": "Living Room Music Box",
"kind": "esp32_music_box",
"externalId": "aabbccddeeff",
"isConnected": true,
"state": { "playing": false, "volume": 80, "btConnected": true },
"lastSeen": "2024-01-15T10:00:00Z"
}
]
GET /api/iot/devices/[id]
Get a single IoT device plus its 50 most recent events.
PATCH /api/iot/devices/[id]
Rename a device or update its config.
Request:
{ "name": "Kitchen Music Box" }
DELETE /api/iot/devices/[id]
Remove a device. Associated iot_events rows are set to NULL device reference (not deleted).
POST /api/iot/control
Send a direct action to a device.
Request:
{
"deviceId": "uuid",
"action": "play",
"songId": "media-file-uuid"
}
Actions: play, stop, volume (+ level: 0–100), bt_connect (+ mac), plug_on, plug_off.
POST /api/iot/command
Natural-language intent → action. The server classifies the command with Claude and routes it to the device.
Request:
{ "deviceId": "uuid", "text": "Play something relaxing" }
GET /api/iot/status
Live state snapshot.
Query: ?deviceId=uuid — single device; omit for all devices.
GET /api/iot/stream-song
Stream a PCM audio file to an ESP32. LAN-only.
Query: ?deviceId=uuid&songId=media-file-uuid
Response: audio/pcm binary stream.
GET /api/iot/events
Cursor-paginated list of iot_events rows, newest first.
Query parameters:
| Parameter | Type | Description |
|---|---|---|
deviceId | uuid | Filter by device |
event | string | Filter by event name |
level | string | info, warn, or error |
cursor | string | Pagination cursor (returned as nextCursor) |
limit | number | Items per page (default 100) |
Response:
{
"items": [
{
"id": "uuid",
"iotDeviceId": "uuid",
"deviceName": "Living Room Music Box",
"event": "play",
"detail": "Bohemian Rhapsody",
"level": "info",
"createdAt": "2024-01-15T10:00:00Z"
}
],
"nextCursor": "cursor-token"
}
Memory
GET /api/memory
List structured memory entries (facts).
Query parameters:
?category=family— Filter by category?q=birthday— Full-text search
Response:
{
"entries": [
{
"id": "uuid",
"category": "family",
"key": "spouse_birthday",
"value": "March 15",
"updatedAt": "2024-01-10T08:00:00Z"
}
]
}
POST /api/memory
Create or upsert a structured fact.
Request:
{
"category": "preferences",
"key": "coffee_order",
"value": "Oat milk latte"
}
DELETE /api/memory/[id]
Delete a fact by ID.
GET /api/memory/raw
Get raw file contents.
Response:
{
"soul": "# Soul\nYou are a helpful assistant...",
"memory": "# Memory\n## Family\n- **spouse**: Sarah"
}
PUT /api/memory/soul
Update the SOUL.md personality file.
Request:
{ "content": "# Soul\nYou are a witty, helpful assistant..." }
Memory Chunks (RAG)
Long-tail memory store used by the retrieval-augmented generation system. See Memory System for architecture details.
GET /api/memory/chunks
List chunks, optionally filtered or searched.
Query parameters:
?q=kitchen renovation— Semantic (vector) search; returns ranked results?personId=uuid— Filter by person attribution; omit for all chunks
Response (list):
{
"chunks": [
{
"id": "uuid",
"householdId": "uuid",
"personId": null,
"source": "chat_extract",
"category": "plans",
"title": "Kitchen renovation",
"content": "User is planning a kitchen renovation. Budget ~$40k, timeline Q3.",
"observedAt": "2024-06-01T14:00:00Z",
"pinned": false,
"promoted": false,
"embeddingModel": "nomic-embed-text"
}
]
}
Response (semantic search, ?q=...):
{
"chunks": [...],
"formatted": "## Relevant Memory\n- [Household, observed 2024-06-01]: User is planning..."
}
POST /api/memory/chunks
Create a chunk. The server embeds it automatically.
Request:
{
"source": "note",
"content": "User mentioned they are considering switching to a standing desk.",
"category": "preferences",
"personId": null,
"pinned": false
}
Response: 201 Created
{ "chunk": { "id": "uuid", ... } }
GET /api/memory/chunks/[id]
Get a single chunk by ID.
PATCH /api/memory/chunks/[id]
Update a chunk. Re-embeds automatically when content or title changes.
Request (partial):
{
"content": "Updated content here.",
"pinned": true
}
DELETE /api/memory/chunks/[id]
Delete a chunk. Removed from retrieval immediately.
Household
Configuration for the household and its members. See Memory System — Multi-Person Households.
GET /api/household
Get the current household configuration and member list.
Response:
{
"household": {
"id": "uuid",
"displayName": "Smith Household",
"mode": "single",
"timezone": "America/New_York",
"locale": "en-US"
},
"persons": [
{
"id": "uuid",
"householdId": "uuid",
"displayName": "Alex",
"pronouns": "he/him",
"role": "primary",
"aliases": ["Al"],
"birthDate": "1990-03-15"
}
]
}
PUT /api/household
Update household settings and replace the member list atomically.
Request:
{
"displayName": "Smith Household",
"timezone": "America/New_York",
"locale": "en-US",
"mode": "multi",
"persons": [
{
"displayName": "Alex",
"pronouns": "he/him",
"role": "primary",
"aliases": ["Al"]
},
{
"displayName": "Sarah",
"pronouns": "she/her",
"role": "spouse",
"aliases": ["mom", "wife"]
}
]
}
Validation: switching mode from "multi" to "single" is rejected if persons contains more than one entry — remove the extras first.
Response: Updated household + persons (same shape as GET).
Cameras
GET /api/cameras
List all camera configurations.
POST /api/cameras
Create a camera.
Request:
{
"name": "Front Door",
"url": "rtsp://192.168.1.100:554/stream",
"type": "rtsp",
"captureIntervalSec": 60,
"visionPrompt": "Describe the scene...",
"alertRules": [],
"enabled": true
}
GET /api/cameras/[id]
Get camera details.
PUT /api/cameras/[id]
Update camera configuration.
DELETE /api/cameras/[id]
Delete a camera and its events.
POST /api/cameras/[id]/test
Capture and analyze a single test frame. Returns the vision analysis result.
Response:
{
"imagePath": "camera-captures/uuid/test_1705312800.jpg",
"analysis": { "scene": "Driveway, no activity", "people_count": 0 }
}
GET /api/cameras/[id]/events
List camera events.
Query: ?level=warning — Filter by alert level
GET /api/cameras/[id]/latest
Get the most recent capture for a camera.
POST /api/cameras/events/alerts
Process alert events.
Media
GET /api/media
List media files with pagination, filtering, and sorting.
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
type | string | — | video, audio, image, document |
search | string | — | Filename search |
sort | string | createdAt | Sort field |
order | string | desc | asc or desc |
page | number | 1 | Page number |
limit | number | 50 | Items per page |
Response:
{
"items": [
{
"id": "uuid",
"name": "movie.mp4",
"type": "video",
"mimeType": "video/mp4",
"size": 1073741824,
"duration": 7200,
"width": 1920,
"height": 1080,
"thumbnailPath": ".thumbnails/uuid.jpg"
}
],
"total": 150,
"page": 1,
"pages": 3
}
POST /api/media/upload
Upload a file. Uses multipart/form-data.
GET /api/media/scan
Trigger a full media rescan.
GET /api/media/[id]
Get detailed file info with metadata.
GET /api/media/[id]/thumbnail
Serve the thumbnail image (returns image binary).
POST /api/media/[id]/position
Update playback position.
Request:
{ "position": 1234.5 }
GET /api/media/photos
List image files specifically.
GET /api/media/library
Get library statistics.
Playlists
GET /api/playlists
List all playlists.
POST /api/playlists
Create a playlist.
Request:
{ "name": "Road Trip Mix", "description": "Songs for driving" }
GET /api/playlists/[id]
Get playlist with ordered items.
PUT /api/playlists/[id]
Update playlist metadata.
DELETE /api/playlists/[id]
Delete a playlist.
POST /api/playlists/[id]/items
Add an item to the playlist.
Request:
{ "mediaFileId": "uuid", "position": 0 }
DELETE /api/playlists/[id]/items/[itemId]
Remove an item from the playlist.
Jobs (Scheduler)
GET /api/jobs
List all cron jobs.
POST /api/jobs
Create a job.
Request:
{
"name": "Daily Weather",
"schedule": "0 6 * * *",
"actionType": "get_weather",
"actionPayload": { "location": "San Francisco" },
"enabled": true
}
PATCH /api/jobs/[id]
Update a job (e.g., enable/disable).
Request:
{ "enabled": false }
POST /api/jobs/[id]/run
Execute a job immediately.
GET /api/jobs/[id]/runs
Get execution history for a job.
Webhooks
GET /api/webhooks
List all webhooks.
POST /api/webhooks
Create a webhook. Returns the generated secret.
Request:
{
"name": "Doorbell Trigger",
"actionType": "send_notification",
"actionPayload": { "title": "Doorbell", "body": "Someone at the door" }
}
POST /api/webhooks/[id]
Trigger a webhook.
Headers: X-Webhook-Secret: <secret>
PATCH /api/webhooks/[id]/manage
Update webhook configuration.
DELETE /api/webhooks/[id]/manage
Delete a webhook.
Notifications
GET /api/notifications
List notifications. Optional filters: ?deviceId=uuid&status=pending
GET /api/notifications/[id]
Get notification details.
POST /api/notifications/send
Create and send a notification.
Request:
{
"title": "Reminder",
"body": "Take out the trash",
"deviceId": "uuid",
"level": "info",
"category": "REMINDER"
}
Actions
GET /api/actions
List all registered actions with their parameter schemas.
POST /api/actions/[name]/execute
Execute a named action.
Request: The action's parameters as JSON body.
curl -X POST http://localhost:3000/api/actions/send_notification/execute \
-H "Content-Type: application/json" \
-d '{"title": "Test", "body": "Hello"}'
Skills
GET /api/skills
List loaded skills and their manifests.
Plugins
Plugins are zip-uploaded extensions that add widgets, settings pages, LLM actions, and cron jobs. See Plugins for the full architecture.
GET /api/plugins
List all installed plugins.
Response:
[
{
"slug": "prayer-times",
"name": "Prayer Times",
"version": "1.0.0",
"enabled": true,
"status": "enabled",
"manifest": { "..." : "..." }
}
]
status is one of "enabled", "installed" (disabled), or "missing" (folder removed).
GET /api/plugins/[slug]
Get manifest, status, and grantedPermissions for a single plugin.
POST /api/plugins/install
Upload and install a plugin zip. Uses multipart/form-data with a file field.
Response:
{
"slug": "prayer-times",
"manifest": { "..." : "..." },
"requiredPermissions": ["network", "notifications", "tablet.sound"],
"warnings": []
}
The plugin is installed with enabled=false. POST to /<slug>/enable to activate it.
POST /api/plugins/[slug]/enable
Enable a plugin. The user must pass the permission set they are granting.
Request:
{ "permissions": ["network", "notifications", "tablet.sound"] }
Calls loadPlugin(slug) synchronously — the plugin's init() runs before the response is returned.
POST /api/plugins/[slug]/disable
Disable a plugin. Calls unloadPlugin(slug), which invokes dispose() and tears down all cron, action, and event registrations. No restart required.
DELETE /api/plugins/[slug]
Disable, then delete the on-disk folder and all DB rows (plugins, plugin_oauth_tokens, plugin_secrets) for the plugin. Irreversible.
GET /api/plugins/[slug]/settings
Returns the settings schema and current values, plus OAuth connection state per declared provider.
Response:
{
"schema": {
"fields": [
{ "key": "city", "label": "City", "type": "text", "required": true },
{ "key": "apiKey", "label": "API Key", "type": "secret" }
]
},
"values": { "city": "New York" },
"oauth": {
"google": { "connected": true, "accountLabel": "you@gmail.com" }
}
}
secret fields are never included in values.
POST /api/plugins/[slug]/settings
Save settings. Non-secret values are written to data/plugins/<slug>/settings.json; secret values go to plugin_secrets. The plugin's genie.settings.onChange fires via the file watcher.
Request:
{ "city": "London", "apiKey": "sk-..." }
GET /api/plugins/widgets/enabled
List widgets for all enabled plugins. Used by the home-page and tablet dashboard.
Response:
[
{
"slug": "prayer-times",
"widget": {
"title": "Prayer Times",
"size": "medium",
"spec": { "layout": "stack", "blocks": [] }
},
"hasReact": false
}
]
GET /api/plugins/[slug]/widget-data
Invoke the plugin's data provider and return the latest payload.
Response: Plugin-defined JSON object.
GET /api/plugins/[slug]/widget.js
Serve the compiled React widget bundle. Cached with Cache-Control: public, max-age=3600, immutable.
Query: ?v=<version> — cache-busting version from the manifest.
GET /api/plugins/[slug]/assets/[path]
Serve a static asset from data/plugins/<slug>/assets/<path>. Path-traversal-guarded: the resolved path must remain inside the plugin folder. Returns 400 if the path escapes.
OAuth routes
GET /api/plugins/oauth/[provider]/connect?slug=<slug>
Redirect the browser to the provider's OAuth authorize URL.
GET /api/plugins/oauth/[provider]/callback
Exchange the authorization code, encrypt and store the token, then redirect to /settings/<slug>.
Files
GET /api/files
List files in storage.
GET /api/files/[id]
Get file details.
GET /api/files/list
Advanced file listing with directory browsing.
GET /api/files/stats
Storage usage statistics.
POST /api/files/upload
Upload a file to storage. Uses multipart/form-data.