- The live-call surface —
POST /v1/agents/{agentId}/conversations— spins up a LiveKit room and dispatches a runtime worker. This is the existing dispatch endpoint and is unchanged. - The history surface — list, detail, transcript, tags, rerun analysis — covered below.
POST /v1/internal/conversations/{id}/turns) records turns as the call proceeds. It is server-to-server only and is not part of the public surface.
Lifecycle
A conversation is created at dispatch — the row exists before the first user word reaches the runtime. While the call is live, the runtime POSTs turn events to the internal recorder, which upserts oneconversation_turn row per turnIndex (plus zero-to-many conversation_retrieval_source rows under it). Once the runtime emits the end-of-call event, the analysis pipeline runs against the now-frozen transcript and writes summary, callStatus, and the per-call metrics. From there a re-analysis can be queued by the caller via POST /v1/conversations/:id/analysis/rerun — it does not erase the existing summary, it overwrites it on completion.
Identifiers
Theid on a Conversation doubles as the LiveKit room name the call ran in — they’re allocated together at dispatch and never diverge. Treat it as opaque: it’s stable, unique within an organization, and safe to embed in URLs.
A conversation row is never deleted by the API. Tags can be removed; the conversation itself goes away only when its parent agent is deleted.
Authorization
All public conversation endpoints are organization-scoped. They read the requester’s active organization from the session and only operate on conversations whose parent agent belongs to it. Cross-tenant lookups return404 conversation_not_found — no enumeration distinction between “wrong org” and “doesn’t exist.” A session without an active organization returns 404 no_active_organization.
There is no per-conversation ACL beyond the organization gate: any session member of the owning org can read, tag, or rerun analysis on any conversation in the org. Per-member visibility constraints are tracked on the deployments spec, not on conversations.
Pagination
The list endpoint paginates with an opaque cursor, ordered(startedAt DESC, id DESC) to break ties deterministically:
limitdefaults to 20, max 100. Out-of-range values are clamped, not rejected.cursoris opaque — clients should not parse it. It encodesstartedAtplusidfor the last row of the previous page and is stable as long as that conversation isn’t deleted.- The response shape is
{ items: ConversationSummary[]; nextCursor: string | null; hasMore: boolean }.hasMoreis equivalent tonextCursor !== nulltoday but is surfaced explicitly so clients can branch on a boolean without parsing the cursor; an emptyitemsarray on a follow-up page is also possible if a filter removes every match between cursors. - Filters are URL params (
branch_id,date_from,date_to,status,direction,duration_min_sec,duration_max_sec,language,user_id,tag_ids) and can be combined freely with the cursor.tag_idsis AND across the list (a conversation must carry every listed tag id) and accepts the repeated form (?tag_ids=a&tag_ids=b) or a single comma-separated value (?tag_ids=a,b).summary_mode=includeembeds the post-callsummaryon each item — leave it off (the defaultexclude) when the table doesn’t need the text. Changing the filter set invalidates the cursor — clients should fetch the first page when filters change.
Departures from the ElevenLabs reference
This endpoint is intentionally “ElevenLabs-familiar” rather than ElevenLabs-compatible — we mirror their filter names and item shape where the choice is good and deviate where it isn’t. The deviations:
We also skip filters that require schema we haven’t shipped yet:
rating_min / rating_max / has_feedback_comment (no feedback schema), evaluation_params / data_collection_params / tool_names* / topic_ids / exclude_statuses / conversation_initiation_source (evaluation framework, data collection, tool-use tracking, and topic discovery are out-of-scope per the spec).
Internal recorder
- Auth is the
AGENT_RUNTIME_SHARED_SECRETenv var, presented asAuthorization: Bearer <secret>. The guard does a constant-time compare and returns401 invalid_service_tokenon mismatch. - Network exposure — the route lives at
/v1/internal/...and is not included in the public OpenAPI surface. Only the agent runtime (deployed alongside the API) should reach it. - Idempotency is on the
(conversationId, turnIndex)tuple — the table has a unique index on those columns and the service upserts. A worker retrying a batch will re-emit the sameturnIndexand the API will replace the row in place, not duplicate it. Retrieval sources for that turn are replaced atomically when the turn upserts. - Versioning — every event carries
"v": 1. Readers reject any version they don’t recognise; emitters must not mix versions in one request. - Batching —
eventsis an array, intentional. The runtime may stream one event at a time or batch the entire call’s turns at end-of-call. The whole batch is wrapped in a single Postgres transaction: if any event in the array is rejected (unknown version, mismatched conversation id, …), every prior turn upsert in the same request is rolled back too. Retry the whole batch — partial replay is not supported by this contract. - Final envelope — set
"final": true(with optionaldurationSeconds,callStatus,endedReason,summary,messageCount) to commit end-of-call metrics in the same transaction as the last turn batch. The cachedmessageCountis always recomputed fromCOUNT(*)overconversation_turn; the runtime-reported value is advisory.
204 No Content once the transaction commits. It does not trigger analysis — analysis runs on the end-of-call event the runtime fires separately.
Endpoint summary
Per-endpoint pages auto-generate from the OpenAPI spec:Related guides
- Conversations operate guide — what the dashboard renders and how teams use it.
- Branches — how conversations associate with branches and versions.