Chat
Operations tagged Chat in the public API.
About Chat
POST /chat runs one chat turn: a visitor message in, the chatbot's reply out, both stored as one message of a conversation. POST /chat/stream runs the same turn and streams the reply as server-sent events. Each turn is billed a message credit, so send an Idempotency-Key: a retry with the same key never runs or bills a second turn.
Conversations. A turn without session_id starts a new conversation and emits conversation.started; every stored turn emits conversation.message.created. The reply carries the conversation's session_id (api_…). Send it back to continue the thread: the server rebuilds the history from the stored turns, so you never resend earlier messages. Only an api_ session can take a new turn here; conversations from the widget, hosted page, dashboard or a channel are read through /conversations.
IDs you will meet.
- Message
id—POST /chatreturns it asid,POST /chat/streamasquery_idin its trailingmetadataevent. Use it withGET /messages/{id}andPOST /messages/{id}/feedback. conversation_id—POST /chatonly. The stream carries no conversation id: look it up withGET /conversations/by_session/{sessionId}.session_id— send it back to continue the conversation.
IDs on this page
- Chatbot id
<CHATBOT_ID> - Integer.
Get it from
- GET /v1/chatbots returns it as
data[].id - POST /v1/chatbots returns it as
id - GET /v1/chatbots/by_handle/{handle} returns it as
id - Webhook chatbot.created carries it as
data.object.id - In the dashboard, it is the number in a chatbot's address:
/dashboard/chatbots/{id}.
- GET /v1/chatbots returns it as
- API session id
<SESSION_ID> - String:
api_followed by 40 characters. A session id from another surface (ws_,widget-,sess_,ch-) is 422VALIDATION_FAILED.Get it from
- POST /v1/chat returns it as
session_id - GET /v1/conversations/{id} returns it as
session_id - GET /v1/conversations returns it as
data[].session_id POST /chat/streamsends it assession_idin its trailingmetadataevent (and in the JSON replay of an idempotent retry).
- POST /v1/chat returns it as
Send a chat message
id, the visitor's query, the response, its verdict, confidence_score and sources) plus conversation_id, session_id, the actions the turn ran and its token usage. Use it for back-end jobs and integrations; use POST /chat/stream when a person watches the reply appear.
Conversations. Without session_id the turn starts a new conversation (emitting conversation.started); keep the returned session_id (api_ + 40 characters) and conversation_id. To continue, send the session_id back with the next message: the server rebuilds the history from the conversation's stored turns, so earlier messages are never resent. Only an api_ session can be continued — another surface's session id is 422 VALIDATION_FAILED, and an unknown one, or one of another chatbot, is 404 NOT_FOUND.
Billing. Each turn costs a message credit. Send an Idempotency-Key: a retry with the same key replays the first answer and is not billed again. A chatbot of any status answers here, draft and inactive included; the widget and hosted page serve only active ones.
Every stored turn emits conversation.message.created; a turn that first asks for a human also emits conversation.handoff.requested.- Required scope
- chat:send
- Idempotency
- Accepts Idempotency-Key
- Webhook events
- conversation.startedconversation.message.created
- Rate limits
Integer.chatbot_id(body)Chatbot idPlaceholder in the samples:
<CHATBOT_ID>Get it from
- GET /v1/chatbots returns it as
data[].id - POST /v1/chatbots returns it as
id - GET /v1/chatbots/by_handle/{handle} returns it as
id - Webhook chatbot.created carries it as
data.object.id - In the dashboard, it is the number in a chatbot's address:
/dashboard/chatbots/{id}.
- GET /v1/chatbots returns it as
String:session_id(body)API session idapi_followed by 40 characters. A session id from another surface (ws_,widget-,sess_,ch-) is 422VALIDATION_FAILED.Placeholder in the samples:
<SESSION_ID>Get it from
- POST /v1/chat returns it as
session_id - GET /v1/conversations/{id} returns it as
session_id - GET /v1/conversations returns it as
data[].session_id POST /chat/streamsends it assession_idin its trailingmetadataevent (and in the JSON replay of an idempotent retry).
- POST /v1/chat returns it as
Header parameters
Idempotency-KeystringOptionalMakes a retry safe. The first 2xx answer is stored for 24 h and replayed for the same key withIdempotency-Replayed: true; a retry while the first request runs is 409IDEMPOTENCY_IN_FLIGHT; the same key with a different payload is 422IDEMPOTENCY_KEY_REUSED; a malformed key is 400INVALID_IDEMPOTENCY_KEY.Min length:
16Max length:128Pattern:^[A-Za-z0-9_-]+$
Request body
chatbot_idintegerRequiredThe chatbot to talk to. A chatbot of another account is 404NOT_FOUND; itsstatusdoes not matter.messagestringRequiredThe visitor's message, 1–2000 characters.Min length:
1Max length:2000session_idstringnullableOptionalContinues a conversation: thesession_idan earlier turn returned (api_+ 40 characters). Omit it to start a new conversation. A session id from another surface is 422VALIDATION_FAILED; an unknown one, or one of another chatbot, is 404NOT_FOUND.Max length:
255conversation_historyarray of objectOptionalIgnored while server-side history is on (the default): the history the model sees is rebuilt from the stored turns ofsession_id, and a new conversation has none. At most 20{role, content}items,rolebeinguserorassistant.Max items:
20
Responses
- 200Success.The stored
Messageplus the conversation identifiers and what the turn ran and cost.sourcesare the citations as displayed with the live answer.objectstringRequiredAlways
messageidintegerRequiredThe message id, forGET /messages/{id}andPOST /messages/{id}/feedback.conversation_idintegerRequiredThe conversation the message belongs to (GET /conversations/{id}).chatbot_idintegerRequiredThe chatbot that replied.querystringnullableRequiredThe visitor's message.responsestringnullableRequiredThe assistant's reply.statusstringRequiredOutcome, derived fromverdictwhen there is one:answered,partial_answerorno_answer.confidence_scorenumberRequiredAnswer confidence, 0–100, derived fromverdict:error→ 0;none→retrieval_confidence÷ 10 (≤ 10);clarify/contact/meta/small_talkkeepretrieval_confidence(usually 0 — these turns run no retrieval);full/partial→ the evidence strength (retrieval_confidence: the rank-weighted relevance of the knowledge chunks) fused with the answer grounding (how much of each sentence of the reply one of its sources supports) in log-odds space, apartialanswer carrying a lower prior — so a reply that restates strong sources reads high and one that wanders off them reads lower than its evidence. Under thecalibratedrollback model it is instead the verdict band (partial40–70,full70–100) when the answer verdict is enabled (CHAT_VERDICT_ENABLED) and equalsretrieval_confidenceotherwise; messages stored before the verdict contract carry that retrieval number here andnullinretrieval_confidence. Show this number as "confidence in the answer"; useretrieval_confidencefor "how well the knowledge base matched".Minimum:
0Maximum:100verdictstringnullableRequiredWhat the reply is:full/partialanswer from the knowledge base,none(refusal — no information),clarify(asked a clarifying question),contact(handoff / contact details),meta(about the assistant itself),small_talk,error(the fallback reply), orgeneral(ageneralknowledge-mode chatbot answered from general knowledge because its knowledge base did not cover the question).confidence_scoreis derived from it.nullon messages stored before the verdict contract.Possible values
fullpartialnoneclarifycontactmetasmall_talkerrorgeneralnullretrieval_confidencenumbernullableRequiredKnowledge match (evidence strength), 0–100: the rank-weighted relevance score of the knowledge chunks retrieved for the turn — the best passages carry the number (the plain mean under thecalibratedrollback model). Says how well the knowledge base matched the question, not whether the reply answered it — a refusal over well-matched sources can score high here whileconfidence_scorestays ≤ 10.nullon messages stored before the verdict contract.Minimum:
0Maximum:100mediaarray of ChatMediaItemRequiredMedia cards the answer offered;[]when none.feedbackstringnullableRequiredThe rating recorded on the reply (POST /messages/{id}/feedback), ornullwhen it has none.Possible values
positivenegativenullsourcesarray of objectRequiredCitations the answer drew on.input_tokensintegerRequiredPrompt tokens the turn used.output_tokensintegerRequiredCompletion tokens the turn used.cost_usdstringRequiredProvider cost of the turn in USD, as a decimal string.currencystringRequiredAlways
USDcreated_atstringnullableRequiredWhen the turn was stored (ISO 8601, UTC).Format:
date-timesession_idstringnullableRequiredSend it back assession_idto continue this conversation.actionsarray of anyRequiredActions the turn ran.usageobjectRequiredToken usage reported by the model provider.
- 400A tenant selector header named a tenant other than the API key's own (
ACCOUNT_SELECTOR_NOT_SUPPORTED): an all-digitX-Account-Idother than the key owner's id or, whileworkspaces.context_enabledis on, anX-Workspace-Idother than the key's workspace (ws_prefix optional, case-insensitive). A header naming the key's own tenant, or a non-numericX-Account-Id, is ignored; a key cannot switch tenants, so send neither. A mutation that honoursIdempotency-Keyalso returnsINVALID_IDEMPOTENCY_KEYwhen that header is not 16–128[A-Za-z0-9_-]characters.Error codesACCOUNT_SELECTOR_NOT_SUPPORTEDINVALID_IDEMPOTENCY_KEY
- 401Missing or invalid API key.
Error codesUNAUTHENTICATED
- 402A billing gate refused the request (plan limit, monthly AI budget or message credits). See
error.code.Error codesCHATBOT_BUDGET_EXCEEDEDMESSAGE_CREDITS_EXCEEDEDUSER_BUDGET_EXCEEDED
- 403The API key is missing a required scope.
Error codesACCOUNT_SUSPENDEDINSUFFICIENT_SCOPE
- 404The resource does not exist on this account.
Error codesNOT_FOUND
- 409The request conflicts with the current resource state.
Error codesIDEMPOTENCY_IN_FLIGHT
- 422Validation failed. See
error.details.Error codesIDEMPOTENCY_KEY_REUSEDVALIDATION_FAILED
- 429Rate limited. Honour the Retry-After header before retrying.
Error codesRATE_LIMITED
- 500Every LLM provider failed (
error.details.reason=chat_generation_failed) or the answer had nochoices(invalid_structure). Renderfallback_responseas the bot's reply for this turn; nothing was billed or persisted.Error codesSERVER_ERROR
- 503Every LLM provider ran out of the turn budget (
error.details.reason=upstream_timeout). Honour theRetry-Afterheader; renderfallback_responseas the bot's reply for this turn instead of re-running it.Error codesSERVICE_UNAVAILABLE
Request
curl -X POST 'https://api.agentency.com/v1/chat' \
-H "Authorization: Bearer <YOUR_KEY>" \
-H "Content-Type: application/json"
-d '{
"chatbot_id": 1,
"message": "message",
"session_id": 1,
"conversation_history": []
}'Response
Example 200 response
{
"object": "message",
"id": 1,
"conversation_id": 1,
"chatbot_id": 1,
"query": "What are your opening hours?",
"response": "This assistant isn't ready to answer questions yet. Please check back soon.",
"status": "partial_answer",
"confidence_score": 0.5,
"verdict": "partial",
"retrieval_confidence": 0,
"media": [],
"feedback": null,
"sources": [],
"input_tokens": 0,
"output_tokens": 0,
"cost_usd": "0.000000",
"currency": "USD",
"created_at": "2026-01-01T00:00:00+00:00",
"session_id": "api_Q7mK2xVb9RtL4nWc8ZpH3sYd6JfA1gEu5oTi0vNq",
"actions": [],
"usage": {
"prompt_tokens": 0,
"completion_tokens": 0,
"total_tokens": 0
}
}Stream a chat message
POST /chat — same body, scope, billing and stored message — but answers text/event-stream and sends the reply as it is generated. Errors before the stream opens (a missing scope, an unknown chatbot or session, no credits, a rate limit) are ordinary JSON error responses; honour Retry-After on 429.
Reading the stream. Append each content chunk's choices[0].delta.content to the reply. A final_text event, when one comes, replaces the reply with the text that was stored. data: [DONE] ends the reply but not the stream: keep reading until the metadata event, which always comes last. It carries query_id (the message id), session_id, status, verdict, confidence_score, sources, actions and usage — or, when the turn failed after the stream opened, an error and no query_id.
Conversations. The stream carries no conversation id. Send the session_id from the metadata event back with the next message to continue the conversation, and look the conversation up with GET /conversations/by_session/{sessionId} when you need its id.
Retries. Send an Idempotency-Key: a retry with the key of a turn that already completed does not stream or bill again. An event stream cannot be replayed, so the retry answers application/json (ChatStreamReplay: query_id, session_id, status, usage) with Idempotency-Replayed: true. A turn that failed frees its key, so the retry runs a new turn.
Every stored turn emits conversation.message.created; a turn that first asks for a human also emits conversation.handoff.requested.- Required scope
- chat:send
- Idempotency
- Accepts Idempotency-Key
- Webhook events
- conversation.startedconversation.message.created
- Rate limits
Integer.chatbot_id(body)Chatbot idPlaceholder in the samples:
<CHATBOT_ID>Get it from
- GET /v1/chatbots returns it as
data[].id - POST /v1/chatbots returns it as
id - GET /v1/chatbots/by_handle/{handle} returns it as
id - Webhook chatbot.created carries it as
data.object.id - In the dashboard, it is the number in a chatbot's address:
/dashboard/chatbots/{id}.
- GET /v1/chatbots returns it as
String:session_id(body)API session idapi_followed by 40 characters. A session id from another surface (ws_,widget-,sess_,ch-) is 422VALIDATION_FAILED.Placeholder in the samples:
<SESSION_ID>Get it from
- POST /v1/chat returns it as
session_id - GET /v1/conversations/{id} returns it as
session_id - GET /v1/conversations returns it as
data[].session_id POST /chat/streamsends it assession_idin its trailingmetadataevent (and in the JSON replay of an idempotent retry).
- POST /v1/chat returns it as
Header parameters
Idempotency-KeystringOptionalMakes a retry safe. The first 2xx answer is stored for 24 h and replayed for the same key withIdempotency-Replayed: true; a retry while the first request runs is 409IDEMPOTENCY_IN_FLIGHT; the same key with a different payload is 422IDEMPOTENCY_KEY_REUSED; a malformed key is 400INVALID_IDEMPOTENCY_KEY.Min length:
16Max length:128Pattern:^[A-Za-z0-9_-]+$
Request body
chatbot_idintegerRequiredThe chatbot to talk to. A chatbot of another account is 404NOT_FOUND; itsstatusdoes not matter.messagestringRequiredThe visitor's message, 1–2000 characters.Min length:
1Max length:2000session_idstringnullableOptionalContinues a conversation: thesession_idan earlier turn returned (api_+ 40 characters). Omit it to start a new conversation. A session id from another surface is 422VALIDATION_FAILED; an unknown one, or one of another chatbot, is 404NOT_FOUND.Max length:
255conversation_historyarray of objectOptionalIgnored while server-side history is on (the default): the history the model sees is rebuilt from the stored turns ofsession_id, and a new conversation has none. At most 20{role, content}items,rolebeinguserorassistant.Max items:
20
Responses
- 200The turn as a
text/event-streambody; each event is oneChatStreamEvent. A retry with theIdempotency-Keyof a turn that already completed does not stream or bill again: it answersapplication/json(ChatStreamReplay) withIdempotency-Replayed: true.Summary of the original streamed turn, returned instead of a second stream when a retry reuses theIdempotency-Keyof a completed turn.objectstringRequiredAlways
chat_stream_replayquery_idintegerRequiredMessage id of the original turn — use it withGET /messages/{id}andPOST /messages/{id}/feedback.session_idstringRequiredThe conversation's session id. Send it back assession_idto continue the conversation.statusstringRequiredOutcome, derived fromverdictwhen there is one:answered,partial_answerorno_answer.usageChatStreamUsageRequiredWhat the streamed turn consumed.
- 400A tenant selector header named a tenant other than the API key's own (
ACCOUNT_SELECTOR_NOT_SUPPORTED): an all-digitX-Account-Idother than the key owner's id or, whileworkspaces.context_enabledis on, anX-Workspace-Idother than the key's workspace (ws_prefix optional, case-insensitive). A header naming the key's own tenant, or a non-numericX-Account-Id, is ignored; a key cannot switch tenants, so send neither. A mutation that honoursIdempotency-Keyalso returnsINVALID_IDEMPOTENCY_KEYwhen that header is not 16–128[A-Za-z0-9_-]characters.Error codesACCOUNT_SELECTOR_NOT_SUPPORTEDINVALID_IDEMPOTENCY_KEY
- 401Missing or invalid API key.
Error codesUNAUTHENTICATED
- 402A billing gate refused the request (plan limit, monthly AI budget or message credits). See
error.code.Error codesCHATBOT_BUDGET_EXCEEDEDMESSAGE_CREDITS_EXCEEDEDUSER_BUDGET_EXCEEDED
- 403The API key is missing a required scope.
Error codesACCOUNT_SUSPENDEDINSUFFICIENT_SCOPE
- 404The resource does not exist on this account.
Error codesNOT_FOUND
- 409The request conflicts with the current resource state.
Error codesIDEMPOTENCY_IN_FLIGHT
- 422Validation failed. See
error.details.Error codesIDEMPOTENCY_KEY_REUSEDVALIDATION_FAILED
- 429Rate limited. Honour the Retry-After header before retrying.
Error codesRATE_LIMITED
- 500Unexpected server error. Retry with the same Idempotency-Key.
Error codesSERVER_ERROR
Request
curl -X POST 'https://api.agentency.com/v1/chat/stream' \
-H "Authorization: Bearer <YOUR_KEY>" \
-H "Content-Type: application/json"
-d '{
"chatbot_id": 1,
"message": "message",
"session_id": 1,
"conversation_history": []
}'Response
Example 200 response
{
"object": "chat_stream_replay",
"query_id": 0,
"session_id": "string",
"status": "string",
"usage": {
"input_tokens": 0,
"output_tokens": 0,
"total_tokens": 0,
"cost_usd": "string"
}
}Built from the response schema, not recorded from a live call.