The Convai analytics data model. Read this before writing custom queries — it is the shared vocabulary between the SDK, the REST API, and the recipes.
Account your billing/ownership boundary; resolved from your API key
└── App (App Key) an integration boundary — one per game/app/project
└── Character an AI character configuration
└── Session one continuous user↔character interaction
└── Interaction (Trace) one logical pipeline invocation (a turn)
└── Component span a timed segment within a trace (ASR, LLM, TTS, ...)
You can filter most queries by any level: accountId (implicit, from API key), appKey, characterId, sessionId, interactionId, plus optional dimensions like experienceId and endUserId.
Your Convai account. It is resolved server-side from the API key — you never pass account override fields explicitly. Every query is forcibly scoped to your account before any data is read.
The application-level integration boundary. One Convai account can have many app keys (e.g. one per game, or one per environment). The SDK accepts appKey as a filter on most endpoints.
The AI character configuration — system prompt, voice, knowledge bank attachments, etc. Identified by characterId. A single session is always with one character.
A deployed integration — a game, world, or pixel-streamed/no-code experience. Identified by experienceId. Useful for product analytics ("how many minutes was experience X used this week?").
A continuous user↔character interaction. Bounded by connect/disconnect events. Identified by sessionId. Contains:
- start/end timestamps
- the character involved
- a list of interactions (turns)
- session-lifecycle events: connect, disconnect, reconnect, interruption, terminal
GET /v1/analytics/sessions/{id} returns the full per-session timeline of every metric emitted during the session.
A single logical AI-processing unit. Not just a user voice turn — any meaningful pipeline invocation:
- user voice turn (ASR → LLM → TTS → Neurosync)
- user text turn (LLM → TTS → Neurosync)
- dynamic context update that triggers an LLM call
- forced response
- TTS-only generation
- Knowledge Bank call
- Memory call
- terminal event
Identified by interactionId (sometimes called traceId — same thing). GET /v1/analytics/interactions/{id} returns the full component waterfall + provider/model attribution + status + error codes for one interaction.
A timed segment within an interaction. Each component span has:
processor—asr,llm,tts,neurosync,transport,knowledge_bank,memory,vad,stt, ...start,end,durationMsprovider/model(where applicable)status—ok,error,timeout,cancelled- attributes (provider error code, prompt fingerprint, audio bytes, etc.)
Component spans are what recipes/charts/latency_waterfall.ts renders.
The atomic unit of queryable telemetry. Each metric is identified by:
metric_name— e.g.voice.user_to_bot_latency,neurosync.turn_summary,llm.ttfb,tts.text_to_first_audio,stt.transcript_aggregation,vad.speech_confirmation_delaymetric_type— coarse categorization:TTFBMetricsData,ProcessingMetricsData,NeuroSyncMetricsData,CustomLatencyMetricsData,UserBotLatencyMetricsData,LLMUsageMetricsData,TTSUsageMetricsData,SmartTurnMetricsDataevent_time— timestamp- account, character, session, and interaction identifiers
value— numeric measurement (or null for count-only metrics)tags— structured metadata such asprovider,voice_provider, and metric-specific attributes (p50/p95/p99/max for*.turn_summarymetrics, etc.)
To discover what's queryable for your account: client.catalog() returns the metric names + types + units + descriptions visible at your plan tier.
See metrics-reference.md for the full catalog.
Most endpoints accept either:
- a relative range string:
last_15m,last_1h,last_24h,last_7d,last_30d - explicit
startTime/endTime(ISO 8601, UTC)
Defaults vary by endpoint — see each SDK function's docstring. The default is last_24h for most queries.
Where percentiles apply, all of p50, p75, p90, p95, p99 are available; defaults are [p50, p95, p99]. Other aggregations: count, sum, avg, min, max, unique.
SDK and MCP calls use camelCase names. Direct REST calls use snake_case query keys and canonical snake_case values. For example, SDK code uses client.breakdown({ groupBy: "characterId", measure: "uniqueSessions" }); the equivalent REST query is GET /v1/analytics/breakdown?group_by=character_id&measure=unique_sessions.
client.breakdown(...) accepts groupBy for any of:
processor— ASR / LLM / TTS / Neurosync / etc.provider/voiceProvider— OpenAI, Anthropic, ElevenLabs, ...model— gpt-4o, claude-sonnet-4-7, ...characterIdsessionIdinteractionTypestatusmetricName
Raw REST callers should use processor, provider, voice_provider, model, character_id, metric_name, metric_type, app_key, experience_id, or status as group_by values.
Status breakdowns are forward-compatible with status-tagged telemetry. If group_by=status returns only an empty group or no rows, treat that as unavailable status data rather than synthesizing ok/error/timeout/cancelled values.
The REST surface deliberately exposes a small set of composable endpoints rather than one endpoint per question. The SDK wraps the common compositions as named convenience methods (client.errors.summary, client.providers.compare, client.usage.summary, client.latency.byComponent) so agents get discoverability without the REST surface ballooning. Both layers refer to the same underlying data; the convenience methods are pure delegation. See the TypeScript SDK source for what each delegates to.