Long audio (async transcription)

Beta: Long audio is an experimental endpoint under active development. Expect occasional bugs and rough edges, and expect the API to change as the beta evolves.

Use asynchronous transcription for recordings longer than five minutes and up to 30 minutes. For shorter recordings, use Transcribe, which returns its result in the same request.

When to use async transcription

Recording lengthUse
Up to five minutesPOST /audio/v1/transcribe
More than five minutes, up to 30 minutesPOST /audio/v1/transcriptions

The duration limit is based on decoded audio. Recordings longer than 30 minutes must be split before submission.

Submit a job

Submit to:

POST https://api.labs.bandwidth.com/audio/v1/transcriptions

Authenticate with the X-BW-LABS-API-KEY header. The request accepts the same audio and transcription parameters as Transcribe, along with the callback parameters described below.

Upload audio

Send a WAV recording as audio/wav, or raw little-endian signed 16-bit linear16 audio as application/octet-stream. WAV uploads must not include encoding or sample_rate query parameters. Raw uploads require encoding=linear16 and sample_rate=8000 or 16000; channels can be 1 or 2 and defaults to 1.

Direct uploads are limited to 512 MiB and 30 minutes of decoded audio. Set multichannel=true only when the audio has exactly two channels.

curl --request POST \
  'https://api.labs.bandwidth.com/audio/v1/transcriptions?callback_url=https%3A%2F%2Fhooks.example.com%2Fstt' \
  --header 'X-BW-LABS-API-KEY: bwa_key_...' \
  --header 'Content-Type: audio/wav' \
  --header 'X-Callback-Auth-Name: X-Customer-Token' \
  --header 'X-Callback-Auth-Value: callback-secret' \
  --data-binary @call.wav

The service accepts the job and returns 202 with its ID and initial status:

{"id":"7d3b8c9e-2d8a-4a8e-9e4f-3f4a1d5a2c10","status":"queued"}

Submit a public audio URL

For a URL submission, send one audio_url and, optionally, a callback object in a JSON body. The URL must be HTTPS and publicly reachable. The service allows up to 512 MiB for the downloaded audio. The entire download, including redirects and reading the response body, must finish within 60 seconds, and the service follows at most three redirects. The downloaded audio must still decode to no more than 30 minutes.

curl --request POST \
  'https://api.labs.bandwidth.com/audio/v1/transcriptions' \
  --header 'X-BW-LABS-API-KEY: bwa_key_...' \
  --header 'Content-Type: application/json' \
  --data '{"audio_url":"https://example.com/recordings/call.wav","callback":{"url":"https://hooks.example.com/stt","auth_header_name":"X-Customer-Token","auth_header_value":"callback-secret"}}'

Optional submission parameters

Where and whatBehavior
Query: multichannelfalse by default. Set to true to transcribe exactly two input channels independently.
Query: callback_urlURL to notify when processing completes. For uploads, pass it in the query. For JSON submissions, it may also be passed in the query, or use the url field in the JSON callback object. Use an HTTPS callback URL. HTTP is accepted, but the callback and any credential header you configure then travel in plaintext.
Upload request headers: X-Callback-Auth-Name and X-Callback-Auth-ValueHeaders carrying your callback header name and value for an upload. A JSON URL submission may carry the same values as auth_header_name and auth_header_value in its callback object.
JSON field: callbackOptional object for URL submissions with url, auth_header_name, and auth_header_value fields.

Do not send callback_auth_header_name or callback_auth_header_value as query parameters. The service rejects them with 400 invalid_params.

Poll for a result

Fetch a job with:

GET https://api.labs.bandwidth.com/audio/v1/transcriptions/{id}

Authenticate each request with the same API key. The response includes the job ID, status, progress, creation and update times, and either result or error. result is null until the job completes, and error is null unless the job fails.

{
  "id":"7d3b8c9e-2d8a-4a8e-9e4f-3f4a1d5a2c10",
  "status":"processing",
  "progress":0.5,
  "created_at":"2026-09-03T16:00:00Z",
  "updated_at":"2026-09-03T16:04:12Z",
  "result":null,
  "error":null
}

The status lifecycle is:

StatusMeaning
queuedThe job was accepted and is waiting to be processed.
processingAudio is being transcribed. progress reports the current progress.
completedThe result is available in result.
errorProcessing failed. error contains a code and message.

Result shape

When status is completed, result has the same shape as a successful Transcribe response. For a single-channel job, it includes request_id, text, words, segments, audio_duration_seconds, and model_info. PII redaction fields appear when the corresponding redaction options are enabled.

With multichannel=true, the result instead contains channels[] with one entry per input channel. Each entry has channel, text, words, and segments; redacted results may also include redacted_entities. The multichannel result does not include top-level text, words, or segments.

