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 length | Use |
|---|---|
| Up to five minutes | POST /audio/v1/transcribe |
| More than five minutes, up to 30 minutes | POST /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 what | Behavior |
|---|---|
Query: multichannel | false by default. Set to true to transcribe exactly two input channels independently. |
Query: callback_url | URL 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-Value | Headers 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: callback | Optional 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:
| Status | Meaning |
|---|---|
queued | The job was accepted and is waiting to be processed. |
processing | Audio is being transcribed. progress reports the current progress. |
completed | The result is available in result. |
error | Processing 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.
| Status | Code or response | Meaning |
|---|---|---|
413 | Request entity too large | An HTTP layer rejected an oversized request before the job was created. Reduce the request body. |
400 | invalid_params | The 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. |
401 | Unauthorized | The API key or authenticated identity is missing or no longer valid. |
404 | Not found | The job ID is unknown, expired, deleted, or belongs to another API key. |
429 | job_limit_reached | The 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. |
429 | job_submission_busy | This instance is already processing its three concurrent submission slots. Retry the request after five seconds; the response includes Retry-After: 5. |
503 | job_platform_unavailable | Async 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);