ThunderPhone 2.0 is live.Self-serve, from 2¢/min.Read the announcement

Calls

Calls

List and inspect every call your agents handle: read transcripts and grades, export recordings and structured data, and report issues on calls that need review.

Every inbound call to your phone numbers, every call triggered from POST /v1/call, 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

MethodPathDescription
GET/v1/callsList calls (paginated, filterable)
GET/v1/calls/exportBulk export up to 1,000 calls as CSV or JSON
GET/v1/calls/{call_id}Retrieve a single call
GET/v1/calls/{call_id}/audioGet a time-limited recording URL
GET/v1/consent-announcements/{asset_hash}Stream the consent announcement that played on a call
GET/v1/calls/{call_id}/transcriptFetch the transcript
GET/v1/calls/{call_id}/historyFetch the full structured turn history
GET/v1/calls/{call_id}/gradeFetch the latest AI quality grade
POST/v1/calls/{call_id}/gradeRun (or re-run) AI grading
GET/v1/calls/{call_id}/issue-reportsList issue reports for a call
POST/v1/calls/{call_id}/issue-reportsFile an issue report
POST/v1/calls/{call_id}/listenMint a listen-only token for a live call
POST/v1/calls/{call_id}/whisperSend private text guidance to the agent on a live call
POST/v1/calls/{call_id}/promote-to-scenarioTurn a real call into a regression test scenario

Call object

{
  "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",
  "consent_evidence": {},
  "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
}
FieldTypeDescription
call_idintegerServer-assigned call id (also the key in every other endpoint on this page)
agent_idinteger | nullAgent that handled the call
agent_variant_idinteger | nullSplit-testing variant selected for this call; null when no split test ran
agent_variant_labelstring | nullLabel of that variant, snapshotted at call time (survives variant deletion)
directionstringinbound, outbound, test (test call in the agent builder), or web (web call from the embeddable widget)
from_numberstringE.164 caller number. Test calls (direction: "test") use an internal identifier
to_numberstringE.164 destination number
start_time, end_timetimestamp | nullISO 8601 UTC. end_time is null while status="in_progress"
duration_secondsinteger | nullDerived
statusstringin_progress, completed, or failed
end_reasonstring | nullFree-form reason e.g. caller_hangup, agent_end, timeout, ai_transfer
recording_urlstring | nullStorage URL when set — not guaranteed playable. Use /audio for a signed, playable URL
recording_started_attimestamp | nullWhen 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_simulationbooleanTrue if the call was produced by a simulation run
is_test_callbooleanDeprecated alias for is_simulation; retained during the transition
is_scenario_callbooleanTrue only for runs launched from a scenario batch — ad-hoc simulations stay false
prompt_snapshotstring | nullThe user-authored agent prompt in effect at call time
tool_snapshotarray | nullThe tool definitions the agent had at call time
cloud_run_execution_namestring | nullInternal identifier for the worker that ran the call. Useful only when corresponding with support; may be null
livekit_room_name, livekit_room_sidstring | nullInternal identifiers for the underlying media room. Do not build on these; they exist for supervision/support tooling
product, voicestring | nullThe Agent config in effect at call time
billable_minutesnumber | nullMinutes billed, rounded to the nearest quarter minute (15-second increments, minimum 0.25). Straight-to-voicemail calls still report their actual metered minutes here, but the charge is capped at one minute at the plan rate.
billing_total_centsinteger | nullFinal bill in USD cents
billed_attimestamp | nullWhen billing finalised
grade_scoreinteger | null0–100 quality score from the latest AI grading
grade_statusstring | nullpending, completed, or failed
grade_updated_attimestamp | nullLast grading run
call_outcomestring | nullFrom the latest grade: success, failure, unknown, or no_conversation
issue_countintegerTotal issue reports filed against this call
critical_issue_countintegerSubset with severity critical
consent_evidenceobjectConsent-announcement evidence record — see Consent evidence. {} when no announcement played

Calls on agents with the consent announcement enabled carry a read-only consent_evidence object recording exactly what played and what the caller did afterwards:

