Capabilities
Asynchronous transcription
Submit a long recording as a job and collect the transcript when it finishes
Synchronous transcription returns a transcript in one request. Use it when the recording fits in the request body and you can wait for processing. For recordings too large or too long for that approach, use asynchronous transcription: submit the recording, receive a job, and collect the transcript when it finishes.
Use it for recordings measured in minutes or hours — meetings, interviews, podcasts, call archives — and for video files, whose audio track it extracts. For live audio, use streaming transcription instead; for a short clip, the synchronous endpoint is simpler.
How it works
Sending the audio
Three ways, and the right one depends mostly on size.
| Way | Field | Use it when |
|---|---|---|
| Inline | audioData.content | The recording is small. Audio is base64-encoded, which makes the request about a third larger than the file. |
| Multipart | multipart/form-data | The recording is large. The file is streamed rather than held whole. |
| By URL | audioUri | The file already lives somewhere reachable, such as cloud storage. |
Inline
curl -X POST https://api.inworld.ai/stt/v1/transcribe:async \
-H "Authorization: Basic $INWORLD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"transcribeConfig": {
"modelId": "inworld/inworld-stt-1",
"audioEncoding": "LINEAR16"
},
"audioData": { "content": "<base64-encoded audio>" }
}'Multipart
Send the config first and the file second. The upload is streamed as it arrives, so a config that comes after the audio is read too late.
curl -X POST https://api.inworld.ai/stt/v1/transcribe:async \
-H "Authorization: Basic $INWORLD_API_KEY" \
-F 'transcribeConfig={"modelId":"inworld/inworld-stt-1","audioEncoding":"LINEAR16"};type=application/json' \
-F "file=@recording.wav"By URL
{
"transcribeConfig": { "modelId": "inworld/inworld-stt-1", "audioEncoding": "LINEAR16" },
"audioUri": "https://storage.googleapis.com/your-bucket/recording.wav?..."
}The URL must use https and serve the file directly without your credentials. It can be public or include its own authorization, such as a signed cloud-storage URL. Redirects are refused, so use the URL that serves the file. Audio and video files are fetched the same way. The submit request waits for the fetch: an unreachable URL fails the submit, not the job, and submitting a long recording takes as long as fetching it.
Most convenient sharing links redirect — file-sharing services, shortened URLs, and console URLs such as storage.cloud.google.com. Use the direct serving URL, for example https://storage.googleapis.com/<bucket>/<object>.
Configuration
transcribeConfig takes the same fields as synchronous transcription, with two differences.
Every audio encoding is accepted, including the compressed formats streaming rejects — MP3, FLAC, OGG_OPUS — because the audio is a stored file rather than a live stream. AUTO_DETECT works where the file carries headers.
Video files work too. Send an MP4 (also .m4v and .mov), WEBM or MXF and its audio track is extracted and transcribed; the video is discarded and you are billed for the audio duration. Use async for video: a few minutes of 1080p exceeds the synchronous endpoint's ~16 MB cap even when the audio is small. An async job accepts up to 512 MB. With audioUri, Inworld fetches the file from its URL, so you do not upload it in the submit request. The codec inside the container is read from the file — see audio formats for the full list.
Live-session fields are ignored: inactivityTimeoutSeconds and endOfTurnConfidenceThreshold mean nothing to a recording that is already complete.
Custom vocabulary works here too. Pass prompts exactly as in synchronous transcription, with the same limits: at most 100 terms, each using only letters, digits, spaces, and . , ' ( ) : ! ? ; - — a term with any other character (such as & or @) rejects the submit with INVALID_ARGUMENT. See best practices for how to choose terms.
Speaker diarization, voice profiles and word timestamps all work as they do synchronously. Their output lands in the transcript document rather than in the operation — see below.
Reading the result
While the job runs, the operation reports done: false. When it finishes:
{
"name": "workspaces/my-workspace/sttTranscriptionJobs/6f1c.../operations/1790...",
"done": true,
"response": {
"resultUri": "https://storage.googleapis.com/...?X-Goog-Signature=...",
"expireTime": "2026-10-01T12:00:00Z",
"language": "English",
"usage": { "transcribedAudioMs": 7200000 }
}
}A job that failed carries an error instead of a response, with a code and a message. Either way the operation is done, so polling ends.
resultUri points at the transcript document:
{
"jobId": "6f1c...",
"transcript": "Full text of the recording...",
"language": "English",
"segments": [
{ "endTimeMs": 7200000, "transcript": "Full text of the recording..." }
],
"usage": { "transcribedAudioMs": 7200000, "modelId": "inworld/inworld-stt-1" }
}Word timestamps and speaker labels
Asking for includeWordTimestamps adds a wordTimestamps array to each segment, and enableSpeakerDiarization adds a speaker to each word in it:
{
"segments": [
{
"endTimeMs": 25861,
"transcript": "Furthermore, our universal fit phone cases...",
"wordTimestamps": [
{ "word": "Furthermore", "startTimeMs": 60, "endTimeMs": 870, "speaker": 0 },
{ "word": "our", "startTimeMs": 1240, "endTimeMs": 1420, "speaker": 0 }
]
}
]
}Speaker identifiers are small integers starting at 0, scoped to the one job: they mark "same speaker in this recording", not a person you can recognise across jobs. There is no segment-level speaker field — group words by their speaker value to build per-speaker segments.
Voice profiles arrive the same way. Asking for voiceProfileConfig.enableVoiceProfile adds a voiceProfile object to each segment. The profile has the same categories and { label, confidence } shape as a synchronous response.
language is the detected language's name, not a BCP-47 tag. The millisecond fields arrive as JSON numbers. A field with a value of zero is omitted rather than sent. Examples include startTimeMs for the first segment or first word.
segments currently contains a single entry spanning the whole recording. The field is a list so that finer segmentation can be added without changing the document's shape. Do not rely on there being exactly one entry.
Limits
| Limit | Value |
|---|---|
| Audio per job | 512 MB |
| Jobs in flight per account | Depends on your plan |
| Result retention | 7 days from completion, after which the transcript is deleted and its link stops working |
| Submitted audio retention | 7 days, after which the recording is deleted |
audioUri fetch timeout | 5 minutes, and the submit waits for it |
Submitting while you are already at your in-flight limit is refused with RESOURCE_EXHAUSTED, before the audio is transferred. Wait for a job to finish and submit again.
The link is signed once, when the job completes. Polling the operation again returns the same link rather than a fresh one. The seven days run from completion, however late you collect the result. Download the transcript and keep your own copy if you need it after that; neither it nor the submitted audio is a permanent store.
Errors
| What you see | What it means |
|---|---|
401 / 403 | The API key is missing, wrong, or belongs to a different environment. |
RESOURCE_EXHAUSTED | Either the in-flight job limit, or the request rate limit. The message says which. |
INVALID_ARGUMENT | A malformed request — check audioEncoding, modelId, and the audioUri rules above. |
FAILED_PRECONDITION | The audioUri could not be fetched. The request was well formed; the fetch was not. It is answered on the submit, since that is when the fetch happens. |