7 Use the Nous Chat API
Use the Nous Chat API to send a question from your own script or application to a Nous workspace. The request runs a Nous conversation with the workspace’s configured model. It can use knowledge and agents that the token owner is allowed to access.
7.1 Get a token and workspace address
Create a personal API token, or ask a workspace admin for a service-account token if an integration needs its own identity. Copy the token when it is shown, and use the Nous address of that workspace. A personal token acts as its owner; a service-account token acts as that account. Neither token grants access to documents or agents beyond its identity’s permissions.
Set these values in your terminal, replacing the token. https://nous.sophea.ai is the production NOUS_URL; for another deployment, use that workspace’s Nous address. Keep the token out of source control and shared logs.
NOUS_URL='https://nous.sophea.ai'
NOUS_TOKEN='<paste-your-token>'
curl -sS -X GET "$NOUS_URL/api/me" \
-H "Authorization: Bearer $NOUS_TOKEN"GET /api/me checks the token and workspace identity. In Power Automate, use an HTTP action with method GET, this URI, the Bearer token header, and no body. If it does not return your expected identity, check the token and workspace address before sending a chat request.
7.2 Send your first message
This request starts a conversation with the default agent. message is the question or instruction you want the assistant to answer.
For Power Automate cloud flows, use the submit-and-poll instructions, not a long synchronous HTTP action. The example below remains useful for clients that wait for a complete answer.
If such a client may stop waiting before the answer finishes, use the asynchronous API or the existing known-session recovery pattern.
curl -sS -X POST "$NOUS_URL/api/chat/send-chat-message" \
-H "Authorization: Bearer $NOUS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"message": "Summarize our onboarding policy",
"knowledge_search_enabled": true,
"stream": false
}'knowledge_search_enabled: true lets the agent search indexed sources available to the token owner when relevant. It does not force a search. Search also requires an enabled search tool and an eligible knowledge scope. stream: false returns one JSON response, which is easier to use in a first integration.
7.3 Choose request options
| Field | What it does | If omitted |
|---|---|---|
message |
The prompt sent to the assistant. Required. | The request is invalid. |
knowledge_search_enabled |
true lets the agent search permitted workspace knowledge when useful; false turns off ambient search. |
No knowledge search is requested. |
stream |
false returns one JSON response; true returns JSON objects one line at a time. |
true: the response is a stream of JSON lines, not one JSON object. |
chat_session_info |
Starts a new conversation. Add {"persona_id":123} to choose an accessible agent. |
A new conversation uses the default agent when chat_session_id is also omitted. |
chat_session_id |
Continues an existing conversation using the ID returned by an earlier response. | A new conversation starts when chat_session_info is also omitted. |
include_citations |
false removes citation markers from the answer. |
true: citations are included when available. |
Send chat_session_info or chat_session_id, never both in the same request. For everyday chat, start with the first example and add an option only when you need it.
For stream: true, the HTTP content type is text/event-stream, but the body contains newline-delimited JSON rather than data: event frames. Parse one JSON object per line.
7.4 Read the response
With stream: false, the JSON response includes the answer. The first response of a new conversation also includes its chat_session_id. Save that ID and keep using it for later messages; follow-up responses can contain "chat_session_id": null. The following is an excerpt; document objects have more fields in the actual response.
{
"answer": "The policy requires a first-week check-in. [1]",
"chat_session_id": "cba35529-7e57-4c57-8f84-0c9733277f5c",
"message_id": 42,
"top_documents": [
{
"document_id": "onboarding-policy",
"semantic_identifier": "Onboarding policy"
}
],
"citation_info": [
{
"type": "citation_info",
"citation_number": 1,
"document_id": "onboarding-policy"
}
],
"error_msg": null,
"error_code": null,
"error_is_retryable": null,
"stop_reason": null
}top_documents lists retrieved sources, and citation_info connects numbered citations to documents. An answer may have no citations if the agent did not retrieve a source.
HTTP 200 alone does not mean that the answer completed. Before using answer, check error_msg, error_code and stop_reason. Normal full JSON completion has null errors and a null stop_reason after the canonical answer is saved. A valid saved answer can be empty. "stop_reason": "user_cancelled" means generation stopped, not normal completion. A failed or uncertain completion returns a structured error and an empty answer.
For stream: true, treat answer deltas as provisional and keep reading until the stream ends without a transport or response error. Normal completion requires all three:
- A final saved message matching the reserved assistant
message_idand conversation. Recognize its existing structural fields, includingmessage_type: "assistant",messageandfiles; it has notypediscriminator. - That model’s indexed
stoppacket with a null or omittedstop_reason. - Error-free end of stream, with no answer delta after that Stop.
The server sends final detail before Stop. Accept either order when reading, but never stop parsing at Stop: a later error overrides the completion candidate. Detail alone, Stop alone, cancelled or unknown reasons, and a partial stream are not normal success.
CHAT_COMPLETION_FAILED means the answer’s completion could not be established. CHAT_RESPONSE_UNAVAILABLE means the answer was saved but its final details could not be returned. Both have a false retry hint. Reopen the conversation or retrieve GET /api/chat/get-chat-session/<chat_session_id> before sending another message; do not automatically resend an uncertain turn. This completion check does not add durable process-loss recovery or guarantee exactly-once external actions.
The web chat’s answer timings distinguish preparation from successfully completed server processing. Neither is the duration of your HTTP request. ChatFullResponse does not add a total request-duration field.
7.5 Submit a turn and poll its result
Use this opt-in transport for ordinary single-model chat when your client should not wait for the whole answer in one HTTP request. Existing synchronous and streaming routes are unchanged.
POST /api/chat/send-chat-message-async accepts the existing chat request options, but rejects deep_research: true. The retired llm_overrides field is ignored if sent, and the turn is answered by one model. The scalar llm_override, permitted knowledge search, tools/MCP, files, citations, projects and transient context retain their existing semantics. The legacy stream field does not affect this response: generation is consumed as a stream internally, while the HTTP response is always JSON.
Both new routes require an authenticated identity in the current workspace. A supplied chat_session_id must refer to that identity’s own undeleted session. Public sharing, project participation or administrator access to another owner’s session does not satisfy this requirement. You may also start a new session with the existing chat_session_info option or by omitting both session fields. For integrations, precreate a dedicated session and save its ID before sending, so an uncertain submit can be reviewed. Send turns sequentially; this API does not add ordering guarantees for concurrent clients.
Send a JSON object with the Bearer token and Content-Type: application/json, for example:
{
"chat_session_id": "cba35529-7e57-4c57-8f84-0c9733277f5c",
"message": "Summarize our onboarding policy",
"knowledge_search_enabled": true
}HTTP 202 means the identifiers and processing fence have been committed, not that generation has completed. The entire response body has these fields:
{
"chat_session_id": "cba35529-7e57-4c57-8f84-0c9733277f5c",
"user_message_id": 41,
"reserved_assistant_message_id": 42
}| Field | Type and meaning |
|---|---|
chat_session_id |
UUID of the accepted conversation. |
user_message_id |
Integer or null, depending on the existing turn semantics. |
reserved_assistant_message_id |
Integer identifying the reserved assistant message. Save it with the session ID. |
There is no answer, job ID or Location header. The response has Cache-Control: no-store; it does not implement native HTTP automatic polling.
At most four asynchronous consumers run per API process, with no pending queue. This is not a fleet-wide limit or a limit on all provider/tool threads. If no slot is available, submit returns HTTP 429 with Retry-After: 10 before chat side effects. Preparation has a 30-second acceptance wait. On expiry, submit returns a safe HTTP 503, not 202; a late acceptance cannot start model generation or external-agent dispatch. Preparation may already have had effects or committed a reservation. Other validation, access, token-rate-limit or preparation errors are not accepted turns. Do not automatically repeat a POST after an error, timeout or ambiguous response.
After 202, retrieve the exact accepted assistant:
GET /api/chat/get-chat-message-result/<chat_session_id>/<reserved_assistant_message_id>Authorized reads return HTTP 200, Cache-Control: no-store, and all of these fields:
| Field | Type and meaning |
|---|---|
chat_session_id |
UUID of the conversation. |
reserved_assistant_message_id |
Integer identifying this assistant message. |
status |
RUNNING, COMPLETED, FAILED or CANCELLED. |
result |
Canonical saved ChatMessageDetail only for COMPLETED; otherwise null. It includes message, sources/citations, files and saved timing where available, not the legacy answer field. |
error |
Caller-safe error string for FAILED/CANCELLED; otherwise null. |
error_code |
Error-code string for FAILED/CANCELLED; otherwise null. |
error_is_retryable |
Boolean retry hint or null. It is not permission to resend an uncertain POST. Cancellation, interruption and uncertain completion have a false retry hint. |
RUNNING has null result and error fields, including while a selected background agent is still active. Use result.message only when status is COMPLETED; an empty string is a valid completed answer. FAILED and CANCELLED have no result. Missing or conflicting completion evidence becomes failure rather than success inferred from visible text.
Invalid, foreign, unowned, deleted or source-restricted session/message results return an opaque 404. Missing or revoked credentials retain normal authentication failure behavior. Polling starts no generation, repeats no tools and does not return provisional text as a completed answer. Only this result route is exempt from chat billing attribution; submit remains chat. On billing-enforced installations, use X-Sophea-Operation-ID on creation, submit and full-session recovery. It attributes the operation and is not an idempotency key.
The consumer continues independently of the submit connection while its API process remains alive. A restart does not resume ordinary generation. Existing stale-reservation recovery may report FAILED with GENERATION_INTERRUPTED; this is not an immutable-state guarantee if a live writer merely loses its processing signal. There is no new cancellation endpoint, durable queue or exactly-once guarantee for external actions. A flow timeout or client disconnect is not proof of cancellation. Retain IDs and use known-session recovery to review uncertain outcomes, never to resend automatically.
7.6 Use a Power Automate cloud flow
These are documented HTTP instructions checked against the API contract and the Microsoft sources below, not an executed or certified Power Automate flow. They use cloud actions; Windows and Power Automate Desktop are not required. Confirm that your organization’s licensing and policies permit the ordinary HTTP action and access to the workspace URL. Use Authorization: Bearer <token>, not the HTTP with Microsoft Entra ID connector.
Check GET /api/me with that token and workspace URL first, as described above. Stop if the returned identity is not the intended account. Use your organization’s secret storage. Enable Secure Inputs and Secure Outputs on actions handling the token or conversation content, including Compose/variables and recovery actions where applicable. Never commit tokens or expose headers in screenshots or run-history exports.
1. Prepare the flow and create a dedicated session.
Initialize String variables Nous_URL (the workspace’s base URL without a trailing slash), Chat_session_id (empty) and Chat_status (RUNNING). Add a Compose action named Operation_ID, with expression guid(), before Create_chat_session. Generate it once per run.
Add an HTTP action named Create_chat_session:
| Setting | Value |
|---|---|
| Method | POST |
| URI expression | concat(variables('Nous_URL'), '/api/chat/create-chat-session') |
| Headers | Authorization: Bearer <token>, Content-Type: application/json; on billing-enforced installations, X-Sophea-Operation-ID with expression outputs('Operation_ID'). |
| Body | The JSON object below. Use an accessible agent ID instead of 0 when needed. |
| Settings | Retry Policy: None; Asynchronous Pattern: Off where exposed. |
{
"persona_id": 0
}After a successful creation response, set Chat_session_id to body('Create_chat_session')?['chat_session_id']. Require a nonempty ID before submission. Keep this ID outside the submit/poll scopes for recovery. Create one session per flow run and keep other turns out until this turn is resolved.
2. Submit once.
Add an HTTP action named Submit_chat, with method POST and URI expression concat(variables('Nous_URL'), '/api/chat/send-chat-message-async'). Use the same headers as creation, including the same outputs('Operation_ID') for billing-enforced installations. Set Retry Policy: None and Asynchronous Pattern: Off where the setting is exposed. The 202 response has no Location or native automatic polling contract.
Use this object in the body editor, not a quoted JSON string. Insert the session expression as shown and replace the prompt, or use dynamic content for it:
{
"chat_session_id": "@{variables('Chat_session_id')}",
"message": "Summarize our onboarding policy",
"knowledge_search_enabled": true
}Continue only after HTTP 202 with both chat_session_id and reserved_assistant_message_id. Check that the returned session matches Chat_session_id, and save the assistant ID from body('Submit_chat')?['reserved_assistant_message_id']. Save user_message_id when non-null. A missing ID or uncertain submit goes to recovery, not another send.
3. Poll within a bounded scope.
Add Do until with this condition expression:
not(equals(variables('Chat_status'), 'RUNNING'))
Set its limits to Count: 180 and Timeout: PT30M. Inside it, add the HTTP action Get_chat_result, with method GET, the Bearer header, no body and Retry Policy: None. Polling needs no X-Sophea-Operation-ID. Use this exact URI expression:
concat(variables('Nous_URL'), '/api/chat/get-chat-message-result/', body('Submit_chat')?['chat_session_id'], '/', string(body('Submit_chat')?['reserved_assistant_message_id']))After each successful GET, set Chat_status from body('Get_chat_result')?['status']. Add a Condition for equals(variables('Chat_status'), 'RUNNING'), and in its Yes branch add Delay of 10 seconds. Do not delay after a terminal status. An unknown or missing status is an error, not completion.
After the loop, branch on Chat_status:
COMPLETED: consumebody('Get_chat_result')?['result']?['message'], including a valid empty answer. Require the expected result object and matching IDs.FAILEDorCANCELLED: terminate with the caller-safeerroranderror_code; do not consume a result or send again.- Any other state, including
RUNNINGafter an exhausted count, is unresolved. Retain IDs and terminate for review. A successful loop action alone does not establish completion.
4. Handle failed and timed-out actions without resending.
Put submission in a Scope named Submit_scope and polling plus terminal validation in Poll_scope. Run Poll_scope only after Submit_scope succeeds and accepted IDs have been validated. Add failure-handler scopes using Configure run after for has failed, has timed out and is skipped as applicable, for both scopes. Also handle failed creation without submitting. Do not place another Submit_chat in a retry, recovery or loop branch.
The handlers retain Chat_session_id and any accepted message IDs, record a safe outcome and terminate. A bounded recovery GET may inspect concat(variables('Nous_URL'), '/api/chat/get-chat-session/', variables('Chat_session_id')) with the same Bearer identity and, on billing-enforced installations, the same X-Sophea-Operation-ID: outputs('Operation_ID'). Set recovery Retry Policy: None too. Follow the known-session completion checks; this GET returns history, not the small result object. If recovery itself fails, retain the IDs for manual review rather than retry indefinitely.
HTTP failure, missing accepted IDs, loop failure, timeout or an exhausted polling limit never triggers a second turn. Missing history after an uncertain send is not proof of nonacceptance. Review the conversation and any external actions manually before deciding on a new turn. Stopping a flow or reaching its timeout does not prove server work was cancelled, and ordinary work is not resumed after an API restart.
Microsoft references: request, loop and retry limits, HTTP asynchronous-pattern controls, and Power Automate Do until example. Designer settings can vary; the asynchronous-pattern source describes Azure Logic Apps controls. No Microsoft flow execution or screenshots are claimed here.
7.7 Recover after your client stops waiting
For an ordinary single-model request, create a dedicated conversation before sending the question. This gives you its ID even if the later response is cut off. Keep other messages out of that conversation until this turn is resolved.
On billing-enforced installations, generate one X-Sophea-Operation-ID before creation and send the same value on creation, send and full-session retrieval. The header provides attribution, not deduplication. Power Automate users should use the asynchronous cloud-flow sequence; this section preserves recovery for the existing streaming route and uncertain submissions.
- Send
POST /api/chat/create-chat-sessionwith{"persona_id":0}for the default agent, or an accessible agent’s ID. Continue only after a successful response, and save itschat_session_id. - Send one
POST /api/chat/send-chat-messagewith that ID, yourmessageand"stream":true. Disable automatic POST retries in your HTTP client. If the stream supplies its reservation metadata, saveuser_message_idandreserved_assistant_message_id. - If the stream times out, disconnects or has an uncertain ending, retrieve
GET /api/chat/get-chat-session/<chat_session_id>using the same identity. Do not send the question again to recover it.
These curl commands demonstrate the generic API sequence. Run creation first, then replace the placeholder with its returned ID before running the send and retrieval commands. The 120-second send limit is a caller deadline, not a server cancellation or an expected answer duration.
CHAT_OPERATION_ID="$(uuidgen)"
curl -sS --fail-with-body --retry 0 --max-time 30 \
-X POST "$NOUS_URL/api/chat/create-chat-session" \
-H "Authorization: Bearer $NOUS_TOKEN" \
-H "X-Sophea-Operation-ID: $CHAT_OPERATION_ID" \
-H "Content-Type: application/json" \
-d '{"persona_id":0}'
CHAT_SESSION_ID='<returned-chat_session_id>'
curl -sS --fail-with-body --retry 0 --max-time 120 \
-X POST "$NOUS_URL/api/chat/send-chat-message" \
-H "Authorization: Bearer $NOUS_TOKEN" \
-H "X-Sophea-Operation-ID: $CHAT_OPERATION_ID" \
-H "Content-Type: application/json" \
-d "{\"chat_session_id\":\"$CHAT_SESSION_ID\",\"message\":\"Summarize our onboarding policy\",\"knowledge_search_enabled\":true,\"stream\":true}"
curl -sS --fail-with-body --retry 0 --max-time 30 \
"$NOUS_URL/api/chat/get-chat-session/$CHAT_SESSION_ID" \
-H "Authorization: Bearer $NOUS_TOKEN" \
-H "X-Sophea-Operation-ID: $CHAT_OPERATION_ID"Retrieval returns messages and replay packets, not the full-response answer field. In this dedicated single-turn conversation, identify the user message and its assistant child (parent_message equals the user’s message_id). If you saved the reserved assistant ID, it must match. Read the matching assistant’s message, sources, citations and files, not provisional stream text.
Check errors and access before accepting an answer:
| Retrieved state | What to do |
|---|---|
HTTP 401/403, or is_restricted: true |
Stop. Resolve credentials or access before further retrieval; do not resend the turn. |
Assistant error or error_code is non-null |
Treat the turn as failed, even if message contains text. Keep its IDs and report the error; a retry hint does not make POST replay safe. |
Assistant is_generating: true |
The turn is still in progress. Any visible text is provisional; retrieve again within your polling budget. |
Assistant is_generating: false, unrestricted, null errors, with a matching replay obj.type: "stop" and null/omitted stop_reason |
After a complete, successful GET, accept the saved message. A valid completed answer can be empty. For this single-assistant conversation, its replay is the sole entry in packets. |
Cancelled/unknown stop_reason, missing matching assistant or missing normal replay Stop |
Not normal completion. If no cancellation or error is reported, retrieve again within your polling budget, even when is_generating is false. Keep the IDs; do not infer success from HTTP 200, non-empty text or is_generating: false alone. |
| No visible user or assistant turn after an ambiguous send | Only a snapshot. It does not prove the POST was never accepted or that resending is safe. |
Poll only GET, with a delay between requests, a per-request timeout, and a finite overall deadline or maximum number of attempts. Allow a couple of minutes for an interrupted turn to be classified; long answers can need a larger budget. If the budget expires, stop polling, retain the session and message IDs, and leave the outcome unresolved for a person to review. Neither a caller deadline nor closing the stream confirms cancellation.
An admitted turn can finish after the client disconnects while the server remains running. Server-process loss does not automatically resume the model or tools; retrieval can report GENERATION_INTERRUPTED. That error is not a promise that the saved state can never change, so never automatically resend it. The send endpoint has no idempotency key and does not guarantee exactly-once tool effects. A person must review the saved conversation and any external actions before deciding to send a new turn. This pattern is not durable process-loss recovery or a certified Power Automate flow.
7.8 Run the Python sample
From a checkout of the platform repository, run:
(
cd nous
NOUS_ACCESS_TOKEN="$NOUS_TOKEN" uv run --locked python \
backend/scripts/api_inference_sample.py \
--onyx-url "$NOUS_URL" \
--test-question "Summarize our onboarding policy"
)The sample reads nested obj.type: "message_delta" packets and prints each obj.content fragment as it arrives. It parses newline-delimited JSON even though the response content type is text/event-stream. Printed text is provisional until the command exits successfully.
Exit status 0 requires a saved assistant message matching the created conversation and reserved assistant ID, one normal null/omitted-reason Stop, and error-free EOF. Detail and Stop may arrive in either order. The final saved message is not printed again. A complete empty answer is valid.
A recoverable tool-step error inside a packet does not by itself fail the turn. A top-level response error, cancelled or unknown ending, duplicate completion signal, malformed data, late error or truncated response produces nonzero exit status; any text already printed may be incomplete. An answer delta after Stop is rejected rather than printed.
Both requests have bounded timeouts, reject redirects and are never automatically resent. Use the final workspace URL and NOUS_ACCESS_TOKEN; the legacy DANSWER_API_KEY is not read. Interrupting the command closes its connection but does not confirm that server work was cancelled. Use known-session recovery to inspect an uncertain turn, not to automatically resend it.
7.9 Continue the conversation
Use the chat_session_id from the first response for every later message in that conversation. Replace the example ID with the one your first request returned. Do not replace your saved ID with the null value from a follow-up response.
curl -sS -X POST "$NOUS_URL/api/chat/send-chat-message" \
-H "Authorization: Bearer $NOUS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"message": "What should the manager do next?",
"chat_session_id": "cba35529-7e57-4c57-8f84-0c9733277f5c",
"knowledge_search_enabled": true,
"stream": false
}'7.10 Start with another agent
List the agents available to your token, then use an agent ID in the first message of a new conversation:
curl -sS "$NOUS_URL/api/agents?page_size=100" \
-H "Authorization: Bearer $NOUS_TOKEN"
curl -sS -X POST "$NOUS_URL/api/chat/send-chat-message" \
-H "Authorization: Bearer $NOUS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"message": "Summarize our onboarding policy",
"chat_session_info": {"persona_id": 123},
"knowledge_search_enabled": true,
"stream": false
}'Replace 123 with an accessible agent’s ID. For later messages to that agent, use the returned chat_session_id instead of chat_session_info.
7.11 If something does not work
- Authentication or access fails: confirm the workspace address, token expiry, and the token owner’s access to that workspace and agent. Never send the token to support in a screenshot or message.
- The API returns HTTP 422 (validation error): make sure the request has a body containing a JSON object, valid JSON syntax, and the required
messagefield. SetContent-Type: application/json. In Power Automate, parse JSON stored as text into an object before passing it to the HTTP action. - The legacy send response arrives as JSON lines rather than one JSON object: set
"stream":falseon/api/chat/send-chat-message. The asynchronous submit route always returns JSON. - The answer has no knowledge citations: send
"knowledge_search_enabled":true, confirm that the token owner can access the source, and check that the agent has an enabled search tool. The agent may still decide no search is needed. See Knowledge Search for how search scopes work. - The HTTP request succeeds but the answer is unusable: check the structured error and
stop_reason, and validate the complete stream when streaming. If completion is uncertain, use known-session recovery; do not automatically resend the request.