Import Conversations
Send Voxli the chats your agent had with real users, in OpenTelemetry’s GenAI messages format.
Importing a chat
Send each chat in one request to POST /conversations/, with your API key. Its messages use the message format of the OpenTelemetry GenAI semantic conventions: each message has a role and a list of parts.
import osimport requestsapi_key = os.getenv("VOXLI_API_KEY")base_url = os.getenv("VOXLI_API_URL", "https://api.voxli.io")def text(role, content, **fields):return {"role": role, "parts": [{"type": "text", "content": content}], **fields}# One chat, with the tool call the agent made and what the tool returned.messages = [text("user", "Where is my order NS-1042?", timestamp="2026-11-04T09:12:00Z"),{"role": "assistant","parts": [{"type": "tool_call","id": "call_1","name": "check_order","arguments": {"order": "NS-1042"},}],},{"role": "tool","parts": [{"type": "tool_call_response","id": "call_1","response": {"status": "shipped", "eta": "Friday"},}],},text("assistant","NS-1042 has shipped and should arrive on Friday.",timestamp="2026-11-04T09:12:03Z",),]response = requests.post(f"{base_url}/conversations/",headers={"Authorization": f"Bearer {api_key}"},json={"externalId": "chat-8f2c1a", # your agent's own id for the chat"metadata": {"channel": "web"},"messages": messages,},timeout=30,)response.raise_for_status()conversation = response.json()print(conversation["id"], "imported" if response.status_code == 201 else "replaced")
externalId is your agent’s own id for the chat. Send the same id again to replace the chat’s messages, for example once the chat has ended.
Traces
Voxli links a conversation to the chat’s traces when they carry the same id as externalId: traces your agent sends to Voxli with an OpenTelemetry exporter, and traces Voxli reads from a connected Langfuse or LangSmith about a minute after each import. Send Traces covers both.
The transcript you import stays as you sent it. The traces show what your agent did in each turn: its model calls, tool calls and their results, retrievals and guardrails.
Request fields
- externalId (required) - your agent’s id for the chat, up to 256 characters.
- messages (required) - the chat’s messages in order, from 1 to 2,000.
- startedAt (optional) - when the chat started, in UTC. Without it, Voxli uses the first message’s
timestamp, or the time of the import. - metadata (optional) - any JSON you want to keep with the chat, such as its channel. If you import the chat again without it, the earlier metadata stays.
Message fields
- role (required) -
userfor the person chatting,assistantfor your agent,toolfor what a tool returned. Leave outsystemmessages: they aren’t part of what the user saw. - parts (required) - the message’s parts in order, from 1 to 100.
- timestamp (optional) - when the message was sent, in UTC. This field isn’t part of OpenTelemetry’s format.
Other fields, such as a participant’s name, are accepted and not kept. Each message can be up to 100 KB.
Parts
Each part has a type:
- text -
content, the text. Onuserandassistantmessages. Ausermessage needs some text. - tool_call - a tool your agent called, on an
assistantmessage:id,name, andargumentsas any JSON. Anassistantmessage can mix text and tool calls in the order they happened. - tool_call_response - what the tool returned, on a
toolmessage: the call’sid, andresponseas any JSON. The call must come earlier in the chat.
Other part types, such as images or files, aren’t supported.
Response
Voxli answers 201 when the chat is new and 200 when it replaced a chat imported before with the same externalId. The body has the conversation’s id in Voxli.
A request that isn’t a complete chat gets 422 and imports nothing. That covers a user message without text, a part on a role that can’t have it, a response whose call isn’t in the chat, and a top-level field the API doesn’t know.
What’s next
- Send Traces - show what your agent did in each turn.
- Import a conversation - every field of this endpoint in the API reference.
- Tools, Events, and Actions - how your agent reports tool calls during a test.