Transcription API
Version v1 · Last updated: October 1, 2026
The Online Transcript API lets you transcribe YouTube videos, audio files and video files from your own code, with the same engine and automatic punctuation as the website. It is included in the Pro and Max plans at no extra cost; every request uses the same monthly minutes as the website.
The API is asynchronous: you submit a job, receive an id immediately, and then ask for the result. A short file is usually ready in a few seconds; a one-hour video in about a minute.
Authentication
Create a key in My profile → API access on the website. Keys start with ot_live_ and are shown only once: store them as a secret on your server and never put them in browser or mobile app code. You can have up to 5 active keys and revoke any of them at any time.
Send the key in every request, in either of these headers:
Authorization: Bearer ot_live_xxxxxxxxxxxxxxxx
X-API-Key: ot_live_xxxxxxxxxxxxxxxxBase URL: https://api.online-transcript.com/api/v1. All responses are JSON in UTF-8.
Quick start
# 1. Submit a YouTube video
curl -X POST https://api.online-transcript.com/api/v1/transcriptions \
-H "Authorization: Bearer $OT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://www.youtube.com/watch?v=jNQXAC9IVRw"}'
# → 202 {"id": "tr_8f3c...", "status": "queued", ...}
# 2. Ask for the result until status is "completed" or "failed"
curl https://api.online-transcript.com/api/v1/transcriptions/tr_8f3c... \
-H "Authorization: Bearer $OT_API_KEY"
# 3. Or download it straight as plain text
curl "https://api.online-transcript.com/api/v1/transcriptions/tr_8f3c...?format=txt" \
-H "Authorization: Bearer $OT_API_KEY" -o transcript.txtPOST /transcriptions — create a job
From a YouTube URL (JSON)
POST /api/v1/transcriptions
Content-Type: application/json
{
"url": "https://www.youtube.com/watch?v=VIDEO_ID",
"punctuate": true
}From a file (multipart/form-data)
curl -X POST https://api.online-transcript.com/api/v1/transcriptions \
-H "Authorization: Bearer $OT_API_KEY" \
-F "file=@interview.mp4" \
-F "punctuate=true"| Field | Type | Description |
|---|---|---|
url | string | YouTube link (youtube.com, youtu.be, m.youtube.com). Use this or file. |
file | file | Audio: mp3, wav, m4a, ogg, opus, flac, aac. Video: mp4, mov, mkv, webm, avi, m4v, 3gp, mpeg, mpg, wmv, flv, ts. Up to 1 GB. Videos are converted to audio on our server and deleted right away. |
punctuate | boolean | Optional, default true. Adds punctuation and paragraphs without changing words. Send false to get the raw engine output only (slightly faster). |
Response: 202 Accepted with the transcription object in status queued and a Location header pointing to it. The transcript keeps the language spoken in the audio; it is never translated.
GET /transcriptions/{id} — status and result
GET /api/v1/transcriptions/tr_8f3c... → JSON object
GET /api/v1/transcriptions/tr_8f3c...?format=txt → text/plain (only when completed)Statuses: queued → processing → completed or failed. Poll every 5–10 seconds; there is no benefit in polling faster, and it counts towards the rate limit.
GET /transcriptions — list jobs
Returns your most recent jobs, newest first, without the text. Parameters: limit (1–100, default 20) and status (queued, processing, completed, failed).
GET /api/v1/transcriptions?limit=10&status=completed
{"object": "list", "data": [ {...}, {...} ]}DELETE /transcriptions/{id} — delete a job
Deletes a completed or failed job and its text from our servers. Jobs still running cannot be deleted (409). Results are deleted automatically 7 days after they are created.
DELETE /api/v1/transcriptions/tr_8f3c...
{"id": "tr_8f3c...", "object": "transcription", "deleted": true}GET /usage — monthly allowance
{
"object": "usage",
"plan": "pro",
"period": "2026-10",
"unlimited": false,
"limit_minutes": 300,
"used_minutes": 42,
"used_seconds": 2533,
"remaining_minutes": 257,
"transcriptions": 18,
"active_jobs": 1,
"max_active_jobs": 3,
"rate_limit_per_minute": 60
}On Max, unlimited is true and limit_minutes / remaining_minutes are null. The allowance resets on the first day of each calendar month (UTC) and is shared with the website.
The transcription object
{
"id": "tr_8f3c1d0e9a7b6c5d4e3f2a1b",
"object": "transcription",
"status": "completed",
"source": {"type": "youtube", "url": "https://www.youtube.com/watch?v=..."},
"title": "Me at the zoo",
"duration_seconds": 19.0,
"punctuate": true,
"created_at": "2026-10-01T21:24:14+00:00",
"started_at": "2026-10-01T21:24:14+00:00",
"completed_at": "2026-10-01T21:24:31+00:00",
"text": "Alright, so here we are, in front of the elephants...",
"raw_text": "Alright so here we are in front of the elephants..."
}source.typeisyoutube(withurl) orfile(withfilename).textandraw_textonly appear when the status iscompleted.raw_textis the engine output before punctuation.titleandduration_secondsare filled in once processing starts.- When the status is
failedthere is anerrorfield:{"code": "...", "message": "..."}.
Errors
Every error response has the same shape:
{"error": {"code": "QUOTA_EXCEEDED", "message": "You reached your monthly limit of 300 minutes."}}Errors when calling the API (HTTP status)
| HTTP | Code | Meaning |
|---|---|---|
| 400 | MISSING_INPUT | No 'url' and no 'file' were sent. |
| 400 | INVALID_URL | The URL is not a YouTube link. |
| 400 | UNSUPPORTED_FORMAT | The file extension is not supported. |
| 401 | INVALID_API_KEY | Missing, malformed or revoked key. |
| 403 | PLAN_REQUIRED | The account is no longer on Pro or Max. |
| 404 | NOT_FOUND | The transcription does not exist or belongs to another account. |
| 409 | NOT_READY | You asked for ?format=txt before the job completed. |
| 409 | JOB_IN_PROGRESS | You tried to delete a job that is still running. |
| 413 | FILE_TOO_LARGE | The upload is larger than 1 GB. |
| 429 | RATE_LIMITED | More than 60 requests in a minute. Honour the Retry-After header. |
| 429 | TOO_MANY_ACTIVE_JOBS | You already have 3 jobs queued or processing. |
| 429 | QUOTA_EXCEEDED | Your monthly minutes are used up. |
| 503 | API_DISABLED | The API is temporarily switched off for maintenance. |
Reasons a job ends in status "failed"
| error.code | Meaning |
|---|---|
QUOTA_EXCEEDED | The audio turned out longer than the minutes you have left. |
DOWNLOAD_FAILED | The YouTube video is private, removed, age- or region-restricted. |
SERVICE_MAINTENANCE | YouTube downloads are temporarily unavailable. Upload the file instead. |
NO_AUDIO_TRACK | The uploaded video has no audio. |
INVALID_MEDIA | The file could not be decoded. |
TRANSCRIPTION_FAILED | The engine failed. Retry in a few minutes. |
SERVER_RESTARTED | The server restarted while the job was running. Submit it again. |
INTERNAL_ERROR | Unexpected error. Submit it again; if it persists, contact us. |
Minutes are only charged for jobs that complete. A failed job costs nothing.
Limits
- Monthly minutes: 300 on Pro, unlimited on Max (fair use), shared with the website.
- Rate limit: 60 requests per minute per key. Above that you get 429 with
Retry-After. - Concurrent jobs: 3 queued or processing per account.
- File size: 1 GB per upload.
- Retention: results are kept for 7 days and then deleted. Save the text on your side.
- Sources: YouTube links and uploaded files. Other websites are not supported.
Examples
Python (requests)
import os, time, requests
API = "https://api.online-transcript.com/api/v1"
HEADERS = {"Authorization": "Bearer " + os.environ["OT_API_KEY"]}
def transcribe(url=None, path=None, punctuate=True):
if path:
with open(path, "rb") as f:
r = requests.post(API + "/transcriptions", headers=HEADERS,
files={"file": f}, data={"punctuate": str(punctuate).lower()})
else:
r = requests.post(API + "/transcriptions", headers=HEADERS,
json={"url": url, "punctuate": punctuate})
r.raise_for_status()
job_id = r.json()["id"]
while True:
time.sleep(5)
job = requests.get(API + "/transcriptions/" + job_id, headers=HEADERS).json()
if job["status"] == "completed":
return job["text"]
if job["status"] == "failed":
raise RuntimeError(job["error"]["code"] + ": " + job["error"]["message"])
print(transcribe(url="https://www.youtube.com/watch?v=jNQXAC9IVRw"))
# print(transcribe(path="meeting.mp3"))Node.js 18+ (fetch)
import { readFile } from "node:fs/promises";
const API = "https://api.online-transcript.com/api/v1";
const headers = { Authorization: "Bearer " + process.env.OT_API_KEY };
async function transcribe({ url, path }) {
let body;
if (path) {
body = new FormData();
body.append("file", new Blob([await readFile(path)]), path.split("/").pop());
} else {
headers["Content-Type"] = "application/json";
body = JSON.stringify({ url });
}
const res = await fetch(API + "/transcriptions", { method: "POST", headers, body });
const job = await res.json();
if (!res.ok) throw new Error(job.error.code + ": " + job.error.message);
delete headers["Content-Type"];
for (;;) {
await new Promise((r) => setTimeout(r, 5000));
const j = await (await fetch(API + "/transcriptions/" + job.id, { headers })).json();
if (j.status === "completed") return j.text;
if (j.status === "failed") throw new Error(j.error.code + ": " + j.error.message);
}
}
console.log(await transcribe({ url: "https://www.youtube.com/watch?v=jNQXAC9IVRw" }));Check your remaining minutes
curl https://api.online-transcript.com/api/v1/usage -H "Authorization: Bearer $OT_API_KEY"Support
Questions or a use case that needs higher limits? Write to support@online-transcript.com. Use of the API is subject to the Terms of Service and the Privacy Policy.