HTTP API
Every dashboard endpoint — the three-step upload protocol, session and audio reads, STT evaluation and pricing — with status codes and validation rules.
The dashboard exposes a small HTTP API. The SDKs use the upload endpoints; the console uses the rest.
There is no authentication. The authorization header is accepted and
ignored; any non-empty API key works. Every endpoint, including audio download,
is open to anyone who can reach the service. Bind it to localhost or put it
behind an authenticating proxy.
Upload protocol
Three steps, in order.
POST /v1/sessions
Creates a session from a manifest.
Headers
| Header | Required | Notes |
|---|---|---|
content-type: application/json | yes | |
idempotency-key | no | When present, must equal session_id or the request is a 400 |
authorization: Bearer <key> | no | Accepted, not validated |
Response 201
{ "session_id": "9f2c…", "upload_urls": { "events.jsonl": "/v1/uploads/9f2c…/events.jsonl" } }Re-posting the same manifest is idempotent.
PUT /v1/uploads/{session_id}/{object_name}
Uploads one object. No authorization header — the returned URL is the credential.
| Constraint | Value |
|---|---|
Allowed object_name | events.jsonl, call.audio, caller.audio, agent.audio |
| Max body | 128 MiB |
| Success | 204 No Content |
| Unknown object name | 404 |
| Over the cap | 413 |
The body is streamed to a .part file and renamed on success, so a failed upload
never leaves a partially written object in place.
The 128 MiB cap is a hard limit, not a soft one. Raw stereo PCM at 16 kHz is roughly 3.8 MB per minute, so a call longer than about 35 minutes cannot be uploaded. The binding constraint arrives sooner at higher sample rates: at 48 kHz the limit is around 12 minutes.
POST /v1/sessions/{session_id}/complete
Marks the session complete and verifies every object.
{
"objects": {
"events.jsonl": { "byte_size": 48213, "sha256": "3f9a…" },
"call.audio": { "byte_size": 5898240, "sha256": "c14b…" }
}
}| Outcome | Status |
|---|---|
| Accepted | 202 |
| Byte size or digest mismatch | 400 |
| A declared object was never uploaded | 400 |
The session's status becomes:
| Status | Meaning |
|---|---|
ready | Objects verified and operations imported |
partial | Objects verified but no operations could be read |
Verification is why a truncated upload is a 400 rather than a call with
quietly missing turns. partial is what surfaces as unverifiable in the
dashboard.
Reading sessions
GET /v1/sessions
Lists sessions with status and manifest summary.
GET /v1/sessions/{session_id}
Full session detail: manifest, status, timestamps and imported operations.
Audio
GET /v1/sessions/{session_id}/audio/{track}
| Parameter | Values |
|---|---|
track | call | caller | agent | mixed |
preview | wav — wrap raw PCM in a WAV header for browser playback |
from_ms, to_ms | Cut a clip |
Honours HTTP Range. This is not optional politeness — Safari refuses to play
any media response that does not support ranges.
mixed is WAV-preview only. Audio is stored once as raw PCM; the WAV
wrapper is generated per request rather than stored as a second copy.
GET /v1/sessions/{session_id}/audio/{track}/peaks
Server-rendered waveform envelope, one peak per pixel column, so a long call does not ship megabytes of samples to the browser.
STT evaluation
GET /v1/sessions/{session_id}/stt-evaluation
Every measured production and challenger value for the call: streaming timing, per-turn transcripts, alignment, estimated WER, risk judgements and the cohort comparison.
POST /v1/sessions/{session_id}/challenger-evaluation
Queues a challenger run.
{ "model": "elevenlabs_scribe_v2" }| Outcome | Status |
|---|---|
| Queued | 202 |
Unsupported model | 422 |
Supported models come from CHALLENGER_MODELS, currently one entry: ElevenLabs
Scribe v2. Requires ELEVENLABS_API_KEY.
GET /v1/sessions/{session_id}/challenger-evaluation
Polls job status. Jobs execute off the request thread on a two-worker in-process pool, with state in SQLite.
Jobs that were queued or in_progress when the process stopped are marked
failed on startup and are not resumed. A half-finished run is never
presented as complete — re-queue it.
Pricing
GET /v1/pricing · PUT /v1/pricing
Read or override the cost model used for switch decisions.
Published costs are list prices with provenance (estimated_list_price vs
configured), not invoices. They exist to compare options, not to reconcile a
bill.
Pages and health
| Method | Path | Purpose |
|---|---|---|
GET | / | Call console |
GET | /stt-evaluation?session=<id> | STT evaluation workspace |
GET | /health | Health check |
Uploading manually
Useful for replaying an archived package:
SESSION=9f2c1e4a-3b7d-4c88-9a21-0e5f7b2d1c34
BASE=http://localhost:8000
curl -sS -X POST "$BASE/v1/sessions" \
-H 'content-type: application/json' \
-H "idempotency-key: $SESSION" \
--data-binary @manifest.json
for object in events.jsonl call.audio; do
curl -sS -X PUT "$BASE/v1/uploads/$SESSION/$object" --data-binary "@$object"
done
curl -sS -X POST "$BASE/v1/sessions/$SESSION/complete" \
-H 'content-type: application/json' \
-d "{\"objects\":{
\"events.jsonl\":{\"byte_size\":$(wc -c < events.jsonl),\"sha256\":\"$(shasum -a 256 events.jsonl | cut -d' ' -f1)\"},
\"call.audio\":{\"byte_size\":$(wc -c < call.audio),\"sha256\":\"$(shasum -a 256 call.audio | cut -d' ' -f1)\"}
}}"Next
Session package
The exact on-disk format both SDKs produce — manifest, event stream and audio — and the invariants that make it safe to consume.
Metrics glossary
Every number VaaniEval reports, the milestones it is derived from, and the exact conditions under which it is reported as unavailable instead.