Wiring up a transcription API looks like a one-afternoon job. You POST a media URL, you get JSON back, you ship. Two weeks later your billing page shows 412 tasks for 380 videos, three users say the subtitles are missing, and a support ticket reports that a transcript "expired."
None of those failures are about transcription quality. They are about the contract around the transcription: how retries get deduplicated, how you wait, and what happens when one part of a multi-feature request fails.
I have been integrating the Transcript API into a content pipeline, and its documentation is unusually explicit about those contracts, which makes it a good concrete example. The patterns below apply to any asynchronous media API.
The work is asynchronous, so the shape of your code changes
Transcription is not a request-response operation. A 90-minute recording takes real time to process, so the create call does not wait for it.
POST /transcriptions returns HTTP 202 with a task object, immediately:
{
"id": "req_example_001",
"object": "transcription",
"status": "queued",
"poll_url": "/openapi/v1/transcriptions/req_example_001",
"retry_after": 3
}
Three fields matter. id is your handle for everything else. poll_url is where you check progress. retry_after is how long the server wants you to wait before asking again.
If you are building a web app, this means you cannot answer the HTTP request with a transcript. You store the task id, return it to the client, and let a worker finish the job. Every "why does my request time out at 30 seconds" bug traces back to skipping this step.
Idempotency is what stops duplicate billing
The most valuable header in this API is not Authorization. It is Idempotency-Key.
Same key and same body, and the server returns the original task with HTTP 200 instead of creating a second one. Same key with a different body, and you get HTTP 409 idempotency_conflict, which tells you that you reused a key you should not have.
That combination is exactly what you want in production. A submission that times out mid-flight is the worst case for a naive client: you do not know whether the task was created. Without idempotency you either retry and pay twice, or you skip the retry and silently lose the job. With a stable key, retrying is free.
import os, requests
def create_task(source_url, entity_id):
return requests.post(
"https://videotranscriber.ai/openapi/v1/transcriptions",
headers={
"Authorization": f"Bearer {os.environ['VT_API_KEY']}",
"Content-Type": "application/json",
"Idempotency-Key": f"transcribe-{entity_id}",
},
json={"source_url": source_url, "transcription_model": "advanced"},
).json()
Keys must be 1 to 128 visible ASCII characters with no spaces. Derive them from an id you already own, not from a timestamp. And generate a new key whenever you intentionally change the model or the requested features, because a changed body with a reused key is precisely the 409 case.
Poll with the value the server hands you
The documented loop is boring on purpose: read retry_after, sleep, then GET /transcriptions/{request_id}. Stop when the status reaches a terminal state.
Terminal states are succeeded, partial_succeeded, failed, and cancelled. Everything before that is queued or processing, and neither is a reason to poll faster.
Rate limit headers (X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset) and Retry-After are returned, so you can back off intelligently instead of guessing. A tight polling loop buys you HTTP 429 rate_limit_exceeded or concurrency_limit_exceeded and nothing else.
Treat partial success as a first-class outcome
Here is the case most integrations get wrong. A single task can request several features at once: transcription, speaker diarization, chapters, translation, audio events. If chapters succeed and translation fails, the task status is partial_succeeded, not failed.
The status response reports every feature separately, with its own status, quota usage, and error object:
{
"type": "translation",
"status": "succeeded",
"target_language": "es",
"used_quotas": 1,
"error": null
}
And the result endpoint returns only what worked. Features that failed or were never requested come back as null.
So the correct client behavior is: on partial_succeeded, persist the parts that succeeded and log features[].error for the rest. Do not throw away a good transcript because chapter generation failed. Billing follows the same logic, which is fairer than it sounds: reservations for features that fail or get cancelled are released, and partial success charges only the successful features.
Errors arrive in two layers, and both are yours to handle
Request-level failures use HTTP status codes plus a stable English error.code. The documentation is explicit that you should branch on code, never on message.
Task-level failures are a separate layer. A status query returns HTTP 200 even when the task itself failed, so you must inspect the task's own error field. The common ones are task_timeout, provider_unavailable, transcription_failed, and no_speech_detected.
That last one is worth planning for. Silence, music-only uploads, and very noisy files can legitimately contain no speech. It is a data problem, not a retry problem, and retrying it just burns quota.
Retention windows will bite you eventually
Results are available for 30 days. Subtitle download URLs are private, signed, and valid for up to one hour. Task records are kept for the creation month plus the following calendar month in UTC.
If your application needs a transcript six months from now, copy it into your own storage the moment the task succeeds. Do not treat result_url as permanent. And when a download URL expires mid-session, calling the result endpoint again during the retention window issues a fresh one, which is the easy fix people miss before concluding the feature is broken.
When the client is already an AI Agent
The patterns above assume the client is a service you control. If the client is an AI Agent, you can skip most of the glue. The same backend exposes a remote Model Context Protocol server at https://videotranscriber.ai/mcp (Streamable HTTP, Bearer API Key) with three tools that map 1:1 to the three calls in this article: create_transcription, get_transcription, get_transcription_result. Configure it as a single MCP entry in the agent client and the auth, polling, and persistence steps live in the agent's own tool loop instead of in your service.
For agents that need an explicit workflow rather than a blank tool surface, the transcribe-media Agent Skill from the official VideoTranscriberAI/videotranscriber-skills repository installs the same rules this article describes: create once, persist the request id, respect retry_after, stop on a terminal status, and treat partial_succeeded as a partial win. Install with npx skills add VideoTranscriberAI/videotranscriber-skills --skill transcribe-media --agent codex --global, and the MCP server is configured separately on top of it.
Crucially, none of this changes the contract. Idempotency keys, retry_after polling, partial_succeeded handling, two-layer errors, and the 30-day retention window all still apply, because the MCP server and the Skill both call the same OpenAPI backend.
What the surviving integration looks like
The version that holds up in production is roughly 150 lines: an idempotent create, a poller that respects retry_after, a status handler with four branches instead of two, and a persistence step that runs on success.
The Transcript API from Video Transcriber AI gives you 1,500 API Quota on first key creation with no credit card, which is enough to run this loop against real media before you commit to anything. Quota does not expire, so the free allocation is not wasted if you park the project for a month.
Read the error code list before you write the first line. It saved me a round of guessing.


Top comments (0)