Skip to content

Call Reports API

Everything the Call Center Report shows is available as JSON: headline metrics, the quality scorecard, weekly trend, contact reasons, agent performance and shift comparison, plus a per-call table you can page through and drill into.

Typical uses are pulling compliance numbers into your own BI tool, mirroring per-call scores into a workforce management system, and polling for progress while a batch of uploads is being analyzed.

All endpoints accept either a session cookie or an API key — see Authentication. Results are scoped to the calling user exactly as in the web app: your own projects, projects shared with you, and every project in teams you administer.


ParameterTypeDescription
project_idintLimit to a single project. Omit to aggregate across every call project you can see
teamintTeam id, for team-scoped access. Ignored if you are not a member of that team
start_datedateStart of the window, YYYY-MM-DD (inclusive)
end_datedateEnd of the window, YYYY-MM-DD (inclusive, through 23:59:59)
date_rangestringNamed preset instead of explicit dates: mtd, last_month, last_30_days, last_90_days, ytd

project_id and team apply to every endpoint on this page. The date parameters apply to the report, the PDF export and the calls list; the detail, analysis-status, custom-questions and recluster endpoints are project-scoped and ignore them.

date_range wins when both it and explicit dates are sent. With no date parameters at all, the whole history is returned — unlike the web report, which defaults to the last 30 days.


The aggregated report for the selected projects and window.

Response (200 OK) — abridged, with long arrays cut to two entries:

{
"date_range": {
"start": "2026-05-01T00:00:00+00:00",
"end": "2026-07-30T00:00:00+00:00",
"preset": "last_90_days"
},
"total_calls_all_time": 57,
"headline": {
"total_calls": 57,
"analyzed_hours": 7.3,
"average_score": 74.2,
"weighted_compliance": 78.4,
"scored_calls": 57,
"critical_error_pct": 7.0,
"negative_sentiment_pct": 32.5,
"goal_reached_pct": 75.0,
"calls_with_metadata": 57
},
"scorecard": [
{
"criterion_id": "verification",
"name": "Identity verification",
"section": "Opening",
"weight_pct": 15.4,
"average_pct": 75.4,
"previous_pct": 71.0,
"delta_pct": 4.4,
"critical_errors": 0,
"calls": 40,
"evaluated_calls": 31,
"not_applicable_calls": 9
}
],
"trend": [
{ "week": "2026-06-29", "calls": 10, "average_score": 65.0, "negative_sentiment_pct": 30.0 }
],
"contact_reasons": [
{ "topic": "Billing dispute", "calls": 8, "volume_pct": 20.0 }
],
"agents": [
{
"agent": "Ana Diaz",
"agent_id": 1,
"calls": 26,
"average_score": 72.7,
"critical_error_pct": 7.7,
"negative_sentiment_pct": 50.0,
"goal_reached_pct": 76.9
}
],
"shifts": [
{ "shift": "day", "total_calls": 46, "average_score": 74.8, "calls_with_metadata": 46 }
],
"shift_hours": { "day_start": 8, "day_end": 20 }
}

Reading the sections:

FieldMeaning
total_calls_all_timeCalls in scope before the date filter. When headline.total_calls is 0 and this is not, your window is simply wrong
headlineVolume and quality summary for the window. average_score is the plain mean of call scores; weighted_compliance is total points over total possible points, so long rubrics don’t get diluted
scorecardOne row per rubric criterion. previous_pct and delta_pct compare against the immediately preceding window of the same length, and are null unless both start_date and end_date are set. not_applicable_calls counts the calls where the criterion was skipped and awarded full points; those calls are included in average_pct, so a criterion with a high not_applicable_calls reads high because it rarely applied. evaluated_calls + not_applicable_calls is less than calls when the window includes calls analyzed before Uspeech recorded applicability
trendWeekly buckets, week being the Monday of each bucket
contact_reasonsTop reasons by volume. A trailing row with an empty topic and an other_topics count is the “everything else” bucket
agentsPer-agent rollup. agent_id is Uspeech’s internal agent id — the one to pass to the explorer’s agent filter — not your own agent_id from the metadata
shiftsSame shape as headline plus shift (day / night). Only calls with a real call date/time are classified
shift_hoursThe day-shift window used, from the project’s settings