FieldTypeDescription
asset_version, asset_hashstringIdentity of the exact audio asset that played; asset_hash keys the consent announcement audio endpoint
language, announcement_textstringAnnouncement language and verbatim text
playback_started_at, playback_completed_at, capture_started_attimestampOrdered: playback completes before caller-audio capture is enabled
continuation_eventobject | absentFirst caller activity after the announcement completed: {event_type: "speech" | "dtmf", at, participant_identity, dtmf_digit?}. Only actual confirmed speech or a keypad press counts — a merely connected line never produces this event
objection_eventsarray | absentDetectable objection/withdrawal signals, each {event_type, at, participant_identity, dtmf_digit?, seconds_after_playback_completed?} with event_type one of dtmf_during_announcement, hangup_during_announcement, hangup_after_announcement_without_activity (the latter only for hangups shortly after the notice with no caller activity; a later silent hangup is not recorded as an objection)

continuation_event and objection_events document caller behavior around the notice; they are not proof the caller heard the announcement and are not a substitute for affirmative (press-1) consent where a jurisdiction requires it.


List calls

cURL
curl 'https://api.thunderphone.com/v1/calls?limit=50' \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"
Python
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"]:
    ...
Node.js
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());

Query parameters

ParamTypeDefaultDescription
limitinteger50Page size (1–200)
offsetinteger0
agent_idintegerFilter by agent
directionstringinbound, 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.
statusstringin_progress, completed, failed
phone_numberstringMatches either side of the call (from_number or to_number); formatted numbers are normalized before matching
from_numberstringE.164
to_numberstringE.164
start_dateISO 8601Earliest start_time
end_dateISO 8601Latest start_time

Paginated response

