Conversations
Operations tagged Conversations in the public API.
About Conversations
A conversation is one thread between a visitor and a chatbot, from any surface: POST /chat, the website widget, the hosted agent page, the dashboard Test Chat or a messaging channel. Each turn in it — the visitor's message and the reply — is a message.
Lifecycle. The first turn creates the conversation and emits conversation.started; each stored turn emits conversation.message.created; conversation.handoff.requested fires once, when the conversation first asks for a human. Lists show a conversation once it has a message. It stays readable until you delete it with DELETE /conversations/{id}, which emits conversation.ended.
IDs you will meet.
id— the conversation id, an integer, in every/conversations/{id}path. A message carries it asconversation_id.session_id— a string whose prefix names the surface that started the thread (api_,ws_,widget-,sess_,ch-). Send anapi_one back toPOST /chatto continue the thread; look any of them up withGET /conversations/by_session/{sessionId}.- Message
id— an integer, forGET /messages/{id}andPOST /messages/{id}/feedback.POST /chatreturns it asid,POST /chat/streamasquery_id.
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
- Review status id
<REVIEW_STATUS_ID> - Integer.
Get it from
- GET /v1/conversations returns it as
data[].review_status.id - GET /v1/conversations/{id} returns it as
review_status.id - The account's review-status catalogue is managed in the dashboard;
/v1has no operation that lists it.
- GET /v1/conversations returns it as
- Session id
<SESSION_ID> - String. Its prefix names the surface that started the conversation:
api_(POST /chat,POST /chat/stream),ws_(website widget and hosted agent page),widget-(a widget or agent-page turn sent without a minted session),sess_(dashboard Test Chat) orch-{platform}-{integrationId}-{externalChatId}(messaging channels). Older conversations can carry another shape.Get it from
- POST /v1/chat returns it as
session_id - GET /v1/conversations returns it as
data[].session_id - GET /v1/conversations/{id} returns it as
session_id POST /chat/streamsends it assession_idin itsmetadataevent (and in the JSON replay of an idempotent retry).
- POST /v1/chat returns it as
- Conversation id
<CONVERSATION_ID> - Integer.
Get it from
- POST /v1/chat returns it as
conversation_id - GET /v1/conversations returns it as
data[].id - GET /v1/conversations/by_session/{sessionId} returns it as
id - GET /v1/messages/{id} returns it as
conversation_id - GET /v1/customers/{id}/timeline returns it as
data[].chat_session_id - Webhook conversation.started carries it as
data.object.id - Webhook conversation.handoff.requested carries it as
data.object.id POST /chat/streamreturns no conversation id: readsession_idfrom itsmetadataevent, then look the conversation up withGET /conversations/by_session/{sessionId}.
- POST /v1/chat returns it as
- Message id
<MESSAGE_ID> - Integer.
Get it from
- POST /v1/chat returns it as
id - GET /v1/conversations/{id}/messages returns it as
data[].id - Webhook conversation.message.created carries it as
data.object.id POST /chat/streamsends it asquery_idin itsmetadataevent (and in the JSON replay of an idempotent retry).
- POST /v1/chat returns it as
List conversations
chatbot_id for one chatbot's conversations; the other filters combine with it. A chatbot_id of another account is 404 NOT_FOUND. There is no channel filter: read channel on each conversation.- Required scope
- conversations:read
- Idempotency
- Read-only
- Rate limits
Integer.chatbot_id(query)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
Integer.review_status_id(query)Review status idPlaceholder in the samples:
<REVIEW_STATUS_ID>Get it from
- GET /v1/conversations returns it as
data[].review_status.id - GET /v1/conversations/{id} returns it as
review_status.id - The account's review-status catalogue is managed in the dashboard;
/v1has no operation that lists it.
- GET /v1/conversations returns it as
Query parameters
chatbot_idintegernullableOptionalRestrict to one chatbot. Omit to list every conversation in the account.Minimum:
1statusstringnullableOptionalAnswer status:answered(at least one answered turn),not_answered(unanswered or partial turns and no answered one) orpartial_answer. Any other value is 422 VALIDATION_FAILED.Possible values
answerednot_answeredpartial_answernulldate_fromstringnullableOptionalOnly conversations started on or after this UTC calendar day (YYYY-MM-DD).Format:
datedate_tostringnullableOptionalOnly conversations started on or before this UTC calendar day (YYYY-MM-DD, inclusive); must not precededate_from.Format:
datesearchstringnullableOptionalConversations with a visitor message containing every word of the term, in any order (Arabic spelling variants such as ه/ة, ي/ى and أ/إ/ا match each other; at most 100 characters are matched).Max length:
255review_status_idintegernullableOptionalOnly conversations whose effective review status is this id from the account catalogue. A custom status matches explicit assignments; a system status also matches legacy rows whoseis_reviewedresolves to it. An id outside the catalogue is 422 VALIDATION_FAILED.Minimum:
1limitintegerOptionalPage size, clamped to 1–100.Default:
20Minimum:1Maximum:100starting_afterstringOptionalCursor from a previous page'snext_cursor: returns the page after it. A cursor minted for another list,sortororderis 422INVALID_CURSOR.ending_beforestringOptionalCursor from a previous page'sprevious_cursor: returns the page before it. Wins when both cursors are sent.created[gte]stringOptionalOnly rows created on or after this instant:YYYY-MM-DD(whole UTC day) or an RFC 3339 timestamp with a zone. Anything else is 422INVALID_PARAMETER.created[lte]stringOptionalOnly rows created on or before this instant:YYYY-MM-DD(whole UTC day) or an RFC 3339 timestamp with a zone. Anything else is 422INVALID_PARAMETER.
Responses
- 200Success.
objectstringRequiredAlways
listdataarray of ConversationResourceRequiredhas_morebooleanRequirednext_cursorstringnullableRequiredprevious_cursorstringnullableRequiredurlstringRequiredAlways
/v1/conversations
- 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.Error codesACCOUNT_SELECTOR_NOT_SUPPORTED
- 401Missing or invalid API key.
Error codesUNAUTHENTICATED
- 403The API key is missing a required scope.
Error codesACCOUNT_SUSPENDEDINSUFFICIENT_SCOPE
- 404The resource does not exist on this account.
Error codesNOT_FOUND
- 422Validation failed. See
error.details. - 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 GET 'https://api.agentency.com/v1/conversations' \
-H "Authorization: Bearer <YOUR_KEY>" \
-H "Content-Type: application/json"Response
Example 200 response
{
"object": "list",
"data": [
{
"object": "conversation",
"id": 1,
"session_id": "api_Q7mK2xVb9RtL4nWc8ZpH3sYd6JfA1gEu5oTi0vNq",
"chatbot_id": 1,
"status": "active",
"channel": "api",
"is_reviewed": false,
"review_status": {
"id": 1,
"name": "Needs review",
"color": "neutral",
"counts_as_reviewed": false
},
"handoff_requested": false,
"handoff_reason": null,
"external_user_id": null,
"external_user_name": null,
"started_at": "2026-01-01T00:00:00+00:00",
"created_at": "2026-01-01T00:00:00+00:00",
"updated_at": "2026-01-01T00:00:00+00:00"
}
],
"has_more": false,
"next_cursor": null,
"previous_cursor": null,
"url": "/v1/conversations"
}Retrieve a conversation by session id
session_id instead of its integer id. Use it after POST /chat/stream, whose metadata event carries session_id but no conversation id, or when your own records keep the session id.
A session id from any surface is accepted (api_, ws_, widget-, sess_, ch-); only POST /chat is limited to api_ sessions. A session id of another account is 404 NOT_FOUND.- Required scope
- conversations:read
- Idempotency
- Read-only
- Rate limits
String. Its prefix names the surface that started the conversation:sessionId(path)Session idapi_(POST /chat,POST /chat/stream),ws_(website widget and hosted agent page),widget-(a widget or agent-page turn sent without a minted session),sess_(dashboard Test Chat) orch-{platform}-{integrationId}-{externalChatId}(messaging channels). Older conversations can carry another shape.Placeholder in the samples:
<SESSION_ID>Get it from
- POST /v1/chat returns it as
session_id - GET /v1/conversations returns it as
data[].session_id - GET /v1/conversations/{id} returns it as
session_id POST /chat/streamsends it assession_idin itsmetadataevent (and in the JSON replay of an idempotent retry).
- POST /v1/chat returns it as
Path parameters
sessionIdstringRequiredThe conversation'ssession_id, exactly as an earlier response returned it.
Responses
- 200
ConversationResourceobjectstringRequiredAlways
conversationidintegerRequiredThe conversation id, used in every/conversations/{id}path. Messages carry it asconversation_id.session_idstringRequiredThe session id the surface minted; its prefix names that surface (api_,ws_,widget-,sess_,ch-). Send anapi_one back toPOST /chatto continue the conversation.chatbot_idintegerRequiredThe chatbot the conversation is with.statusstringRequiredLifecycle state. Conversations are createdactive;completedandabandonedare reserved values no current surface sets.Possible values
activecompletedabandonedchannelstringRequiredThe surface the conversation arrived through:api,widget,direct_link(hosted agent page),dashboard(Test Chat), a messaging platform, orunknownfor an older conversation whose origin was never recorded.Possible values
widgetdirect_linkdashboardapitelegramwhatsappfacebookinstagramslackdiscordlineviberwechatsmsunknownis_reviewedbooleanRequiredWhether the effective review status counts as reviewed (review_status.counts_as_reviewed).review_statusobjectRequiredThe conversation's effective status in the account's review catalogue.POST /conversations/{id}/reviewsets it.handoff_requestedbooleanRequiredWhether the conversation asked for a human.conversation.handoff.requestedfires once, when this turns true.handoff_reasonstringnullableRequiredWhy the handoff was requested, when a reason was recorded.external_user_idstringnullableRequiredThe visitor id the embedding page passed to the widget or hosted agent page, or the platform chat id on a messaging channel;nullwhen none. Claimed by the host page, never verified: display data only.external_user_namestringnullableRequiredThe visitor display name the embedding page or messaging channel supplied;nullwhen none. Display data only, never verified.started_atstringnullableRequiredWhen the conversation started (ISO 8601, UTC).created_atstringnullableRequiredWhen the conversation was created (ISO 8601, UTC).updated_atstringnullableRequiredWhen the conversation last changed (ISO 8601, UTC).
- 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.Error codesACCOUNT_SELECTOR_NOT_SUPPORTED
- 401Missing or invalid API key.
Error codesUNAUTHENTICATED
- 403The API key is missing a required scope.
Error codesACCOUNT_SUSPENDEDINSUFFICIENT_SCOPE
- 404The resource does not exist on this account.
Error codesNOT_FOUND
- 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 GET 'https://api.agentency.com/v1/conversations/by_session/<SESSION_ID>' \
-H "Authorization: Bearer <YOUR_KEY>" \
-H "Content-Type: application/json"Response
Example 200 response
{
"object": "conversation",
"id": 1,
"session_id": "api_Q7mK2xVb9RtL4nWc8ZpH3sYd6JfA1gEu5oTi0vNq",
"chatbot_id": 1,
"status": "active",
"channel": "api",
"is_reviewed": false,
"review_status": {
"id": 1,
"name": "Needs review",
"color": "neutral",
"counts_as_reviewed": false
},
"handoff_requested": false,
"handoff_reason": null,
"external_user_id": null,
"external_user_name": null,
"started_at": "2026-01-01T00:00:00+00:00",
"created_at": "2026-01-01T00:00:00+00:00",
"updated_at": "2026-01-01T00:00:00+00:00"
}Retrieve a conversation
channel), its review state and its handoff flag. Its messages are listed by GET /conversations/{id}/messages. A deleted conversation, or one of another account, is 404 NOT_FOUND.- Required scope
- conversations:read
- Idempotency
- Read-only
- Rate limits
Integer.id(path)Conversation idPlaceholder in the samples:
<CONVERSATION_ID>Get it from
- POST /v1/chat returns it as
conversation_id - GET /v1/conversations returns it as
data[].id - GET /v1/conversations/by_session/{sessionId} returns it as
id - GET /v1/messages/{id} returns it as
conversation_id - GET /v1/customers/{id}/timeline returns it as
data[].chat_session_id - Webhook conversation.started carries it as
data.object.id - Webhook conversation.handoff.requested carries it as
data.object.id POST /chat/streamreturns no conversation id: readsession_idfrom itsmetadataevent, then look the conversation up withGET /conversations/by_session/{sessionId}.
- POST /v1/chat returns it as
Path parameters
idintegerRequiredThe conversation id.
Responses
- 200
ConversationResourceobjectstringRequiredAlways
conversationidintegerRequiredThe conversation id, used in every/conversations/{id}path. Messages carry it asconversation_id.session_idstringRequiredThe session id the surface minted; its prefix names that surface (api_,ws_,widget-,sess_,ch-). Send anapi_one back toPOST /chatto continue the conversation.chatbot_idintegerRequiredThe chatbot the conversation is with.statusstringRequiredLifecycle state. Conversations are createdactive;completedandabandonedare reserved values no current surface sets.Possible values
activecompletedabandonedchannelstringRequiredThe surface the conversation arrived through:api,widget,direct_link(hosted agent page),dashboard(Test Chat), a messaging platform, orunknownfor an older conversation whose origin was never recorded.Possible values
widgetdirect_linkdashboardapitelegramwhatsappfacebookinstagramslackdiscordlineviberwechatsmsunknownis_reviewedbooleanRequiredWhether the effective review status counts as reviewed (review_status.counts_as_reviewed).review_statusobjectRequiredThe conversation's effective status in the account's review catalogue.POST /conversations/{id}/reviewsets it.handoff_requestedbooleanRequiredWhether the conversation asked for a human.conversation.handoff.requestedfires once, when this turns true.handoff_reasonstringnullableRequiredWhy the handoff was requested, when a reason was recorded.external_user_idstringnullableRequiredThe visitor id the embedding page passed to the widget or hosted agent page, or the platform chat id on a messaging channel;nullwhen none. Claimed by the host page, never verified: display data only.external_user_namestringnullableRequiredThe visitor display name the embedding page or messaging channel supplied;nullwhen none. Display data only, never verified.started_atstringnullableRequiredWhen the conversation started (ISO 8601, UTC).created_atstringnullableRequiredWhen the conversation was created (ISO 8601, UTC).updated_atstringnullableRequiredWhen the conversation last changed (ISO 8601, UTC).
- 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.Error codesACCOUNT_SELECTOR_NOT_SUPPORTED
- 401Missing or invalid API key.
Error codesUNAUTHENTICATED
- 403The API key is missing a required scope.
Error codesACCOUNT_SUSPENDEDINSUFFICIENT_SCOPE
- 404The resource does not exist on this account.
Error codesNOT_FOUND
- 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 GET 'https://api.agentency.com/v1/conversations/<CONVERSATION_ID>' \
-H "Authorization: Bearer <YOUR_KEY>" \
-H "Content-Type: application/json"Response
Example 200 response
{
"object": "conversation",
"id": 1,
"session_id": "api_Q7mK2xVb9RtL4nWc8ZpH3sYd6JfA1gEu5oTi0vNq",
"chatbot_id": 1,
"status": "active",
"channel": "api",
"is_reviewed": false,
"review_status": {
"id": 1,
"name": "Needs review",
"color": "neutral",
"counts_as_reviewed": false
},
"handoff_requested": false,
"handoff_reason": null,
"external_user_id": null,
"external_user_name": null,
"started_at": "2026-01-01T00:00:00+00:00",
"created_at": "2026-01-01T00:00:00+00:00",
"updated_at": "2026-01-01T00:00:00+00:00"
}Delete a conversation
conversation.ended (there is no separate deletion event): a receiver that needs to tell a deletion apart should follow up with GET /conversations/{id}, which is 404 once the conversation is deleted.- Required scope
- conversations:write
- Idempotency
- Accepts Idempotency-Key
- Webhook events
- conversation.ended
- Rate limits
Integer.id(path)Conversation idPlaceholder in the samples:
<CONVERSATION_ID>Get it from
- POST /v1/chat returns it as
conversation_id - GET /v1/conversations returns it as
data[].id - GET /v1/conversations/by_session/{sessionId} returns it as
id - GET /v1/messages/{id} returns it as
conversation_id - GET /v1/customers/{id}/timeline returns it as
data[].chat_session_id - Webhook conversation.started carries it as
data.object.id - Webhook conversation.handoff.requested carries it as
data.object.id POST /chat/streamreturns no conversation id: readsession_idfrom itsmetadataevent, then look the conversation up withGET /conversations/by_session/{sessionId}.
- POST /v1/chat returns it as
Path parameters
idintegerRequiredThe conversation id.
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_-]+$
Responses
- 200Success.
objectstringRequiredAlways
conversationidintegerRequireddeletedbooleanRequired
- 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
- 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_REUSED
- 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 DELETE 'https://api.agentency.com/v1/conversations/<CONVERSATION_ID>' \
-H "Authorization: Bearer <YOUR_KEY>" \
-H "Content-Type: application/json"Response
Example 200 response
{
"object": "conversation",
"id": 1,
"deleted": true
}List a conversation's messages
order=asc to read the thread from its first turn). Each Message carries the visitor's text (query), the reply (response), its verdict and confidence, the citations it drew on and the turn's token cost.- Required scope
- conversations:read
- Idempotency
- Read-only
- Rate limits
Integer.id(path)Conversation idPlaceholder in the samples:
<CONVERSATION_ID>Get it from
- POST /v1/chat returns it as
conversation_id - GET /v1/conversations returns it as
data[].id - GET /v1/conversations/by_session/{sessionId} returns it as
id - GET /v1/messages/{id} returns it as
conversation_id - GET /v1/customers/{id}/timeline returns it as
data[].chat_session_id - Webhook conversation.started carries it as
data.object.id - Webhook conversation.handoff.requested carries it as
data.object.id POST /chat/streamreturns no conversation id: readsession_idfrom itsmetadataevent, then look the conversation up withGET /conversations/by_session/{sessionId}.
- POST /v1/chat returns it as
Path parameters
idintegerRequiredThe conversation id.
Query parameters
limitintegerOptionalPage size, clamped to 1–100.Default:
20Minimum:1Maximum:100starting_afterstringOptionalCursor from a previous page'snext_cursor: returns the page after it. A cursor minted for another list,sortororderis 422INVALID_CURSOR.ending_beforestringOptionalCursor from a previous page'sprevious_cursor: returns the page before it. Wins when both cursors are sent.created[gte]stringOptionalOnly rows created on or after this instant:YYYY-MM-DD(whole UTC day) or an RFC 3339 timestamp with a zone. Anything else is 422INVALID_PARAMETER.created[lte]stringOptionalOnly rows created on or before this instant:YYYY-MM-DD(whole UTC day) or an RFC 3339 timestamp with a zone. Anything else is 422INVALID_PARAMETER.
Responses
- 200Success.
objectstringRequiredAlways
listdataarray of MessageRequiredhas_morebooleanRequirednext_cursorstringnullableRequiredprevious_cursorstringnullableRequiredurlstringRequired
- 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.Error codesACCOUNT_SELECTOR_NOT_SUPPORTED
- 401Missing or invalid API key.
Error codesUNAUTHENTICATED
- 403The API key is missing a required scope.
Error codesACCOUNT_SUSPENDEDINSUFFICIENT_SCOPE
- 404The resource does not exist on this account.
Error codesNOT_FOUND
- 422Validation failed. See
error.details.Error codesINVALID_CURSORINVALID_PARAMETER
- 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 GET 'https://api.agentency.com/v1/conversations/<CONVERSATION_ID>/messages' \
-H "Authorization: Bearer <YOUR_KEY>" \
-H "Content-Type: application/json"Response
Example 200 response
{
"object": "list",
"data": [
{
"object": "message",
"id": 1,
"conversation_id": 1,
"chatbot_id": 1,
"query": "How do refunds work?",
"response": "Refunds are issued to the original payment method within 5 business days.",
"status": "answered",
"confidence_score": 96.3,
"verdict": "full",
"retrieval_confidence": 91.31,
"media": [],
"feedback": null,
"sources": [],
"input_tokens": 33,
"output_tokens": 157,
"cost_usd": "0.000000",
"currency": "USD",
"created_at": "2026-01-01T00:00:00+00:00"
}
],
"has_more": false,
"next_cursor": null,
"previous_cursor": null,
"url": "/v1/conversations/1/messages"
}Retrieve a message
conversation_id names the conversation it belongs to. A message of another account is 404 NOT_FOUND.- Required scope
- conversations:read
- Idempotency
- Read-only
- Rate limits
Integer.id(path)Message idPlaceholder in the samples:
<MESSAGE_ID>Get it from
- POST /v1/chat returns it as
id - GET /v1/conversations/{id}/messages returns it as
data[].id - Webhook conversation.message.created carries it as
data.object.id POST /chat/streamsends it asquery_idin itsmetadataevent (and in the JSON replay of an idempotent retry).
- POST /v1/chat returns it as
Path parameters
idintegerRequiredThe message id.
Responses
- 200
MessageResourceOne chat turn: the visitor's message and the assistant's reply.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-time
- 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.Error codesACCOUNT_SELECTOR_NOT_SUPPORTED
- 401Missing or invalid API key.
Error codesUNAUTHENTICATED
- 403The API key is missing a required scope.
Error codesACCOUNT_SUSPENDEDINSUFFICIENT_SCOPE
- 404The resource does not exist on this account.
Error codesNOT_FOUND
- 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 GET 'https://api.agentency.com/v1/messages/<MESSAGE_ID>' \
-H "Authorization: Bearer <YOUR_KEY>" \
-H "Content-Type: application/json"Response
Example 200 response
{
"object": "message",
"id": 1,
"conversation_id": 1,
"chatbot_id": 1,
"query": "How do refunds work?",
"response": "Refunds are issued to the original payment method within 5 business days.",
"status": "answered",
"confidence_score": 96.3,
"verdict": "full",
"retrieval_confidence": 91.31,
"media": [],
"feedback": null,
"sources": [],
"input_tokens": 33,
"output_tokens": 157,
"cost_usd": "0.000000",
"currency": "USD",
"created_at": "2026-01-01T00:00:00+00:00"
}Set a conversation's review state
reviewed: true (the default when the body omits it) moves it to the account catalogue's system "Reviewed" status; reviewed: false moves it to "Needs review". Either replaces a custom status set in the dashboard.
The call sets a state rather than toggling one, so a retry never flips it back. It emits no webhook event.- Required scope
- conversations:write
- Idempotency
- Accepts Idempotency-Key
- Webhook events
- None
- Rate limits
Integer.id(path)Conversation idPlaceholder in the samples:
<CONVERSATION_ID>Get it from
- POST /v1/chat returns it as
conversation_id - GET /v1/conversations returns it as
data[].id - GET /v1/conversations/by_session/{sessionId} returns it as
id - GET /v1/messages/{id} returns it as
conversation_id - GET /v1/customers/{id}/timeline returns it as
data[].chat_session_id - Webhook conversation.started carries it as
data.object.id - Webhook conversation.handoff.requested carries it as
data.object.id POST /chat/streamreturns no conversation id: readsession_idfrom itsmetadataevent, then look the conversation up withGET /conversations/by_session/{sessionId}.
- POST /v1/chat returns it as
Path parameters
idintegerRequiredThe conversation id.
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
reviewedbooleanOptionalThe review state to set:truemarks the conversation reviewed,falsemarks it as needing review. Defaults totruewhen omitted.
Responses
- 200
ConversationResourceobjectstringRequiredAlways
conversationidintegerRequiredThe conversation id, used in every/conversations/{id}path. Messages carry it asconversation_id.session_idstringRequiredThe session id the surface minted; its prefix names that surface (api_,ws_,widget-,sess_,ch-). Send anapi_one back toPOST /chatto continue the conversation.chatbot_idintegerRequiredThe chatbot the conversation is with.statusstringRequiredLifecycle state. Conversations are createdactive;completedandabandonedare reserved values no current surface sets.Possible values
activecompletedabandonedchannelstringRequiredThe surface the conversation arrived through:api,widget,direct_link(hosted agent page),dashboard(Test Chat), a messaging platform, orunknownfor an older conversation whose origin was never recorded.Possible values
widgetdirect_linkdashboardapitelegramwhatsappfacebookinstagramslackdiscordlineviberwechatsmsunknownis_reviewedbooleanRequiredWhether the effective review status counts as reviewed (review_status.counts_as_reviewed).review_statusobjectRequiredThe conversation's effective status in the account's review catalogue.POST /conversations/{id}/reviewsets it.handoff_requestedbooleanRequiredWhether the conversation asked for a human.conversation.handoff.requestedfires once, when this turns true.handoff_reasonstringnullableRequiredWhy the handoff was requested, when a reason was recorded.external_user_idstringnullableRequiredThe visitor id the embedding page passed to the widget or hosted agent page, or the platform chat id on a messaging channel;nullwhen none. Claimed by the host page, never verified: display data only.external_user_namestringnullableRequiredThe visitor display name the embedding page or messaging channel supplied;nullwhen none. Display data only, never verified.started_atstringnullableRequiredWhen the conversation started (ISO 8601, UTC).created_atstringnullableRequiredWhen the conversation was created (ISO 8601, UTC).updated_atstringnullableRequiredWhen the conversation last changed (ISO 8601, UTC).
- 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
- 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/conversations/<CONVERSATION_ID>/review' \
-H "Authorization: Bearer <YOUR_KEY>" \
-H "Content-Type: application/json"
-d '{
"reviewed": true
}'Response
Example 200 response
{
"object": "conversation",
"id": 1,
"session_id": "api_Q7mK2xVb9RtL4nWc8ZpH3sYd6JfA1gEu5oTi0vNq",
"chatbot_id": 1,
"status": "active",
"channel": "api",
"is_reviewed": true,
"review_status": {
"id": 1,
"name": "Reviewed",
"color": "success",
"counts_as_reviewed": true
},
"handoff_requested": false,
"handoff_reason": null,
"external_user_id": null,
"external_user_name": null,
"started_at": "2026-01-01T00:00:00+00:00",
"created_at": "2026-01-01T00:00:00+00:00",
"updated_at": "2026-01-01T00:00:00+00:00"
}Rate a message's reply
positive) or thumbs down (negative) on the reply and returns the message with feedback set. A later call overwrites the earlier rating; the API cannot clear it. It emits no webhook event.- Required scope
- conversations:write
- Idempotency
- Accepts Idempotency-Key
- Webhook events
- None
- Rate limits
Integer.id(path)Message idPlaceholder in the samples:
<MESSAGE_ID>Get it from
- POST /v1/chat returns it as
id - GET /v1/conversations/{id}/messages returns it as
data[].id - Webhook conversation.message.created carries it as
data.object.id POST /chat/streamsends it asquery_idin itsmetadataevent (and in the JSON replay of an idempotent retry).
- POST /v1/chat returns it as
Path parameters
idintegerRequiredThe message id.
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
feedbackstringRequiredThe rating of the reply:positive(thumbs up) ornegative(thumbs down). It replaces any earlier rating of the same message.Possible values
positivenegative
Responses
- 200
MessageResourceOne chat turn: the visitor's message and the assistant's reply.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-time
- 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
- 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/messages/<MESSAGE_ID>/feedback' \
-H "Authorization: Bearer <YOUR_KEY>" \
-H "Content-Type: application/json"
-d '{
"feedback": "feedback"
}'Response
Example 200 response
{
"object": "message",
"id": 1,
"conversation_id": 1,
"chatbot_id": 1,
"query": "How do refunds work?",
"response": "Refunds are issued to the original payment method within 5 business days.",
"status": "answered",
"confidence_score": 96.3,
"verdict": "full",
"retrieval_confidence": 91.31,
"media": [],
"feedback": "positive",
"sources": [],
"input_tokens": 33,
"output_tokens": 157,
"cost_usd": "0.000000",
"currency": "USD",
"created_at": "2026-01-01T00:00:00+00:00"
}