Lumorig has two kinds of credentials. People sign in to the studio with a browser session. Code (your server, the SDK, the CLI, MCP clients) uses an API key. Both act on a workspace: the unit that owns characters, credits, keys and webhooks.
| Browser session | API key | |
|---|---|---|
| Who | A signed-in person | Your code, with no person attached |
| How | httpOnly cookie, set by signing in at /login | Authorization: Bearer lr_sk_… |
| Workspace | The active one (switcher), or the Lumorig-Workspace header (the older MascotForge-Workspace spelling also works) | The key's workspace, always |
| Powers | Your role in that workspace | Admin: content, credits, keys, webhooks. Not people or billing. |
| Lifetime | 30 days from last use; revocable | Until revoked |
Signing in#
There are no passwords. Sign in at /login with a one-time email link, or with Google or GitHub where the server has them configured. The first sign-in creates your account and a personal workspace on the Free plan.
- Email links work once and expire after 15 minutes. Opening one shows a confirm button, so mail scanners that prefetch links can't use them up. Requests are rate-limited per address and per network.
- Google and GitHub sign you in to the account with the same verified email address, and can be connected or disconnected under Settings → Account. An unverified provider address never signs in to an existing account.
- Sessions are random tokens stored only as hashes. Each lasts 30 days from its last use. Settings → Account lists every signed-in browser, and you can revoke one or sign out everywhere.
Workspaces, roles and seats#
Everything you make belongs to a workspace, and everyone in it shares its characters and credit balance. You can belong to several (your personal one plus teams) and switch between them in the studio's top bar.
| Role | Can |
|---|---|
| Owner | Everything an admin can, plus change the plan, transfer ownership and delete the workspace. Exactly one per workspace. |
| Admin | Invite and remove people, change roles, create and revoke API keys, manage webhooks, make characters. |
| Member | Make, edit, animate and export characters, and spend the workspace's credits. Can't see or manage keys, webhooks or people. |
Seats cap how many people a workspace holds: Free 1, Maker 1, Studio 3. A pending invite holds a seat until it's accepted, revoked or expires (7 days). Inviting beyond the limit returns 402 with code seat_limit:
{
"error": {
"code": "seat_limit",
"message": "Studio includes 3 seats and all are taken (2 members and 1 pending invite). Remove a member or revoke an invite to free one.",
"details": { "seats": 3, "used": 3, "members": 2, "pending": 1 }
}
}Invites are sent by email and can only be accepted by someone signed in with the invited address (or a Google/GitHub account with that verified address). Managing people needs a signed-in person: API keys get 403 session_required on these endpoints.
| Endpoint | Does |
|---|---|
GET/workspace | The workspace this request acts on: name, plan, your role, and seats. Works with keys. |
GET/workspace/members | Everyone in the workspace, owner first. Works with keys. |
POST/workspace/invites | Invite { "email", "role": "member" | "admin" }. Admins; session only. |
PATCH/workspace/members/{user_id} | Change a role. Admins; only the owner changes another admin. |
DELETE/workspace/members/{user_id} | Remove someone, or leave (your own id). Owners transfer first. |
POST/workspace/transfer | Make another member the owner; you become an admin. Owner only. |
API keys#
A key belongs to one workspace and acts with admin powers there: create and edit mascots, spend its credits, manage webhooks and other keys. It isn't tied to the person who made it (the key list shows who did), so it keeps working when they leave. Keys look like lr_sk_ followed by 32 random characters. Keys created before the rename start with mf_sk_ and keep working. We store only a hash, so the full key is shown exactly once, when you create it.
Admins and the owner create keys in the studio under Settings → API keys. After that you can also create keys from code (below).
Sending the key#
Send it as a Bearer token in the Authorization header on every request to /api/v1. A missing or unknown key returns 401 with code unauthorized.
curl https://lumorig.com/api/v1/mascots \
-H "Authorization: Bearer $LUMORIG_API_KEY"MCP clients that cannot set headers may pass the key as a query parameter instead: https://lumorig.com/api/mcp?key=lr_sk_…. Prefer the header where you can; URLs end up in logs.
Managing keys#
Keys can create, list and revoke other keys in the same workspace. Listing never returns the secret, only its first 12 characters.
| Endpoint | Does |
|---|---|
POST/keys | Create a key. Body: { "name": "CI" } (optional). Returns { id, key, prefix }; key is shown once. |
GET/keys | List active keys: id, name, prefix, created_at, last_used_at. |
DELETE/keys/{id} | Revoke a key. Requests with it return 401 from then on. |
curl -X POST https://lumorig.com/api/v1/keys \
-H "Authorization: Bearer $LUMORIG_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "CI" }'
# 201 → { "id": "key_…", "key": "lr_sk_…", "prefix": "lr_sk_AbCdEf" }Public and shared bundles#
Two read-only endpoints are built to be called from browsers without a key: the player bundle and the still preview. They serve a mascot in two cases.
Public mascots#
Set public: true on a mascot (PATCH /mascots/{id}) and its /bundle and /preview work for anyone who has the id. The bundle is served with open CORS so <lumorig-player> can load it from any origin.
Signed share links#
For a private mascot, POST /mascots/{id}/share returns a token and a ready bundle_url with ?token= attached. The token is an HMAC-signed mascot id plus expiry; ttl_seconds defaults to 3600 and is capped at 30 days. An expired or mismatched token returns 403 forbidden.
curl -X POST https://lumorig.com/api/v1/mascots/msc_…/share \
-H "Authorization: Bearer $LUMORIG_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "ttl_seconds": 604800 }'
# → { "token": "…", "expires_at": "2026-10-03T12:00:00.000Z", "bundle_url": "/api/v1/mascots/msc_…/bundle?token=…" }Environment variables#
The SDK, the CLI and the local MCP server read the same two variables.
| Variable | Used by | Meaning |
|---|---|---|
LUMORIG_API_KEY | SDK, CLI, MCP (stdio) | Your lr_sk_… key. For the CLI it overrides the key saved by lumorig login. |
LUMORIG_URL | SDK, CLI, MCP (stdio) | API origin. Defaults to https://lumorig.com; set it for a self-hosted server. |
LUMORIG_HOME | CLI | Where lumorig keeps its config and companion state. Defaults to ~/.config/lumorig. |
Names from before the rename (MASCOTFORGE_API_KEY, MASCOTFORGE_URL, MASCOTFORGE_HOME) are still read as fallbacks.
Self-hosting#
A server started without MF_LOCAL=0 runs in local mode: /login offers Continue locally, which signs in as the local owner of the workspace that holds everything created before accounts existed. Set MF_LOCAL=0 in production, plus MF_PUBLIC_URL, RESEND_API_KEY and MAIL_FROM for email, and GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET and GITHUB_CLIENT_ID/GITHUB_CLIENT_SECRET to offer those buttons (callback: {MF_PUBLIC_URL}/api/auth/oauth/google/callback, and …/github/callback). Without a mailer, development servers print sign-in links to the console.