Skip to content

Uploading Calls for Analysis

When a call project has automatic analysis enabled, every recording is analyzed as soon as it finishes transcribing — no separate “run analysis” call is needed.

The important thing to know: the API upload endpoint is the same one the web app uses. Dropping a file into the Conversations panel and POSTing it to /api/files/ run through exactly the same server-side path, so a recording uploaded by a script is transcribed, gets its CallRecord, and is queued for automatic analysis identically to one uploaded by hand. There is nothing API-specific to turn on.

All endpoints require an API key — see Authentication.


  1. Configure the project once — enable automatic analysis and choose the analysis types.
  2. POST /api/files/ — upload each recording, optionally with its call metadata.
  3. Poll GET /calls/api/analysis-status/?project_id=… until in_progress is false.
  4. Read the results from the Call Reports API.

Steps 2–4 are all you repeat day to day.

Every step needs the project’s numeric ID. If you don’t have it, Projects shows how to list your projects and where to find the ID in the web app.


Automatic analysis is configured on the project, via PATCH /api/projects/{id}/. You can do this from the web app’s analysis wizard instead — the settings are the same either way.

Fields:

FieldTypeDescription
auto_analysis_enabledboolMaster switch. Analysis only happens when this is true.
auto_analysis_typeslistWhich analyses to run per call. One or more of calls_analysis_copc, calls_analysis_no_protocol, custom_questions.
auto_analysis_questionslist of stringsThe question set answered per call. Required when custom_questions is enabled.
evaluation_rubricintID of the rubric to score against. Required when calls_analysis_copc is enabled.
auto_analysis_batch_sizeintDispatch a batch once this many calls are waiting. Default 20, allowed range 120.
auto_analysis_max_wait_secondsintFlush a partial batch once its oldest waiting call is this old. Default 300.

Validation — the request is rejected with 400 when the configuration would produce unusable runs:

  • The project’s audio_type is not call — automatic analysis is call-projects only.
  • auto_analysis_types is empty.
  • calls_analysis_copc is requested without an evaluation_rubric.
  • custom_questions is requested without any auto_analysis_questions.
  • auto_analysis_batch_size is outside 120.

Example — enable per-call analysis with no protocol plus two custom questions:

Terminal window
curl -X PATCH https://app.uspeech.io/api/projects/312/ \
-H "Authorization: Api-Key $USPEECH_KEY" \
-H "Content-Type: application/json" \
-d '{
"auto_analysis_enabled": true,
"auto_analysis_types": ["calls_analysis_no_protocol", "custom_questions"],
"auto_analysis_questions": ["Was a callback offered?", "Was the discount mentioned?"],
"auto_analysis_batch_size": 20,
"auto_analysis_max_wait_seconds": 300
}'

GET /api/projects/{id}/ returns the same fields, so you can check the current configuration before changing it.


POST /api/files/ — identical to the Transcription API upload, and it accepts the call metadata in the same request.

Content-Type: multipart/form-data

FieldRequiredDescription
projectyesThe project ID.
fileyesThe recording (mp3, wav, m4a, flac, ogg, mp4, …), or an srt/vtt transcript.
file_typeyesaudio for recordings, or srt / vtt for transcripts you already have.
call_datetime, agent_id, agent_name, direction, caller_number, campaignnoCall metadata — see Call Metadata.

Metadata is optional here, but supplying it is what makes the report groupable by agent, direction, campaign and date. For bulk loads you can skip it at upload time and send a CSV manifest instead — rows are matched to files by name, including files uploaded later.

Terminal window
curl -X POST https://app.uspeech.io/api/files/ \
-H "Authorization: Api-Key $USPEECH_KEY" \
-F "project=312" \
-F "file_type=audio" \
-F "file=@./call_001.mp3" \
-F "call_datetime=2026-06-15T09:12:00Z" \
-F "agent_id=A-06" \
-F "agent_name=Ana Diaz" \
-F "direction=inbound" \
-F "campaign=Retention"

⚠️ Important: 201 Created means the upload was accepted and queued. Transcription and analysis both happen asynchronously — see the polling section below.

A file uploaded under a name the project already has is treated as the same call — the new record replaces its predecessor rather than adding a second one. This makes retries safe: if a batch upload fails halfway, re-running it will not double-count calls in the report.


Calls are not analyzed one at a time — they are analyzed in small batches, which is why results appear in groups rather than one by one.

A batch for a given analysis type is dispatched when either condition is met:

  • auto_analysis_batch_size calls are waiting — the batch goes immediately.
  • The oldest waiting call reaches auto_analysis_max_wait_seconds — the partial batch is flushed by a sweep that runs every 60 seconds.

So a partial batch is never stranded: with the defaults, a lone call uploaded at the end of the day is analyzed within about five minutes. If you upload in large runs, lower auto_analysis_max_wait_seconds only if you need results sooner than that — smaller batches cost more per call.


Two levels of visibility:

