Quickstart
This script uploads one audio file, waits for the transcription and saves subtitles. It uses only curl and jq, so you can follow every request before you move to your own language.
Before you start
Section titled “Before you start”- An Unlimited plan and an API key. See Authentication.
bash,jqandcurl7.84 or later. The script reads the ETag of each uploaded part withcurl’s%header{etag}write-out, which needs that version.- Your key in an environment variable, and an audio file called
interview.mp3in the current directory:
export SCRIBEWREN_API_KEY='swp_0190EXAMPLE_EXAMPLEEXAMPLEEXAMPLE'The script
Section titled “The script”#!/usr/bin/env bashset -euo pipefailAPI=https://api.scribewren.com/v1AUTH="Authorization: Bearer $SCRIBEWREN_API_KEY"WORK=$(mktemp -d)trap 'rm -rf "$WORK"' EXIT
# 1. Create the media record. "options" is required.SIZE=$(wc -c < interview.mp3 | tr -d ' ')MEDIA=$(jq -n --argjson size "$SIZE" '{filename: "interview.mp3", sizeBytes: $size, contentType: "audio/mpeg", options: {language: "auto", diarization: true}}' | curl -sS --fail-with-body -X POST "$API/media" -H "$AUTH" -H 'Content-Type: application/json' -d @-)MEDIA_ID=$(echo "$MEDIA" | jq -er '.data.id')PART_SIZE=$(echo "$MEDIA" | jq -er '.data.upload.partSizeBytes')
# 2. Split the file into parts of partSizeBytes. The last part is shorter.split -a 4 -b "$PART_SIZE" interview.mp3 "$WORK/part-"
# 3. For each part: ask for a presigned URL, PUT the bytes to it, and keep the ETag exactly as returned.N=0for f in "$WORK"/part-*; do N=$((N + 1)) URL=$(jq -n --argjson n "$N" '{partNumbers: [$n]}' | curl -sS --fail-with-body -X POST "$API/media/$MEDIA_ID/parts" -H "$AUTH" -H 'Content-Type: application/json' -d @- | jq -er '.data.parts[0].url') ETAG=$(curl -sS -f -X PUT --upload-file "$f" "$URL" -o /dev/null -w '%header{etag}') jq -n --argjson n "$N" --arg etag "$ETAG" '{partNumber: $n, etag: $etag}' >> "$WORK/etags.jsonl"done
# 4. Complete the upload with every part, in order. This queues the transcription.TX_ID=$(jq -s '{parts: .}' "$WORK/etags.jsonl" | curl -sS --fail-with-body -X POST "$API/media/$MEDIA_ID/complete" -H "$AUTH" -H 'Content-Type: application/json' -d @- | jq -er '.data.transcription.id')
# 5. Poll until the job ends. A webhook can replace this loop.while :; do TX=$(curl -sS --fail-with-body "$API/transcriptions/$TX_ID" -H "$AUTH") STATUS=$(echo "$TX" | jq -er '.data.status') case "$STATUS" in succeeded) break ;; failed | canceled) echo "$TX" | jq '.data | {status, errorCode}' >&2; exit 1 ;; esac sleep 5done
# 6. Read the segments page by page. nextSeq is null after the last page.SEQ=0while [ "$SEQ" != null ]; do PAGE=$(curl -sS --fail-with-body "$API/transcriptions/$TX_ID/segments?fromSeq=$SEQ&limit=500&includeWords=false" -H "$AUTH") echo "$PAGE" | jq -c '.data.segments[] | {startMs, speakerId, text}' SEQ=$(echo "$PAGE" | jq -r '.data.nextSeq')done
# 7. Export subtitles.curl -sS --fail-with-body "$API/transcriptions/$TX_ID/export?format=srt" -H "$AUTH" -o interview.srtWhat each step does
Section titled “What each step does”- Create the media record.
POST /mediaregisters the file and returns the media ID and anuploadobject withpartSizeBytesandpartCount. Theoptionsobject is required. See Uploads for every field. - Split the file. Each part except the last is exactly
partSizeByteslong. The number of parts must equalpartCount. - Upload the parts.
POST /media/{id}/partsreturns a presigned URL for each part number you ask for. You send the bytes withPUTstraight to that URL, not to the API, and the response carries anETagheader. The URL is valid for one hour, so request it just before you use it. Keep the ETag value exactly as returned, including its quotes. - Complete.
POST /media/{id}/completetakes{ partNumber, etag }for every part, in order. The API assembles the file, checks its size, creates the transcription and returns both objects. Repeating the call after a success returns the same result. - Poll.
GET /transcriptions/{id}reportsstatus,stageandprogress. The loop ends onsucceeded. Onfailedit printserrorCode. Prefer webhooks over polling in production, and poll no faster than every few seconds. - Read segments. Each page returns up to
limitsegments and anextSeqcursor. See Segments. - Export.
GET /transcriptions/{id}/exportreturns the file itself. See Export.
Step 6 prints one JSON object per segment:
{"startMs":0,"speakerId":"019a4f2e-8a10-7c4b-9e21-6f0b3d5a7c11","text":"Welcome to the show."}When something fails
Section titled “When something fails”403 FEATURE_NOT_IN_PLANon the first call means the key’s owner is not on Unlimited.403 PLAN_LIMIT_EXCEEDEDon step 1 or 4 means a plan limit was reached.error.details.limitsays which one. See Errors.400 UPLOAD_INVALIDon step 4 means the parts do not match the upload. Checkerror.details.reason.429 RATE_LIMITEDmeans wait forRetry-Afterseconds. See Rate limits.
Can't find what you need? Contact support, email [email protected], or open Help and then Contact in the app. scribewren.com