Transcription API
🎙️ Transcription API
Section titled “🎙️ Transcription API”The transcription flow has three resources:
- File (
/api/files/) — the audio you upload. Once uploaded, transcription starts automatically. - Transcript (
/api/transcripts/) — the result, with metadata (language, duration, word count) and download links for SRT and DOCX. - Task (
/api/tasks/) — the background job that did the work (useful for retry).
All endpoints in this section require either a session cookie or an API key — see Authentication.
The flow at a glance
Section titled “The flow at a glance”POST /api/files/— upload an audio file inside an existing project. The response gives you afile.id. Transcription is enqueued automatically.- Poll
GET /api/files/{id}/untilstatusbecomestranscribed(success) orfailed. GET /api/transcripts/orGET /api/transcripts/{id}/— read the resulting transcript record.GET /api/transcripts/{id}/download/?type=srt|docx— fetch a time-limited download URL for the file.
POST /api/files/
Section titled “POST /api/files/”Upload an audio file. Auto-triggers transcription when the caller’s subscription has remaining minutes.
Auth: API key or session.
Content-Type: multipart/form-data
Form fields:
| Field | Required | Description |
|---|---|---|
project | yes | ID of the project the file belongs to. Must be visible to the caller — see Projects for how to look the ID up. |
file | yes | The audio file. Common formats are supported (mp3, wav, m4a, flac, ogg, mp4, …). |
file_type | yes | Set to audio for transcription. Other values (csv, xlsx, srt, vtt) are used for non-audio uploads. |
Response (201 Created):
{ "id": 4217, "project": 312, "original_filename": "interview.wav", "file_type": "audio", "status": "transcribing", "duration_seconds": 1834.2, "error_reason": "", "asr_engine": "", "uploaded_at": "2026-05-17T14:02:11Z"}Example:
curl -X POST https://app.uspeech.io/api/files/ \ -H "Authorization: Api-Key $USPEECH_KEY" \ -F "project=312" \ -F "file_type=audio" \ -F "file=@./interview.wav"import osimport requests
BASE_URL = "https://app.uspeech.io"HEADERS = {"Authorization": f"Api-Key {os.environ['USPEECH_KEY']}"}
with open("interview.wav", "rb") as fh: response = requests.post( f"{BASE_URL}/api/files/", headers=HEADERS, data={"project": 312, "file_type": "audio"}, files={"file": ("interview.wav", fh, "audio/wav")}, timeout=300, )response.raise_for_status()file_id = response.json()["id"]import { openAsBlob } from 'node:fs';
const BASE_URL = 'https://app.uspeech.io';const HEADERS = { Authorization: `Api-Key ${process.env.USPEECH_KEY}` };
const form = new FormData();form.set('project', '312');form.set('file_type', 'audio');form.set('file', await openAsBlob('./interview.wav'), 'interview.wav');
const response = await fetch(`${BASE_URL}/api/files/`, { method: 'POST', headers: HEADERS, // do not set Content-Type — FormData sets the boundary body: form,});if (!response.ok) throw new Error(await response.text());const fileId = (await response.json()).id;⚠️ Important: a 201 only means the upload was accepted and the job was queued. The transcript itself is produced asynchronously — see the polling pattern below.
GET /api/files/{id}/
Section titled “GET /api/files/{id}/”Read a single file, including its current status.
Auth: API key or session.
Response (200 OK): same shape as the POST response above.
File status reference
Section titled “File status reference”| Status | Meaning |
|---|---|
uploaded | File saved, transcription not yet queued (used by non-audio uploads). |
transcribing | Transcription is in progress. |
transcribed | Transcription completed successfully. A Transcript record now exists. |
failed | Transcription failed. The error_reason field in this same response tells you why (audio_too_short, hallucinated_transcript, private_transcription_unavailable, or empty for other errors) — see Status Codes & Errors. |
analyzing, completed | Set when downstream analysis is running or finished (not relevant for the transcription-only flow). |
Polling pattern
Section titled “Polling pattern”file_id=4217while :; do resp=$(curl -sS "https://app.uspeech.io/api/files/${file_id}/" \ -H "Authorization: Api-Key $USPEECH_KEY") status=$(echo "$resp" | jq -r .status) echo "$(date -u +%H:%M:%S) status=$status" case "$status" in transcribed) break ;; failed) echo "failed, reason: $(echo "$resp" | jq -r '.error_reason // "unknown"')" break ;; esac sleep 10doneimport time
while True: file_data = requests.get( f"{BASE_URL}/api/files/{file_id}/", headers=HEADERS, timeout=30 ).json() status = file_data["status"] print(f"status={status}") if status == "transcribed": break if status == "failed": raise RuntimeError(f"failed, reason: {file_data.get('error_reason') or 'unknown'}") time.sleep(10)const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
while (true) { const fileData = await fetch(`${BASE_URL}/api/files/${fileId}/`, { headers: HEADERS, }).then((r) => r.json()); console.log(`status=${fileData.status}`); if (fileData.status === 'transcribed') break; if (fileData.status === 'failed') { throw new Error(`failed, reason: ${fileData.error_reason || 'unknown'}`); } await sleep(10_000);}💡 Tip: transcription typically takes ~1/8 of the audio’s duration. A 10-minute call is usually done in well under two minutes.
GET /api/transcripts/
Section titled “GET /api/transcripts/”List transcripts visible to the caller (scoped to projects they can see).
Auth: API key or session.
Response (200 OK): paginated list. Each item:
{ "id": 901, "uploaded_file": 4217, "language": "en", "duration_seconds": 1834.2, "word_count": 3120, "srt_path": "tenant/312/results/transcripts/interview.srt", "docx_path": "tenant/312/results/transcripts/interview.docx", "status": "completed", "error_reason": "", "asr_engine": "external", "created_at": "2026-05-17T14:05:33Z", "updated_at": "2026-05-17T14:08:47Z"}status here is queued → processing → completed | failed. When status is failed, error_reason holds a machine-readable cause (audio_too_short, hallucinated_transcript, private_transcription_unavailable) or is empty for other errors — see Status Codes & Errors.
asr_engine records which speech recognition service produced the transcript: external (the default provider) or private (USpeech-operated infrastructure, used when the project requires private transcription). It is also returned on the file resource, and stays empty until transcription completes.
GET /api/transcripts/{id}/
Section titled “GET /api/transcripts/{id}/”Retrieve a single transcript by ID. Same field shape as the list response.
curl https://app.uspeech.io/api/transcripts/901/ \ -H "Authorization: Api-Key $USPEECH_KEY"transcript = requests.get( f"{BASE_URL}/api/transcripts/901/", headers=HEADERS, timeout=30).json()const transcript = await fetch(`${BASE_URL}/api/transcripts/901/`, { headers: HEADERS,}).then((r) => r.json());If the transcript exists but isn’t visible to the caller, the response is 404 Not Found.
GET /api/transcripts/{id}/download/
Section titled “GET /api/transcripts/{id}/download/”Return a time-limited URL for the transcript artefact (SRT or DOCX). The URL points at private storage — use it from the same machine that received the response.
Auth: API key or session.
Query parameters:
| Param | Required | Description |
|---|---|---|
type | no | srt (default) or docx. |
Response (200 OK):
{ "download_url": "https://…/interview.srt?X-Amz-Signature=…"}If the requested artefact hasn’t been generated (e.g. the transcript failed), the response is 404 Not Found.
Example:
# Get the URL …url=$(curl -sS "https://app.uspeech.io/api/transcripts/901/download/?type=srt" \ -H "Authorization: Api-Key $USPEECH_KEY" | jq -r .download_url)
# … then download.curl -o interview.srt "$url"# Get the URL …url = requests.get( f"{BASE_URL}/api/transcripts/901/download/", headers=HEADERS, params={"type": "srt"}, timeout=30,).json()["download_url"]
# … then download. The URL is pre-signed, so it needs no auth header.with open("interview.srt", "wb") as fh: fh.write(requests.get(url, timeout=120).content)import { writeFile } from 'node:fs/promises';
// Get the URL …const { download_url: url } = await fetch( `${BASE_URL}/api/transcripts/901/download/?type=srt`, { headers: HEADERS },).then((r) => r.json());
// … then download. The URL is pre-signed, so it needs no auth header.const srt = await fetch(url).then((r) => r.arrayBuffer());await writeFile('interview.srt', Buffer.from(srt));Retrying a failed transcription
Section titled “Retrying a failed transcription”If a file ends up in failed, you can retry it via the Task API.
- Find the transcription Task for the file at
GET /api/tasks/?task_type=transcription(or follow the link from the web app). - Call
POST /api/tasks/{task_id}/retry/to re-queue it.
Don’t retry on terminal failures like audio_too_short — see Status Codes & Errors for the full list of error_reason values. Retrying is the right move for private_transcription_unavailable and for failures with an empty error_reason.
End-to-end example
Section titled “End-to-end example”# 1. Uploadresp=$(curl -sS -X POST https://app.uspeech.io/api/files/ \ -H "Authorization: Api-Key $USPEECH_KEY" \ -F "project=312" -F "file_type=audio" \ -F "file=@./interview.wav")file_id=$(echo "$resp" | jq -r .id)
# 2. Pollwhile :; do resp=$(curl -sS "https://app.uspeech.io/api/files/${file_id}/" \ -H "Authorization: Api-Key $USPEECH_KEY") status=$(echo "$resp" | jq -r .status) [ "$status" = "transcribed" ] && break [ "$status" = "failed" ] && { echo "transcription failed: $(echo "$resp" | jq -r '.error_reason // "unknown"')" exit 1 } sleep 10done
# 3. Find the transcript and download SRTtranscript_id=$(curl -sS "https://app.uspeech.io/api/transcripts/?uploaded_file=${file_id}" \ -H "Authorization: Api-Key $USPEECH_KEY" | jq -r '.results[0].id')
url=$(curl -sS "https://app.uspeech.io/api/transcripts/${transcript_id}/download/?type=srt" \ -H "Authorization: Api-Key $USPEECH_KEY" | jq -r .download_url)
curl -o interview.srt "$url"import osimport time
import requests
BASE_URL = "https://app.uspeech.io"HEADERS = {"Authorization": f"Api-Key {os.environ['USPEECH_KEY']}"}
# 1. Uploadwith open("interview.wav", "rb") as fh: response = requests.post( f"{BASE_URL}/api/files/", headers=HEADERS, data={"project": 312, "file_type": "audio"}, files={"file": ("interview.wav", fh, "audio/wav")}, timeout=300, )response.raise_for_status()file_id = response.json()["id"]
# 2. Pollwhile True: file_data = requests.get( f"{BASE_URL}/api/files/{file_id}/", headers=HEADERS, timeout=30 ).json() if file_data["status"] == "transcribed": break if file_data["status"] == "failed": raise SystemExit( f"transcription failed: {file_data.get('error_reason') or 'unknown'}" ) time.sleep(10)
# 3. Find the transcript and download SRTtranscripts = requests.get( f"{BASE_URL}/api/transcripts/", headers=HEADERS, params={"uploaded_file": file_id}, timeout=30,).json()transcript_id = transcripts["results"][0]["id"]
url = requests.get( f"{BASE_URL}/api/transcripts/{transcript_id}/download/", headers=HEADERS, params={"type": "srt"}, timeout=30,).json()["download_url"]
with open("interview.srt", "wb") as fh: fh.write(requests.get(url, timeout=120).content)import { openAsBlob } from 'node:fs';import { writeFile } from 'node:fs/promises';
const BASE_URL = 'https://app.uspeech.io';const HEADERS = { Authorization: `Api-Key ${process.env.USPEECH_KEY}` };const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// 1. Uploadconst form = new FormData();form.set('project', '312');form.set('file_type', 'audio');form.set('file', await openAsBlob('./interview.wav'), 'interview.wav');
const upload = await fetch(`${BASE_URL}/api/files/`, { method: 'POST', headers: HEADERS, body: form,});if (!upload.ok) throw new Error(await upload.text());const fileId = (await upload.json()).id;
// 2. Pollwhile (true) { const fileData = await fetch(`${BASE_URL}/api/files/${fileId}/`, { headers: HEADERS, }).then((r) => r.json()); if (fileData.status === 'transcribed') break; if (fileData.status === 'failed') { throw new Error(`transcription failed: ${fileData.error_reason || 'unknown'}`); } await sleep(10_000);}
// 3. Find the transcript and download SRTconst transcripts = await fetch( `${BASE_URL}/api/transcripts/?uploaded_file=${fileId}`, { headers: HEADERS },).then((r) => r.json());const transcriptId = transcripts.results[0].id;
const { download_url: url } = await fetch( `${BASE_URL}/api/transcripts/${transcriptId}/download/?type=srt`, { headers: HEADERS },).then((r) => r.json());
await writeFile('interview.srt', Buffer.from(await fetch(url).then((r) => r.arrayBuffer())));