Skip to main content

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:

ParameterTypeDescription
deviceIduuidFilter by device
eventstringFilter by event name
levelstringinfo, warn, or error
cursorstringPagination cursor (returned as nextCursor)
limitnumberItems 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:

ParameterTypeDefaultDescription
typestringvideo, audio, image, document
searchstringFilename search
sortstringcreatedAtSort field
orderstringdescasc or desc
pagenumber1Page number
limitnumber50Items 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.