# Calls API

Starting a call, listing call history, and fetching a call's transcript/recording. All endpoints require an [API key or dashboard token](/docs/authentication).

## Start a call

```
POST /agents/{agent_id}/calls
```

Body (optional):

```json
{ "conversation_id": "9e3c..." }
```

Omit `conversation_id` to start a fresh conversation; pass a previous call's `conversation_id` to continue it with full prior history as LLM context (multi-turn context carries across calls, not just within one).

Response `201`:

```json
{
  "call_id": "5b1a...",
  "conversation_id": "9e3c...",
  "ws_url": "/calls/5b1a.../talk",
  "expires_at": "2026-08-17T12:05:00Z"
}
```

`ws_url` is relative and single-use — connect the [talk WebSocket](/docs/talk-websocket) to it before `expires_at`. `402` if the account balance can't cover the call — no `call_id` is issued in that case.

## List calls for an agent

```
GET /agents/{agent_id}/calls
```

```json
[
  {
    "id": "5b1a...",
    "agent_id": "...",
    "conversation_id": "9e3c...",
    "status": "ended",
    "recording_status": "uploaded",
    "created_at": "2026-08-17T12:00:00Z",
    "duration_seconds": 47.2,
    "cost_usd": 0.031
  }
]
```

`cost_usd` is what was actually debited for the call — not an estimate.

## List all calls (across agents)

```
GET /calls
```

Same shape as above plus `agent_name`, across every agent on the account — the cross-agent call log the dashboard's Calls tab uses. Newest first.

## Get one call

```
GET /calls/{call_id}
```

Adds the full turn-by-turn `messages` array, a `metrics` block, `recording_expires_at`, and (once the recording is processed) a signed `recording_url` (valid ~1 hour, generate a fresh one by re-fetching):

```json
{
  "id": "5b1a...",
  "status": "ended",
  "duration_seconds": 47.2,
  "cost_usd": 0.031,
  "messages": [
    { "id": "...", "role": "user", "speaker": "caller", "content": "um what are your hours?", "timestamp": "...", "is_final": true },
    { "id": "...", "role": "assistant", "speaker": "agent", "content": "We're open...", "timestamp": "...", "is_final": true }
  ],
  "metrics": {
    "word_count": { "caller": 34, "agent": 88 },
    "turn_count": 6,
    "turns_by_speaker": { "caller": 3, "agent": 3 },
    "talk_ratio": { "caller": 0.279, "agent": 0.721 },
    "message_count": 6
  },
  "recording_url": "https://.../signed?token=...",
  "recording_expires_at": "2026-09-26T12:00:00Z"
}
```

### Transcript message schema (frozen)

The keys on each `messages[]` entry are stable and additive-only — existing keys never change type or disappear:

| key | notes |
|---|---|
| `id` | message id |
| `role` | `user` or `assistant` — unchanged, kept for back-compat |
| `speaker` | `caller` or `agent` — stable label derived from `role` |
| `content` | **verbatim** — filler words ("um", "uh", repeats) are preserved as spoken |
| `timestamp` | when the message was stored (same value the old `created_at` held) |
| `is_final` | always `true` — only finalised text is stored; partial hypotheses never are |

Wakili never rewrites stored transcript text. Pass `?cleanup=true` to apply a light readability pass (filler-word and duplicate removal, capitalisation) **to the response only** — the stored rows are untouched. Default is off.

`recording_status` is one of `recording` (call just ended, upload in progress), `uploaded`, `empty` (no audio captured), `failed`, `deleted` (removed via the API), or `expired` (past the retention window). `recording_url` is only present when `recording_status` is `uploaded`.

## Cost breakdown

```
GET /calls/{call_id}/cost
```

```json
{
  "call_id": "5b1a...",
  "stt_seconds": 512.4,
  "llm_prompt_tokens": 18422,
  "llm_completion_tokens": 3110,
  "tts_characters": 9004,
  "total_charged_usd": 0.62
}
```

Per-component consumption plus the single dollar amount actually debited for the call, so you don't have to reassemble cost per provider. `total_charged_usd` is the real ledger debit, not a re-derived estimate.

## Metrics only

```
GET /calls/{call_id}/metrics
```

Returns just the `metrics` block from `GET /calls/{call_id}` — `word_count`, `turn_count`, `turns_by_speaker`, `talk_ratio`, `message_count`.

## Recording

```
GET /calls/{call_id}/recording      # 302 redirect to a short-lived signed URL, 404 if none
DELETE /calls/{call_id}/recording   # deletes the audio object, 204 (recording_status → "deleted")
```

Recordings are retained for a fixed window (30 days by default), surfaced as `recording_expires_at` on the call detail. After that they're purged and `recording_status` becomes `expired`.

## Call status values

`pending` (created, WebSocket not yet connected) → `active` (WebSocket connected, in progress) → `ended` (WebSocket closed, either turn completed normally, idle timeout, or provider error — see [talk WebSocket errors](/docs/talk-websocket#errors-and-call-end)).

When a call reaches `ended`, Wakili fires a [`call.ended` webhook](/docs/webhooks-api) to any endpoint you've registered.