{
  "request_id":"6f58c1c6-7e0c-4bb8-9d72-3fb3d4c5c1aa",
  "audio_duration_seconds":372.4,
  "model_info":{"name":"bw-listen-en","version":"current"},
  "channels":[
    {
      "channel":0,
      "text":"Please send the invoice today.",
      "words":[{"word":"Please","start":0.00,"end":0.42}],
      "segments":[{"start":0.00,"end":2.18,"text":"Please send the invoice today."}]
    },
    {
      "channel":1,
      "text":"I will send it this afternoon.",
      "words":[{"word":"I","start":0.00,"end":0.18}],
      "segments":[{"start":0.00,"end":2.04,"text":"I will send it this afternoon."}]
    }
  ]
}

Callbacks

Callbacks are notifications, not a result channel. When a job completes successfully, the service sends a POST to callback_url with only the job ID and completed status:

{"id":"7d3b8c9e-2d8a-4a8e-9e4f-3f4a1d5a2c10","status":"completed"}

After a failed delivery, the next delivery attempt is scheduled no sooner than 30 seconds later, and the service's periodic sweep may delay it further. Up to 10 failures are recorded; after the tenth failure, delivery stops. Delivery is at least once, so receivers should deduplicate notifications by job ID. No callback is sent when a job ends in error. A failed worker job has error.code set to chunk_failed.

For an upload, send X-Callback-Auth-Name and X-Callback-Auth-Value request headers with your callback header name and value. For a JSON URL submission, put those values in the callback object as auth_header_name and auth_header_value. Do not put callback credentials in query parameters. The callback does not include a service signature header. After receiving a notification, always fetch the result through the authenticated GET /audio/v1/transcriptions/{id} request.

Delete a job

Deleting a job removes the job record and its stored audio and results. It does not remove separately retained session captures or usage records, and usage for a completed job is still recorded. If the platform's own retention capture for the job had not finished when you deleted it, that capture is cancelled rather than rebuilt from the deleted data; a capture that had already completed is kept.

Delete a job with:

DELETE https://api.labs.bandwidth.com/audio/v1/transcriptions/{id}

The endpoint returns 204 No Content when the job is deleted. A job ID that is unknown, expired, or belongs to another API key returns 404.

Limits and errors

Retention uses separate clocks. The job record expires seven days after its last update. Each stored object, including the uploaded audio and the result, expires on its own seven-day lifecycle starting when it was written. A job record can therefore outlive its result; a GET then reports the result as no longer available. Once the stored audio or result has expired, the platform does not rebuild a retention capture from it. A job's completed status and its callback describe the transcription result only, not retention. Lifecycle removal may occur shortly after expiry, so these are not exact erasure times. Delete data sooner when it is no longer needed. The 512 MiB upload limit is shared by direct uploads and audio downloaded from audio_url.

StatusCode or responseMeaning
413Request entity too largeAn HTTP layer rejected an oversized request before the job was created. Reduce the request body.
400invalid_paramsThe request failed validation. This includes unsupported content types, invalid JSON or audio_url values, callback credentials in query parameters, audio over the shared 512 MiB limit, decoded audio over 30 minutes (1800 seconds), or multichannel=true without exactly two channels. The message identifies the condition.
401UnauthorizedThe API key or authenticated identity is missing or no longer valid.
404Not foundThe job ID is unknown, expired, deleted, or belongs to another API key.
429job_limit_reachedThe per-key in-flight limit is four. Its ledger counts queued and processing jobs, plus completed jobs whose final bookkeeping has not yet finished. A job is normally released shortly after it completes, but it stays counted while its final usage bookkeeping retries, and an outage of the usage store can prolong this. Retry the request after 30 seconds; the response includes Retry-After: 30. The message is the per-key in-flight transcription limit has been reached.
429job_submission_busyThis instance is already processing its three concurrent submission slots. Retry the request after five seconds; the response includes Retry-After: 5.
503job_platform_unavailableAsync transcription jobs are temporarily unavailable. For example, the message is async transcription jobs are unavailable.

The async endpoint reports its own upload-size and decoded-duration validation as 400 invalid_params; an HTTP gateway can return 413 before the request reaches the endpoint.

SDK examples

The SDKs provide helpers for submitting, inspecting, waiting for, and deleting jobs. See SDKs for installation.

job = client.transcriptions.submit("call.wav")
current = client.transcriptions.get(job.id)  # TranscriptionJob: status, progress
result = client.transcriptions.wait(job.id)  # Transcription; raises TranscriptionJobError if the job fails
print(result.text)
client.transcriptions.delete(job.id)
import { readFile } from "node:fs/promises";

const job = await client.transcriptions.submit({ audio: await readFile("call.wav"), raw: false });
const current = await client.transcriptions.get(job.id);
const result = await client.transcriptions.wait(job.id);
console.log(result.text);
await client.transcriptions.delete(job.id);