Skip to main content

REST API

Lumio exposes a RESTful API following HATEOAS conventions.

Base URLs

EnvironmentURL
Productionhttps://api.lumio.vision/v1
Production Previewhttps://lumio.api.prod.zaflun.dev/v1
Staginghttps://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 groupPath prefixDescription
Auth/v1/authToken exchange, refresh, logout, OAuth link/authorize
Users/v1/users/meCurrent-user profile, login connections, sessions
Accounts/v1/accountsCreate/dissolve/leave accounts, member invites, login assignments
Login Connections/v1/login-connectionsDelete login connections by UUID
Members & Invites/v1/accounts/{id}/members, /v1/invitesTeam management
Roles/v1/rolesRBAC role CRUD and permission catalog
Tokens/v1/tokensPopout/API token management (/tokens/me = current permissions)
Overlays/v1/overlaysOverlay configuration CRUD
Sounds/v1/soundsSound library management, upload, playback control, and streaming
Uploads/v1/uploadsFile uploads (multipart) and presigned download URLs
Chat/v1/chat/*History, send, moderate, user profiles, notes, raid/poll/prediction
Events/v1/eventsEvent history, single event, emit/test
Emotes/v1/emotesChannel 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-streamsActive and upcoming YouTube broadcasts (reads from Redis cache written by the YouTube polling worker)
Connections/v1/connectionsApp credentials and channel OAuth flow
Bot Connections/v1/bot-connections, /v1/bot-status, /v1/bot-toggle, /v1/bot-rejoinCustom bot identity OAuth and control
Bot Commands/v1/bot-commandsCross-platform command CRUD and global overrides
Bot Modules/v1/bot-modulesModeration module configs (link/spam/word/timed), extension bot module kill switch
Bot Module Triggers/v1/internal/bot-modulesInternal trigger resolution for Bot Module Worker (SystemKey)
Automations/v1/automationsVisual automation CRUD and manual execution
Automation Extension Nodes/v1/automation/extension-nodes, /v1/automation/webhooksExtension node listing and webhook receiver
Channel Status/v1/channel-status, /v1/spotify/manual-connectLive 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/notificationsIn-app notifications, actions, and delivery preferences
Ideas Hub/v1/ideas, /v1/ideas/adminCommunity 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-tokensStreamElements JWT token storage
Discord Guilds/v1/discord-guilds/exchangeDiscord guild bot install exchange
Abuse Reports/v1/abuse-reportsUser-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/playlistsRead-only global safe-song playlists
Songs/v1/songsRead-only song metadata and copyright status
Platforms/v1/platforms/metadataStatic platform metadata (name, icon, features, scopes)
Features/v1/features/enabled, /v1/providers/enabledPublic feature/provider flag reads
Extension Bundles/v1/extension-bundlesServe extension bundle files (JS, CSS, assets)
Extension Version Files/v1/extensions/{id}/versions/{version}/filesList files in an extension version bundle
Extension Uploads/v1/extensions/{id}/uploadsUpload/serve files from extension editors (icons, images)
Fonts/v1/fonts/{family}DSGVO-compliant font proxy (CSS + woff2), public scope
Developer Applications/v1/developer/applicationSubmit and view developer applications
Developer Teams/v1/developer/teamsTeam CRUD, members, invites, roles, permissions
Developer Store Profiles/v1/developer/profiles/{slug}, /v1/developer/teams/{slug}/profilePublic developer and team profile pages
Admin Developer Applications/v1/admin/developer-applicationsReview, approve, reject developer applications
Health/v1, /v1/healthLiveness 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:

ParameterTypeDescription
limitintegerPage size (default 20, max 100)
offsetintegerPagination offset
searchstringFilter 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

FieldTypeRequiredDescription
filefileYesAudio file (MP3, WAV, OGG, FLAC). Max size set by max_sound_file_size plan limit.
namestringNoDisplay name (defaults to filename without extension)

Errors:

  • 400 — Unsupported file type or file exceeds size limit
  • 409 — Account has reached the max_sounds plan limit

Response: 201 Created with the new sound object.

PATCH /v1/sounds/\{id\}

Update sound metadata.

Permission: sounds:edit

Body:

FieldTypeDescription
namestringNew 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:

FieldTypeRequiredDescription
volumefloatNoPlayback volume 0.0–1.0 (default 1.0)
targetstringNoTarget 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:

FieldTypeRequiredDescription
targetstringNoTarget 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: installedAutomationNodes query and automationWebhookUrl query 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:

FieldTypeRequiredNotes
login_connection_idUUIDYesID of the login connection to assign
providerstringYesPlatform slug, e.g. "twitch", "google"
user_idUUIDNoDefaults 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.

FieldTypeDescription
display_namestringUpdate display name (cannot be empty)
emailstringUpdate email address
active_account_idUUIDSwitch active account (must be a member)
clear_active_accountboolClear active account (go to user-only mode)
streamer_modeboolToggle Streamer Mode on/off
extension_dev_modeboolToggle 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:shopify in 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 description to null to 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" — if is_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

FieldTypeRequired
namestringYes
descriptionstring | nullNo
permissionsstring[]Yes (may be empty)

UpdateAdminRoleRequest

FieldTypeNotes
namestringOptional; trimmed
descriptionstring | nullnull = clear; omit = leave unchanged
permissionsstring[]Optional; replaces via diff

AdminRoleResponse

FieldType
idUUID
namestring
descriptionstring or null
is_systemboolean
permissionsstring[]
member_countinteger
created_atISO-8601 string
updated_atISO-8601 string

AdminRoleMemberResponse

FieldType
user_idUUID
display_namestring
emailstring or null
avatar_urlstring or null
assigned_atISO-8601 string

AdminPermissionInfoResponse

FieldType
permissionstring
categorystring

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:

FieldTypeRequiredNotes
slugstringYesRegex ^[a-z0-9]+(?:-[a-z0-9]+)*$, length 2–40, immutable after creation
namestringYesDisplay name
descriptionstring | nullNo
price_monthlyintegerYesCents (or the minor unit of currency)
price_yearlyintegerYesCents
currencystring | nullNoISO-4217 code; defaults to "USD"
is_publicbooleanYesWhether the plan is visible on the public pricing page
sort_orderintegerYesSort position in pricing pages
max_overlaysintegerYes
max_storage_bytesintegerYes
max_upload_size_bytesintegerYes
max_integrationsintegerYes
chat_retention_daysintegerYes0 = keep forever
max_soundsintegerYesMaximum number of sounds per account
max_sound_file_sizeintegerYesMaximum size per sound file in bytes
max_sound_storage_bytesintegerYesTotal sound storage quota in bytes
stripe_product_idstring | nullNoPaste from Stripe dashboard
stripe_monthly_price_idstring | nullNoPaste from Stripe dashboard
stripe_yearly_price_idstring | nullNoPaste from Stripe dashboard

PATCH /v1/admin/plans/{id}

Requires plans:edit. Body: UpdatePlanRequest. Returns 200 + AdminPlanResponse.

  • The slug is immutable and is therefore not part of UpdatePlanRequest.
  • 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:

FieldTypeNotes
namestring
descriptionstring | null
price_monthlyinteger
price_yearlyinteger
currencystring
is_publicboolean
sort_orderinteger
max_overlaysinteger
max_storage_bytesinteger
max_upload_size_bytesinteger
max_integrationsinteger
chat_retention_daysinteger
max_soundsinteger
max_sound_file_sizeinteger
max_sound_storage_bytesinteger
stripe_product_idstring | null
stripe_monthly_price_idstring | null
stripe_yearly_price_idstring | 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

FieldType
idUUID
slugstring
namestring
descriptionstring or null
price_monthlyinteger
price_yearlyinteger
currencystring
is_publicboolean
sort_orderinteger
max_overlaysinteger
max_storage_bytesinteger
max_upload_size_bytesinteger
max_integrationsinteger
chat_retention_daysinteger
max_soundsinteger
max_sound_file_sizeinteger
max_sound_storage_bytesinteger
stripe_product_idstring or null
stripe_monthly_price_idstring or null
stripe_yearly_price_idstring or null
features[AdminPlanFeatureResponse]
accounts_usinginteger

AdminPlanFeatureResponse

FieldType
feature_idUUID
feature_keystring
labelstring
enabledboolean

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:

ParameterDescription
platformPlatform slug: twitch, youtube, kick, or trovo
platform_user_idThe platform-native user identifier

Responses:

StatusBodyDescription
200Refreshed 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

MethodPathPermissionDescription
GET/v1/notifications/preferencesAuthList all delivery-channel preferences for the current user
PATCH/v1/notifications/preferences/{type}AuthSet 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: notificationPreferences query and updateNotificationPreference mutation 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

MethodPathDescriptionPermission
GET/v1/ideasList ideas (filter, sort, paginate)Public
GET/v1/ideas/:idGet idea with timelinePublic
POST/v1/ideasCreate ideaideas:create
PATCH/v1/ideas/:idUpdate ideaideas:edit / ideas:moderate_edit
DELETE/v1/ideas/:idDelete ideaideas:delete / ideas:moderate_delete

Query parameters for GET /v1/ideas:

ParameterTypeDescription
statusstringFilter by status slug
category_idUUIDFilter by category
tag_idsstringComma-separated list of tag UUIDs
author_idUUIDFilter by author
searchstringFull-text search on title and description
sortstringnewest, most_voted, most_commented, recently_updated
limitintegerPage size
offsetintegerPage offset

Voting

MethodPathDescriptionPermission
POST/v1/ideas/:id/voteVote on an ideaideas:vote
DELETE/v1/ideas/:id/voteRemove voteideas:vote

POST body: { "vote_type": "up" | "down" }.

Comments

MethodPathDescriptionPermission
GET/v1/ideas/:id/commentsList comments (nested)Public
POST/v1/ideas/:id/commentsCreate commentideas:comment_create
PATCH/v1/ideas/comments/:idUpdate commentideas:comment_edit
DELETE/v1/ideas/comments/:idDelete commentideas: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

MethodPathDescriptionPermission
GET/v1/ideas/:id/participantsList participants for @mention autocompleteAuth only

Returns the union of the idea author, voters, and commenters. Supports an optional search query parameter.

Voters

MethodPathDescriptionPermission
GET/v1/ideas/:id/votersList voters for an ideaPublic

Categories

MethodPathDescriptionPermission
GET/v1/ideas/categoriesList all categoriesPublic
POST/v1/ideas/categoriesCreate categoryAdmin ideas:edit
PATCH/v1/ideas/categories/:idUpdate categoryAdmin ideas:edit
DELETE/v1/ideas/categories/:idDelete categoryAdmin ideas:delete

Tags

MethodPathDescriptionPermission
GET/v1/ideas/tagsList / search tagsPublic
POST/v1/ideas/tagsCreate tagideas:create
DELETE/v1/ideas/tags/:idDelete tagAdmin ideas:delete

GET /v1/ideas/tags supports an optional search query parameter.

Status (Moderation)

MethodPathDescriptionPermission
PATCH/v1/ideas/:id/statusChange idea statusideas: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:

FieldTypeDescription
idUUIDApplication ID
user_idUUIDApplicant's user ID
application_typestring"solo" or "team"
display_namestringDeveloper/team display name
slugstringURL slug (3-50 chars, lowercase, hyphens)
descriptionstringDescription of the developer/team
motivationstringWhy the user wants developer access
what_to_buildstringWhat extensions the user plans to build
github_urlstring or nullGitHub profile URL
website_urlstring or nullWebsite URL
experiencestring or nullDevelopment experience
avatar_keystring or nullUpload key for avatar image
statusstring"pending", "approved", or "rejected"
review_notesstring or nullNotes from the reviewer
reviewed_byUUID or nullReviewer's user ID
reviewed_atISO-8601 or nullReview timestamp
created_atISO-8601Submission timestamp
updated_atISO-8601Last 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:

FieldTypeRequiredDescription
application_typestringYes"solo" or "team"
display_namestringYesDeveloper/team display name
slugstringYesURL slug (3-50 chars, starts with lowercase letter, only lowercase letters, digits, hyphens)
descriptionstringYesDescription
motivationstringYesWhy you want developer access
what_to_buildstringYesWhat you plan to build
github_urlstringNoGitHub profile URL
website_urlstringNoWebsite URL
experiencestringNoDevelopment 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: myDeveloperApplication query and submitDeveloperApplication mutation 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:

ParameterTypeDescription
statusstringFilter 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:

FieldTypeRequiredDescription
notesstringNoOptional 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:

FieldTypeRequiredDescription
notesstringYesRejection 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

MethodPathPermissionDescription
POST/v1/developer/teamsAuth (developer)Create a new team
GET/v1/developer/teamsAuth (developer)List teams the user belongs to
GET/v1/developer/teams/{id}team-settings:readGet team details
PATCH/v1/developer/teams/{id}team-settings:editUpdate team name/description/URLs
DELETE/v1/developer/teams/{id}Team owner onlyDelete a team

Team Members

MethodPathPermissionDescription
GET/v1/developer/teams/{id}/membersteam-members:readList team members
PATCH/v1/developer/teams/{id}/members/{member_id}/roleteam-members:editChange a member's role
DELETE/v1/developer/teams/{id}/members/{member_id}team-members:removeRemove a member

Team Invites

MethodPathPermissionDescription
POST/v1/developer/teams/{id}/invitesteam-members:inviteCreate an invite link
GET/v1/developer/teams/{id}/invitesteam-members:inviteList active invites
DELETE/v1/developer/teams/{id}/invites/{invite_id}team-members:inviteRevoke an invite
GET/v1/developer/team-invites/{code}AuthLook up an invite by code
POST/v1/developer/team-invites/{code}/acceptAuthAccept an invite

Team Roles (RBAC)

MethodPathPermissionDescription
GET/v1/developer/teams/{id}/rolesteam-members:readList roles for a team
POST/v1/developer/teams/{id}/rolesteam-settings:editCreate a custom role
PATCH/v1/developer/teams/{id}/roles/{rid}team-settings:editUpdate a role
DELETE/v1/developer/teams/{id}/roles/{rid}team-settings:editDelete a role
GET/v1/developer/teams/permissionsAuthList 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:

FieldTypeDescription
idUUIDDeveloper profile ID
display_namestringDeveloper display name
slugstringURL slug
descriptionstring or nullBio/description
github_urlstring or nullGitHub profile URL
website_urlstring or nullWebsite URL
avatar_keystring or nullAvatar upload key
created_atISO-8601Profile creation date
statsobject{ extension_count, total_installs, avg_rating }
extensionsarrayList 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:

FieldTypeDescription
idUUIDTeam ID
namestringTeam name
slugstringURL slug
descriptionstring or nullTeam description
github_urlstring or nullGitHub URL
website_urlstring or nullWebsite URL
avatar_keystring or nullAvatar upload key
created_atISO-8601Team creation date
statsobject{ extension_count, total_installs, avg_rating }
membersarray[{ user_id, display_name, avatar_url, role }]
extensionsarrayPublished 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:

FieldTypeRequiredDescription
install_idUUIDYesThe 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:

ParameterTypeDescription
limitintegerMax results (default 50, max 200)
offsetintegerPagination offset
sinceISO-8601Only 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:

ParameterTypeDescription
periodstringAggregation 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:

ParameterDescription
extension_idUUID of the extension
versionSemver version string (e.g. 1.2.0)
pathFile 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.gz is 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:

ParameterDescription
idUUID of the extension
versionSemver 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:

ParameterTypeDescription
limitintegerMax results (default 50, max 200)
offsetintegerPagination offset
levelstringFilter 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"
}
]
}