Captions API · v1 is live

Captions in.
Clean rows out.

TubeExtract reads a video’s existing caption track, runs it against a schema you write in plain English, and hands back typed JSON. No audio pipeline, no OCR, no selectors to maintain.

One POST to start a job, one GET to collect it. A credit is one video, however long it runs — so two hundred videos in one batch cost two hundred credits, and nothing extra for the call.

Starter credits land on every new account. No card, no sales call.

POST /v1
// a schema, not scraping code
{
"videos": ["…/watch?v=dQw4w9"],
"schema": {
"columns": [
{ "name": "restaurant_name" },
{ "name": "food_ordered" }
],
"multiple": true
}
}
200 OKjob queued
rows returned
4 validated
manual + auto-generated captions200 videos per batch callresults deleted on fetchtyped, validated JSONself-serve keysno audio processing
Live demo

Watch a caption track become rows

Edit the schema, press run, and follow the job through the same phases the API reports. This page isn't authenticated, so the run below replays a recorded response instead of billing your account.

restaurant_name
Name of the restaurant mentioned
food_ordered
What food was ordered there

Recorded response · no credits spent

job idle
captions (raw)
00:04:12 "…he finally tried the panettone at Osteria"
00:04:31 "…ordered it with an espresso, said it was…"
00:07:58 "…next stop was a tiny place near the station…"
00:09:20 "…the pastrami on rye at Katz's, obviously…"
00:11:47 "…and a morning bun from Tartine to finish."
result (structured)
{ "results": [
{ "restaurant_name": "Osteria Francescana", "food_ordered": "panettone" },
{ "restaurant_name": "Burger King", "food_ordered": "chicken fries" },
{ "restaurant_name": "Katz's Delicatessen", "food_ordered": "pastrami on rye" },
{ "restaurant_name": "Tartine Bakery", "food_ordered": "morning bun" },
] }
usage: { durationSeconds: 782, chunkCount: 4 }
How it works

Three calls, start to finish

The captions already exist on YouTube. TubeExtract's whole job is to fetch them, chunk them sensibly, and make a model fill in the columns you asked for.

1

Fetch the caption track

Send a YouTube URL. We pull the manual track if there is one, the auto-generated track if there isn’t. Nothing is transcribed, so there’s no audio cost and no wait on a speech model.

2

Run it against your schema

Describe each column with a name and a sentence of plain English. The transcript is chunked with overlap so a fact split across a chunk boundary still lands in one row.

3

Collect typed JSON

Read GET /v1/{job_id} when your webhook says it is ready. You get one flat object, or an array when you asked for multiple records. Every value is validated against the column you defined.

Built for real usage

The parts you only notice in production

Schema, not scraping code

A name and a description per column. Nothing to update when a channel changes its intro or a video runs long.

Batching is built in

Two hundred videos go in one call and come back as one batch of jobs. A bad URL fails on its own without sinking the rest.

Self-serve keys

Generate, label and revoke keys from the dashboard. Each one carries its own usage stats, so you can tell staging from production.

Results deleted on fetch

The result JSON and the submitted URL leave our database the moment your GET succeeds. Fetch twice and the second answer is result: null.

Credit math you can predict

One credit per video that returns a result, whatever its length. Over four hours it counts as two. Failed jobs cost nothing.

Honest failure modes

No captions means a clear error, not a guess. Every failure returns a typed code you can branch on instead of a stack trace.

The API

Basic usage

Submit, then collect. Two calls and a header — everything else is your schema.

const API = 'https://api.tubeextract.dev/v1';
const auth = { 'X-API-Key': process.env.TBX_KEY };
 
// 1. Submit. One video, or up to 200 in the same call.
const { job_id } = await fetch(API, {
method: 'POST',
headers: { ...auth, 'Content-Type': 'application/json' },
body: JSON.stringify({
videos: ['https://youtube.com/watch?v=...'],
schema: {
columns: [
{ name: 'restaurant_name', description: 'Name of the restaurant' },
{ name: 'food_ordered', description: 'What food was ordered' },
],
multiple: true,
},
webhook_url: 'https://example.com/hooks/tubeextract',
}),
}).then((r) => r.json());
 
// 2. Collect, when your webhook says it is ready.
const job = await fetch(API + '/' + job_id, { headers: auth }).then((r) => r.json());
 
console.log(job.results);
not sentGET /v1/:job_id
Response body
{
"job_id": "8f2c1e40-91a3-4b7e-9c5d-2a6f0d3b7c11",
"status": "done",
"videos": 1,
"credits_charged": 1,
"results": [
{ "video_id": "dQw4w9WgXcQ", "status": "done", "rows": {
"results": [
{ "restaurant_name": "Osteria Francescana", "food_ordered": "panettone" },
{ "restaurant_name": "Katz's Delicatessen", "food_ordered": "pastrami on rye" }
] } }
]
}
Endpoints
POST/v1
GET/v1/:job_id
GET/v1/usage
Error codes
no_captionsthe video has neither a manual nor an auto track.
batch_too_largemore than 200 videos in one call.
schema_invalida column is missing a name or a description.
insufficient_creditstop up, then retry the same body.

Full reference, webhook payloads and per-language SDKs live in the docs.

FAQ

Questions we actually get

Anything missing? Write to timurcagribek@gmail.com — a person, not a ticket queue, answers.

The request fails with no_captions. TubeExtract only works on videos that already have a manual or auto-generated YouTube caption track — there is no audio-transcription fallback, by design, because that is what keeps a job cheap and fast.

One credit per video that returns a result, whatever its length or how many rows come back. Videos over four hours count as two, since they take roughly twice the work. Jobs that fail are never charged, and every response reports what it charged.

The result JSON and the submitted video URL are deleted from our database the moment your GET request succeeds. A second fetch of the same job returns status: done with result: null.

Yes, from the dashboard. Revocation is immediate and permanent — anything still using that key starts getting 401s right away, so rotate before you revoke.

No. Starter credits and any top-ups stay on the account until they are used.

Up to 200 videos in a single call. They are queued as separate jobs under one batch id, so a bad URL in the batch does not sink the rest.

Try it on your own video

Starter credits are already on the account when you sign up. Point the API at one video, check the rows, then batch the rest.