Chat
The Chat resource exposes Strata's prompt loop and conversation history over HTTP. It is the main endpoint of the public API — send a message, get the AI's reply, either streamed or as a single JSON object. Every endpoint on this page requires an API key with the chat scope.
The base URL is https://app.strata.kronisys.com/v1 (or your org's deployment with /v1 appended). The examples below use <API_BASE_URL> for that prefix — for example, <API_BASE_URL>/chat resolves to https://app.strata.kronisys.com/v1/chat. Keys look like sk_strata_live_…; call from your backend only, never client-side code — the key is a secret that must never be exposed in a browser or mobile bundle.
POST /chat
Send a message to a Strata model and stream or collect the response. The same AI engine, tools, and per-tenant Azure AI Foundry routing the web app uses runs behind this endpoint.
Headers
| Header | Required | Description |
|---|---|---|
Authorization | yes | Bearer sk_strata_live_... |
Content-Type | yes | application/json |
Accept | no | Set to text/event-stream for SSE. Any other value returns one-shot JSON. |
Idempotency-Key | no | Replay-safe key for non-streaming requests, 24h window. See Reliability. |
Body
Send either message (a single string) or messages (a conversation array). They are mutually exclusive; if both are present, message wins.
| Field | Type | Required | Description |
|---|---|---|---|
message | string | one of | A single user message. |
messages | array | one of | Conversation history as { role, content } objects. role is user, assistant, or system. The last message should be a user turn. |
model | string | no | A model id the key is allowed to use. Defaults to the key's first allowed model. See Models. |
mode | string | no | auto (default), technical, or simple. An org may pin a required mode that overrides this. |
permission | string | no | auto (default), read, or write. Caps whether SQL tools may run mutating statements — but only takes effect when the key holds a SQL extension scope. A chat-only key has no SQL tools, so this field does nothing for it. An unrecognized value returns 400 invalid_request. |
conversation_id | string | no | A UUID to continue an existing conversation. Omit to start a new one. When messages holds only the new message, Strata loads the conversation's earlier messages as history: the version shown in the app, where a message was edited or an answer regenerated. |
custom_instructions | string | no | Standing guidance for the assistant on this turn, up to 4,000 characters. |
work_style | string | no | The actor's preferred working style, up to 4,000 characters. |
timezone | string | no | An IANA timezone (e.g. America/New_York), up to 64 characters, used for date/time context. |
{
"message": "Top 10 customers by revenue last quarter",
"model": "gpt-5.4",
"mode": "auto"
}
NoteThere is no
connection_idfield and nostreambody flag. Streaming is chosen by theAcceptheader. SQL tools run against the actor user's own active SQL Server connection, configured in Strata, not a per-request value.
KeyThe
chatscope alone does not let a key run SQL. Therun_sqltool only appears when the key also holds thesql.read(read-only) orsql.write(read + write) extension scope, and the actor (user or connection bot) has a SQL Server connection. Achat-only key answers prompts but can never touch the database. Atlassian works the same way:search_jiraappears only when the key holdsjira.readorjira.writeand the actor can reach the organization's pinned Atlassian site, andsearch_confluenceonly when the key holdsconfluence.readorconfluence.write.write_jiraadditionally requires the organization to allow Jira writes andwrite_confluencerequires it to allow Confluence writes — resolved per product, so with a product's writes off its create and comment tools are never offered, whatever the key carries. See Extension access for the full scope model and Atlassian extensions for the organization's connection modes.
Example request
curl -X POST <API_BASE_URL>/chat \
-H "Authorization: Bearer sk_strata_live_..." \
-H "Content-Type: application/json" \
-d '{
"message": "Summarize Q1 revenue by region",
"model": "claude-sonnet"
}'
Example response
A non-streaming request returns a single JSON object:
{
"id": "f04a72c1-...",
"conversation_id": "9b1e4c8d-...",
"model": "claude-sonnet",
"content": "Q1 revenue totaled $14.2M across four regions...",
"tool_calls": [],
"finish_reason": "stop",
"usage": { "prompt_tokens": 3120, "completion_tokens": 540, "total_tokens": 3660 },
"created_at": "2026-06-24T18:22:14Z"
}
| Field | Description |
|---|---|
id | Unique id for this response. |
conversation_id | The conversation this turn belongs to. Persist it to continue the thread. |
model | The model that produced the reply. |
content | The assistant's full text reply. |
tool_calls | Currently always an empty array. Tool activity is reported on the stream as step events. |
finish_reason | stop on a complete reply, length if the output was truncated. |
usage | Token counts, or null if the provider did not report them. |
created_at | ISO 8601 timestamp. |
Streaming with Accept: text/event-stream
When you set Accept: text/event-stream, the response is a text/event-stream connection. Each event is a data: line carrying a JSON object with a type. The event types are:
Event type | Payload |
|---|---|
meta | { conversation_id, model } — always the first event |
content | { content } — an incremental text chunk |
step | { status, label, tool } — progress detail as the AI runs a tool/action step |
done | { conversation_id, model, finish_reason } — final event |
error | { error, code } — terminal failure (5xx details are scrubbed to a generic message) |
Sub-agent events
A turn that splits its work across parallel sub-agents adds progress events. None carries a sub-agent's output (the turn answers in content), but label is the short name the model gave a task or step and can echo its wording, so treat it like prompt text if you log streams. Every event has groupKey, depth and parentSubrunId; per-sub-agent events add seq and, once running, subrunId.
Event type | Payload |
|---|---|
subagent_spawned | label, model, effort, isResume |
subagent_progress | label, model, status (running), liveTokens (estimate), elapsedMs |
subagent_step | label, status, tool |
subagent_done | label, model, status (success, error, stopped), total_tokens, elapsed_ms, errorCode, attempt, resumable, critique (null or { verdict, reason, missing }) |
subagent_group_done | results: one { subrunId, seq, label, status, total_tokens, elapsedMs } per sub-agent |
Files and web sources a sub-agent produces arrive as the ordinary file-ready and web_search events. When a group finishes, the turn's step with tool: "subagents" summarises it in a subAgents field. Sub-agents of an API turn are read-only, so the web app's subagent_approval_* events (subagent_approval_required, subagent_approval_resolved, subagent_approval_auto, subagent_approval_refused and subagent_approval_failed) are never sent on the API. If you share a parser with that stream: subagent_approval_failed reports an action that was approved and then failed, with state set to failed when it was not done or unconfirmed when it may have been, and reasonText saying why.
Requests to create an agent
Agents can't be created through /chat. When a turn asks Strata to create or schedule an agent, the stream carries a step with tool: "agent_draft" and the label Agents are created in the web app, and the reply in content points the person to the Agents page in the Strata web app. No agent_draft event is ever sent on the API; that draft card exists only in the web app, where a person creates the agent from it. See Create an agent from a chat.
NoteSwitch on
typeand ignore any event type you don't recognize. Strata may emit additional internal progress signals on the wire, and new event types are added over time — treating the set above as canonical and skipping the rest keeps your parser forward-compatible.
TipRead the
metaevent first and persistconversation_idbefore processing content. If the stream drops, reconnect by sending the next prompt with the sameconversation_id— do not blind-retry the original POST.
Parsing SSE
In the browser, fetch the stream and parse it manually (native EventSource cannot send an Authorization header):
const res = await fetch(`${API_BASE_URL}/chat`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
Accept: "text/event-stream",
},
body: JSON.stringify({ message: "hello" }),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const frames = buffer.split("\n\n");
buffer = frames.pop();
for (const frame of frames) {
const line = frame.split("\n").find(l => l.startsWith("data:"));
if (!line) continue;
const event = JSON.parse(line.slice(5).trim());
handle(event); // switch on event.type
}
}
From the command line, pass -N to disable curl's output buffering:
curl -N -X POST <API_BASE_URL>/chat \
-H "Authorization: Bearer sk_strata_live_..." \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{"message":"hello"}'
Models
The model field accepts any model id in the API key's allowed list — a subset an admin chose when minting the key, which itself is a subset of the org's enabled models. Strata runs a DB-driven Model Catalog that Kronisys curates centrally, served through Microsoft Azure AI Foundry; each organization enables the models it wants and admins manage that set in the Model Catalog (Organization → Models). The catalog spans many models across OpenAI, Anthropic, xAI, DeepSeek, Mistral AI, Cohere, and Microsoft.
Common chat model ids include gpt-6-astra, gpt-6-sol, gpt-6-luna, claude-opus-5-5, claude-sonnet-5-5, claude-sonnet-5, grok-4.6, deepseek-v4-pro, gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna, gpt-5.4, gpt-5.4-mini, claude-sonnet (Claude Sonnet 4.6, scheduled to retire), claude-opus (Claude Opus 4.8, scheduled to retire), grok-4.3, and grok-4-fast. The Model Catalog shows each model's retirement date. Use the catalog id, not the Foundry deployment name — deepseek-v4-pro, not DeepSeek-V4-Pro. Rather than hard-coding a list, call GET <API_BASE_URL>/health — it returns the calling key's allowed_models as stored on the key — or check the in-app Model Catalog for the live set. A key minted before your organization enabled a model doesn't include it until an admin edits the key or mints a new one. A request that names a retired model is treated as a request for its replacement and checked exactly as that request would be. If the key and your organization allow the replacement, the call runs on it, the response's model field names it, and a strata-model-retired response header carries the retired id you sent. If not, the call returns 403 model_not_allowed with the replacement's id in requested_model. On the retirement date Strata also swaps the retired id for its replacement in your organization's API key model lists, unless your organization turned automatic migration off. Update any code that hard-codes the old id; Strata can't change it for you. A model that the key, the org, the actor's role or the actor's per-user access does not allow returns 403 model_not_allowed (a built-in role doesn't restrict models, and roles conferred by the actor's groups add to the role's list); if an admin removes a model after the key was minted, that model is rejected at request time too.
Beyond that allow-list, the org may cap how much of a given model each actor may use. When the requested model's allowance is exhausted, the call returns 429 model_allowance_exhausted — unless the org configured a cheaper fallback, in which case the request instead succeeds on the fallback model. When that happens, the model field in the response names the model that actually ran and a strata-model-fallback response header names the one you requested. Read both if you need to know exactly which model produced a reply.
Errors
| Status | Slug | When |
|---|---|---|
| 400 | invalid_request | Neither message nor messages supplied, or a malformed conversation_id |
| 401 | invalid_token / missing_token | Key missing, malformed, expired, or revoked |
| 403 | insufficient_scope | Key lacks the chat scope |
| 403 | model_not_allowed | Requested model not allowed by the key or the org's current policy |
| 403 | org_suspended | The key's organization is suspended |
| 404 | conversation_not_found | conversation_id does not exist or belongs to a different actor |
| 409 | request_in_progress | An identical Idempotency-Key request is still running |
| 429 | rate_limit_exceeded / budget_exhausted / usage_limit_exceeded / model_allowance_exhausted | Per-key rate window, monthly budget, org usage pool, or the requested model's per-model allowance exhausted |
| 503 | policy_lookup_failed / budget_check_failed | Policy or budget enforcement temporarily unavailable — retry |
See Errors for the full reference and the problem+json shape.
POST /query
A convenience wrapper for one-shot questions. It sends a single prompt, runs the same AI loop, and returns the answer as JSON. The conversation is ephemeral — it is never persisted, so there is no conversation_id to follow up on. Always returns JSON regardless of the Accept header.
Note
/queryis for natural-language questions, not raw SQL. The AI decides whether to call its SQL tools. There is no endpoint to run SQL yourself without the model in the loop; constrain mutations by sendingpermission: "read", which forces read-only tools.
Headers
| Header | Required | Description |
|---|---|---|
Authorization | yes | Bearer sk_strata_live_... |
Content-Type | yes | application/json |
Body
| Field | Type | Required | Description |
|---|---|---|---|
query | string | yes | The question to ask. |
model | string | no | A model id the key is allowed to use. Defaults to the key's first allowed model. |
mode | string | no | auto, technical, or simple. |
permission | string | no | auto (default), read, or write — same SQL-mutation cap as on /chat, and only meaningful when the key holds a SQL extension scope. An unrecognized value returns 400 invalid_request. |
{
"query": "What was total Q1 revenue, and which region led?",
"model": "gpt-5.4"
}
Example request
curl -X POST <API_BASE_URL>/query \
-H "Authorization: Bearer sk_strata_live_..." \
-H "Content-Type: application/json" \
-d '{ "query": "How many active customers do we have right now?" }'
Example response
{
"id": "a1b2c3d4-...",
"model": "gpt-5.4",
"answer": "There are 1,284 active customers as of today...",
"finish_reason": "stop",
"usage": { "prompt_tokens": 410, "completion_tokens": 96, "total_tokens": 506 },
"created_at": "2026-06-24T18:30:00Z"
}
Errors
| Status | Slug | When |
|---|---|---|
| 400 | invalid_request | Missing query, or an unrecognized permission |
| 403 | model_not_allowed | Requested model not allowed by the key or org |
| 429 | rate_limit_exceeded / budget_exhausted / usage_limit_exceeded / model_allowance_exhausted | Limits exhausted |
GET /conversations
List conversations the API key's actor has created, newest first. Page backward in time with the before cursor.
Headers
| Header | Required | Description |
|---|---|---|
Authorization | yes | Bearer sk_strata_live_... |
Query parameters
| Parameter | Type | Description |
|---|---|---|
limit | integer | Page size, 1–100. Default 25. |
before | string | ISO 8601 timestamp. Returns conversations updated strictly before this time — pass the updated_at of the last row you saw to page. |
Example request
curl "<API_BASE_URL>/conversations?limit=10" \
-H "Authorization: Bearer sk_strata_live_..."
Example response
{
"data": [
{
"id": "9b1e4c8d-...",
"title": "Q1 revenue by region",
"model": "claude-sonnet",
"mode": "auto",
"created_at": "2026-06-04T18:22:11Z",
"updated_at": "2026-06-04T18:24:03Z"
}
],
"has_more": true
}
has_more is true when the page filled to limit, meaning there may be older conversations — request the next page with before set to the last row's updated_at.
GET /conversations/:id
Read a single conversation with its full message history.
Headers
| Header | Required | Description |
|---|---|---|
Authorization | yes | Bearer sk_strata_live_... |
Example request
curl <API_BASE_URL>/conversations/9b1e4c8d-... \
-H "Authorization: Bearer sk_strata_live_..."
Example response
{
"id": "9b1e4c8d-...",
"title": "Q1 revenue by region",
"model": "claude-sonnet",
"mode": "auto",
"created_at": "2026-06-04T18:22:11Z",
"updated_at": "2026-06-04T18:24:14Z",
"messages": [
{
"id": 101,
"role": "user",
"content": "Summarize Q1 revenue by region",
"created_at": "2026-06-04T18:22:11Z"
},
{
"id": 102,
"role": "assistant",
"content": "Q1 revenue totaled $14.2M...",
"created_at": "2026-06-04T18:22:14Z"
}
]
}
Errors
| Status | Slug | When |
|---|---|---|
| 404 | not_found | Conversation does not exist or belongs to a different actor |
DELETE /conversations/:id
Soft-delete a conversation. The record is hidden from list responses but retained in the platform database — Strata keeps chat history for employee-activity tracking and the retention-purge sweep, so this never hard-deletes.
Headers
| Header | Required | Description |
|---|---|---|
Authorization | yes | Bearer sk_strata_live_... |
Example request
curl -X DELETE <API_BASE_URL>/conversations/9b1e4c8d-... \
-H "Authorization: Bearer sk_strata_live_..."
Example response
204 No Content with an empty body.
Errors
| Status | Slug | When |
|---|---|---|
| 404 | not_found | Conversation does not exist or belongs to a different actor |
Reliability
- Idempotency. Send an
Idempotency-Keyheader on a non-streamingPOST /chatso a network-retry replays the original result instead of creating a duplicate conversation or double-billing. The key is scoped to your API key and held for 24 hours. A retry while the first request is still running returns409 request_in_progress; once it finishes, the same key returns the cached result. Streaming requests are not idempotent — reconnect instead. - Rate limits and budgets. Each key has a per-hour rate window and an optional monthly token budget, and all spend counts toward your org's usage pool. Watch the
X-RateLimit-*andX-Strata-Budget-*response headers. See Rate limits and budgets. - Streaming reconnects. On a dropped SSE connection, send your next prompt with the same
conversation_idrather than re-POSTing the same request.