Skip to main content
apps/api/app/routers/calls.py, prefix /api/v1/calls. See Calls and call artifacts.

POST /calls/outbound

Bearer. 201. Places a call and registers a CallLog with status initiated.
from_number is an optional caller-ID override. custom_variables are merged over the agent’s defaults at call time and substituted into the prompts — see Agent configuration. Returns OutboundCallResponse: call_id, status, provider_call_sid, from_number, to_number, agent_id, custom_variables.

POST /calls/inbound

Bearer. 201. The runtime calls this from the answer webhook, using a bot JWT.
Returns InboundCallRegisterResponse: call_id, status, provider_call_sid, call_type, from_number, to_number, agent_id.

POST /calls/web

Bearer. 201. Registers a browser websocket session so it gets a CallLog and artifacts. The agent must be agent_category: websocket.
Returns WebCallRegisterResponse: call_id, status, call_type (always web), agent_id, custom_variables. Calling this is optional. If a browser connects to WS /agent/{org_id}/{agent_id} with no call_id, the runtime registers one itself. Pre-register only when you need the call_id before the session starts — then pass it as ?call_id= on the WebSocket URL. The runtime discards a call_id whose call_type is not web or whose agent_id does not match, and falls back to auto-creating one.

PATCH /calls/{call_id} and PATCH /calls/by-provider-sid/{provider_call_sid}

Bearer. Both take the same partial body and return the same CallLogResponse. The by-SID variant exists for provider hangup callbacks, which know the SID and not the call_id.
Every field is optional, but an empty body returns 400 No fields to update — only fields you actually set are applied. duration is derived, never sent. When a patch carries end_time_utc, the service computes the duration from start_time_utc and clamps it at zero; a patch without end_time_utc has any duration stripped. The minio:// URIs you write are rewritten in the response to the two proxy routes below.

GET /calls/{call_id}/recording and GET /calls/{call_id}/transcript

Bearer. Stream the artifact out of MinIO through the API, so clients never need MinIO credentials. 404 when the call or the object is missing, 400 when the stored URL is not a usable object key.

GET /calls/{call_id}/metrics and PUT /calls/{call_id}/metrics

Bearer (bot JWT supported). Pipeline metrics live in the separate CallMetrics collection — they are not returned by GET /calls/{call_id}. PUT body (CallMetricsBody):
The runtime writes metrics at call end via PUT. Writes are once per call — a second PUT for the same call_id is ignored and returns the existing document. GET returns 404 when no metrics exist yet. GET adds four fields to summary that are not in the stored document: avg_stt_secs, avg_tts_secs, avg_llm_secs, and avg_latency_secs, computed on the fly from latencies.breakdowns. Each stage average is null if that stage never appears in a breakdown; avg_latency_secs sums avg_llm_secs and avg_tts_secs (STT excluded), using whichever of those two are actually available. Turns with no user_turn_start_time (bot-initiated, no real user turn) are excluded from all four.

GET /calls/org/{org_id}

Bearer, must be a member of that organisation. Query parameters limit (default 50, 1500) and offset (default 0). Returns CallLogListResponse: {calls: [...], limit, offset, total}.

GET /calls/org/{org_id}/analytics

Bearer, must be a member of that organisation — same check as the list route above, so a non-member gets 403 and an unknown organisation 404. No query parameters: the figures are all-time over that organisation’s call logs. Returns CallAnalyticsResponse:
Four things are not obvious from the field names:
  • “Connected” means call_response == "answered", nothing else. total_duration_seconds and average_duration_seconds are computed over connected calls only, not over all attempts — so the average is per answered call.
  • trend_vs_last_week_pct is null, not 0, when the previous week had no attempts. It is the difference between two connection rates in percentage points (this week minus last, rounded to 1 dp), not a change in call volume.
  • agent_performance is capped at the ten busiest agents. It is a leaderboard, not a complete per-agent breakdown, and each row’s agent_id and agent_name are whatever the call logs recorded at the time.
  • model_usage ranks each stage (stt/tts/llm) by call volume, not by agent count. Each call is joined to its agent’s current model config, grouped by (model, provider), and only the single busiest model per stage is returned — not a full ranking. A stage is null if no call could be matched to a model (e.g. the agent was deleted). A model that used to be popular but was reconfigured away shows under its old name for calls made before the change and its new name for calls after, since the join uses the agent’s config as it is now, not as it was at call time.

GET /calls/{call_id}

Bearer. One CallLogResponse, active organisation only. Bot JWTs work here.