Percentage fields are null rather than 0 when nothing in the window could be measured — no scored calls, no sentiment, no known outcome.

Example:

Terminal window
curl -H "Authorization: Api-Key $USPEECH_KEY" \
"https://app.uspeech.io/calls/api/report/?project_id=312&date_range=last_30_days"

A paginated, filterable table of analyzed calls, spanning every analysis batch in the selected projects.

Additional query parameters:

ParameterTypeDescription
agentint / noneUspeech agent id (from the report’s agents[].agent_id), or none for calls with no agent
sentimentstringpositive, neutral or negative
directionstringinbound or outbound
criticalbooltrue returns only calls with a critical error
searchstringMatches file name, topic, contact reason, caller number or campaign
orderingstringcall_datetime, score_percentage, mistake_count or agent__name, optionally prefixed with -. Defaults to -call_datetime
pageintPage number; the page size is 25

Unknown values for sentiment, direction and ordering are ignored rather than rejected.

Response (200 OK):

{
"count": 57,
"next": "https://app.uspeech.io/calls/api/calls/?page=2",
"previous": null,
"results": [
{
"id": 28,
"file_id": 458,
"file_name": "call_027.mp3",
"call_datetime": "2026-07-28T09:15:00Z",
"agent": "Luis Gomez",
"direction": "inbound",
"campaign": "Retention",
"topic": "Address change",
"sentiment": "positive",
"score_percentage": 66.3,
"has_critical_error": false,
"mistake_count": 0,
"missing_prompt_count": 1,
"goal_reached": true
}
]
}

id is the call’s id for the detail endpoint below; file_id is the uploaded file, which is what the Transcription and Call Metadata endpoints use. topic is the codified contact reason once grouping has run, and the raw extracted topic before that.

Example — the worst-scoring inbound calls of the month:

Terminal window
curl -H "Authorization: Api-Key $USPEECH_KEY" \
"https://app.uspeech.io/calls/api/calls/?date_range=mtd&direction=inbound&ordering=score_percentage"

Everything known about one call, merged across analysis types.

Response (200 OK):

{
"record": {
"id": 28,
"file_id": 458,
"file_name": "call_027.mp3",
"call_datetime": "2026-07-28T09:15:00Z",
"call_datetime_source": "manifest",
"agent": "Luis Gomez",
"direction": "inbound",
"campaign": "Retention",
"caller_number": "+34600111222",
"topic": "Address change",
"sentiment": "positive",
"score_percentage": 66.3,
"total_points": 19.89,
"max_possible_points": 30.0,
"has_critical_error": false,
"mistake_count": 0,
"missing_prompt_count": 1,
"goal_reached": true
},
"copc": { "…": "per-criterion scores, justifications and quotes" },
"no_protocol": { "…": "detected mistakes and missed prompts" },
"custom_questions": { "…": "answers to the project's custom questions" }
}

record is the explorer row plus the caller number, the raw point totals and call_datetime_source. The other three keys carry the free-text output of each analysis type and are null when that analysis has not run for this call. Their internal structure follows the analysis result format and is best inspected against a real call.

Each entry in copc.criterion_scores carries an applicability field — "evaluated" or "not_applicable":

{
"criterion_id": "closing_summary",
"justification": "The customer hung up before the agent could summarize the agreement.",
"quote": "[04:12] Customer: I have to go — goodbye.",
"applicability": "not_applicable",
"score": 15,
"max_points": 15,
"is_critical_error": false
}

A criterion that did not apply is awarded its full max_points, so it is indistinguishable from a perfect score on score alone — applicability is the only field that separates them. If you compute your own per-criterion averages, filter out not_applicable rows first; otherwise a conditional criterion reads high simply because it rarely applied. See How Scoring Works.

Errors: 404 when the call does not exist or belongs to a project you cannot see.


