/docs /read-api

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.

last updated 2026-11-04 view as markdown

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>
ParameterMeaning
conversation_idThe session's id: the gen_ai.conversation.id your spans carry.
attribute + valueAny attribute on a turn's root span, such as request_id (one turn) or user.id (many sessions). Returns the most recent matching session.
daysWith attribute: how far back to search, 1 to 90 days. Default 30.
offsetPage 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
}
  • test is 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 is null for live traffic.
  • Attribute lookups also return matched_trace_ids (the turns of this session carrying the attribute) and other_matches (older matching sessions, up to 10).
  • steps[].args and steps[].result are JSON text, cut at 4,000 characters.
  • session_evals[].turn is the index of the turn the verdict points at.
  • markers are named span events your agent sent (milestones); errors are exception events.
  • 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

StatusWhen
400Neither or both of conversation_id and attribute, or attribute without value.
401Missing, revoked or expired key, or a key that isn't a read key.
404No session with that id, or none matching the attribute within days.
504The attribute lookup took too long. Retry with a smaller days.