Skip to main content
The chat routes run one conversation turn for the end user the credential belongs to: the message goes through the agent’s preprocessors, model, tools, and postprocessors, and the reply is stored in the thread. Use them from a service, app, or device of your own; inside a tool, job, or webhook call Agents.invoke instead, which runs the same pipeline without a token. Verified against lua-cli 3.33.0.

Base URL and authentication

Chat routes require a valid credential and no scope; the key’s owner is the end user of the conversation, so one key is one end user. Clearing another end user’s history is the exception and needs org:manage on the agent. The host, the bearer header, and the error envelope are on the REST API overview.

Threads

Every turn belongs to a thread. The default thread is one per end user and agent; a threadId in the body is appended to it as a suffix, so two callers with the same key and the same threadId share one conversation and a fresh value starts a clean one. The history routes take the same threadId as a query parameter. Equivalent: lua chat -e production -m "<text>" -t <threadId> sends a turn; lua chat clear clears history.

Endpoints

POST /chat/generate/:agentId

Runs one turn and answers when the reply is complete.
string
required
The agent to talk to.
string
default:"unknown"
Sets Lua.request.channel for the turn, which tools, processors, and the model resolver read, and metadata.channel on the turn’s log lines. Any string is accepted; use api for a direct integration. The value also picks the reply’s formatting rulebook: web selects the plain-Markdown rulebook the desktop and admin dashboard chat use, and every other value, including pop, which the web widget sends, selects the messaging rulebook whose replies may carry ::: formatting component blocks. clientCapabilities: ["plain-markdown"] forces plain Markdown on any channel.
string
A free-form tag stored on the request, for example the sender’s address on the channel you are bridging. It is not an end-user id; the end user comes from the credential.
string
Pass false to mark the turn as not interactive, the way channel bridges and automations do; an interactive turn is one an end user is typing live.
string
Pass true to run the turn as a background turn.
Body
array
required
The user turn as an array of content parts. Each part is { "type": "text", "text" }, { "type": "image", "image", "mediaType"? }, or { "type": "file", "data", "mediaType" }; image and data take a URL or base64. An empty array, a null element, or a text part whose text is not a string answers 400.
string
Thread suffix, see Threads.
object
{ "timezone" }, the caller’s IANA timezone (at most 64 characters) used for date and time answers. Falls back to the end user’s stored timezone, then country, then UTC.
string[]
What your client renders. ["plain-markdown"] asks for plain Markdown with no ::: formatting component markers; without it the reply may contain them for you to parse. The CLI always sends it.
any
Arbitrary data exposed to tools as Lua.request.webhook.payload for this turn.
string
Extra text injected into the prompt for this turn.
string
Replaces the persona for this turn.
object
reasoning: { effort?, show? } and verbosity. effort is one of off, minimal, low, medium, high, max, clamped to what the resolved model supports; show defaults to true and false drops the reasoning trace from the response. verbosity is low, medium, or high. The request wins over the agent’s modelSettings.reasoning, field by field.
string
A provider/model model code for this turn. An unknown or unapproved code falls back to the agent’s model.
integer
Preview an agent version that is not promoted, at least 1. Needs agents:write on the agent; runs in an isolated thread, is billed, and is kept out of production history and analytics.
boolean
default:"false"
Lets the reply carry navigation components for the web widget.
string or object
A persona text, or { base?, voice?, text? } for channel-aware variants, used instead of the deployed persona.
array
[{ skillId, sandboxId }] runs sandbox versions of skills; preprocessorOverride and postprocessorOverride take [{ preprocessorId, sandboxId }] and [{ postprocessorId, sandboxId }]; envOverride is a map of environment variables that beats every stored value. This is what lua chat -e sandbox sends.
object
{ "url" }, a permanent recording of a voice note. Stored as message metadata and never sent to the model; the transcript goes in messages.
Fields not listed here, such as humanMentions, replyTo, sharedThreadId, roomId, clientTools, artefactIds, and desktopSessionId, serve the Lua desktop app and Spaces and are ignored or refused on a plain API turn. Response 201 with the model output when the turn ran, or with an in-band outcome when it did not. Check type before reading text.
string
The reply, after postprocessors.
string
Why the model stopped, for example stop or tool-calls.
object
Tokens of the last step: inputTokens, outputTokens, totalTokens. totalUsage carries the sum over every step.
array
The tool calls the model made, with toolResults beside them.
array
One entry per model step, each with its own text, tool calls, and usage.
array
The reasoning trace when the model produced one and show was not false; reasoningText joins it.
various
Sources the model cited, files it produced, provider warnings, and the raw request and response metadata. citations is added when the agent cited documents.
When a preprocessor or a governance rule stops the turn, the body is { "type": "preprocessor_blocked" | "governance_blocked", "text", "finishReason", "usage", "timestamp" }, and a governance block adds governanceBlock: { stage, ruleId?, reason? }. When the turn fails after it was admitted, the body is { "type": "error", "textDelta", "error", "code"?, "timestamp" }: textDelta is a line safe to show the end user, error the developer line, and code is USER_CODE_ERROR when your tool threw, TURN_INTERRUPTED when a restart cut the turn, or PROVIDER_REJECTED with reason, providerStatus, keyOwner, and transient: false when the model provider refused the request. A billing refusal (the organization has no credits, or the caller needs a seat on this agent) is also reported as type: "error", never as a status. Errors Once the turn is admitted, /chat/generate answers 201 and reports failures in the body. No chat route answers 424: a preprocessor block and a provider refusal are both in-band outcomes; the 424 body belongs to POST /ai/generate/:agentId. Example

