Conversations
Read the conversations your bots have had - across chat, voice, SMS, and WhatsApp - including messages and step-by-step traces. Read endpoints need conversations:read; delete needs conversations:write.
Endpoints
| Method & path | Ability | Notes |
|---|---|---|
GET /api/v1/conversations | conversations:read | Paginated list (see below) |
GET /api/v1/conversations/{id} | conversations:read | Includes messages[] |
GET /api/v1/conversations/{id}/trace | conversations:read | Step-by-step execution trace |
DELETE /api/v1/conversations/{id} | conversations:write | Hard delete (cascades) |
List conversations
Pages with limit (default 100, max 500) and offset, where limit + offset ≤ 10000. The total count is returned in the X-Total-Count response header.
Filters (strict allow-list - an unknown query parameter returns 400):
| Parameter | Filters by |
|---|---|
bot_id | A specific bot |
channel | web | sms | whatsapp | api | agent | voice | post_call |
external_call_id, external_linked_id, source_conversation_id | Your external identifiers |
since, until | RFC 3339 timestamps |
analysis.sentiment, analysis.topic, analysis.outcome | Post-call analysis |
errors_only | true returns only conversations where something went wrong, the ones with an error_count above 0. false is the same as leaving it out, and any other value returns 400. |
curl "https://app.hania.ai/api/v1/conversations?channel=voice&limit=50" \
-H "Authorization: Bearer $HANIA_API_KEY"
List item fields: id, bot_id, bot_name, channel, customer_id, external_call_id, external_linked_id, message_count, started_at, ended_at, last_message_at, last_message_preview (≤ 100 chars), optional first_user_message, has_trace, optional post_call_analysis, optional source_conversation_id, error_count.
error_count is how many things went wrong in the conversation, so anything above 0 is worth a look. It adds up failed tool calls, messages with an error, one for a reply a messaging channel wouldn't accept, and one for a run that failed, was interrupted or expired. It's always present, and 0 when nothing went wrong.
last_message_preview skips the notices Hania adds by itself, such as a finished background run or video, and shows the latest real message.
first_user_message is the first thing the user said in the conversation, which often makes a better label than the last message. If it's longer than 100 characters, it's cut there and ends in "...". It's left out when the conversation has no user message yet.
Retrieve a conversation
GET /api/v1/conversations/{id} returns the conversation plus its messages[] in ascending order. Each message: id, role, content, optional metadata (JSON string), created_at.
For an agent run it can also have stop_reason, when the run ended early, goal, the completion check's record, and output_valid, set to false when the final output didn't fit the agent's output schema. See reading the outcome.
An assistant message's metadata also says how its model call went: response_id (the provider's id for the response), provider (the service called), upstream_provider (the company that actually ran the model, when the service reports one) and native_finish_reason (the provider's own stop reason, which can differ from finish_reason). Older messages don't have them.
When a reply fails, a message's metadata has an error object saying why:
{
"error": {
"stage": "turn",
"message": "…",
"at": "2026-11-12T09:41:07Z"
}
}
In a chat started from the dashboard, it's on the reply that failed. On the widget and messaging channels, it's on the customer's message that didn't get an answer. message is the underlying reason, meant for working out what went wrong. It isn't the text the person saw, which is the reply's content. at is when it happened, as an RFC 3339 time. Tool results have their own error flag, a plain true, so check that error is an object.
When a reply couldn't be delivered on WhatsApp, Messenger, Instagram, Telegram, Slack or SMS, the conversation has a top-level delivery_error with the same three fields, and stage set to "delivery". message is the reason the platform gave. It's left out when every reply was delivered. It's a record rather than a status: a reply delivered later doesn't remove it, and a newer failure replaces it.
A test conversation has a top-level test set to true. That covers the dashboard's test chat and the ones the assistant starts when you ask it to try an agent. It's left out on every other conversation.
Goal
A conversation can carry a goal that each agent-mode reply is checked against. Reading it needs conversations:read; the other calls need conversations:write.
GET /api/v1/conversations/{id}/goalreturns the goal's status, ornullwhen the conversation never had one. After the goal is cleared it still returns the record, withactive: falseandoutcome: "cleared".PUT /api/v1/conversations/{id}/goalwith{ "condition": "…" }sets the goal, or replaces the one there, and returns the status. Setting it doesn't start a reply: send the condition as the next message, or usegoalon/chat/send-streamto do both in one call.DELETE /api/v1/conversations/{id}/goalclears it and returns{ "cleared": true }, orfalsewhen there was nothing to clear.POST /api/v1/conversations/{id}/goal/resumeresets an active goal's counters (start time, turns checked, tokens and retries) when a person reopens a saved conversation, and returns the status. It doesn't continue a paused goal: only the person's next message does that.
{
"condition": "Every product in the spring range has a price and a photo",
"active": true,
"paused": false,
"started_at": "2026-11-07T09:02:11Z",
"checks": 2,
"latest_reason": "Three products still have no photo.",
"waiting": false,
"usage": {
"prompt_tokens": 48210, "completion_tokens": 3105, "total_tokens": 51315,
"cached_tokens": 30112, "cost_usd": 0.0412, "tool_call_count": 17
}
}
checks is how many replies have been checked. waiting is true while the check waits for work running in the background. usage covers the conversation since started_at, with cost_usd in US dollars. outcome is achieved, impossible or cleared, and stays empty until the goal ends, so treat the goal as finished only once it's set. reason means something different depending on the state:
| State | What reason means |
|---|---|
active, not paused, no retry_at | Not used. latest_reason holds the last check's reason. |
active with retry_at | Why the last reply failed. Another try is due at retry_at. When the conversation's computer is offline, retry_at goes back to empty without a retry, and the next message continues the goal. |
active and paused | Why the goal paused. The person's next message continues it. |
not active, with outcome | Why it ended. |
Errors: 400 with a message to show for an empty condition or one over 4,000 characters, an invalid body, or a badly formed id; 404 "Conversation not found" for a conversation outside your workspace; 409 TEAM_CONVERSATION for a team conversation, which can't have a goal (its GET returns null).
The conversation itself (retrieve) also carries the stored goal record as goal. For a goal set on the conversation it adds condition, started_at, paused, retries, retry_at, waiting_since, next_checkin_at and checkins to the fields an agent run has, and checks there is the list of verdicts. Any of these keys can be missing or empty, so treat a missing key and null the same. For display, the status above is the simpler shape.
Trace
GET /api/v1/conversations/{id}/trace returns the execution steps - useful for debugging tool calls and model turns. Each step: id, step_number, action_type, action_name, result_summary, items_count, started_at, completed_at, duration_ms, error_message, and (when applicable) model, prompt_tokens, completion_tokens, total_tokens, tool_function, tool_execution_ms.
A step that made a model call also has response_id, finish_reason (stop, tool_calls, length, content_filter or error), provider and upstream_provider. Each is left out when it isn't known, for example on older steps.
model, provider or upstream_provider, and their messages' metadata has no provider or upstream_provider. Everything else is the same.Delete
DELETE /api/v1/conversations/{id} permanently removes the conversation along with its messages, trace, and any stored media. This cannot be undone.