> ## Documentation Index
> Fetch the complete documentation index at: https://docs.inworld.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# 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](https://docs.inworld.ai/stt/quickstart.md) instead; for a short clip, the synchronous endpoint is simpler.

## How it works

<Steps titleSize="h3">
    <Step title="Submit the recording">
      `POST /stt/v1/transcribe:async` returns immediately with an **operation** — a handle naming the job, not the transcript.
    </Step>
    <Step title="Poll the operation">
      `GET /lro/v1alpha/{operation name}` reports whether the job has finished. Poll every few seconds; a long recording takes a while.
    </Step>
    <Step title="Download the transcript">
      A finished operation carries a link to the transcript. The link is signed and needs no credentials.
    </Step>
</Steps>

## 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

```bash
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.

```bash
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

```json
{
  "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.

<Tip>
  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>`.
</Tip>

## 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](https://docs.inworld.ai/stt/audio-formats.md) 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](https://docs.inworld.ai/stt/best-practices.md) for how to choose terms.

[Speaker diarization](https://docs.inworld.ai/stt/speaker-diarization.md), [voice profiles](https://docs.inworld.ai/stt/voice-profiles.md) 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:

```json
{
  "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:

```json
{
  "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:

```json
{
  "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](https://docs.inworld.ai/stt/voice-profiles.md) 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.

<Note>
  `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.
</Note>

<Note>
  `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.
</Note>

## 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 |

<Note>Asynchronous transcription requires a payment method on your account. Without one, a submit returns `402`.</Note>

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. |