POST /chat/stream/:agentId

Runs one turn and streams it. Same path, query, and body as generate, plus one query parameter.
string
ui switches the body to the AI SDK UI message stream, which useChat consumes and which carries reasoning parts and typed data-lua-* parts. Without it the body is the newline-delimited format this section documents.
Response 200 with Content-Type: text/event-stream, but the body is not SSE: it is a sequence of JSON objects separated by blank lines, with no data: prefix. Split on newlines, skip empty lines, and parse each line as JSON. Every chunk carries type, and most carry timestamp (Unix time in milliseconds). The server closes the response when the turn ends; there is no closing chunk on a normal turn, so treat the close as the end. The only { "type": "finish" } line the stream ever writes closes the two short-circuits that never run the model: an APPROVE/DENY reply to a governance approval, and a turn that only delivers a room message. Postprocessors run on both routes: the model’s text streams first, then postprocess-complete carries the final text when one changed it. Errors Statuses are the same as generate and are answered before the stream opens; after the first chunk a failure arrives as an error chunk and the response ends. Example

GET /chat/history/:agentId

Returns the thread’s messages, oldest first.
string
required
The agent.
string
Thread suffix; omit it for the default thread.
Response 200 with an array of messages.
string
Message id.
string
user or assistant.
string
ISO 8601 timestamp.
array
Typed parts: type is text, reasoning, tool, image, video, audio, file, source-url, source-document, or a data-lua-* part, with the matching field (text, image, data and mediaType, toolName, toolCallId, input, output, url, title).
string
chat or voice; absent on older rows.
string
The model that served an agent turn, when recorded.
Equivalent: User.getChatHistory() inside a tool.

DELETE /chat/history/:agentId

Deletes the caller’s messages in one thread, or in every thread with this agent.
string
required
The agent.
string
Thread suffix; omit it to clear the default thread.
string
Another end user’s id, email address, or mobile number. Needs org:manage on the agent; an organization-admin grant does not reach a private agent.
Response 200 with { "success": true, "deletedMessages": <count> }. Errors Equivalent: lua chat clear --thread <id> and lua chat clear --user <identifier>.

GET /chat/last-message/:agentId

Returns the last message of the thread, or null when it is empty. Same parameters as GET /chat/history/:agentId; the body is one message in the history shape.

POST /chat/welcome/:agentId

Records a message as the agent’s own on the web channel, without running a model turn, so a widget can open with a greeting that later turns remember.
string
required
The text to record. Missing text answers 400 with Message content is required.
Response 201 with an empty body.

POST /chat/messages/:messageId/feedback

Marks an agent reply as good or bad, for the feedback analytics in the admin dashboard.
string
required
The message id from the history.
string
required
good or bad.
string
The agent, for messages the platform cannot map to a stored turn. Send it.
string
The end user’s text that prompted the reply.
string
Why the reply was bad.
string
What a better reply would have said.
Response 201 with { "status": "success", "message": "Feedback saved successfully", "feedbackId" }.
The remaining /chat/* routes (reactions, client telemetry, approval and client-op resumes, the ?protocol=ui companions) serve the Lua desktop, the web widget, and Spaces and are not part of the public contract.

Long turns

A turn that runs research tools or delegates to other agents can take minutes. Prefer /chat/stream for any agent with tools, so heartbeats keep the connection open and you see progress. If a connection is cut mid-turn the platform still finishes the turn and stores the reply in the thread; do not resend the same message, send a follow-up on the same threadId.

See also