REST API
Lumio exposes a RESTful API following HATEOAS conventions.
Base URLs
| Environment | URL |
|---|---|
| Production | https://api.lumio.vision/v1 |
| Production Preview | https://lumio.api.prod.zaflun.dev/v1 |
| Staging | https://lumio.api.staging.zaflun.dev/v1 |
Response Format
All responses follow the HATEOAS format with embedded links:
{
"data": { ... },
"_links": {
"self": { "href": "/v1/overlays/123" },
"collection": { "href": "/v1/overlays" }
}
}
Interactive Documentation
- Swagger UI — Available at
/v1/swagger-ui/when the API is running - OpenAPI spec — Download from
/v1/api-doc/openapi.json
Endpoints Overview
All paths are under /v1. Request and response bodies use snake_case. Permissions use the resource:action format.
| Resource group | Path prefix | Description |
|---|---|---|
| Auth | /v1/auth | Token exchange, refresh, logout, OAuth link/authorize |
| Users | /v1/users/me | Current-user profile, login connections, sessions |
| Accounts | /v1/accounts | Create/dissolve/leave accounts, member invites, login assignments |
| Login Connections | /v1/login-connections | Delete login connections by UUID |
| Members & Invites | /v1/accounts/{id}/members, /v1/invites | Team management |
| Roles | /v1/roles | RBAC role CRUD and permission catalog |
| Tokens | /v1/tokens | Popout/API token management (/tokens/me = current permissions) |
| Overlays | /v1/overlays | Overlay configuration CRUD |
| Sounds | /v1/sounds | Sound library management, upload, playback control, and streaming |
| Uploads | /v1/uploads | File uploads (multipart) and presigned download URLs |
| Chat | /v1/chat/* | History, send, moderate, user profiles, notes, raid/poll/prediction |
| Events | /v1/events | Event history, single event, emit/test |
| Emotes | /v1/emotes | Channel and user emote fetching |
| YouTube Memberships | /v1/youtube/memberships/tiers, /v1/admin/privacy/youtube/member/{id} | Read observed YouTube member-tier badges (account-scoped); GDPR-Art-17 erasure of cached member data (system_admin only) — see Member Badges |
| YouTube Streams | /v1/youtube/active-streams | Active and upcoming YouTube broadcasts (reads from Redis cache written by the YouTube polling worker) |
| Connections | /v1/connections | App credentials and channel OAuth flow |
| Bot Connections | /v1/bot-connections, /v1/bot-status, /v1/bot-toggle, /v1/bot-rejoin | Custom bot identity OAuth and control |
| Bot Commands | /v1/bot-commands | Cross-platform command CRUD and global overrides |
| Bot Modules | /v1/bot-modules | Moderation module configs (link/spam/word/timed), extension bot module kill switch |
| Bot Module Triggers | /v1/internal/bot-modules | Internal trigger resolution for Bot Module Worker (SystemKey) |
| Automations | /v1/automations | Visual automation CRUD and manual execution |
| Automation Extension Nodes | /v1/automation/extension-nodes, /v1/automation/webhooks | Extension node listing and webhook receiver |
| Channel Status | /v1/channel-status, /v1/spotify/manual-connect | Live status and Spotify manual polling |
| Spotify | /v1/spotify/* | State, playback, queue, devices, playlists, search |
| Copyright | /v1/copyright/* | Safe/blocked songs, playlist imports, community voting |
| Notifications | /v1/notifications | In-app notifications, actions, and delivery preferences |
| Ideas Hub | /v1/ideas, /v1/ideas/admin | Community ideas, votes, comments, @mention autocomplete, categories, tags |
| OBS | /v1/integrations/obs, /v1/obs-remote/* | OBS config + remote stream/recording/scene control |
| SE Tokens | /v1/se-tokens | StreamElements JWT token storage |
| Discord Guilds | /v1/discord-guilds/exchange | Discord guild bot install exchange |
| Abuse Reports | /v1/abuse-reports | User-submitted abuse reports |
| Webhooks | /v1/webhooks/* | Platform webhooks (Twitch, YouTube, Kick, Trovo, Shopify, Stripe) |
| Billing | /v1/billing/* | Stripe checkout/portal/status/invoices/coupon |
| Playlists | /v1/playlists | Read-only global safe-song playlists |
| Songs | /v1/songs | Read-only song metadata and copyright status |
| Platforms | /v1/platforms/metadata | Static platform metadata (name, icon, features, scopes) |
| Features | /v1/features/enabled, /v1/providers/enabled | Public feature/provider flag reads |
| Extension Bundles | /v1/extension-bundles | Serve extension bundle files (JS, CSS, assets) |
| Extension Version Files | /v1/extensions/{id}/versions/{version}/files | List files in an extension version bundle |
| Extension Uploads | /v1/extensions/{id}/uploads | Upload/serve files from extension editors (icons, images) |
| Fonts | /v1/fonts/{family} | DSGVO-compliant font proxy (CSS + woff2), public scope |
| Developer Applications | /v1/developer/application | Submit and view developer applications |
| Developer Teams | /v1/developer/teams | Team CRUD, members, invites, roles, permissions |
| Developer Store Profiles | /v1/developer/profiles/{slug}, /v1/developer/teams/{slug}/profile | Public developer and team profile pages |
| Admin Developer Applications | /v1/admin/developer-applications | Review, approve, reject developer applications |
| Health | /v1, /v1/health | Liveness probes |
| Admin | /v1/admin/* | System-admin-only endpoints (feature flags, users, accounts, OAuth clients, system keys/connections, providers, coupons, audit log, bot control) |
For the exhaustive parameter list of every endpoint, use the Swagger UI at /v1/swagger-ui/ or download /v1/api-doc/openapi.json. Feature-specific docs under Features document the most commonly used endpoints with their resource:action permissions.
Sounds
All sound endpoints require the feature:sounds feature flag to be enabled for the account.
GET /v1/sounds
List sounds in the account's library.
Permission: sounds:read
Query parameters:
| Parameter | Type | Description |
|---|---|---|
limit | integer | Page size (default 20, max 100) |
offset | integer | Pagination offset |
search | string | Filter by name (partial match) |
Response:
{
"data": {
"items": [
{
"id": "uuid",
"account_id": "uuid",
"name": "Tada",
"file_name": "tada.mp3",
"content_type": "audio/mpeg",
"size_bytes": 45312,
"duration_secs": 2.1,
"storage_key": "sounds/{account_id}/{uuid}.mp3",
"created_by": "uuid",
"created_at": "2026-05-28T10:00:00Z",
"updated_at": "2026-05-28T10:00:00Z"
}
],
"total": 1
}
}
GET /v1/sounds/\{id\}
Get a single sound by ID.
Permission: sounds:read
Response: Sound object (same shape as list items).
POST /v1/sounds
Upload a new sound file.
Permission: sounds:create
Body: multipart/form-data
| Field | Type | Required | Description |
|---|---|---|---|
file | file | Yes | Audio file (MP3, WAV, OGG, FLAC). Max size set by max_sound_file_size plan limit. |
name | string | No | Display name (defaults to filename without extension) |
Errors:
400— Unsupported file type or file exceeds size limit409— Account has reached themax_soundsplan limit
Response: 201 Created with the new sound object.
PATCH /v1/sounds/\{id\}
Update sound metadata.
Permission: sounds:edit
Body:
| Field | Type | Description |
|---|---|---|
name | string | New display name |
Response: Updated sound object.
DELETE /v1/sounds/\{id\}
Delete a sound.
Permission: sounds:delete
Response: 204 No Content.
POST /v1/sounds/\{id\}/play
Trigger playback of a sound in browser sources.
Permission: sounds:play
Body:
| Field | Type | Required | Description |
|---|---|---|---|
volume | float | No | Playback volume 0.0–1.0 (default 1.0) |
target | string | No | Target overlay key. Omit to broadcast to all browser sources. |
Response: 200 { "data": { "success": true } }
POST /v1/sounds/\{id\}/stop
Stop playback of a sound in browser sources.
Permission: sounds:play
Body:
| Field | Type | Required | Description |
|---|---|---|---|
target | string | No | Target overlay key. Omit to broadcast to all browser sources. |
Response: 200 { "data": { "success": true } }
GET /v1/sounds/\{id\}/stream
Stream the audio bytes for a sound. Used by browser sources to load and play the audio.
Permission: sounds:read
Response: Raw audio bytes with the correct Content-Type header (e.g. audio/mpeg) and Cache-Control: private, max-age=3600.
Protocol parity: All endpoints have matching GraphQL queries/mutations — see GraphQL.
Automation Extension Nodes
GET /v1/automation/extension-nodes
List all installed automation node extensions for the current account.
Permission: automations:read
Response:
{
"data": [
{
"extension_id": "uuid",
"install_id": "uuid",
"name": "Shopify Order Trigger",
"node_type": "trigger",
"input_schema": { ... },
"output_schema": { ... },
"trigger_mode": "webhook",
"icon": "shopping-cart",
"color": "#96bf48"
}
]
}
GET /v1/automation/webhooks/\{install_id\}/\{automation_id\}
Get the webhook URL and secret for an installed trigger node within a specific automation.
Permission: automations:read
Response:
{
"data": {
"webhook_url": "https://api.lumio.vision/v1/automation/webhooks/{extension_id}/{install_id}",
"webhook_secret": "64-char-hex-string"
}
}
POST /v1/automation/webhooks/\{extension_id\}/\{install_id\}
Receive an external webhook for a trigger node. The external service must include the X-Webhook-Secret header matching the stored secret.
Auth: X-Webhook-Secret header (not JWT).
Request body: Arbitrary JSON payload. Forwarded to the extension handler as ctx.webhookBody.
Response: 200 on success, 401 if the secret is invalid, 404 if the extension/install is not found.
Protocol parity:
installedAutomationNodesquery andautomationWebhookUrlquery in GraphQL -- see GraphQL.
Login Assignments
Login assignments link a user's login connection (e.g. their Twitch login) to a specific Lumio account. A single user can own multiple accounts; login assignments control which platform identity is associated with which account.
Own assignments are always allowed without permissions. The
login-assignments:*permissions only apply when managing another user's assignments.
GET /v1/accounts/login-assignments
Returns all login assignments for the caller's active account.
Permission: login-assignments:read (or own account — no permission required for the account owner).
Response:
{
"data": [
{
"provider": "twitch",
"login_connection_id": "00000000-0000-0000-0000-000000000001",
"user_id": "00000000-0000-0000-0000-000000000002",
"assigned_at": "2026-01-15T10:00:00Z"
}
]
}
POST /v1/accounts/login-assignments
Assign a login connection to the active account.
Permission: login-assignments:create (or own account).
Request body:
| Field | Type | Required | Notes |
|---|---|---|---|
login_connection_id | UUID | Yes | ID of the login connection to assign |
provider | string | Yes | Platform slug, e.g. "twitch", "google" |
user_id | UUID | No | Defaults to the authenticated user |
Response: 201 Created with the new assignment object.
DELETE /v1/accounts/login-assignments/:provider
Remove the login assignment for a given provider from the active account.
Permission: login-assignments:delete (or own account).
Path parameter: :provider — platform slug (e.g. twitch, google).
Response: 204 No Content.
DELETE /v1/login-connections/:id
Delete a login connection by its UUID. The connection must belong to the authenticated user.
Permission: None (own connections only).
Path parameter: :id — UUID of the login connection.
Response: 204 No Content.
Protocol parity: All four endpoints have matching GraphQL operations — see GraphQL.
Feature Flags & Status
GET /users/me
Returns the authenticated user's profile, account memberships, permissions, feature statuses, login connections, and preferences.
The response includes streamer_mode (boolean) — the user's Streamer Mode preference. Use PATCH /users/me with { "streamer_mode": true } to toggle it.
The UserResponse also includes admin_permissions (admin-scope permission strings), enabled_features (list of enabled feature keys for the active account), login_connections (OAuth login identities linked to the user, filtered by enabled providers), is_developer (boolean — true when the user has a developer profile or the extension_dev_mode admin override), and extension_dev_mode (boolean — when true, extension bundle serving loads the latest draft/testing version instead of the published version for extensions the user develops).
The feature_statuses field is a merged list of account-scope feature statuses and the user-scope system:account_creation status.
{
"data": {
"id": "...",
"feature_statuses": [
{ "key": "feature:bots", "enabled": true, "reason": null },
{ "key": "feature:music", "enabled": false, "reason": "plan_locked" },
{ "key": "system:account_creation", "enabled": true, "reason": null }
],
...
}
}
The reason field is one of: "global_off", "plan_locked", "account_override", "user_override", or null (when enabled).
PATCH /users/me
Updates the authenticated user's profile, active account, or preferences. All fields are optional — at least one must be provided.
| Field | Type | Description |
|---|---|---|
display_name | string | Update display name (cannot be empty) |
email | string | Update email address |
active_account_id | UUID | Switch active account (must be a member) |
clear_active_account | bool | Clear active account (go to user-only mode) |
streamer_mode | bool | Toggle Streamer Mode on/off |
extension_dev_mode | bool | Toggle Extension Developer Mode — loads draft/testing versions in editors |
Response: full UserResponse (same shape as GET /users/me). When switching accounts, a token field with a fresh JWT is included.
Stale-membership filtering on active_account_id
If the JWT carries an accountId the user is no longer a member of (e.g. the
account owner removed them after the token was issued), the resolver returns
active_account_id: null rather than the stale claim. Permissions are computed
against the corrected scope. The dashboard shell reads this signal and routes
the user to onboarding instead of rendering an account context they can no
longer access.
The REST DELETE /v1/accounts/{id}/members/{membership_id} (admin kick) and POST /v1/accounts/{id}/leave (self-leave) endpoints invalidate the Redis permission cache for the removed user, matching the existing GraphQL removeMember mutation behaviour — two-protocol parity for cache invalidation.
GET /v1/accounts/{id}/enabled-features
Returns the list of enabled feature keys for the given account. The caller's active account must match {id}. Works with popout-token auth (no account:read permission required). Excludes system:* flags.
Response:
{ "data": ["feature:bots", "feature:music", "feature:connections"] }
GET /v1/accounts/{id}/feature-statuses
Returns the full feature status list for the given account (key, enabled, reason). The caller's active account must match {id}. Works with popout-token auth (no account:read permission required). Excludes system:* flags.
Response:
{
"data": [
{ "key": "feature:bots", "enabled": true, "reason": null },
{ "key": "feature:music", "enabled": false, "reason": "plan_locked" },
{ "key": "integration:shopify", "enabled": true, "reason": null }
]
}
POST /v1/accounts — account creation disabled (403)
When the system:account_creation flag is disabled (globally or per user), this endpoint returns:
HTTP 403
{
"error": "Account creation is currently disabled",
"error_code": "account_creation_disabled"
}
This mirrors the GraphQL error extensions.code: "ACCOUNT_CREATION_DISABLED" with message "Account creation is currently disabled".
PATCH /v1/admin/users/{id} — account creation override
The request body accepts an optional account_creation_override field:
{ "account_creation_override": "default" }
Valid values: "default" (inherit global flag), "allow" (always permitted), "deny" (always blocked). The override is cached in Redis and invalidated immediately on save. Requires users:edit admin permission.
The account_creation_override field is also present in GET /v1/admin/users/{id} and the user-list response.
Platform-filtered connection lists
GET /users/me/login-connections, GET /connections/channel, GET /bot-connections, and GET /admin/providers filter by platform flags:
- Login connections are filtered by
platform:{x}:login— platforms with the login sub-flag disabled are excluded. - Channel connections are filtered by
platform:{x}:channel. - Bot connections are filtered by
platform:{x}:bot. - Admin providers exclude integration-only entries (e.g., Shopify does not appear — its flag is
integration:shopifyin the Feature Flags page, not a platform provider).
Protocol parity: All endpoints above have matching GraphQL queries/mutations — see GraphQL.
Admin Role Management
All endpoints below are under the /v1/admin scope and check admin-scope permissions (not account permissions). The caller must have the admin permission listed for each endpoint.
GET /v1/admin/admin-roles
Requires admin-roles:read. Returns [AdminRoleResponse] — the full list of admin roles with their permissions and member counts.
POST /v1/admin/admin-roles
Requires admin-roles:create. Body: CreateAdminRoleRequest. Returns 201 + AdminRoleResponse.
Validation errors (400):
"Name is required"— empty name"Name must be 100 characters or less"— name too long"Invalid permission: <perm>"— unknown permission string"Role name already in use"— duplicate name
admin:access is auto-injected if not included in the permissions list.
GET /v1/admin/admin-roles/{id}
Requires admin-roles:read. Returns AdminRoleResponse. Returns 404 "Admin role not found" if not found.
PATCH /v1/admin/admin-roles/{id}
Requires admin-roles:edit. Body: UpdateAdminRoleRequest (all fields optional). Returns AdminRoleResponse.
- Omit a field to leave it unchanged
- Set
descriptiontonullto clear it - Permissions are applied as a diff; unknown/legacy permissions are preserved
- Same validation error wording as
POST
DELETE /v1/admin/admin-roles/{id}
Requires admin-roles:delete. Returns 204 on success.
- 400
"Cannot delete system admin role"— ifis_system = true - 404
"Admin role not found"— if not found
GET /v1/admin/admin-roles/{id}/members
Requires admin-roles:read. Returns [AdminRoleMemberResponse] — all users assigned to the role.
PUT /v1/admin/admin-roles/{id}/members/{userId}
Requires admin-roles:edit. Assigns the given user to the role. Idempotent. Returns 204.
- 404
"Admin role not found"— if the role does not exist
DELETE /v1/admin/admin-roles/{id}/members/{userId}
Requires admin-roles:edit. Removes the user's role assignment. Returns 204.
GET /v1/admin/admin-permissions
Requires admin-roles:read. Returns [AdminPermissionInfoResponse] — the full catalog of admin-scope permissions with category labels. Source: lo_auth::rbac::all_admin_permissions().
Request / Response Types
CreateAdminRoleRequest
| Field | Type | Required |
|---|---|---|
name | string | Yes |
description | string | null | No |
permissions | string[] | Yes (may be empty) |
UpdateAdminRoleRequest
| Field | Type | Notes |
|---|---|---|
name | string | Optional; trimmed |
description | string | null | null = clear; omit = leave unchanged |
permissions | string[] | Optional; replaces via diff |
AdminRoleResponse
| Field | Type |
|---|---|
id | UUID |
name | string |
description | string or null |
is_system | boolean |
permissions | string[] |
member_count | integer |
created_at | ISO-8601 string |
updated_at | ISO-8601 string |
AdminRoleMemberResponse
| Field | Type |
|---|---|
user_id | UUID |
display_name | string |
email | string or null |
avatar_url | string or null |
assigned_at | ISO-8601 string |
AdminPermissionInfoResponse
| Field | Type |
|---|---|
permission | string |
category | string |
Protocol parity: All 9 endpoints have matching GraphQL queries/mutations — see GraphQL.
Admin Plan Management
All endpoints below are under the /v1/admin scope and check admin-scope permissions. Request and response bodies use snake_case.
GET /v1/admin/plans
Requires plans:read. Returns [AdminPlanResponse] — the full list of plans with their feature assignments and account counts.
POST /v1/admin/plans
Requires plans:create. Body: CreatePlanRequest. Returns 201 + AdminPlanResponse.
Validation errors (400):
"Invalid slug format"— slug does not match^[a-z0-9]+(?:-[a-z0-9]+)*$or is outside 2–40 chars"Price cannot be negative"— monthly or yearly price is negative"Limit cannot be negative"— a numeric limit is negative
Conflict errors (409):
"Plan slug already in use"— another plan already uses this slug
Authentication errors (401): missing or invalid JWT / missing plans:create.
Fields of CreatePlanRequest:
| Field | Type | Required | Notes |
|---|---|---|---|
slug | string | Yes | Regex ^[a-z0-9]+(?:-[a-z0-9]+)*$, length 2–40, immutable after creation |
name | string | Yes | Display name |
description | string | null | No | |
price_monthly | integer | Yes | Cents (or the minor unit of currency) |
price_yearly | integer | Yes | Cents |
currency | string | null | No | ISO-4217 code; defaults to "USD" |
is_public | boolean | Yes | Whether the plan is visible on the public pricing page |
sort_order | integer | Yes | Sort position in pricing pages |
max_overlays | integer | Yes | |
max_storage_bytes | integer | Yes | |
max_upload_size_bytes | integer | Yes | |
max_integrations | integer | Yes | |
chat_retention_days | integer | Yes | 0 = keep forever |
max_sounds | integer | Yes | Maximum number of sounds per account |
max_sound_file_size | integer | Yes | Maximum size per sound file in bytes |
max_sound_storage_bytes | integer | Yes | Total sound storage quota in bytes |
stripe_product_id | string | null | No | Paste from Stripe dashboard |
stripe_monthly_price_id | string | null | No | Paste from Stripe dashboard |
stripe_yearly_price_id | string | null | No | Paste from Stripe dashboard |
PATCH /v1/admin/plans/{id}
Requires plans:edit. Body: UpdatePlanRequest. Returns 200 + AdminPlanResponse.
- The
slugis immutable and is therefore not part ofUpdatePlanRequest. - All fields are required on update — the handler rewrites the full editable field set.
- On success, the feature cache is invalidated for every account currently on the plan.
Errors:
- 400
"Price cannot be negative"/"Limit cannot be negative"— validation failures - 401 — missing
plans:edit - 404
"Plan not found"— no plan with that ID
Fields of UpdatePlanRequest:
| Field | Type | Notes |
|---|---|---|
name | string | |
description | string | null | |
price_monthly | integer | |
price_yearly | integer | |
currency | string | |
is_public | boolean | |
sort_order | integer | |
max_overlays | integer | |
max_storage_bytes | integer | |
max_upload_size_bytes | integer | |
max_integrations | integer | |
chat_retention_days | integer | |
max_sounds | integer | |
max_sound_file_size | integer | |
max_sound_storage_bytes | integer | |
stripe_product_id | string | null | |
stripe_monthly_price_id | string | null | |
stripe_yearly_price_id | string | null |
DELETE /v1/admin/plans/{id}
Requires plans:delete. Returns 204 No Content on success.
- 401 — missing
plans:delete - 404
"Plan not found"— no plan with that ID - 409
"Cannot delete plan: N account(s) still reference it. Migrate them to a different plan first."— one or more accounts still point at this plan; migrate them before retrying
Deletion cascades to plan_features via the foreign key constraint. No other data is affected.
Request / Response Types
AdminPlanResponse
| Field | Type |
|---|---|
id | UUID |
slug | string |
name | string |
description | string or null |
price_monthly | integer |
price_yearly | integer |
currency | string |
is_public | boolean |
sort_order | integer |
max_overlays | integer |
max_storage_bytes | integer |
max_upload_size_bytes | integer |
max_integrations | integer |
chat_retention_days | integer |
max_sounds | integer |
max_sound_file_size | integer |
max_sound_storage_bytes | integer |
stripe_product_id | string or null |
stripe_monthly_price_id | string or null |
stripe_yearly_price_id | string or null |
features | [AdminPlanFeatureResponse] |
accounts_using | integer |
AdminPlanFeatureResponse
| Field | Type |
|---|---|
feature_id | UUID |
feature_key | string |
label | string |
enabled | boolean |
Protocol parity: All four endpoints have matching GraphQL queries/mutations — see GraphQL.
Chat Moderation
POST /v1/chat/moderate is the REST counterpart of the GraphQL moderateChat mutation and behaves identically — same fields, same per-platform support, same chat:clear_user broadcast on ban/timeout. See Chat for the moderation-permission matrix and GraphQL for the field semantics.
Body (snake_case):
{
"action": "ban" | "timeout" | "delete",
"platform": "twitch" | "youtube" | "kick" | "trovo",
"user_id": "<platform user id>", // required for ban / timeout
"message_id": "<platform message id>", // required for delete
"duration_secs": 300, // optional; default 300, YouTube range 1..86400
"reason": "spam", // optional, written to moderation_log
"live_chat_id": "<id>" // YouTube only, optional — auto-resolved from Redis when omitted
}
Permission required matches the action: chat:ban, chat:timeout, or chat:delete. Failures surface in the standard envelope with data.success = false and data.details.
Protocol parity: mirror at GraphQL
moderateChat(input: ModerationInput!)— see GraphQL.
Chat Profile Refresh
POST /v1/chat/users/\{platform\}/\{platform_user_id\}/refresh
Force a fresh enrichment of a platform user's profile, bypassing the normal staleness interval. Returns the refreshed profile in the same shape as GET /v1/chat/users/{platform}/{platform_user_id}.
Permission: chat:refresh_user
Path parameters:
| Parameter | Description |
|---|---|
platform | Platform slug: twitch, youtube, kick, or trovo |
platform_user_id | The platform-native user identifier |
Responses:
| Status | Body | Description |
|---|---|---|
200 | Refreshed UnifiedProfile (snake_case) | Enrichment succeeded; profile is updated in DB and Redis |
429 | { "error": "REFRESH_COOLDOWN", "retry_after_seconds": N } | Cooldown active; retry after N seconds (max 600) |
The cooldown is 10 minutes per (account, platform, user) triple, enforced via a Redis SETNX key.
Protocol parity: mirror at GraphQL
refreshPlatformUserProfile(platform, platformUserId)— see GraphQL.
Notifications
User-scoped in-app notifications. See Notifications for the full endpoint reference.
POST /v1/notifications/{id}/action supports action accept_invite for type: "invite" notifications: it adds the addressed user to the account referenced by data.accountId with the invite's role. Action decline_invite deletes the backing account_invites row.
Notification preferences
| Method | Path | Permission | Description |
|---|---|---|---|
GET | /v1/notifications/preferences | Auth | List all delivery-channel preferences for the current user |
PATCH | /v1/notifications/preferences/{type} | Auth | Set the delivery channel for a notification type |
PATCH body: { "channel": "off" | "in_app" | "in_app_email" }. Returns 400 for unknown channel values or locked types (e.g. invite).
Protocol parity:
notificationPreferencesquery andupdateNotificationPreferencemutation in GraphQL — see GraphQL.
Ideas Hub
Community idea board with voting, comments, moderation, categories, and tags. All endpoints require the system:ideas_hub feature flag to be enabled. GET endpoints are public (no auth required). Mutation endpoints require authentication and the permissions noted below.
Ideas
| Method | Path | Description | Permission |
|---|---|---|---|
GET | /v1/ideas | List ideas (filter, sort, paginate) | Public |
GET | /v1/ideas/:id | Get idea with timeline | Public |
POST | /v1/ideas | Create idea | ideas:create |
PATCH | /v1/ideas/:id | Update idea | ideas:edit / ideas:moderate_edit |
DELETE | /v1/ideas/:id | Delete idea | ideas:delete / ideas:moderate_delete |
Query parameters for GET /v1/ideas:
| Parameter | Type | Description |
|---|---|---|
status | string | Filter by status slug |
category_id | UUID | Filter by category |
tag_ids | string | Comma-separated list of tag UUIDs |
author_id | UUID | Filter by author |
search | string | Full-text search on title and description |
sort | string | newest, most_voted, most_commented, recently_updated |
limit | integer | Page size |
offset | integer | Page offset |
Voting
| Method | Path | Description | Permission |
|---|---|---|---|
POST | /v1/ideas/:id/vote | Vote on an idea | ideas:vote |
DELETE | /v1/ideas/:id/vote | Remove vote | ideas:vote |
POST body: { "vote_type": "up" | "down" }.
Comments
| Method | Path | Description | Permission |
|---|---|---|---|
GET | /v1/ideas/:id/comments | List comments (nested) | Public |
POST | /v1/ideas/:id/comments | Create comment | ideas:comment_create |
PATCH | /v1/ideas/comments/:id | Update comment | ideas:comment_edit |
DELETE | /v1/ideas/comments/:id | Delete comment | ideas:comment_delete / ideas:moderate_comment |
Comment bodies contain sanitized HTML from a rich text editor. @mentions are stored as <span data-mention-id="UUID" class="mention">@Name</span>.
Participants
| Method | Path | Description | Permission |
|---|---|---|---|
GET | /v1/ideas/:id/participants | List participants for @mention autocomplete | Auth only |
Returns the union of the idea author, voters, and commenters. Supports an optional search query parameter.
Voters
| Method | Path | Description | Permission |
|---|---|---|---|
GET | /v1/ideas/:id/voters | List voters for an idea | Public |
Categories
| Method | Path | Description | Permission |
|---|---|---|---|
GET | /v1/ideas/categories | List all categories | Public |
POST | /v1/ideas/categories | Create category | Admin ideas:edit |
PATCH | /v1/ideas/categories/:id | Update category | Admin ideas:edit |
DELETE | /v1/ideas/categories/:id | Delete category | Admin ideas:delete |
Tags
| Method | Path | Description | Permission |
|---|---|---|---|
GET | /v1/ideas/tags | List / search tags | Public |
POST | /v1/ideas/tags | Create tag | ideas:create |
DELETE | /v1/ideas/tags/:id | Delete tag | Admin ideas:delete |
GET /v1/ideas/tags supports an optional search query parameter.
Status (Moderation)
| Method | Path | Description | Permission |
|---|---|---|---|
PATCH | /v1/ideas/:id/status | Change idea status | ideas:moderate_status |
PATCH body: { "status": "<status_slug>" } (e.g. open, in_progress, done, declined).
Protocol parity: All endpoints above have matching GraphQL queries/mutations — see GraphQL.
Developer Applications
Endpoints for the developer application flow. Users apply to become developers; admins review and approve or reject applications.
GET /v1/developer/application
Returns the authenticated user's latest developer application.
Auth: JWT (user-level, no account context required).
Response:
| Field | Type | Description |
|---|---|---|
id | UUID | Application ID |
user_id | UUID | Applicant's user ID |
application_type | string | "solo" or "team" |
display_name | string | Developer/team display name |
slug | string | URL slug (3-50 chars, lowercase, hyphens) |
description | string | Description of the developer/team |
motivation | string | Why the user wants developer access |
what_to_build | string | What extensions the user plans to build |
github_url | string or null | GitHub profile URL |
website_url | string or null | Website URL |
experience | string or null | Development experience |
avatar_key | string or null | Upload key for avatar image |
status | string | "pending", "approved", or "rejected" |
review_notes | string or null | Notes from the reviewer |
reviewed_by | UUID or null | Reviewer's user ID |
reviewed_at | ISO-8601 or null | Review timestamp |
created_at | ISO-8601 | Submission timestamp |
updated_at | ISO-8601 | Last update timestamp |
Returns 404 "No developer application found" if the user has not submitted an application.
POST /v1/developer/application
Submit a developer application.
Auth: JWT (user-level).
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
application_type | string | Yes | "solo" or "team" |
display_name | string | Yes | Developer/team display name |
slug | string | Yes | URL slug (3-50 chars, starts with lowercase letter, only lowercase letters, digits, hyphens) |
description | string | Yes | Description |
motivation | string | Yes | Why you want developer access |
what_to_build | string | Yes | What you plan to build |
github_url | string | No | GitHub profile URL |
website_url | string | No | Website URL |
experience | string | No | Development experience |
Validation errors (400):
"application_type must be 'solo' or 'team'"— invalid type"Slug must be between 3 and 50 characters"— slug length"Slug must start with a lowercase letter"— slug format"Slug must contain only lowercase letters, digits, and hyphens"— slug characters
Conflict errors (409):
"You already have a developer profile"— user is already a developer"You already have a pending application"— previous application still pending"Slug is already taken"— slug in use
Response: 201 Created with the application object.
Protocol parity:
myDeveloperApplicationquery andsubmitDeveloperApplicationmutation in GraphQL — see GraphQL.
Admin Developer Applications
Admin endpoints for reviewing developer applications. All require developer-verifications:read or developer-verifications:edit admin-scope permissions.
GET /v1/admin/developer-applications
List developer applications with optional status filter.
Permission: developer-verifications:read (admin-scope).
Query parameters:
| Parameter | Type | Description |
|---|---|---|
status | string | Filter by status: "pending", "approved", "rejected" |
Response: { "items": [...], "total": N } where each item contains id, user_id, application_type, display_name, slug, status, created_at, user_display_name, user_avatar_url.
GET /v1/admin/developer-applications/\{id\}
Get full details of a specific application.
Permission: developer-verifications:read (admin-scope).
Returns 404 "Developer application not found" if not found.
POST /v1/admin/developer-applications/\{id\}/approve
Approve a developer application. Creates the developer profile, seeds default team roles (for team applications), and sends approval notifications (in-app + email).
Permission: developer-verifications:edit (admin-scope).
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
notes | string | No | Optional reviewer notes |
Response: { "success": true, "new_status": "approved" }
POST /v1/admin/developer-applications/\{id\}/reject
Reject a developer application. Sends rejection notifications.
Permission: developer-verifications:edit (admin-scope).
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
notes | string | Yes | Rejection reason (required) |
Response: { "success": true, "new_status": "rejected" }
Protocol parity: All four endpoints have matching GraphQL queries/mutations — see GraphQL.
Developer Teams
Team management endpoints with team-scoped RBAC. All team operations (except create and list) check require_team_permission() against the caller's team role.
Team CRUD
| Method | Path | Permission | Description |
|---|---|---|---|
POST | /v1/developer/teams | Auth (developer) | Create a new team |
GET | /v1/developer/teams | Auth (developer) | List teams the user belongs to |
GET | /v1/developer/teams/{id} | team-settings:read | Get team details |
PATCH | /v1/developer/teams/{id} | team-settings:edit | Update team name/description/URLs |
DELETE | /v1/developer/teams/{id} | Team owner only | Delete a team |
Team Members
| Method | Path | Permission | Description |
|---|---|---|---|
GET | /v1/developer/teams/{id}/members | team-members:read | List team members |
PATCH | /v1/developer/teams/{id}/members/{member_id}/role | team-members:edit | Change a member's role |
DELETE | /v1/developer/teams/{id}/members/{member_id} | team-members:remove | Remove a member |
Team Invites
| Method | Path | Permission | Description |
|---|---|---|---|
POST | /v1/developer/teams/{id}/invites | team-members:invite | Create an invite link |
GET | /v1/developer/teams/{id}/invites | team-members:invite | List active invites |
DELETE | /v1/developer/teams/{id}/invites/{invite_id} | team-members:invite | Revoke an invite |
GET | /v1/developer/team-invites/{code} | Auth | Look up an invite by code |
POST | /v1/developer/team-invites/{code}/accept | Auth | Accept an invite |
Team Roles (RBAC)
| Method | Path | Permission | Description |
|---|---|---|---|
GET | /v1/developer/teams/{id}/roles | team-members:read | List roles for a team |
POST | /v1/developer/teams/{id}/roles | team-settings:edit | Create a custom role |
PATCH | /v1/developer/teams/{id}/roles/{rid} | team-settings:edit | Update a role |
DELETE | /v1/developer/teams/{id}/roles/{rid} | team-settings:edit | Delete a role |
GET | /v1/developer/teams/permissions | Auth | List all available team permissions |
Protocol parity: All endpoints have matching GraphQL queries/mutations — see GraphQL.
Developer Store Profiles
Public endpoints for developer and team profile pages. No authentication required.
GET /v1/developer/profiles/\{slug\}
Returns a public developer profile by slug, including aggregate stats (extension count, total installs, average rating) and a list of published extensions.
Auth: None (public).
Response:
| Field | Type | Description |
|---|---|---|
id | UUID | Developer profile ID |
display_name | string | Developer display name |
slug | string | URL slug |
description | string or null | Bio/description |
github_url | string or null | GitHub profile URL |
website_url | string or null | Website URL |
avatar_key | string or null | Avatar upload key |
created_at | ISO-8601 | Profile creation date |
stats | object | { extension_count, total_installs, avg_rating } |
extensions | array | List of published extensions with id, short_id, slug, name, description, category, icon_key, pricing_type, pricing_amount, pricing_currency, install_count, rating_avg, rating_count, published_at |
Returns 404 if no developer has the given slug.
GET /v1/developer/teams/\{slug\}/profile
Returns a public team profile by slug, including aggregate stats, team members (with roles), and published extensions.
Auth: None (public).
Response:
| Field | Type | Description |
|---|---|---|
id | UUID | Team ID |
name | string | Team name |
slug | string | URL slug |
description | string or null | Team description |
github_url | string or null | GitHub URL |
website_url | string or null | Website URL |
avatar_key | string or null | Avatar upload key |
created_at | ISO-8601 | Team creation date |
stats | object | { extension_count, total_installs, avg_rating } |
members | array | [{ user_id, display_name, avatar_url, role }] |
extensions | array | Published extensions (same shape as developer profile) |
Returns 404 if no team has the given slug.
Protocol parity: Both endpoints have matching GraphQL queries — see GraphQL.
Extension Bot Module Endpoints
GET /v1/internal/bot-modules/\{account_id\}/triggers
Returns all active extension bot module triggers for the given account. This is an internal endpoint used by the Bot Module Worker to load the trigger set for an account.
Auth: SystemKey only.
Path parameter: account_id — UUID of the account.
Response:
{
"data": [
{
"extension_id": "uuid",
"install_id": "uuid",
"trigger_type": "command",
"trigger_config": { "prefix": "!rank" },
"enabled": true
}
]
}
POST /v1/bot-modules/kill
Immediately disables an extension bot module for the caller's account. The module's triggers are removed from the active set without requiring a Worker restart. Configuration is preserved for re-enabling later.
Auth: Account auth (JWT, API key, or popout token).
Permission: bot-modules:edit
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
install_id | UUID | Yes | The extension install to disable |
Response: 204 No Content.
Developer Extension Endpoints
GET /v1/developer/extensions/\{id\}/errors
Returns recent runtime errors for an extension owned by the authenticated developer. Useful for debugging server function failures and trigger handler exceptions.
Auth: Account auth (JWT or API key). The caller must be the extension owner or a member of the owning developer team.
Permission: extensions:read (developer scope)
Path parameter: id — UUID of the extension.
Query parameters:
| Parameter | Type | Description |
|---|---|---|
limit | integer | Max results (default 50, max 200) |
offset | integer | Pagination offset |
since | ISO-8601 | Only errors after this timestamp |
Response:
{
"data": [
{
"id": "uuid",
"error_type": "handler_timeout",
"message": "Handler exceeded 10s CPU timeout",
"stack_trace": "at handler (server/functions.ts:42:5)",
"occurred_at": "2026-05-20T10:30:00Z",
"install_id": "uuid",
"account_id": "uuid"
}
]
}
GET /v1/developer/extensions/\{id\}/metrics
Returns aggregated metrics for an extension owned by the authenticated developer. Includes trigger invocation counts, error rates, and average execution times.
Auth: Account auth (JWT or API key). The caller must be the extension owner or a member of the owning developer team.
Permission: extensions:read (developer scope)
Path parameter: id — UUID of the extension.
Query parameters:
| Parameter | Type | Description |
|---|---|---|
period | string | Aggregation period: 1h, 24h, 7d, 30d (default 24h) |
Response:
{
"data": {
"total_invocations": 12450,
"error_count": 23,
"error_rate": 0.0018,
"avg_execution_ms": 45,
"p99_execution_ms": 280,
"active_installs": 87,
"period": "24h"
}
}
Extension Bundle Serving
GET /v1/extension-bundles/\{extension_id\}/\{version\}/\{path\}
Serve a file from an extension version's bundle. The path is a catch-all that resolves to a file in the version's extension_version_files table.
Auth: Published versions are publicly accessible (no auth required). Draft and testing versions require developer authentication (JWT, API key, extension token, or system key).
Path parameters:
| Parameter | Description |
|---|---|
extension_id | UUID of the extension |
version | Semver version string (e.g. 1.2.0) |
path | File path within the bundle (e.g. layer.js, styles.css, assets/logo-a1b2c3.png) |
Security:
- Path traversal attempts (
.., null bytes, absolute paths) are rejected with 400 source.tar.gzis never served (returns 403)
Response: The raw file bytes with the stored Content-Type header and Cache-Control: public, max-age=86400, immutable.
Legacy compatibility: GET /v1/extension-bundles/{extension_id}/{version}/bundle.js is preserved as a dedicated route and falls back to the bundle_key column for pre-migration versions.
GET /v1/extensions/\{id\}/versions/\{version\}/files
List all files in an extension version's bundle.
Auth: Published versions are publicly accessible. Draft and testing versions require developer authentication.
Path parameters:
| Parameter | Description |
|---|---|
id | UUID of the extension |
version | Semver version string |
Response:
{
"data": [
{
"id": "uuid",
"version_id": "uuid",
"file_path": "layer.js",
"content_type": "application/javascript",
"size_bytes": 312000,
"content_hash": "sha256-abc123...",
"created_at": "2026-05-26T10:00:00Z"
}
]
}
Protocol parity:
extensionVersionFiles(extensionId, version)query in GraphQL -- see GraphQL.
Extension Upload Endpoints
POST /v1/extensions/\{extension_id\}/uploads
Upload a file from an extension editor (icons, images). Used by the SDK's ctx.upload() method inside extension iframes.
Auth: Extension Token (lm_ext_*). The token's extension_id must match the path parameter.
Body: multipart/form-data with a file field.
Limits: Max 256 KB per file. Max 50 uploads per extension per account. Accepted formats: PNG, JPEG, GIF, SVG, WebP, AVIF.
Deduplication: Files are deduplicated by SHA-256 content hash. Uploading an identical file returns the existing URL without re-uploading.
Response:
{
"data": {
"url": "https://api.lumio.vision/v1/extensions/{extension_id}/uploads/{hash}.png"
}
}
GET /v1/extensions/\{extension_id\}/uploads/\{filename\}
Serve an uploaded file. Public access (URLs are unguessable due to content-hash filenames).
Response: Raw file bytes with correct Content-Type and Cache-Control: public, max-age=86400, immutable.
Extension Install Endpoints
GET /v1/extension-installs/\{id\}/logs
Returns recent execution logs for an extension install. Allows the account owner to monitor what the extension is doing in their account.
Auth: Account auth (JWT, API key, or popout token). The caller's active account must own the install.
Permission: bot-modules:read
Path parameter: id — UUID of the extension install.
Query parameters:
| Parameter | Type | Description |
|---|---|---|
limit | integer | Max results (default 50, max 200) |
offset | integer | Pagination offset |
level | string | Filter by log level: info, warn, error |
Response:
{
"data": [
{
"id": "uuid",
"level": "info",
"message": "Command !rank executed for user xyz",
"trigger_type": "command",
"execution_ms": 34,
"created_at": "2026-05-20T10:30:00Z"
}
]
}