Read API.
Read one session out of TwoTail as the chat the user had: each turn's message, the agent's reply, its tool calls, what the user was shown, milestones, errors and eval verdicts. Written for coding agents debugging the agent that sent the traces. Sending traces instead? See the ingest reference.
Key
Create a Read key in the app under API keys. Read keys start with tt_read_. They can only read, and only their own account. An ingest key (tt_live_) is refused here, and a read key is refused for sending traces. Keep the key in an environment variable such as TWOTAIL_READ_KEY; never commit it.
Endpoint
GET https://www.twotail.ai/api/v1/session X-API-Key: <TWOTAIL_READ_KEY>
| Parameter | Meaning |
|---|---|
conversation_id | The session's id: the gen_ai.conversation.id your spans carry. |
attribute + value | Any attribute on a turn's root span, such as request_id (one turn) or user.id (many sessions). Returns the most recent matching session. |
days | With attribute: how far back to search, 1 to 90 days. Default 30. |
offset | Page back through a long session (see Paging). |
Give either conversation_id or attribute and value, not both. URL-encode the values.
curl -s -H "X-API-Key: $TWOTAIL_READ_KEY" \ "https://www.twotail.ai/api/v1/session?attribute=request_id&value=req_8f2a91"
Response
{
"conversation_id": "conv_7d1e4b",
"started_at": "2026-10-26T09:12:40+00:00",
"test": {"run_id": "2026-11-03-nightly-a1b2c3", "case_id": "books-cheaper-option",
"level": null, "agent_version": "a1b2c3d"},
"session_evals": [
{"name": "user_frustration", "passed": false, "reason": "...", "turn": 4, "evidence": true}
],
"turns": [
{
"index": 3,
"trace_id": "0af7651916cd43dd8448eb211c80319c",
"start": "2026-10-26T09:15:02+00:00",
"user": "Book the cheaper one",
"reply": "Done: the 09:10 option is in your basket...",
"steps": [{"label": "search_options", "args": "{...}", "result": "{...}"}],
"shown": [{"noun": "options", "cards": [{"title": "Option B", "price": "84.00 EUR"}], "chosen": null}],
"facts": {"plan": "pro"},
"markers": [{"name": "acme.option_chosen", "label": "Option chosen"}],
"errors": [{"type": "AI_APICallError", "message": "..."}],
"evals": [{"name": "price_checked", "passed": false, "reason": "..."}],
"cost": 0.0123, "tokens": 18234, "duration_s": 6.2, "error": false
}
],
"total_turns": 7,
"returned_turns": 7,
"first_turn_index": 0,
"offset": 0,
"next_offset": null
}
testis the session's test run attributes (test.run_id,test.case_id,test.level,agent.version), so a session can be checked against its case. It isnullfor live traffic.- Attribute lookups also return
matched_trace_ids(the turns of this session carrying the attribute) andother_matches(older matching sessions, up to 10). steps[].argsandsteps[].resultare JSON text, cut at 4,000 characters.session_evals[].turnis theindexof the turn the verdict points at.markersare named span events your agent sent (milestones);errorsareexceptionevents.- The fields above are a contract under
/v1: fields may be added, none are renamed or removed without a new version.
Paging
A long session is cut to about 100 KB of turns (around 25k tokens), newest first, so one response fits in an agent's context. A turn larger than that still comes back on its own. total_turns is the whole session and first_turn_index the oldest turn returned. When next_offset is not null, call again with offset=<next_offset> for the turns before. An attribute lookup's page ends at the matched turn.
Errors
| Status | When |
|---|---|
400 | Neither or both of conversation_id and attribute, or attribute without value. |
401 | Missing, revoked or expired key, or a key that isn't a read key. |
404 | No session with that id, or none matching the attribute within days. |
504 | The attribute lookup took too long. Retry with a smaller days. |