Skip to content
Lumean API

Keys, scopes and headers

X-API-KEY as a separate surface, eleven domain scopes plus a wildcard, three presets — and why “automation” doesn't cover the basic scenario end to end.

The public API authenticates with a single header and nothing else. It has no anonymous endpoints: every operation in the spec requires a key, and almost every one names a specific scope in its own description. The exception is the AI model catalog: any active key works for it. There is one fewer reference page than there are operations: PUT and PATCH /templates/{template} are collapsed into one (it's a single operation under two methods), and POST /broadcasting/auth is explained in the Realtime section.

X-API-KEY: <your key>

A separate surface, not a second way to sign in

The public surface (/api/public/*) and the dashboard API are different surfaces with different authentication. The dashboard Bearer token doesn't work here; conversely, a key doesn't open dashboard endpoints. The header is called exactly X-API-KEY — neither Authorization nor Api-Key is recognized, and both get the same response as a missing key.

401401 — key rejected
What happened
The request was rejected before any business logic ran: no action was performed and nothing was charged. The body is the same on every endpoint: `{"success": false, "message": "API key is missing. Provide it in the X-API-KEY header.", "data": null}`. The public API does not accept anonymous requests anywhere.
What to do right now
Make sure the header is named exactly `X-API-KEY` (not `Authorization`, not `Api-Key`) and the value is sent in full. If the key is correct, open the developer section and check that the key is active and has not expired. Retrying is pointless: a 401 is deterministic, and a retry with the same key gets the same response.
Open the code page

The key is hashed (sha256) and resolved with a cache of about an hour. The practical consequence: editing a key's scopes or deactivating it in the dashboard doesn't take effect instantly — requests may keep going through on the old state for a while.

Eleven scopes and a wildcard

The scope is checked before any business logic: a key without the required scope gets 403 and nothing is executed. The “Operations” column shows how many public API endpoints the scope opens (reference pages plus POST /broadcasting/auth).

ScopeWhat it opensOperations
orders.readReading orders and chunks, the text chunking preview, WebSocket subscription to orders9
orders.writeCreating, canceling and retrying orders; retrying and regenerating chunks6
orders.downloadPOST /storage/url — exchanging a file path for a temporary link1
templates.readReading templates, browse, config-options4
templates.writeCreating, updating and deleting templates3
voices.readAll voice catalogs: LumVoice, ElevenLabs, HeyGen, Cartesia, Fish Audio; your own clones and their quota14
voices.writeCreating, retrying and archiving Cartesia and Fish Audio voice clones6
billing.readusage, subscriptions3
profile.readGET /user1
reseller.readReseller dashboard: profile and balance, settings, statistics, codes with their full value5
reseller.writeIssuing gift codes from the reseller balance and canceling them2
*Passes any scope check, including scopes that don't exist yet — except reseller.read and reseller.writeall except reseller

The two AI model catalog operations (GET /ai/models and GET /ai/models/{key}) require no scope at all — they're not in the table.

Gotcha

* does not cover the reseller scopes, and they aren't part of any preset: a gift code is bearer money, so a key gets reseller.read and reseller.write only when they are listed explicitly. A wildcard key will get 403 on GET /reseller.

Gotcha

orders.download is not covered by orders.read. A key that reads orders perfectly well and sees the paths in result.files[] will get 403 when exchanging a path for a link. This is the most common cause of an unexpected rejection at the end of a scenario — when the file has already been generated and paid for.

Three presets

When you create a key, its scopes are usually picked with a preset.

PresetScopesDoesn't open
read_onlyorders.read, orders.download, templates.read, voices.read, billing.read, profile.readanything that writes orders, templates and voice clones, and the reseller dashboard
automationthe same plus orders.writePOST, PUT/PATCH and DELETE /templates/{template}, creating, retrying and archiving clones, the reseller dashboard
fullall scopes except the reseller ones (including templates.write and voices.write)the reseller dashboard

The wildcard * is a separate value, not a synonym for the full preset: it also passes checks that didn't exist when the key was issued (but not the reseller scopes). Which of the two ways to “grant everything” the dashboard applies when you pick full isn't specified by the sources — if it matters, check the scope list of the already created key in the dashboard.

The automation trap

The preset named “for automation” doesn't include templates.write — only templates.read. This isn't a documentation typo or a bug: that's how it's designed. By the same logic it also lacks voices.write: for a key that clones voices, this scope is added manually, or you take full.

The consequence is concrete. For TTS, the voice is set not by an order field but by the template's config.tts_settings.voice_id — so the canonical scenario starts with POST /templates. With an automation key it stops at the very first step: 403, and the order is never created.

Do

For the “create a template → order TTS” scenario, take the full preset — or automation with templates.write added manually when you create the key.

Don't

Take automation because the task is called automation, and then look for the cause of the 403 in the order itself — even though it was the template creation step that was rejected.

If the template was created in advance (on the web or with another key), automation is enough: the template's owner is the account, not a specific key. Someone else's template, however, won't open with any key.

403template_access_deniedtemplate_access_denied — no access to the template
What happened
A domain refusal of order creation: HTTP 403, body `{"success": false, "message": "…", "reason": "template_access_denied"}`. The order was not created.
What to do right now
Check that the template is visible to the key. A template is owned by the account, not by a specific key: a template created on the web works for the API and vice versa, but a template that belongs to another account will not open with any key. If the template is yours, the problem is scopes: `automation` does not include `templates.write`, but it does include `templates.read`.
Open the code page

Language and timezone headers

Both headers are optional, and neither is declared as a parameter in the OpenAPI — a client generated from the spec won't know about them, so you'll have to send them manually.

  • X-Locale — the language of human-readable strings: message in responses and names in reference lists such as GET /voices/tags. It has no effect whatsoever on machine fields (reason, statuses, codes), and that's exactly why you must not branch on message. The sources disagree: the OpenAPI names this header in 79 descriptions and never mentions Accept-Language, while the LLM reference does the opposite; if the response language is critical, check the actual behavior with your own key and send both headers.
  • X-Timezone — an IANA zone name (Europe/Moscow). Points in time in responses arrive in ISO-8601 with an explicit offset, UTC by default. A bare offset (+03:00) is rejected: it doesn't know about daylight saving time. The server returns the zone it actually applied in the same header — send garbage and you'll see X-Timezone: UTC in the response.

What a key can't do

The public surface doesn't process payments and doesn't manage itself. With a key you cannot: top up the balance, make transfers, buy subscriptions, change the profile, or manage the API keys themselves. All of that stays in the dashboard. Separately: orders have no update/delete — only cancel and retry, and PUT/PATCH/DELETE /orders/{order} will answer 405/404.

Key smoke test

The cheapest check is the profile: one operation, one scope, no side effects.

curl -s -o /dev/null -w '%{http_code}\n' \
  'https://api.lumean.app/api/public/user' \
  -H 'X-API-KEY: <your key>'
CodeWhat it means
200The key is active and has the profile.read scope
403The key works — authentication passed, only profile.read is missing
401The key wasn't accepted: not sent, truncated, unknown, deactivated or expired

The key nuance is the second row: 403 confirms that the key is alive. If your key lacks profile.read by design, check GET /orders?per_page=1 instead — it needs only orders.read.

GET/userGet profile of API key owner

Get profile of API key owner

Keys are created in the Developers section, which also shows the active scopes and limits of each key. Don't have a key yet? Sign up first — keys are issued only to the account owner.

Feed this page to an AI

Open .mdllm.md

Updated 10/17/2026 · spec 0.0.1