Docs · Native API
Chat stream
The native chat endpoints are what the ShvIA apps use. Compared with the /v1 endpoints they keep your conversations on the server and accept a reasoning effort. POST /api/v1/chat also accepts file attachments and can use your combos. They also have their own conventions, described here.
| Endpoint | Style | Stores the conversation |
|---|---|---|
POST /api/v1/chat | server-sent events | yes |
POST /api/v1/agent/chat | one JSON answer | yes |
POST/api/v1/chat#
Streams the answer as server-sent events.
- Auth: your API key.
- Limit: 60 requests per minute per account by default. See rate limits and quotas.
- Body: JSON, or
multipart/form-datawhen you send files.
curl -sN "$SHVIA_URL/api/v1/chat" \
-H "Authorization: Bearer $SHVIA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"message": "Say hello in one short sentence.",
"profile_name": "'"$SHVIA_MODEL"'",
"session_id": "support-ticket-1042"
}'Request#
| Field | Type | Notes |
|---|---|---|
message | string | the user turn. prompt is accepted as an alias. At most 32000 characters by default. An empty message is answered with an error event |
profile_name | string | the model id, model@server, from models. profile is accepted as an alias |
model, server | string | a bare model name and a server key, as an alternative to profile_name. model does not understand the @: put the composite id in profile_name |
session_id | string | the key that continues a conversation, see below. At most 191 characters |
effort | string | low, medium, high, xhigh or max. Any other value is ignored. A valid value is saved on the conversation |
combo_id | integer | use one of your combos instead of choosing a model. An unknown id is ignored |
files[] | file | attachments, multipart/form-data only |
When no model is given, the gateway uses your saved default, then the installation's default. There is no temperature, no max_tokens, no system field and no way to send a messages array: the conversation is the one stored on the server, and the server adds its own instructions to it.
With a single model, this endpoint does not fall back to your default model the way /v1 and agent/chat do: a model with no key behind it, or one that fails, ends in an error event. Give it other models to try with a combo.
Attachments#
- Text and code files: up to 10 per message, 262144 bytes each and 1048576 bytes in total.
- Images: PNG, JPEG, WebP or GIF, up to 8 per message and 20 MiB each. Send them only to a model that can see images, or the request is refused.
- Archives (zip, tar, tgz) are opened and their text files used.
- Your account group sets a further limit on the number of attachments per message.
Conversations and session_id#
With an API key, every call to this endpoint belongs to a conversation in a project named API. Whether a call continues an earlier conversation depends on session_id:
- With
session_id: the most recent conversation of yours with that same value is continued, and the earlier turns are sent to the model, as many as fit its context window: older ones can be summarized or left out. A new value starts a new conversation. - Without it: every call is a new conversation with one turn.
The stream never tells you the id of the conversation. To read it back, list your conversations and match the session_id field. See conversations. You cannot send a conversation_id to this endpoint; to continue a conversation by its id, use POST /api/v1/agent/chat.
The stream#
The reply is text/event-stream. Each event is one line, data: {"kind": "…", "data": {…}}, followed by a blank line. There are no event: or id: lines and no keep-alive comments. Because the request is a POST with headers, a browser EventSource cannot be used: read the body stream instead. Ignore any kind you do not know, since new ones are added over time. Take the final answer from done. If the installation lets the chat call tools, the stream can also carry tool_call, tool_result and similar events, and a text_clear event, which means that the text shown so far is to be discarded.
data: {"kind":"user_message","data":{"id":5121}}
data: {"kind":"model","data":{"name":"claude-sonnet-4-6@anthropic","model":"claude-sonnet-4-6","profile":"claude-sonnet-4-6@anthropic","server":"anthropic","server_label":"…","target_source":"request","request_id":"3b1a8f7e-…"}}
data: {"kind":"text_delta","data":{"delta":"Hello"}}
data: {"kind":"text_delta","data":{"delta":"! Nice to meet you."}}
data: {"kind":"usage","data":{"model":"claude-sonnet-4-6","profile":"claude-sonnet-4-6@anthropic","server":"anthropic","server_label":"…","prompt_tokens":42,"completion_tokens":8,"total_tokens":50,"thinking_time_ms":null,"response_time_ms":890.4,"reasoning_tokens":null,"effort":"medium","cost":null,"cost_estimated":0.000246}}
data: {"kind":"done","data":{"text":"Hello! Nice to meet you."}}kind | data | When |
|---|---|---|
user_message | {"id"} | first, once your message is saved |
model | the resolved model: name, model, server, target_source, request_id, and a few more fields that serve the apps | before the first token, and again if another model takes over after a failure |
reasoning_delta | {"delta"} | the model streams its reasoning |
text_delta | {"delta"} | each fragment of the answer |
warning | {"codigo": "resposta_nao_salva", "correlation_id"} | the answer reached you but could not be saved. It is not an error: keep the text |
warn | {"kind": "guard.repetition", "message"} | a guard noticed something about the answer, such as a repeating loop. The answer is still delivered and saved |
citations | {"message_id", "citations"} | when the answer used documents from a knowledge base |
error | {"message", "codigo", "status"} | something failed. codigo and status are present only when known |
usage | tokens, timing, effort actually applied, and cost | after the answer, before done |
done | {"text"}, which is empty after an error that ends in no text, and [] after an early refusal | last. It always arrives |
cost is the cost the provider reported, in USD, or null. cost_estimated is tokens times the price table, or null. usage.effort is the effort level the model really applied, or null. The times are numbers that may have decimals, so parse them as floats. Note that an empty data is written as [], not {}.
Error codes#
When an error event has a codigo, switch on it. message is for people and may be in another language.
codigo | Meaning |
|---|---|
mensagem_vazia | the message was empty |
mensagem_muito_longa | the message is over the length limit |
roteamento_sem_alvo | the model or server cannot be resolved. The message can list alternatives |
bot_desativado | the conversation that the account has open in the web app belongs to a bot that is switched off |
modelo_sem_visao | you sent images to a model that cannot see them |
anexo_invalido | an attachment cannot be read, is too large, or is not a supported type |
pacote_sem_texto | an archive held no usable text file |
sessao_id_muito_longa | session_id is over 191 characters |
combo_sem_modelo | the combo has no reachable model |
limite_perfil | your group's limits refused the call: the model is not allowed, or a daily, weekly or monthly token ceiling is reached |
resposta_vazia | the model answered with nothing |
limite_de_requisicoes | the provider is rate limiting. The request limit of the gateway itself is an HTTP 429, not an event |
provedor_recusou_pedido | the provider rejected the request, with status. Another model will not help |
provedor_indisponivel | the provider failed, or has no key from you, with status |
shvia_fora_do_ar | the operator's servers could not be reached |
A stream cut by a timeout sends an error with no codigo, then usage, then done with the partial text, which is saved. An answer with a repeating loop is delivered and saved with an extra warn event.
One more thing to know: if the account has a conversation with a bot open in the web app, the model that the bot is pinned to wins over the one you asked for. Read the model event to see what ran.
POST/api/v1/agent/chat#
One blocking call, one JSON answer.
- Auth: your API key. A key whose account group has no API access is refused with
401. - Limit: 20 requests per minute per account by default, on the counter that the other limited endpoints share. See rate limits and quotas.
curl -s "$SHVIA_URL/api/v1/agent/chat" \
-H "Authorization: Bearer $SHVIA_API_KEY" -H "Content-Type: application/json" \
-d '{ "message": "Say hello in one short sentence.", "profile_name": "'"$SHVIA_MODEL"'" }'{
"request_id": "9eaa5ed8-e7fa-423b-9a93-d9ff3d4c8a03",
"response": "Hello! Nice to meet you.",
"model": "claude-sonnet-4-6",
"server": "anthropic",
"server_label": "…",
"profile_name": "claude-sonnet-4-6@anthropic",
"target_source": "request",
"effort": null,
"tokens": { "prompt": 42, "completion": 8, "total": 50 },
"timing": { "queue_wait_ms": 0, "response_time_ms": 890 },
"conversation_id": 5
}Request#
| Field | Notes |
|---|---|
message | required, at most 32000 characters |
profile_name | the model id, model@server. model and server also work as separate fields |
conversation_id | an integer. Continues one of your own conversations. Without it, every call creates a new one |
effort | low, medium, high, xhigh or max. Unlike the stream endpoint, an invalid value is a 422 |
Like the /v1 endpoints, this one builds a short chain: the model you asked for, then your account's default model. A cloud model with no key behind it, or one that fails with something another model can fix before it sends any text, is replaced by the default model, and the reply names the model that answered: read model, server and profile_name. See models.
To hold a multi-turn conversation, send back the conversation_id of the previous answer. There is no session_id here, no attachments and no streaming. timing.queue_wait_ms is always 0.
Errors#
This endpoint answers with HTTP statuses and a detail text, and no codigo:
| Status | Body | When |
|---|---|---|
| 401 | {"message": "Unauthenticated."} | the credential is not valid |
| 400 | {"detail": "…"} | the model or server is unknown, or the provider rejected the request as invalid. In that second case the status is the provider's own, usually 400, and the body also carries request_id |
| 403 | {"detail": "…"} | your group's limits refused the call: a model that is not allowed, a quota that is used up, or remote servers that the group may not use |
| 404 | {"detail": "…"} | the conversation_id is not yours |
| 422 | {"message", "errors": {…}} | the body failed validation |
| 502 | {"detail": "…", "request_id": "…"} | the model answered with nothing, or failed in a way that is not worth retrying |
| 503 | {"detail": "…", "retry_after": 30, "request_id": "…"} and a Retry-After header | every model of the chain failed in a way that is worth retrying: a rate limit, a used-up provider quota, an overloaded provider, a refused or missing provider key, a server error, or a server that cannot be reached. Wait retry_after seconds. It does not mean that the gateway is down, and a refused provider key is not your gateway key |
| 504 | {"detail": "…", "request_id": "…", "partial": {…}} and X-Inference-Incomplete: read_timeout | the model stopped mid-answer. The partial answer is saved |
A 403 here is not a rate limit: do not retry it without changing something.