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. |
conversation_id | string | no | A UUID to continue an existing conversation. Omit to start a new one. |
{
"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 | Array of { name, args } objects — one entry per tool the AI invoked (e.g. run_sql, search_outlook, search_jira, search_confluence). Empty when no tools were called. |
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 |
tool_call_start | { name, args } — emitted when the AI begins invoking a tool (e.g. run_sql) |
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) |
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 (Admin → Organization Settings → Models). The catalog spans many models across OpenAI, Anthropic, xAI, DeepSeek, Mistral AI, Cohere, and Microsoft.
Common chat model ids include gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna, gpt-5.4, gpt-5.4-mini, claude-sonnet, claude-opus, grok-4.3, and grok-4-fast. Rather than hard-coding a list, call GET <API_BASE_URL>/health — it returns the calling key's allowed_models — or check the in-app Model Catalog for the live set. A model that the key, the org, or the actor's per-user access does not allow returns 403 model_not_allowed; if an admin removes a model after the key was minted, that model is rejected at request time too.
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 | Per-key rate window, monthly budget, or org usage pool 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. |
{
"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 |
| 403 | model_not_allowed | Requested model not allowed by the key or org |
| 429 | rate_limit_exceeded / budget_exhausted / usage_limit_exceeded | 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.