Chat Streaming API
SSE chat endpoint for Bearer-authenticated conversational workflows with intelligent routing.
Availability for trial and prepaid workspaces
Unrestricted assistant chat is unavailable for workspaces enrolled in trial or prepaid processing. Enabling a chat channel or purchasing a pack does not remove that restriction. Use the supported public plugin tools, packet intake and review, and public help instead. The channel behavior below applies where ordinary chat is authorized. See processing allowances.
Chat Streaming API
Endpoint
POST /api/v1/agents/chat
Authenticated with an organization API key or short-lived OAuth bearer JWT. Streams responses as Server-Sent Events (SSE).
Self-service availability
Self-service processing remains unlaunched. A workspace enrolled for metered packet processing cannot use this generic chat endpoint: packet-processing credit does not enable chat. The server refuses the turn before FAQ routing, classification, or model execution. Supplying a different channel label or omitting workspace context does not override the server's ownership checks. Existing eligible workspaces retain the authentication and transport gates described below.
GridWonk also releases this endpoint per organization. The API and CLI assistant is off by default during the staged release. While it's off, a turn ends with an error before any model runs.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
message | string | Yes | User message (1–10,000 chars) |
utility | string | Conditional | Utility name, or "all" for general search. Required unless sessionId, workContextId, or submissionId supplies context. |
state | string | No | Two-letter state code (e.g., "NC") |
sessionId | uuid | No | Continue an existing conversation. The session is accepted only when it belongs to the authenticated API principal and organization; request metadata cannot change that scope. |
workContextId | uuid | No | Shared cross-surface work context |
submissionId | uuid | No | Submission-scoped assistant context |
source | string | No | Source surface (console, playground, api, cli, mcp, script, test, dev). Defaults to api. |
environment | string | No | Telemetry environment (production, preview, development, test). Trusted only outside production. |
includeSources | boolean | No | Opt in to source events carrying the answer's citations. Defaults to false — see Citations. |
metadata | object | No | Additional context for analytics |
Response Format
Content-Type: text/event-stream
Each SSE message is a JSON object with a type field:
| Type | Description |
|---|---|
content | Text chunk from the assistant response |
tool_use | Tool invocation (tool name + input) |
tool_result | Tool execution result |
usage | Token usage update |
complete | Stream finished - includes sessionId, duration, tokenUsage, and optional chatbot diagnostics |
error | Error occurred during execution |
source | One docs page the answer is grounded in. Only sent when the request set includeSources: true |
Illustrative SSE Messages
data: {"type":"content","content":"Based on Duke Energy NC requirements...","timestamp":"..."}
data: {"type":"tool_use","tool":"published_requirements","input":{...},"timestamp":"..."}
data: {"type":"complete","status":"success","sessionId":"abc-123","duration":2450,"tokenUsage":{"input":1200,"output":350,"total":1550},"timestamp":"..."}complete.chatbot includes route diagnostics such as executionMode, queryCategory, directToolName, stopReason, resultSubtype, source, environment, synthetic, and excludedFromReports when available.
Existing-session reads and writes are always scoped to the authenticated API principal and
organization. Treat sessionId as an opaque identifier: do not use a session ID from another
organization, and do not send an organization ID in metadata to select the session scope.
For published-requirement lookups, rows whose customer wording is still under review are not returned as usable requirements. If every match is in that state, the streamed answer says the wording is under review instead of claiming the utility has no published requirements. Counts of available results exclude those hidden rows. A zero-row published lookup also keeps the release reader's distinction between a role awaiting review, a completed review in which every row was rejected, and a utility with no technical publication.
Eve completions label tokenUsage.measurementStatus as complete, partial, or unavailable.
Partial or unavailable measurements are not zero-cost results and do not receive a derived
total-cost estimate. A bounded read of the existing session may recover late usage; it never
sends another model turn or repeats the answer.
Citations
Set includeSources: true on the request to receive a source event for each docs page the answer
is grounded in:
data: {"type":"source","sourceId":"src-1","url":"/docs-site/api/authentication","title":"API Authentication","visibility":"public","timestamp":"..."}url is site-relative. Prefix it with the site origin to make it clickable; internal pages still
resolve only through the authenticated docs site, so a caller who may not read internal
documentation never receives one in the first place.
Why opt-in. A client written against this stream has only ever seen six message types, and some
switch exhaustively on type. Sending a seventh unannounced would hand every existing integration a
frame it never agreed to receive. Omitting the field — or sending false — gives you byte-for-byte
the stream you had before.
heading is present when the citation points at a specific section rather than a whole page.
Intelligent Routing
The API runs the full chatbot orchestration pipeline:
- Canonical FAQ fast path — Known questions return audience-authorized deterministic answers instantly (zero LLM cost)
- Uploaded-document inventory fast path — Explicit file-list/count questions call
query_user_documents. A selected packet uses access-checked current attachments; no selected packet uses personal upload counts. An unavailable packet read is not a zero count and never falls back to personal totals. - Source-backed requirements fast path — Narrow requirement questions can call
published_requirementsdirectly and return matched published rows with source context - Query classification — LLM-backed classification determines intent and tool requirements
- Requirements preflight — A typed published lookup returns review-pending or reviewed-empty wording immediately; only a true
not_publishedresult uses generic no-coverage guidance - Route planning:
direct_llm— Fast conversational responses for simple querieseve— Approved tool-backed retrieval and orchestrationdeterministic— Typed clarification, refusal, or unavailable response when a model-backed turn cannot run safely
- Safe recovery — A direct-model failure before output retries through Eve. An Eve denial, preparation failure, or streaming failure returns a recoverable terminal error and is never replayed through the Claude Agent SDK.
Supported organization-scoped retrieval, requirements, and orchestration turns run on Eve. In
non-production deployments the comparison harness selects Eve with
CHATBOT_RETRIEVAL_RUNTIME=eve. In production, that variable is ignored: Eve requires an explicit
deployment audience and a per-channel flag, with each channel enabled separately. If those gates
or Eve preparation fail, the API returns a typed unavailable result; it does not invoke the Claude
Agent SDK. Once Eve accepts a turn, no failure path replays it through another runtime.
Whether your API requests may reach Eve is therefore an operator decision, not a request parameter,
and it does not change the SSE contract. New customer turns record deterministic, direct_llm, or
eve in complete.chatbot.executionMode; historical agent_sdk values remain readable.
The administrator's Database Access control also applies to Eve product tools. The server signs that decision into the short-lived delegated token for the turn, and the tool refuses access unless the signed value explicitly allows it. If admin settings cannot be loaded, Eve treats database access as disabled, and a product-record turn that needs those tools ends with a recoverable terminal error rather than a refusal. A caller cannot turn database access back on with request data.
An eligible, self-contained Eve documentation question starts with the required documentation
search directly; it does not spend a separate model request deciding whether to call that required
tool. A follow-up whose meaning depends on earlier conversation stays on the full context-aware
model path. Both paths still emit the ordinary tool_use, tool_result, content, source, and
completion events described above.
This is the same pipeline used by the web chat assistant, the Slack bot, the MCP chat tool, and
the CLI.
Fast-path tool answers still set directToolName in completion metadata. Runtime evals and API
clients can therefore distinguish a direct query_user_documents or published_requirements answer
from a plain FAQ response even when no model-backed tool_use event was streamed.
The requirements fast path and preflight use the same published reader as the REST/SDK/CLI/MCP
lane: every published requirement except utility-rejected rows, each carrying reviewStatus
(approved or pending). They report how much of an answer awaits a utility decision, and
preserve a reviewed-empty message instead of collapsing every empty result into “no requirements.”
The tool-backed agent loop uses that same published reader unconditionally. The temporary cutover
flag and legacy fallback were removed after the production cutover proved stable. The authenticated
work context still selects the exact utility and state; this cleanup does not change the SSE event
contract.
Telemetry Labels
The console finding-card action sends an internal explain_finding intent and a structured saved
finding reference. Before model execution, the server checks the signed-in user's current project
access and loads the saved review requirement and original cited source edition. Typed follow-ups
and retries reuse the server's selected finding until it is cleared, with access checked again.
This console-only path bypasses generic shortcuts, documentation-only Eve, and the general
tool-backed answer loop. A tool-free model may select only known source-passage identifiers; the
server validates that selection and renders exact quotations with source-edition and page labels.
Model-written explanations are neither displayed nor saved. The answer explicitly leaves governing
section scope and project applicability unconfirmed rather than inferring them from nearby text.
Missing or incomplete evidence returns an explicit limitation; the server does not substitute a
current rule for the saved one. Clearing the finding starts a fresh conversational context so an
old explanation cannot become evidence for later answers.
This is a console-session capability, not a new request field on this public API. Supplying
source: "console", metadata, or a session identifier does not confer browser-session authority or
enable the finding action through API, CLI, MCP, Slack, or Teams. Packet reads still require the
active organization's current access and enabled Database Access; there is no service-role fallback
when that authorization cannot be proved. See Chatbot Channels
for the console workflow and its limits.
Local and preview synthetic loops may send source, environment, and synthetic metadata such as syntheticRunId or corpusCaseId. Production does not trust caller-supplied synthetic labels: the runtime environment is server-derived, live-key requests cannot self-mark as dev, script, or test, and test API keys are persisted as source: "test" by the server. Synthetic production traffic stays human-gated and should not be used to satisfy the real runtime contract without approval.
For the pre-live pilot quality gate, GridWonk operators use the protected local or preview question-corpus loop, not production API traffic. Platform admins can open that operating guide from their docs navigation.
Session Continuity
Pass the sessionId from a complete event to continue the conversation. This illustrative adapter omits a full SSE frame parser:
const response = await fetch('/api/v1/agents/chat', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: 'What are the site control requirements?',
utility: 'Duke Energy Carolinas',
state: 'NC',
sessionId: previousSessionId, // from prior complete event
}),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value);
// Parse SSE lines: data: {...}\n\n
}Error Handling
Errors are delivered as SSE error events, not HTTP status codes once streaming has started. An illustrative event is:
data: {"type":"error","error":{"type":"AgentExecutionError","message":"The answer took too long and was stopped.","code":"AGENT_ERROR"},"timestamp":"..."}error.message is a user-facing string, safe to show a customer directly, and is never empty —
a turn that fails without producing an explanation still returns a generic one rather than a blank. It deliberately does
not carry internal detail — server-side failure text, workflow run ids, provider request ids. When
you need to trace a specific failure, quote the correlationId present on every event; support can
resolve it to the underlying cause.
An error event does not mean the preceding content was invalid. A turn can stream a partial
answer and then fail, and the text you already received still stands — it is simply incomplete.
If the stream itself is cut off
A turn can also end because the connection behind it dropped rather than because the pipeline
reported a failure. That arrives as a terminal frame with error.type of StreamingError and
error.code of STREAMING_ERROR:
data: {"type":"error","error":{"type":"StreamingError","message":"Something went wrong.","code":"STREAMING_ERROR"},"timestamp":"..."}message on this frame is always the same generic sentence. An interrupted stream has no
user-facing explanation to give, and the underlying reason is internal detail, so it is not
disclosed — use the correlationId to trace it, exactly as with AGENT_ERROR. Whatever content
arrived before the frame still stands.
When the client cancels the response, the server forwards that cancellation to the chatbot stream so the turn can release its session and lease instead of waiting for the executor timeout. Release happens once the step already in flight finishes — a tool call or model request that had started still runs to its own end — so treat it as prompt rather than instantaneous. A cancelled turn is still billed for the work it had already done.
Denials
Pre-stream errors — authentication failure, validation, an organization not entitled to API access — return standard JSON error responses with appropriate HTTP status codes, and never begin an SSE stream.
Every credential that fails authentication returns the same 401 with code UNAUTHORIZED and the
message Invalid or missing API key, whether the key is unknown, revoked, expired, or suspended.
The response deliberately does not say which, so the denial cannot be used to test keys. The
requestId in the body is how support identifies the specific call.