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 |
| 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) |
| Automations | /v1/automations | Visual automation CRUD and manual execution |
| 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 |
| Plugins | /v1/plugins | Plugin registry (list + by type) |
| Features | /v1/features/enabled, /v1/providers/enabled | Public feature/provider flag reads |
| 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.
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), and login_connections (OAuth login identities linked to the user, filtered by enabled providers).
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 |
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 |
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 | |
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 |
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.