{
  "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, so what you export matches what you filtered.

CSV
curl 'https://api.thunderphone.com/v1/calls/export?export_format=csv' \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -o calls.csv
JSON (filtered)
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

Query parameters

ParamRequiredDescription
export_formatyescsv or json
agent_idFilter by agent
directionSame values as List calls (inbound, outbound, test, web; legacy mic/widget accepted)
statusin_progress, completed, failed
phone_numberMatches either side of the call (from_number or to_number); formatted numbers are normalized before matching
from_numberE.164
to_numberE.164
start_dateISO 8601 — earliest start_time
end_dateISO 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:

SignalWhereMeaning
X-Truncatedheader (CSV and JSON)true when the filtered match count exceeds the row cap, else false
X-Total-Countheader (CSV and JSON)Untruncated count of calls matching the filters
truncatedJSON body fieldSame 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

cURL
curl https://api.thunderphone.com/v1/calls/987654321 \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Returns 200 OK with a 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).

cURL
curl https://api.thunderphone.com/v1/calls/987654321/audio \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"
Response
{ "url": "https://storage.googleapis.com/thunderphone-recordings/..." }
StatusCondition
200Recording exists, URL returned
404Call or recording not found

Get caller audio

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

Streams what the caller actually said for one turn of the conversation. ref comes from a transcript entry's caller_audio_refs, present on caller turns whose audio was captured.

StatusCondition
200Audio stream returned
404Call, turn, or audio not found

GET /v1/consent-announcements/{asset_hash}

Streams the exact consent-announcement asset that played at the start of a call, as a 48 kHz mono WAV. Calls that played an announcement carry a consent_evidence object (asset hash, announcement text, playback and capture timestamps); pass its asset_hash here to retrieve the audio whose content hash matches it byte-for-byte. This endpoint is the programmatic evidence-access path; the stored call recording itself also begins with this announcement — it is muxed in at recording finalization, and consent_evidence then carries recording_announcement_offset_seconds / recording_announcement_duration_seconds marking where the prepended segment sits inside the recording. When additional participants joined mid-call (for example a warm-transfer target), the same object also carries a legs array — one entry per joining leg with the leg's identity, join time, its own playback/capture timestamps for the same content-addressed asset, and post-notice continuation details.

Authorization is by evidence linkage: the hash must appear in the consent_evidence of a call your organization can access. Responses are never cacheable (Cache-Control: no-store), and the endpoint is rate-limited per user.

cURL
curl https://api.thunderphone.com/v1/consent-announcements/f1d6b49f4ac8f391cfc933591f53415bc71083a62d723acb4de5606cea3d4d73 \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  --output consent-announcement.wav
StatusCondition
200WAV audio stream returned
404Hash malformed, not linked to any of your org's calls, or the asset is no longer cached
429Rate limit exceeded — retry later

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.

cURL
curl https://api.thunderphone.com/v1/calls/987654321/transcript \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"
Response
{
  "call_id": 987654321,
  "transcripts": [
    {
      "role": "user",
      "content": "Hi, I have a question about my policy.",
      "start_ms": 1200,
      "end_ms": 3400,
      "caller_audio_refs": ["…", "…"]
    },
    {
      "role": "agent",
      "content": "Of course — could I have your member id?",
      "start_ms": 3800,
      "end_ms": 6100,
      "ttfa_ms": 412
    }
  ]
}
FieldTypeDescription
rolestringuser, agent, tool_call, tool_response, or system
contentstringHuman-readable content. For tool_call/tool_response turns this is a JSON string
start_ms, end_msintegerOffset in milliseconds, anchored to the recording when recording_started_at is set. Omitted when timing could not be reconstructed
ttfa_msintegerTime-to-first-audio for the first agent turn of a response. Present on agent turns only
caller_audio_refsarray of stringOpaque references to the audio of what the caller said, in the order they were spoken. Pass each to /caller-audio to fetch it. Present on user turns only, and omitted entirely — never null — when no audio was captured

Caller audio is always the call audio as received, never a noise-suppressed version, so re-listening to two different turns is always comparing like with like. A long caller turn can span several recordings; fetch them in the order given and concatenate.

Query paramDescription
live=trueUse a lenient serializer that includes not-yet-resolved turns — for real-time display while the call is still in progress. Caller turns carry caller_audio_refs here too, so audio can be fetched while the call is still running

Get history

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

cURL
curl https://api.thunderphone.com/v1/calls/987654321/history \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Returns an array of typed entries. See the ThunderPhone call-history schema reference 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

cURL
curl https://api.thunderphone.com/v1/calls/987654321/grade \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"
Response
{
  "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"
}
FieldTypeDescription
idintegerGrade row id
call_idinteger
statusstringpending, completed, or failed
scoreinteger | null0–100
call_outcomestringsuccess, failure, unknown, or no_conversation
outcome_reasoningstringWhy the grader chose that outcome
summarystringOne-paragraph call summary
rubric_breakdownobjectPer-criterion scores
detected_issuesarrayMachine-detected issues; completed grades sync these into issue reports with source: "system"
grader_model, grader_versionstringWhich grader produced the row
error_messagestring | nullSet when status="failed"
graded_attimestamp | nullWhen grading completed
created_at, updated_attimestamp

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.

cURL
curl -X POST 'https://api.thunderphone.com/v1/calls/987654321/grade?force=true' \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"

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:

{
  "detail": "This call was recently submitted for grading. Please retry later.",
  "retryAfterSeconds": 90
}
StatusCondition
200An existing completed grade was reused (no force)
201A new grade completed
429The per-call cooldown is active; use Retry-After or retryAfterSeconds before retrying
502Grading 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.

List reports for a call

cURL
curl https://api.thunderphone.com/v1/calls/987654321/issue-reports \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Returns an array of Issue Report objects — see Issue Reports.

File a report

cURL
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"
  }'
FieldTypeRequiredDescription
titlestringyes1–255 chars
descriptionstringnoFree-form. Defaults to ""
severitystringnoinfo, warning, or critical. Defaults to warning
sourcestringnouser (default) or system
timestamp_secondsinteger ≥ 0 | nullnoOffset into the call where the issue occurred
detected_issue_refstringnoUp to 128 chars — correlates the report with a grader-detected issue
metadataobjectnoArbitrary JSON. Defaults to {}

Returns 201 Created with the Issue Report object. To update a report's status later, use the issue-reports endpoints (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.

cURL
curl -X POST https://api.thunderphone.com/v1/calls/987654321/listen \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"
Response
{
  "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.

StatusCondition
200Token minted
404Call not found
409Call 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.

cURL
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."}'
FieldTypeRequiredDescription
textstringyes1–500 chars of operator guidance
Response (202 Accepted)
{ "status": "sent" }
StatusCondition
202Guidance delivered to the live session
404Call not found
409Call is not live (including the brief window after hang-up)
502Delivery to the call runtime failed