Per fileGET /api/files/{id}/ reports the transcription status (transcribingtranscribed | failed) and an auto_analysis_error field that surfaces the reason automatic analysis was skipped for that file, e.g. quota_exceeded when the subscription ran out of minutes.

Per projectGET /calls/api/analysis-status/?project_id={id} is the endpoint the dashboard itself polls:

{
"analysis_status": {
"calls_analysis_no_protocol": { "completed": 57, "queued": 3 }
},
"analyzed_calls": 57,
"duplicate_uploads": 2,
"in_progress": true,
"reclustering": false,
"last_analyzed_at": "2026-07-29T02:50:11.127124+00:00"
}

Poll it until in_progress is false, then fetch the report. See Call Reports for the full field reference.

Terminal window
while :; do
resp=$(curl -sS "https://app.uspeech.io/calls/api/analysis-status/?project_id=312" \
-H "Authorization: Api-Key $USPEECH_KEY")
in_progress=$(echo "$resp" | jq -r .in_progress)
echo "$(date -u +%H:%M:%S) analyzed=$(echo "$resp" | jq -r .analyzed_calls) in_progress=$in_progress"
[ "$in_progress" = "false" ] && break
sleep 30
done

💡 Tip: poll every 30 seconds or slower. Transcription takes roughly 1/8 of the audio duration, and analysis waits for a batch to fill, so faster polling tells you nothing new.


Once in_progress is false, the analyzed calls are available through the Call Reports API:

  • GET /calls/api/report/?project_id={id} — the aggregated QA report
  • GET /calls/api/calls/?project_id={id} — the per-call explorer
  • GET /calls/api/custom-questions/?project_id={id} — per-call answers to your custom questions
  • GET /calls/api/report/pdf/?project_id={id} — the PDF export

Configure the project, upload a folder of recordings, attach their call metadata, wait for analysis, and fetch the report.

The metadata step matters: without it the calls are still analyzed, but the report has nothing to group them by — no agent, no direction, no campaign, and the upload time standing in for the call time. There are two ways to supply it, and you can mix them freely:

  • Per file at upload, as extra form fields on POST /api/files/ — best when your system already knows the details of each call as it uploads it. This is what step 2 above shows.
  • Per project, as a CSV manifest on POST /api/projects/{id}/call-manifest/ — best for bulk loads and backfills. Rows are matched to files by name, and rows that don’t match anything yet are kept and applied to files uploaded later, so the manifest can be sent before or after the recordings.

The example below uses the manifest, since it suits uploading a whole folder at once. See Call Metadata for the full field reference, the accepted date formats, and how to correct metadata after the fact.

#!/usr/bin/env bash
set -euo pipefail
BASE_URL="https://app.uspeech.io"
AUTH="Authorization: Api-Key ${USPEECH_KEY}"
project_id=312
# 1. Enable automatic analysis
curl -sS -X PATCH "${BASE_URL}/api/projects/${project_id}/" \
-H "$AUTH" -H "Content-Type: application/json" \
-d '{"auto_analysis_enabled": true, "auto_analysis_types": ["calls_analysis_no_protocol"]}' \
> /dev/null
# 2. Upload every recording in ./calls/
for path in ./calls/*.mp3; do
curl -sS -X POST "${BASE_URL}/api/files/" \
-H "$AUTH" \
-F "project=${project_id}" \
-F "file_type=audio" \
-F "file=@${path}" \
| jq -r '"uploaded \(.original_filename) -> \(.id)"'
done
# 3. Attach the call metadata for the whole batch.
# calls_june.csv: filename,call_datetime,agent_id,agent_name,direction,campaign
curl -sS -X POST "${BASE_URL}/api/projects/${project_id}/call-manifest/" \
-H "$AUTH" -F "file=@./calls_june.csv" \
| jq -r '"\(.matched) matched, \(.unmatched | length) waiting for their file"'
# 4. Wait for analysis to settle
while :; do
resp=$(curl -sS "${BASE_URL}/calls/api/analysis-status/?project_id=${project_id}" -H "$AUTH")
[ "$(echo "$resp" | jq -r .in_progress)" = "false" ] && break
sleep 30
done
# 5. Fetch the report
curl -sS "${BASE_URL}/calls/api/report/?project_id=${project_id}" -H "$AUTH" > report.json
echo "report written to report.json"

SymptomLikely cause
File reaches transcribed but nothing is ever analyzedauto_analysis_enabled is false, or auto_analysis_types is empty. Check GET /api/projects/{id}/.
auto_analysis_error is quota_exceededThe subscription ran out of minutes. Analysis is skipped, not retried automatically.
400 on the upload with a metadata field in the bodyThe project is not a call project, or file_type is not audio/srt/vtt.
400 when enabling automatic analysisA rubric is missing for COPC, questions are missing for custom_questions, or the batch size is outside 120.
Report call count is lower than the number of files uploadedRe-uploaded filenames replace the earlier call. duplicate_uploads in the analysis-status response counts them.

See Status Codes & Errors for the full list of response codes.