lua-cli. Every family on this tab is a developer-facing route: /chat/* for conversation turns, /developer/* for agent-scoped data, channels, commerce, persona, and versions, /marketplace/* for agent templates and marketplace skills, and /workflows/* for the workflow runtime.
Verified against lua-cli 3.33.0.
Base URL
All routes are served from one host, chat routes included:LUA_API_URL when you set it, so a self-hosted or staging deployment is addressed by changing that one variable.
Authentication
Every request carries an API key as a bearer token. A user session token from the CLI or admin dashboard works too, but for scripts and servers use a key.401 with No token provided; a key that is invalid, expired, suspended, or revoked answers 401 with Invalid or expired token. A revoked user session adds a code to that body so a client can sign in again instead of retrying. The key identifies the caller: on chat routes the key’s owner is the end user the conversation belongs to.
Scopes
A scoped key must hold the scope each route declares on the agent, or on the organization for organization-wide routes. Legacy keys act as their owner and pass every check the owner would. The families on this tab need:
A wildcard grant such as
agents:* or *:read covers every scope in its area except the sensitive ones, which a key holds only when they were granted by exact name: secrets:read, secrets:write, credits:grant, credits:deduct, billing:manage, negotiated-plans:write, telephony:provision, members:assign-role, api-keys:issue, tokens:mint, rights:assign, marketplace:review, promo-codes:manage, and platform:*. No route on this tab needs one.
A key that can see the agent but lacks the scope answers 403 with code: "INSUFFICIENT_SCOPE" and the requiredScope. A key that cannot see the agent at all, because the agent is private and the key has no grant on it, or because the id belongs to another organization or does not exist, answers 403 with code: "RESOURCE_NOT_PROJECTED"; the two cases are deliberately indistinguishable. A scoped key reaches a private agent only through an explicit agent grant, and the message says so: This credential is not bound to this agent. Issue a credential bound to the agent (or add an agent grant).
Errors
Most routes answer errors in the default Nest shape. Validation failures list every violated constraint:{ statusCode, message, code, ...detail }, where code is one of the workflow error codes and validation failures carry details[] with path, constraint, and message. The statuses you handle across families:
A
424 is built by POST /ai/generate/:agentId only; the chat routes report a provider refusal in-band. It is deterministic: retrying the same request fails identically. Its body tells you whose key was refused, so you know whether the fix is yours or Lua’s:
reason is one of invalid_api_key, forbidden, model_not_found, content_refused, quota_exhausted, or bad_request; keyOwner is byok for a key your organization configured or platform for Lua’s. The same fields ride the chat stream’s error chunk. The CLI maps these statuses to its exit codes: 401 to 9, 403 to 10, 5xx to 11, and 424 to 12.
A 503 whose body is exactly { "error": { "code": "CORE_DRAINING", "message": "Lua is restarting. Please retry your request.", "retryable": true } } means the request never started; retry once after the Retry-After header. Every other 503 carries retryAfterSeconds only when a blind retry is safe, so honor it when present and treat its absence on a write as “the write may have applied”.
Pagination
Two conventions exist, and each family says which it uses. Page-numbered lists (custom data, products, user data, logs) takepage (from 1) and limit and answer a pagination object beside data:
limit (default 50, at most 100) and hand nextCursor back as cursor until it is null. A cursor that does not decode answers 400 with BAD_CURSOR. Delivery lists take limit from 1 to 200 (default 50) and return the newest records first.
Idempotency
Two headers make retries safe, on the routes that honor them:X-Idempotency-Keyon the four channel send routes. Replaying a key returns the first send’s stored outcome instead of sending again, for as long as the delivery record lives; a key whose first attempt failed replays the failure; the same key from another agent is a different key. The CLI adds a UUID key to every POST it makes, so aluacommand that sends never double-sends.Idempotency-Key, or the body fieldidempotencyKey, onPOST /workflows/:agentId/runsand on every item of a batch start. The same key with the same definition and input answers200with the existing run andidempotentReplay: true; the same key with anything else answers409withIDEMPOTENCY_KEY_REUSEDnaming the run that holds it.
Rate limits
Workflow routes are throttled per calling user per minute, in named buckets. A route names exactly one bucket:
Past the limit the route answers
429 with RATE_LIMITED, retryAfterSeconds, bucket, limit, and windowSeconds, plus a Retry-After header and a per-bucket Retry-After-wf-<name>.
The template catalog is throttled per client address: GET /marketplace/templates at 60 per minute, and the template detail and manifest reads at 20 per minute, answered with the plain 429 body { "statusCode": 429, "message": "ThrottlerException: Too Many Requests" }. No other route on this tab declares a limit in the API; whether the chat, channel, data, and commerce routes sit behind an edge rate limit, and at what value, is not documented here.
Versioning
Routes have no version prefix and no version header. Changes are additive: new optional fields and new codes appear without notice, and a client must ignore fields it does not know. A removed or renamed route is announced in the changelog before it goes.See also
- Credentials — key classes and the full scope table
- Errors and exit codes — how the CLI reports the same statuses
- Chat — send a turn to an agent over HTTP
- Workflows — the typed error envelope and every runtime route
- Automate releases in CI — calling the API from a pipeline with a scoped key

