API reference
Sessions
Open a conversation, list conversations, read a transcript, and close one.
Create a session#
POST /v1/assistants/{assistant_id}/sessionsOpens a conversation. Do this once, then send as many turns as you like against
the session_id it returns.
Body#
channel | string | chat or voice. Defaults to chat. Updates itself as the conversation moves between doors. |
visitor | object | Answers to the assistant's pre-chat form, keyed by its field keys. |
metadata | object | Your own labels. Up to 16 keys, string values up to 500 characters. |
Request#
curl -X POST \
"https://studio.zeevaa.ai/api/v1/assistants/9f2b1c84-6e3a-4d17-b0c5-2e7a8f41d9b3/sessions" \
-H "Authorization: Bearer $ZEEVAA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"channel": "chat",
"visitor": { "name": "Sara", "email": "sara@example.com" },
"metadata": { "user_id": "u_8812", "plan": "pro" }
}'Response#
{
"session_id": "c31f9a70-84b2-4e05-9d6c-1a7f3b2e6d48",
"assistant_id": "9f2b1c84-6e3a-4d17-b0c5-2e7a8f41d9b3",
"status": "active",
"channel": "chat",
"greeting": "Hi Sara! How can I help you today?",
"max_call_seconds": 600,
"started_at": "2026-08-04T09:14:22.108Z"
}Keys in visitor that the assistant's form does not declare are dropped rather
than rejected — an extra field in your payload is not an outage. Required fields
that are missing return 422.
Errors#
404 assistant_not_found | Unknown, or not reachable by this key. |
409 assistant_not_published | The assistant is a draft or disabled. |
422 missing_visitor_fields | Required pre-chat fields were not supplied. The message names them. |
List sessions#
GET /v1/sessionsConversations in the key's workspace, newest first.
Query parameters#
assistant_id | string | Restrict to one assistant. |
status | string | active, ended, expired or blocked. |
source | string | api, web or embed. Defaults to api — your integration's own conversations. Pass all for everything. |
from / to | string | ISO 8601 bounds on started_at. |
limit | integer | 1–100. Default 50. |
cursor | string | From next_cursor on the previous page. |
Response#
{
"sessions": [
{
"id": "c31f9a70-84b2-4e05-9d6c-1a7f3b2e6d48",
"assistant_id": "9f2b1c84-6e3a-4d17-b0c5-2e7a8f41d9b3",
"assistant_name": "Marina sales desk",
"title": "What do you have with two bedrooms?",
"status": "ended",
"channel": "chat",
"source": "api",
"ended_reason": "visitor_left",
"summary": "Asked about two-bedroom availability under 2M…",
"visitor": { "name": "Sara", "email": "sara@example.com" },
"metadata": { "user_id": "u_8812" },
"message_count": 6,
"tool_call_count": 2,
"started_at": "2026-08-04T09:14:22.108Z",
"ended_at": "2026-08-04T09:21:47.930Z",
"duration_ms": 445822
}
],
"next_cursor": "c2Vzc2lvbjoxNzU0",
"has_more": true
}title is taken from the first thing the person said. summary is generated
after the conversation ends and may be null for one that is still running.
Sessions with no turns are omitted. Somebody who opened a connection and left is not a conversation, and listing them would make the history mostly noise.
Retrieve a transcript#
GET /v1/sessions/{session_id}One conversation in full.
Response#
{
"id": "c31f9a70-84b2-4e05-9d6c-1a7f3b2e6d48",
"assistant_id": "9f2b1c84-6e3a-4d17-b0c5-2e7a8f41d9b3",
"status": "ended",
"channel": "chat",
"source": "api",
"visitor": { "name": "Sara", "email": "sara@example.com" },
"metadata": { "user_id": "u_8812" },
"summary": "Asked about two-bedroom availability under 2M…",
"started_at": "2026-08-04T09:14:22.108Z",
"ended_at": "2026-08-04T09:21:47.930Z",
"duration_ms": 445822,
"usage": {
"input_tokens": 4820,
"output_tokens": 913,
"total_tokens": 5733,
"tts_characters": 612,
"stt_seconds": 44.2
},
"turns": [
{
"seq": 1,
"id": "msg_01H9",
"role": "user",
"content": "What do you have with two bedrooms under 2M?",
"input_mode": "text",
"offset_ms": 0,
"created_at": "2026-08-04T09:14:31.882Z"
},
{
"seq": 2,
"id": "msg_01HA",
"role": "assistant",
"content": "We have three that match your budget…",
"input_mode": "text",
"offset_ms": 1204,
"first_token_ms": 842,
"created_at": "2026-08-04T09:14:36.117Z",
"tool_calls": [
{
"name": "search_records",
"status": "success",
"duration_ms": 38,
"input": { "bedrooms": 2, "price": { "lte": 2000000 } },
"output": { "total": 3 }
}
]
}
]
}Turn fields#
seq | integer | The sort key. Assigned on write, monotonic, and it cannot tie. Order by this, never by a timestamp — two turns can share a millisecond. |
role | string | user or assistant. |
input_mode | string | text or voice — how this particular turn arrived. |
offset_ms | integer | Milliseconds from the start of the conversation. This is what builds a timeline. |
first_token_ms | integer | Generation start to first streamed word, on assistant turns. The number that decides whether a voice call feels alive. |
tool_calls | array | Tools used on this turn, with inputs, outputs and durations. |
Errors#
404 session_not_found | Unknown, belongs to another workspace, or aged out of the assistant's retention window. |
End a session#
POST /v1/sessions/{session_id}/endBody#
reason | string | visitor_left, assistant_ended or handoff. Defaults to visitor_left. |
Any other value is recorded as visitor_left. An integration cannot label its
own conversations a successful handoff, because outcome reporting that anyone
can write is not reporting.
Request#
curl -X POST \
"https://studio.zeevaa.ai/api/v1/sessions/c31f9a70-84b2-4e05-9d6c-1a7f3b2e6d48/end" \
-H "Authorization: Bearer $ZEEVAA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"reason": "visitor_left"}'Response#
{
"id": "c31f9a70-84b2-4e05-9d6c-1a7f3b2e6d48",
"status": "ended",
"ended_reason": "visitor_left",
"ended_at": "2026-08-04T09:21:47.930Z",
"duration_ms": 445822
}Idempotent. Calling it on an already-closed session returns the session as it stands, with the reason that was recorded first — a later, vaguer reason never overwrites a more specific one.