Progress of automatic analysis for the selected projects — designed for polling while uploads are being processed.

Response (200 OK):

{
"analysis_status": {
"calls_analysis_copc": { "completed": 54, "queued": 3 },
"calls_analysis_no_protocol": { "completed": 57 }
},
"analyzed_calls": 57,
"duplicate_uploads": 2,
"in_progress": true,
"reclustering": false,
"last_analyzed_at": "2026-07-29T02:50:11.127124+00:00"
}
FieldMeaning
analysis_statusPer analysis type, a count of calls in each state (queued, processing, completed, failed)
analyzed_callsCalls with results, ignoring any date filter
duplicate_uploadsFiles uploaded under a name the project already had. They are the same call, so a record replaces its predecessor — which is why the file count can run ahead of the call count
in_progresstrue while any analysis is queued or processing — poll until it is false, then re-fetch the report
reclusteringtrue while contact reasons are being re-grouped
last_analyzed_atTimestamp of the most recent result, or null
Terminal window
while :; do
resp=$(curl -sS "https://app.uspeech.io/calls/api/analysis-status/?project_id=312" \
-H "Authorization: Api-Key $USPEECH_KEY")
[ "$(echo "$resp" | jq -r .in_progress)" = "false" ] && break
sleep 30
done

See Uploading Calls for Analysis for the full upload-then-poll walkthrough.


Per-call answers to the project’s custom questions, gathered across batches.

Response (200 OK):

{
"questions": ["Was a callback offered?", "Was the discount mentioned?"],
"calls": [
{
"call_analysis_id": 904,
"file": "call_027.mp3",
"call_datetime": "2026-07-28T09:15:00Z",
"agent": "Luis Gomez",
"answers": {
"Was a callback offered?": { "answer": "Yes — agent offered a callback for Tuesday.", "quotes": 2 }
}
}
],
"count": 12
}

questions is the union of questions seen across the returned calls, in first-seen order — use it as the column list. A call that was analyzed under an older question set simply has no entry for the newer questions. quotes counts the supporting transcript quotes behind the answer.

Projects without custom questions return empty arrays and count: 0.


Re-group open-ended contact reasons on demand, the same action as Refresh contact reasons in the report.

Reclustering is queued for every accessible open-ended project; projects with frozen predefined codes classify each call on arrival and are skipped, so they never appear in the response. The work runs in the background — poll analysis-status for reclustering to see when it finishes.

Response (202 Accepted):

{ "reclustering": [312, 318], "count": 2 }

reclustering lists the project ids that were queued. Overlapping requests are de-duplicated, so calling this while a recluster is already running is harmless.

Example:

Terminal window
curl -X POST -H "Authorization: Api-Key $USPEECH_KEY" \
"https://app.uspeech.io/calls/api/recluster/?project_id=312"

The same client-ready PDF the report’s Export PDF button produces, including the AI-written executive summary, for the current filters.

Response (200 OK): application/pdf as a file download. Generation runs synchronously and includes an LLM call for the narrative sections, so expect this to take noticeably longer than the JSON endpoint; if the narrative fails, the PDF is still returned with its tables and a note.

The document is written in English by default. Send Accept-Language: es for Spanish — a key-authenticated request has no browser session, so the language preference on the user’s profile does not apply here.

Example:

Terminal window
curl -H "Authorization: Api-Key $USPEECH_KEY" \
-o call_report.pdf \
"https://app.uspeech.io/calls/api/report/pdf/?project_id=312&date_range=last_month"

StatusWhen
401 / 403Missing, malformed, revoked or inactive-user API key
404On the report and PDF endpoints, project_id names a project that does not exist or is not visible to you; on the detail endpoint, the same for the call itself

Note the asymmetry: an unreachable project_id is a 404 on the report and the PDF, but on the list, status, custom-questions and recluster endpoints it simply matches nothing and you get an empty, zeroed result. An empty result is never an error in itself — a project you can see with no analyzed calls looks the same. See Status Codes & Errors for the general error shape.