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
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)
Automations/v1/automationsVisual automation CRUD and manual execution
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
Plugins/v1/pluginsPlugin registry (list + by type)
Features/v1/features/enabled, /v1/providers/enabledPublic feature/provider flag reads
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.

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), 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.

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

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
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
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
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.