Guide · async
Transcribe a file
Upload a file, wait for its job, read the transcript and export it. Asynchronous; nothing to keep open.
A file transcription is stored: it is listed under Transcriptions, can be shared and exported, and its transcript can be read any time later. The work is asynchronous. The upload answers at once; the server transcribes at full speed (much faster than the audio plays) while you wait on its job: upload, then read the job until it is done, then read the transcript.
To transcribe without storing anything, stream the file through a realtime session instead (Stream a file at full speed).
Upload the file
POST /v1/transcriptionstakes the file asmultipart/form-data(file, plus an optionaloptionspart, JSON) and answers201with the transcription. Itsjob.recordingIdnames the recording the file became: everything that follows is addressed by it.settingsare optional:spokenLanguage(a code such asen; absent, the language is detected),speakerLabels(auto,on,off). Most audio and video files (mp3, wav, flac, ogg, m4a, mp4…) up to the size inGET /v1/realtime/capabilities(limits.maxUploadBytes) are accepted; a video keeps only its audio.Send an
Idempotency-Keywith the upload. If the API answers503 unavailablewhile it wakes, the SDKs retry a create only when it carries one, and a retry with the same key returns the first answer instead of a second transcription.Wait for the job
A job moves
queued→transcribing→done, orfailedwitherrorand, when it is machine-readable,errorCode(insufficient_credit: the file costs more than the account's credit). A link job passes throughfetchingfirst (Transcribe a link).There are no webhooks. Read
GET /v1/recordings/{id}/jobuntil the state isdoneorfailed:vs.jobs.waitdoes it every 2 s, and hands each read toonProgress(on_progressin Python) withprogress.pct(0 to 100) andprogress.processedS, the media seconds done so far; the program prints a line only when the state or the percentage changes. A page that is open anyway can follow the transcription's events socket instead (WS /v1/transcriptions/{id}/events, ajob.progressframe per step).Read the transcript
GET /v1/recordings/{id}/transcriptanswers with the whole transcript:sentences(what to show people, withspeaker,startandendin seconds),segments(with word timings inwords) andspeakers. Speakers are numbered from 0;-1means speaker labels were off. Addlang=esto read it translated.Export it
GET /v1/recordings/{id}/exportreturns a file:format=srt,json,xlsxorpdf, withlang=for a translated one. In the SDKs,vs.recordings.exportreturns the response; read its bytes.
TypeScript SDK
import { writeFile } from 'node:fs/promises';
import { VocalStack } from '@vocalstack/sdk';
const vs = new VocalStack({ baseUrl: 'https://api.vocalstack.com', apiKey: process.env.VOCALSTACK_API_KEY });
const tx = await vs.transcriptions.create(
{ file: './talk.mp3', name: 'Talk', settings: { spokenLanguage: 'en', speakerLabels: 'on' } },
{ idempotencyKey: crypto.randomUUID() }, // makes a retried upload safe
);
const recordingId = tx.job!.recordingId;
let last = '';
const job = await vs.jobs.wait(recordingId, {
// Called on every read of the job: print it when it changes.
onProgress: (j) => {
const line = `${j.state} ${j.progress.pct}%`;
if (line !== last) console.log((last = line));
},
});
if (job.state !== 'done') throw new Error(`transcription ${job.state}: ${job.error ?? ''}`);
const { sentences } = await vs.recordings.transcript(recordingId);
for (const s of sentences) console.log(s.speaker >= 0 ? `[speaker ${s.speaker}] ${s.text}` : s.text);
const srt = await vs.recordings.export(recordingId, 'srt');
await writeFile('talk.srt', Buffer.from(await srt.arrayBuffer()));
console.log('wrote talk.srt');
Python SDK
import asyncio
import os
import uuid
from vocalstack import TranscriptionJob, VocalStack
async def main() -> None:
async with VocalStack("https://api.vocalstack.com", api_key=os.environ["VOCALSTACK_API_KEY"]) as vs:
tx = await vs.transcriptions.create(
file="./talk.mp3",
name="Talk",
settings={"spokenLanguage": "en", "speakerLabels": "on"},
idempotency_key=str(uuid.uuid4()), # makes a retried upload safe
)
assert tx.job is not None
recording_id = tx.job.recording_id
last = ""
def on_progress(j: TranscriptionJob) -> None:
# Called on every read of the job: print it when it changes.
nonlocal last
line = f"{j.state} {j.progress.pct}%"
if line != last:
last = line
print(line)
job = await vs.jobs.wait(recording_id, on_progress=on_progress)
if job.state != "done":
raise RuntimeError(f"transcription {job.state}: {job.error}")
snapshot = await vs.recordings.transcript(recording_id)
for s in snapshot.sentences:
print(f"[speaker {s.speaker}] {s.text}" if s.speaker >= 0 else s.text)
srt = await vs.recordings.export(recording_id, "srt")
with open("talk.srt", "wb") as f:
f.write(srt)
print("wrote talk.srt")
asyncio.run(main())
HTTP + WebSocket
#!/usr/bin/env bash
# curl and jq, no SDK.
set -euo pipefail
API=https://api.vocalstack.com
AUTH="Authorization: Bearer $VOCALSTACK_API_KEY"
REC=$(curl -sSf "$API/v1/transcriptions" -H "$AUTH" -H "Idempotency-Key: $(uuidgen)" \
-F file=@talk.mp3 \
-F 'options={"name":"Talk","settings":{"spokenLanguage":"en","speakerLabels":"on"}}' \
| jq -r .job.recordingId)
while :; do
JOB=$(curl -sSf "$API/v1/recordings/$REC/job" -H "$AUTH")
STATE=$(jq -r .state <<<"$JOB")
echo "$STATE $(jq -r .progress.pct <<<"$JOB")%"
case $STATE in
done) break ;;
failed) jq -r .error <<<"$JOB" >&2; exit 1 ;;
esac
sleep 2
done
curl -sSf "$API/v1/recordings/$REC/transcript" -H "$AUTH" \
| jq -r '.sentences[] | if .speaker >= 0 then "[speaker \(.speaker)] \(.text)" else .text end'
curl -sSf "$API/v1/recordings/$REC/export?format=srt" -H "$AUTH" -o talk.srt