> ## Documentation Index
> Fetch the complete documentation index at: https://docs.heylua.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# REST API overview

> Base URL, bearer authentication, scopes, the error envelope, pagination, idempotency, rate limits, and versioning for the Lua REST API

The REST API is the surface the CLI, the SDK runtime objects, and the admin dashboard call. Use it directly when a service of your own needs to talk to an agent, send on a channel, read or write user data and collections, run commerce, manage versions, or drive workflow runs without going through `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:

```text theme={null}
https://api.heylua.ai
```

Paths carry no version segment. The CLI reads the same host from `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](/concepts/credentials) as a bearer token. A user session token from the CLI or admin dashboard works too, but for scripts and servers use a key.

```text theme={null}
Authorization: Bearer <<YOUR_API_KEY>>
Content-Type: application/json
```

A missing or malformed header answers `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](/concepts/credentials#which-scope-an-area-needs) 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:

| Family                                                   | Read                                                                                                  | Write                                                                     |
| -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| [Chat](/reference/rest/chat)                             | Any valid credential; no scope                                                                        | `org:manage` only to clear another end user's history                     |
| [Channels](/reference/rest/channels)                     | `channels:send`                                                                                       | `channels:send`                                                           |
| [User data](/reference/rest/user-data)                   | `knowledge:read`                                                                                      | `knowledge:write`                                                         |
| [Custom data](/reference/rest/custom-data)               | `knowledge:read`                                                                                      | `knowledge:write`                                                         |
| [Commerce](/reference/rest/commerce)                     | `commerce:read`                                                                                       | `commerce:write`                                                          |
| [Persona](/reference/rest/persona)                       | `agents:read`                                                                                         | `agents:write`; `agents:manage` to discard a draft                        |
| [Versions](/reference/rest/versions)                     | `agents:read`                                                                                         | `agents:write`; `agents:manage` to delete                                 |
| [Agent templates](/reference/rest/agent-templates)       | `marketplace:read`                                                                                    | `automations:write`; `org:manage` to transfer ownership                   |
| [Template fleet](/reference/rest/template-fleet)         | `marketplace:read`                                                                                    | `automations:write`                                                       |
| [Marketplace skills](/reference/rest/marketplace-skills) | `automations:read`                                                                                    | `automations:write`; `org:manage` to transfer ownership                   |
| [Workflows](/reference/rest/workflows)                   | `workflows:read`; `workflows:read-outputs` for step payloads, artifacts, journal entries, and exports | `workflows:write` to author, `workflows:execute` to start and act on runs |

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:

```json theme={null}
{ "statusCode": 400, "message": ["searchText must be a string"], "error": "Bad Request" }
```

A permission denial names the resource you asked for, never the one the server resolved behind it:

```json theme={null}
{
  "statusCode": 403,
  "error": "Forbidden",
  "code": "INSUFFICIENT_SCOPE",
  "message": "Insufficient permissions",
  "resourceType": "agent",
  "resourceId": "<<YOUR_AGENT_ID>>",
  "requiredScope": "channels:send"
}
```

Workflow routes use a typed envelope on every error, `{ statusCode, message, code, ...detail }`, where `code` is one of the [workflow error codes](/reference/rest/workflows#error-codes) and validation failures carry `details[]` with `path`, `constraint`, and `message`. The statuses you handle across families:

| Status | Code or message                                                                      | Meaning                                                                                                                        | Fix                                                     |
| ------ | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------- |
| `400`  | Nest shape, or `VALIDATION_FAILED` on workflow routes                                | Validation failed or a filter did not parse                                                                                    | Fix the field the message names                         |
| `401`  | `message`, plus `code` for a revoked user session                                    | No credential, or the credential was refused                                                                                   | Send a valid key; sign in again on `code`               |
| `402`  | `message`; `code` such as `seat_required`                                            | A billing refusal on a route that checks credits or a seat before admitting the request; chat routes report it in-band instead | Add credits, or a seat for the caller                   |
| `403`  | `INSUFFICIENT_SCOPE` or `RESOURCE_NOT_PROJECTED`                                     | Scope or projection denied                                                                                                     | Grant the `requiredScope`, or bind the key to the agent |
| `404`  | `message`                                                                            | The agent, entry, run, or record does not exist                                                                                | Check the id                                            |
| `409`  | Workflow codes such as `IDEMPOTENCY_KEY_REUSED`, `RUN_TERMINAL`, `PAYLOAD_MISMATCH`  | Idempotency key reuse, a run that already ended, a payload edited under you                                                    | Re-read the record; retry with a fresh key or payload   |
| `413`  | `PAYLOAD_TOO_LARGE` with `field` and `max`                                           | A body or field over its cap                                                                                                   | Shrink the field                                        |
| `422`  | `VENDOR_REJECTED` with `vendor` and `vendorStatus`                                   | A vendor refused the request as written                                                                                        | Change the request; a retry fails identically           |
| `423`  | `This agent is disabled.`, `ORG_ARCHIVED`                                            | The agent is disabled for this end user, or the organization is archived                                                       | Enable the agent, or unarchive the organization         |
| `424`  | `PROVIDER_REJECTED`                                                                  | The model provider rejected the request as configured                                                                          | Fix the key or model the body names                     |
| `429`  | `RATE_LIMITED`, `CONCURRENCY_CAP`, `SUSPENDED_CAP` with `retryAfterSeconds`          | A rate limit or a concurrency cap                                                                                              | Wait for the `Retry-After` header                       |
| `503`  | `UPSTREAM_UNAVAILABLE`, `CONTROL_UNAVAILABLE`, `VENDOR_UNAVAILABLE`, `CORE_DRAINING` | A Lua service or a vendor behind the API did not answer                                                                        | Retry only when the body says so                        |

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:

```json theme={null}
{
  "statusCode": 424,
  "code": "PROVIDER_REJECTED",
  "providerStatus": 401,
  "reason": "invalid_api_key",
  "keyOwner": "byok",
  "message": "The model provider rejected the request (401 invalid_api_key): ...",
  "transient": false
}
```

`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](/reference/cli/errors-and-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) take `page` (from 1) and `limit` and answer a `pagination` object beside `data`:

```json theme={null}
{
  "currentPage": 1, "totalPages": 5, "totalCount": 47, "limit": 10,
  "hasNextPage": true, "hasPrevPage": false, "nextPage": 2, "prevPage": null
}
```

Workflow lists are cursor-paged: pass `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-Key` on 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 a `lua` command that sends never double-sends.
* `Idempotency-Key`, or the body field `idempotencyKey`, on `POST /workflows/:agentId/runs` and on every item of a batch start. The same key with the same definition and input answers `200` with the existing run and `idempotentReplay: true`; the same key with anything else answers `409` with `IDEMPOTENCY_KEY_REUSED` naming the run that holds it.

Every other write is applied once per request.

## Rate limits

Workflow routes are throttled per calling user per minute, in named buckets. A route names exactly one bucket:

| Bucket       | Limit per minute | Routes                                                                                 |
| ------------ | ---------------- | -------------------------------------------------------------------------------------- |
| `wf-read`    | 600              | Every `GET`                                                                            |
| `wf-start`   | 30               | `POST /workflows/:agentId/runs`                                                        |
| `wf-batch`   | 5 per principal  | `POST /workflows/:agentId/runs/batch`                                                  |
| `wf-compose` | 20               | `POST /workflows/:agentId/compose`                                                     |
| `wf-mutate`  | 60               | Resume, approve, signal, cancel, erase, retry, resolve, budget, goal actions, reassign |
| `wf-author`  | 30               | Schedules, goal create, artifact upload, run export                                    |

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](/changelog) before it goes.

## See also

* [Credentials](/concepts/credentials) — key classes and the full scope table
* [Errors and exit codes](/reference/cli/errors-and-exit-codes) — how the CLI reports the same statuses
* [Chat](/reference/rest/chat) — send a turn to an agent over HTTP
* [Workflows](/reference/rest/workflows) — the typed error envelope and every runtime route
* [Automate releases in CI](/ship/ci-and-automation) — calling the API from a pipeline with a scoped key
