> ## Documentation Index
> Fetch the complete documentation index at: https://docs.thunderphone.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Calls

> List, inspect, grade, export, and report issues on calls.

Every inbound call to your phone numbers, every call triggered from
[POST /v1/call](/api-reference/outbound-calls), every browser mic
session, and every widget call produces a **call log** record. These
endpoints let you enumerate and inspect them, fetch recordings and
transcripts, record AI quality grades, file issue reports, and export
history.

## Endpoints

| Method | Path                                      | Description                                                                                                    |
| ------ | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `GET`  | `/v1/calls`                               | List calls (paginated, filterable)                                                                             |
| `GET`  | `/v1/calls/export`                        | Bulk export up to 1,000 calls as CSV or JSON                                                                   |
| `GET`  | `/v1/calls/{call_id}`                     | Retrieve a single call                                                                                         |
| `GET`  | `/v1/calls/{call_id}/audio`               | Get a time-limited recording URL                                                                               |
| `GET`  | `/v1/calls/{call_id}/transcript`          | Fetch the transcript                                                                                           |
| `GET`  | `/v1/calls/{call_id}/history`             | Fetch the full structured turn history                                                                         |
| `GET`  | `/v1/calls/{call_id}/grade`               | Fetch the latest AI quality grade                                                                              |
| `POST` | `/v1/calls/{call_id}/grade`               | Run (or re-run) AI grading                                                                                     |
| `GET`  | `/v1/calls/{call_id}/issue-reports`       | List issue reports for a call                                                                                  |
| `POST` | `/v1/calls/{call_id}/issue-reports`       | File an issue report                                                                                           |
| `POST` | `/v1/calls/{call_id}/listen`              | Mint a listen-only token for a live call                                                                       |
| `POST` | `/v1/calls/{call_id}/whisper`             | Send private text guidance to the agent on a live call                                                         |
| `POST` | `/v1/calls/{call_id}/promote-to-scenario` | Turn a real call into a regression [test scenario](/api-reference/test-scenarios#promote-a-call-to-a-scenario) |

## Call object

```json theme={null}
{
  "call_id": 987654321,
  "agent_id": 12,
  "agent_variant_id": null,
  "agent_variant_label": null,
  "direction": "inbound",
  "from_number": "+14155550199",
  "to_number": "+15551234567",
  "start_time": "2026-04-20T18:24:10.113Z",
  "end_time": "2026-04-20T18:25:04.822Z",
  "duration_seconds": 54,
  "status": "completed",
  "end_reason": "caller_hangup",
  "recording_url": null,
  "recording_started_at": "2026-04-20T18:24:12.501Z",
  "is_simulation": false,
  "is_test_call": false,
  "is_scenario_call": false,
  "prompt_snapshot": "You are a helpful support agent for Acme Ops…",
  "tool_snapshot": [],
  "cloud_run_execution_name": null,
  "livekit_room_name": "call-987654321",
  "livekit_room_sid": "RM_xxxxxxxxxxxx",
  "product": "spark",
  "voice": "john",
  "billable_minutes": 1.25,
  "billing_total_cents": 3,
  "billed_at": "2026-04-20T18:25:05.101Z",
  "grade_score": 92,
  "grade_status": "completed",
  "grade_updated_at": "2026-04-20T18:25:11.002Z",
  "call_outcome": "success",
  "issue_count": 0,
  "critical_issue_count": 0
}
```

| Field                                   | Type              | Description                                                                                                                                                    |
| --------------------------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `call_id`                               | integer           | Server-assigned call id (also the key in every other endpoint on this page)                                                                                    |
| `agent_id`                              | integer \| null   | Agent that handled the call                                                                                                                                    |
| `agent_variant_id`                      | integer \| null   | [Split-testing](/api-reference/split-testing) variant selected for this call; `null` when no split test ran                                                    |
| `agent_variant_label`                   | string \| null    | Label of that variant, snapshotted at call time (survives variant deletion)                                                                                    |
| `direction`                             | string            | `inbound`, `outbound`, `test` (test call in the agent builder), or `web` (web call from the embeddable widget)                                                 |
| `from_number`                           | string            | E.164 caller number. Test calls (`direction: "test"`) use an internal identifier                                                                               |
| `to_number`                             | string            | E.164 destination number                                                                                                                                       |
| `start_time`, `end_time`                | timestamp \| null | ISO 8601 UTC. `end_time` is null while `status="in_progress"`                                                                                                  |
| `duration_seconds`                      | integer \| null   | Derived                                                                                                                                                        |
| `status`                                | string            | `in_progress`, `completed`, or `failed`                                                                                                                        |
| `end_reason`                            | string \| null    | Free-form reason e.g. `caller_hangup`, `agent_end`, `timeout`, `ai_transfer`                                                                                   |
| `recording_url`                         | string \| null    | Storage URL when set — not guaranteed playable. Use `/audio` for a signed, playable URL                                                                        |
| `recording_started_at`                  | timestamp \| null | When the recorder actually began capturing (1–3 s after `start_time`). Transcript `start_ms`/`end_ms` are anchored to this when present; `null` on older calls |
| `is_simulation`                         | boolean           | True if the call was produced by a [simulation](/api-reference/test-calls) run                                                                                 |
| `is_test_call`                          | boolean           | Deprecated alias for `is_simulation`; retained during the transition                                                                                           |
| `is_scenario_call`                      | boolean           | True only for runs launched from a [scenario batch](/api-reference/test-scenarios) — ad-hoc simulations stay `false`                                           |
| `prompt_snapshot`                       | string \| null    | The user-authored agent prompt in effect at call time                                                                                                          |
| `tool_snapshot`                         | array \| null     | The tool definitions the agent had at call time                                                                                                                |
| `cloud_run_execution_name`              | string \| null    | **Internal identifier** for the worker that ran the call. Useful only when corresponding with support; may be `null`                                           |
| `livekit_room_name`, `livekit_room_sid` | string \| null    | **Internal identifiers** for the underlying media room. Do not build on these; they exist for supervision/support tooling                                      |
| `product`, `voice`                      | string \| null    | The [Agent](/api-reference/agents) config in effect at call time                                                                                               |
| `billable_minutes`                      | number \| null    | Minutes billed, rounded to the nearest quarter minute (15-second increments, minimum 0.25). Straight-to-voicemail calls bill a flat 1¢ instead.                |
| `billing_total_cents`                   | integer \| null   | Final bill in USD cents                                                                                                                                        |
| `billed_at`                             | timestamp \| null | When billing finalised                                                                                                                                         |
| `grade_score`                           | integer \| null   | 0–100 quality score from the latest AI grading                                                                                                                 |
| `grade_status`                          | string \| null    | `pending`, `completed`, or `failed`                                                                                                                            |
| `grade_updated_at`                      | timestamp \| null | Last grading run                                                                                                                                               |
| `call_outcome`                          | string \| null    | From the latest grade: `success`, `failure`, `unknown`, or `no_conversation`                                                                                   |
| `issue_count`                           | integer           | Total issue reports filed against this call                                                                                                                    |
| `critical_issue_count`                  | integer           | Subset with severity `critical`                                                                                                                                |

***

## List calls

<CodeGroup>
  ```bash cURL theme={null}
  curl 'https://api.thunderphone.com/v1/calls?limit=50' \
    -H "Authorization: Bearer sk_live_YOUR_API_KEY"
  ```

  ```python Python theme={null}
  page = requests.get(
      "https://api.thunderphone.com/v1/calls",
      headers={"Authorization": "Bearer sk_live_YOUR_API_KEY"},
      params={"limit": 50, "direction": "inbound"},
  ).json()
  for call in page["results"]:
      ...
  ```

  ```javascript Node.js theme={null}
  const url = new URL("https://api.thunderphone.com/v1/calls");
  url.searchParams.set("limit", "50");
  const page = await fetch(url, {
    headers: { Authorization: `Bearer ${process.env.THUNDERPHONE_API_KEY}` },
  }).then((r) => r.json());
  ```
</CodeGroup>

### Query parameters

| Param          | Type     | Default | Description                                                                                                                                                                                                                                            |
| -------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `limit`        | integer  | 50      | Page size (1–200)                                                                                                                                                                                                                                      |
| `offset`       | integer  | 0       |                                                                                                                                                                                                                                                        |
| `agent_id`     | integer  |         | Filter by agent                                                                                                                                                                                                                                        |
| `direction`    | string   |         | `inbound`, `outbound`, `test` (test call in the agent builder), `web` (web call from the embeddable widget). Legacy `mic` and `widget` values remain accepted as filters during migration. Historical webhook data may still contain those old values. |
| `status`       | string   |         | `in_progress`, `completed`, `failed`                                                                                                                                                                                                                   |
| `phone_number` | string   |         | Matches either side of the call (`from_number` or `to_number`); formatted numbers are normalized before matching                                                                                                                                       |
| `from_number`  | string   |         | E.164                                                                                                                                                                                                                                                  |
| `to_number`    | string   |         | E.164                                                                                                                                                                                                                                                  |
| `start_date`   | ISO 8601 |         | Earliest `start_time`                                                                                                                                                                                                                                  |
| `end_date`     | ISO 8601 |         | Latest `start_time`                                                                                                                                                                                                                                    |

### Paginated response

```json theme={null}
{
  "results": [ /* Call objects */ ],
  "total": 1234,
  "limit": 50,
  "offset": 0
}
```

Use `offset` + `limit` to page forward; check `total` to know when
you've reached the end.

***

## Export calls

Bulk-download up to 1,000 of the most recent calls as either CSV
(metadata only) or JSON (includes transcripts). The export honors the
same filter parameters as [List calls](#list-calls), so what you export
matches what you filtered.

<CodeGroup>
  ```bash CSV theme={null}
  curl 'https://api.thunderphone.com/v1/calls/export?export_format=csv' \
    -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
    -o calls.csv
  ```

  ```bash JSON (filtered) theme={null}
  curl 'https://api.thunderphone.com/v1/calls/export?export_format=json&status=completed&start_date=2026-07-01T00:00:00Z' \
    -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
    -o calls.json
  ```
</CodeGroup>

### Query parameters

| Param           | Required | Description                                                                                                      |
| --------------- | -------- | ---------------------------------------------------------------------------------------------------------------- |
| `export_format` | yes      | `csv` or `json`                                                                                                  |
| `agent_id`      |          | Filter by agent                                                                                                  |
| `direction`     |          | Same values as List calls (`inbound`, `outbound`, `test`, `web`; legacy `mic`/`widget` accepted)                 |
| `status`        |          | `in_progress`, `completed`, `failed`                                                                             |
| `phone_number`  |          | Matches either side of the call (`from_number` or `to_number`); formatted numbers are normalized before matching |
| `from_number`   |          | E.164                                                                                                            |
| `to_number`     |          | E.164                                                                                                            |
| `start_date`    |          | ISO 8601 — earliest `start_time`                                                                                 |
| `end_date`      |          | ISO 8601 — latest `start_time`                                                                                   |

`limit`/`offset` are not accepted — the export always returns the most
recent matches up to its fixed cap.

### Response

The response includes a `Content-Disposition: attachment` header with a
dated filename (`calls-export-YYYY-MM-DD.csv`).

Exports cap at the 1,000 most recent calls **matching the filters**.
When more calls match than the cap allows, the response carries
truncation signals so you can detect an incomplete export:

| Signal          | Where                 | Meaning                                                                |
| --------------- | --------------------- | ---------------------------------------------------------------------- |
| `X-Truncated`   | header (CSV and JSON) | `true` when the filtered match count exceeds the row cap, else `false` |
| `X-Total-Count` | header (CSV and JSON) | Untruncated count of calls matching the filters                        |
| `truncated`     | JSON body field       | Same signal as `X-Truncated`, for JSON consumers                       |

For a truncated window, narrow the date range (`start_date`/`end_date`)
and export in slices, or use the list endpoint with offset pagination.

***

## Retrieve a call

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.thunderphone.com/v1/calls/987654321 \
    -H "Authorization: Bearer sk_live_YOUR_API_KEY"
  ```
</CodeGroup>

Returns `200 OK` with a [Call object](#call-object), or `404` if the
call does not belong to your organization.

***

## Get recording audio

Returns a short-lived signed URL to the MP3 recording on Google Cloud
Storage. Stream the URL directly to your audio player — do not store
it (it expires).

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.thunderphone.com/v1/calls/987654321/audio \
    -H "Authorization: Bearer sk_live_YOUR_API_KEY"
  ```
</CodeGroup>

```json Response theme={null}
{ "url": "https://storage.googleapis.com/thunderphone-recordings/..." }
```

| Status | Condition                      |
| ------ | ------------------------------ |
| `200`  | Recording exists, URL returned |
| `404`  | Call or recording not found    |

***

## Get caller span audio

`GET /v1/calls/{call_id}/span-audio?ref={ref}`

Streams the caller's actual speech for one turn of the conversation. `ref`
comes from a transcript entry's `audio_span_refs` — user turns carry them
when span audio was captured for the call. The dashboard uses this for the
play-on-hover control on caller turns.

| Status | Condition                      |
| ------ | ------------------------------ |
| `200`  | Audio stream returned          |
| `404`  | Call, span, or audio not found |

***

## Get transcript

Simplified, user-facing transcript (role-tagged speech only; no system
events). For the full structured history including function calls,
interruptions, and latency metrics use [`/history`](#get-history).

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.thunderphone.com/v1/calls/987654321/transcript \
    -H "Authorization: Bearer sk_live_YOUR_API_KEY"
  ```
</CodeGroup>

```json Response theme={null}
{
  "call_id": 987654321,
  "transcripts": [
    {
      "role": "user",
      "content": "Hi, I have a question about my policy.",
      "start_ms": 1200,
      "end_ms": 3400
    },
    {
      "role": "agent",
      "content": "Of course — could I have your member id?",
      "start_ms": 3800,
      "end_ms": 6100,
      "ttfa_ms": 412
    }
  ]
}
```

| Field                | Type    | Description                                                                                                                          |
| -------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `role`               | string  | `user`, `agent`, `tool_call`, `tool_response`, or `system`                                                                           |
| `content`            | string  | Human-readable content. For `tool_call`/`tool_response` turns this is a JSON string                                                  |
| `start_ms`, `end_ms` | integer | Offset in milliseconds, anchored to the recording when `recording_started_at` is set. Omitted when timing could not be reconstructed |
| `ttfa_ms`            | integer | Time-to-first-audio for the first agent turn of a response. Present on agent turns only                                              |

| Query param | Description                                                                                                               |
| ----------- | ------------------------------------------------------------------------------------------------------------------------- |
| `live=true` | Use a lenient serializer that includes not-yet-resolved turns — for real-time display while the call is still in progress |

***

## Get history

Full structured turn history. Use this when you need raw function-call
arguments, interruption markers, ack-prompt annotations, or first-byte
latencies.

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.thunderphone.com/v1/calls/987654321/history \
    -H "Authorization: Bearer sk_live_YOUR_API_KEY"
  ```
</CodeGroup>

Returns an array of typed entries. See the ThunderPhone [call-history
schema reference](https://docs.thunderphone.com/api-reference/calls#get-history)
for the full shape — the top-level entries include `span`, `completion`,
`patch`, `function_call`, `function_response`, `interrupt`, and
`acknowledgement`.

***

## AI call grading

### Get latest grade

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.thunderphone.com/v1/calls/987654321/grade \
    -H "Authorization: Bearer sk_live_YOUR_API_KEY"
  ```
</CodeGroup>

```json Response theme={null}
{
  "id": 88,
  "call_id": 987654321,
  "status": "completed",
  "score": 92,
  "call_outcome": "success",
  "outcome_reasoning": "The caller's policy question was answered fully and confirmed.",
  "summary": "Caller asked about their policy and got a full answer…",
  "rubric_breakdown": { "accuracy": 95, "tone": 90 },
  "detected_issues": [],
  "grader_model": "heuristic-v1",
  "grader_version": "v1",
  "error_message": null,
  "graded_at": "2026-04-20T18:25:11.002Z",
  "created_at": "2026-04-20T18:25:11.002Z",
  "updated_at": "2026-04-20T18:25:11.002Z"
}
```

| Field                            | Type              | Description                                                                                                       |
| -------------------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------- |
| `id`                             | integer           | Grade row id                                                                                                      |
| `call_id`                        | integer           |                                                                                                                   |
| `status`                         | string            | `pending`, `completed`, or `failed`                                                                               |
| `score`                          | integer \| null   | 0–100                                                                                                             |
| `call_outcome`                   | string            | `success`, `failure`, `unknown`, or `no_conversation`                                                             |
| `outcome_reasoning`              | string            | Why the grader chose that outcome                                                                                 |
| `summary`                        | string            | One-paragraph call summary                                                                                        |
| `rubric_breakdown`               | object            | Per-criterion scores                                                                                              |
| `detected_issues`                | array             | Machine-detected issues; completed grades sync these into [issue reports](#issue-reports) with `source: "system"` |
| `grader_model`, `grader_version` | string            | Which grader produced the row                                                                                     |
| `error_message`                  | string \| null    | Set when `status="failed"`                                                                                        |
| `graded_at`                      | timestamp \| null | When grading completed                                                                                            |
| `created_at`, `updated_at`       | timestamp         |                                                                                                                   |

Returns `200 OK` with the latest grade row, or `404` if the call has
not been graded.

### Run grading

Grade the call. Grading runs synchronously within the request. If a
completed grade already exists it is returned as-is unless you pass
`force=true`.

Any request that would invoke the grader — a forced regrade, or a request
whose latest grade is missing, pending, or failed — must first claim a
per-call cooldown. The standard cooldown is 15 minutes. If the latest grade
is a retryable failure, the cooldown is 90 seconds so a retry can recover
sooner. At most one new grading run can be claimed per window.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST 'https://api.thunderphone.com/v1/calls/987654321/grade?force=true' \
    -H "Authorization: Bearer sk_live_YOUR_API_KEY"
  ```
</CodeGroup>

A request inside the active cooldown returns `429 Too Many Requests`. The
`Retry-After` response header and `retryAfterSeconds` body field contain the
same number of seconds to wait:

```json theme={null}
{
  "detail": "This call was recently submitted for grading. Please retry later.",
  "retryAfterSeconds": 90
}
```

| Status | Condition                                                                                         |
| ------ | ------------------------------------------------------------------------------------------------- |
| `200`  | An existing completed grade was reused (no `force`)                                               |
| `201`  | A new grade completed                                                                             |
| `429`  | The per-call cooldown is active; use `Retry-After` or `retryAfterSeconds` before retrying         |
| `502`  | Grading failed — the returned grade row has `status="failed"` and an `error_message`; retry later |

***

## Issue reports

Issue reports let your team (or automated grading) flag specific calls
for review. Reports have a severity and optional title/description and
appear in the org-wide [Issue Reports feed](/api-reference/issue-reports).

### List reports for a call

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.thunderphone.com/v1/calls/987654321/issue-reports \
    -H "Authorization: Bearer sk_live_YOUR_API_KEY"
  ```
</CodeGroup>

Returns an array of Issue Report objects — see [Issue Reports](/api-reference/issue-reports#issue-report-object).

### File a report

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.thunderphone.com/v1/calls/987654321/issue-reports \
    -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "title": "Agent paused too long",
      "description": "Five second silence before responding to the main question.",
      "severity": "warning"
    }'
  ```
</CodeGroup>

| Field                | Type                | Required | Description                                                          |
| -------------------- | ------------------- | -------- | -------------------------------------------------------------------- |
| `title`              | string              | yes      | 1–255 chars                                                          |
| `description`        | string              | no       | Free-form. Defaults to `""`                                          |
| `severity`           | string              | no       | `info`, `warning`, or `critical`. Defaults to `warning`              |
| `source`             | string              | no       | `user` (default) or `system`                                         |
| `timestamp_seconds`  | integer ≥ 0 \| null | no       | Offset into the call where the issue occurred                        |
| `detected_issue_ref` | string              | no       | Up to 128 chars — correlates the report with a grader-detected issue |
| `metadata`           | object              | no       | Arbitrary JSON. Defaults to `{}`                                     |

Returns `201 Created` with the [Issue Report object](/api-reference/issue-reports#issue-report-object).
To update a report's status later, use the
[issue-reports endpoints](/api-reference/issue-reports#update-a-reports-status)
(reports are addressed by `trace_id`).

***

## Live call supervision

Both endpoints work only while `status="in_progress"`; once the call
ends they return `409 Conflict` with `{"detail": "Call is not live."}`.
The dashboard's **Live** page is built on the same primitives.

### Listen in

Mints a hidden, subscribe-only token for the call's media room. The
listener is invisible: the agent, the caller, and the recording are
unaffected.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.thunderphone.com/v1/calls/987654321/listen \
    -H "Authorization: Bearer sk_live_YOUR_API_KEY"
  ```
</CodeGroup>

```json Response theme={null}
{
  "token": "eyJhbGciOi...",
  "room_name": "call-987654321",
  "server_url": "wss://livekit.thunderphone.com"
}
```

Connect to `server_url` with a LiveKit client SDK using `token` and
subscribe to the room's audio. The token is valid for up to 2 hours;
the session ends when the call's room closes.

| Status | Condition        |
| ------ | ---------------- |
| `200`  | Token minted     |
| `404`  | Call not found   |
| `409`  | Call is not live |

### Whisper to the agent

Sends private text guidance that **only the agent hears** — the caller
is never aware of it. The agent weaves the guidance into its next
turns. Use it to coach an agent through an unusual situation mid-call.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.thunderphone.com/v1/calls/987654321/whisper \
    -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"text": "Offer the caller a 10% discount if they mention cancelling."}'
  ```
</CodeGroup>

| Field  | Type   | Required | Description                      |
| ------ | ------ | -------- | -------------------------------- |
| `text` | string | yes      | 1–500 chars of operator guidance |

```json Response (202 Accepted) theme={null}
{ "status": "sent" }
```

| Status | Condition                                                   |
| ------ | ----------------------------------------------------------- |
| `202`  | Guidance delivered to the live session                      |
| `404`  | Call not found                                              |
| `409`  | Call is not live (including the brief window after hang-up) |
| `502`  | Delivery to the call runtime failed                         |

***

## Related

<CardGroup cols={2}>
  <Card title="Outbound Calls" icon="arrow-up-right" href="/api-reference/outbound-calls">
    Trigger a new outbound call via `POST /v1/call`.
  </Card>

  <Card title="Mic Sessions" icon="microphone" href="/api-reference/mic-sessions">
    Test an agent live from a browser.
  </Card>

  <Card title="Issue Reports" icon="triangle-exclamation" href="/api-reference/issue-reports">
    Org-wide feed of issue reports across calls.
  </Card>

  <Card title="Simulations" icon="flask" href="/api-reference/test-calls">
    Programmatic batched simulations against an agent.
  </Card>
</CardGroup>
