Skip to content

Ask AI

Ask a question about Scribewren, such as uploading a file, editing a transcript, plans or the API.

Ctrl or Cmd+Enter to send

Answers are written by AI and can be wrong. For account or billing help, use the chat or contact us. Contact support (opens in a new tab)

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.

  • An Unlimited plan and an API key. See Authentication.
  • bash, jq and curl 7.84 or later. The script reads the ETag of each uploaded part with curl’s %header{etag} write-out, which needs that version.
  • Your key in an environment variable, and an audio file called interview.mp3 in the current directory:
Terminal window
export SCRIBEWREN_API_KEY='swp_0190EXAMPLE_EXAMPLEEXAMPLEEXAMPLE'
#!/usr/bin/env bash
set -euo pipefail
API=https://api.scribewren.com/v1
AUTH="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=0
for 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 5
done
# 6. Read the segments page by page. nextSeq is null after the last page.
SEQ=0
while [ "$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.srt
  1. Create the media record. POST /media registers the file and returns the media ID and an upload object with partSizeBytes and partCount. The options object is required. See Uploads for every field.
  2. Split the file. Each part except the last is exactly partSizeBytes long. The number of parts must equal partCount.
  3. Upload the parts. POST /media/{id}/parts returns a presigned URL for each part number you ask for. You send the bytes with PUT straight to that URL, not to the API, and the response carries an ETag header. 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.
  4. Complete. POST /media/{id}/complete takes { 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.
  5. Poll. GET /transcriptions/{id} reports status, stage and progress. The loop ends on succeeded. On failed it prints errorCode. Prefer webhooks over polling in production, and poll no faster than every few seconds.
  6. Read segments. Each page returns up to limit segments and a nextSeq cursor. See Segments.
  7. Export. GET /transcriptions/{id}/export returns 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."}
  • 403 FEATURE_NOT_IN_PLAN on the first call means the key’s owner is not on Unlimited.
  • 403 PLAN_LIMIT_EXCEEDED on step 1 or 4 means a plan limit was reached. error.details.limit says which one. See Errors.
  • 400 UPLOAD_INVALID on step 4 means the parts do not match the upload. Check error.details.reason.
  • 429 RATE_LIMITED means wait for Retry-After seconds. 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