{"openapi":"3.1.0","info":{"title":"ChannelText API","version":"1.0.0","description":"Get transcripts of YouTube videos, playlists and whole channels programmatically. The API uses the same prepaid balance, jobs and files as the website.\n\nAuthentication: create an API key on your account page and send it as `Authorization: Bearer <key>`. The key is shown once; only its prefix is kept visible. Revoked keys get 401 `unauthorized`.\n\nBilling: $0.008 per delivered transcript, from your prepaid balance. Before a job starts, the cost of up to maxVideos transcripts is reserved (capped by your balance, so it never goes negative). When the job ends you are charged exactly delivered x $0.008 and the rest is released. Videos without captions, blocked, unavailable or over the limit are free. Below $0.008 a job is refused with 402 `insufficient_balance`.\n\nIdempotency: send an `Idempotency-Key` header (any string up to 255 characters) with POST /jobs. Repeating the same key with the same body returns the original job (200, header `Idempotent-Replayed: true`) without starting or charging anything new. The same key with a different body returns 409 `idempotency_conflict`. Keys are scoped to your account and do not expire. A request that was refused (4xx) does not use up its key.\n\nRate limit: 60 requests in a burst per API key, refilled at 60 per minute. Over the limit you get 429 `rate_limited` with a Retry-After header (seconds). Every response to a valid key carries X-RateLimit-Limit and X-RateLimit-Remaining.\n\nAsking again for the same URLs: a job with exactly the same URL set as an earlier job of this account continues from what this account already received. For a channel or playlist with maxVideos, it takes the next videos not yet received (the next older ones) and charges them like any delivered transcript; it does not return the earlier videos again (field-tested 2026-09-26: the same channel with maxVideos 5, asked twice, gave 10 different videos, 5 charged each time). Videos already delivered that are listed again (expected when you ask again for a single video, or for a list without a cap that has no new videos; fixture-tested, not yet field-tested) come back as `unchanged` rows, with no text and no charge, whatever the language. Their text stays in the earlier job: find it with GET /jobs (your jobs, newest first) and read GET /jobs/{id}/results. This is what makes \"update this channel\" cheap. A different URL set (another spelling of the channel, or one more URL) is a new set: its videos are fetched and charged again.\n\nErrors: every error is JSON `{ \"error\": { \"code\": \"...\", \"message\": \"...\" } }`; `job_in_progress` also has `jobId`. Codes are stable; messages may change."},"servers":[{"url":"https://channeltext.com/api/v1"}],"security":[{"bearerAuth":[]}],"paths":{"/jobs":{"post":{"operationId":"createJob","summary":"Start a job","description":"Starts fetching the transcripts of one or more video, playlist or channel URLs. Responds as soon as the job has started; poll GET /jobs/{id} until `state` is no longer queued, running or cancelling. At most 500 videos per job.","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"Makes retries safe: the same key and body return the original job instead of starting a new one.","schema":{"type":"string","minLength":1,"maxLength":255}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobRequest"}}}},"responses":{"200":{"description":"Replay of an earlier request with the same Idempotency-Key and body: the original job, nothing new started or charged. Header `Idempotent-Replayed: true`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Job"}}}},"201":{"description":"Job started. Money for up to `input.maxVideos` transcripts is reserved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Job"}}}},"400":{"description":"`invalid_request` or `invalid_url`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"`insufficient_balance`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`idempotency_conflict` or `job_in_progress`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`invalid_request` (body over 64 KB)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"`invalid_request` (Content-Type is not application/json)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` (see the Retry-After header)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`server_busy` (see the Retry-After header)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-codeSamples":[{"lang":"shell","label":"curl","source":"curl -X POST https://channeltext.com/api/v1/jobs \\\n  -H \"Authorization: Bearer $CT_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Idempotency-Key: my-channel-2026-09-26\" \\\n  -d '{\"urls\": [\"https://www.youtube.com/@somechannel/videos\"], \"maxVideos\": 20, \"formats\": [\"text\", \"srt\"]}'"}]},"get":{"operationId":"listJobs","summary":"Your jobs, newest first","description":"Every job of this account (web, API and MCP), newest first, paginated with offset and limit. Use it to find an earlier job again, for example the one that delivered the videos a later request returned as `unchanged`.","parameters":[{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":50,"default":20}}],"responses":{"200":{"description":"One page of jobs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobList"}}}},"400":{"description":"`invalid_request` (offset or limit)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` (see the Retry-After header)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-codeSamples":[{"lang":"shell","label":"curl","source":"curl \"https://channeltext.com/api/v1/jobs?limit=20\" \\\n  -H \"Authorization: Bearer $CT_API_KEY\""}]}},"/jobs/{id}":{"get":{"operationId":"getJob","summary":"Job status and progress","parameters":[{"name":"id","in":"path","required":true,"description":"Job id returned by POST /jobs.","schema":{"type":"string","pattern":"^[\\w-]{1,40}$"}}],"responses":{"200":{"description":"The job.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Job"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` (see the Retry-After header)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-codeSamples":[{"lang":"shell","label":"curl","source":"curl https://channeltext.com/api/v1/jobs/JOB_ID \\\n  -H \"Authorization: Bearer $CT_API_KEY\""}]}},"/jobs/{id}/results":{"get":{"operationId":"getJobResults","summary":"Transcripts and download links","description":"Per-video text and timed segments, plus download links for each file and the whole job. Works while the job is running (videos delivered so far). Paginated with offset and limit.","parameters":[{"name":"id","in":"path","required":true,"description":"Job id returned by POST /jobs.","schema":{"type":"string","pattern":"^[\\w-]{1,40}$"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200,"default":50}}],"responses":{"200":{"description":"Results page.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Results"}}}},"400":{"description":"`invalid_request` (offset or limit)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` (see the Retry-After header)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-codeSamples":[{"lang":"shell","label":"curl","source":"curl \"https://channeltext.com/api/v1/jobs/JOB_ID/results?limit=50\" \\\n  -H \"Authorization: Bearer $CT_API_KEY\""}]}},"/jobs/{id}/files/{file}":{"get":{"operationId":"getJobFile","summary":"Download a file","description":"`zip` (every file plus all-transcripts.txt and report.csv), `all.txt` (all texts in one file), `report.csv` (one line per video, including failures), or one video's file as `<index>.txt`, `<index>.srt`, `<index>.vtt` or `<index>.json` (timed segments). Use the links in the results rather than building them.","parameters":[{"name":"id","in":"path","required":true,"description":"Job id returned by POST /jobs.","schema":{"type":"string","pattern":"^[\\w-]{1,40}$"}},{"name":"file","in":"path","required":true,"schema":{"type":"string","pattern":"^(zip|all\\.txt|report\\.csv|\\d+\\.(txt|srt|vtt|json))$"}}],"responses":{"200":{"description":"The file, as an attachment.","content":{"application/zip":{"schema":{"type":"string","contentMediaType":"application/zip"}},"text/plain":{"schema":{"type":"string"}},"text/csv":{"schema":{"type":"string"}},"application/json":{"schema":{"type":"object"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` (see the Retry-After header)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-codeSamples":[{"lang":"shell","label":"curl","source":"curl -OJ https://channeltext.com/api/v1/jobs/JOB_ID/files/zip \\\n  -H \"Authorization: Bearer $CT_API_KEY\""}]}},"/balance":{"get":{"operationId":"getBalance","summary":"Prepaid balance","description":"Money reserved for running jobs is already subtracted.","responses":{"200":{"description":"The balance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Balance"}}}},"401":{"description":"`unauthorized`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` (see the Retry-After header)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-codeSamples":[{"lang":"shell","label":"curl","source":"curl https://channeltext.com/api/v1/balance \\\n  -H \"Authorization: Bearer $CT_API_KEY\""}]}},"/openapi.json":{"get":{"operationId":"getOpenApi","summary":"This document","security":[],"responses":{"200":{"description":"OpenAPI 3.1 document.","content":{"application/json":{"schema":{"type":"object"}}}}},"x-codeSamples":[{"lang":"shell","label":"curl","source":"curl https://channeltext.com/api/v1/openapi.json"}]}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"API key from your account page, e.g. `ct_live_…`."}},"schemas":{"ErrorCode":{"type":"string","enum":["invalid_request","invalid_url","unauthorized","insufficient_balance","not_found","idempotency_conflict","job_in_progress","rate_limited","internal_error","server_busy"],"x-codes":{"invalid_request":{"status":400,"description":"The body, a parameter or a header is malformed or out of range (also 413 for a body over 64 KB and 415 for a body that is not JSON). The message says which field."},"invalid_url":{"status":400,"description":"One of the URLs is not a YouTube video, playlist or channel URL. Nothing was started or charged."},"unauthorized":{"status":401,"description":"The Authorization header is missing, or the API key is unknown or revoked."},"insufficient_balance":{"status":402,"description":"Your balance does not cover one transcript ($0.008). Top up on the website; nothing was started."},"not_found":{"status":404,"description":"No such job or file for this API key's account. Jobs of other accounts are reported the same way."},"idempotency_conflict":{"status":409,"description":"This Idempotency-Key was already used with a different request body."},"job_in_progress":{"status":409,"description":"A job with the same URL set is still running for this account. The error carries its `jobId`: poll that job instead of starting another."},"rate_limited":{"status":429,"description":"Too many requests for this API key. Wait the number of seconds in the Retry-After header."},"internal_error":{"status":500,"description":"Unexpected server error. Retry later; job creation is safe to retry with the same Idempotency-Key."},"server_busy":{"status":503,"description":"The server is running its maximum number of jobs. Retry after the Retry-After header."}}},"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"$ref":"#/components/schemas/ErrorCode"},"message":{"type":"string"},"jobId":{"type":"string","description":"Only on `job_in_progress`: the running job with the same URL set."}}}}},"JobRequest":{"type":"object","required":["urls"],"additionalProperties":false,"properties":{"urls":{"description":"Up to 50 YouTube video, Shorts, playlist or channel URLs (or video ids). A string may hold several, one per line.","oneOf":[{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":50},{"type":"string","minLength":1}]},"language":{"type":"string","default":"original","description":"Caption language code such as en, es, pt-BR; `original` = the video's own language. Falls back to the original language."},"formats":{"type":"array","items":{"type":"string","enum":["text","srt","vtt","segments"]},"minItems":1,"default":["text","srt","vtt","segments"],"description":"File types to produce. Text and timed segments are always included so /results can return them."},"maxVideos":{"type":"integer","minimum":1,"maximum":500,"default":500,"description":"Most videos to process (newest first for channels). Reserve and charge are capped by this and by your balance."},"publishedAfter":{"type":"string","description":"Only videos published on or after this date (YYYY-MM-DD or ISO date-time)."},"publishedBefore":{"type":"string","description":"Only videos published on or before this date (YYYY-MM-DD or ISO date-time)."}}},"JobState":{"type":"string","enum":["queued","running","cancelling","finished","cancelled","failed","interrupted"],"description":"queued/running/cancelling = still working; the others are final and billed."},"Job":{"type":"object","required":["id","state","createdAt","input","progress","billing","links"],"properties":{"id":{"type":"string"},"state":{"$ref":"#/components/schemas/JobState"},"createdAt":{"type":"string","format":"date-time"},"finishedAt":{"type":["string","null"],"format":"date-time"},"input":{"type":"object","properties":{"urls":{"type":"array","items":{"type":"string"}},"language":{"type":"string"},"formats":{"type":"array","items":{"type":"string","enum":["text","srt","vtt","segments"]}},"maxVideos":{"type":"integer","description":"Videos this job may process after the balance cap."},"requestedMaxVideos":{"type":"integer"},"publishedAfter":{"type":["string","null"]},"publishedBefore":{"type":["string","null"]}}},"progress":{"type":"object","properties":{"discovered":{"type":["integer","null"],"description":"Videos found so far, when known."},"processed":{"type":"integer","description":"Videos with a result (any status)."},"delivered":{"type":"integer","description":"Videos with a transcript."},"counts":{"type":"object","additionalProperties":{"type":"integer"},"description":"Videos per status (success, no_captions, private, unavailable, blocked_suspect, invalid_url, failed, truncated, skipped, unchanged…). `unchanged` = delivered by an earlier job of this account with the same URL set: no text here, not charged."}}},"billing":{"type":"object","properties":{"reserved":{"type":"string","description":"USD reserved when the job started."},"settled":{"type":"boolean"},"charged":{"type":["string","null"],"description":"USD charged, once settled (= delivered x 0.008)."},"chargedTranscripts":{"type":["integer","null"]},"pricePerTranscript":{"type":"string"}}},"error":{"type":["string","null"]},"notices":{"type":"array","items":{"type":"string"},"description":"Only on POST /jobs, e.g. when the balance limits the videos."},"links":{"type":"object","properties":{"self":{"type":"string"},"results":{"type":"string"}}}}},"Video":{"type":"object","properties":{"index":{"type":"integer"},"videoId":{"type":["string","null"]},"title":{"type":["string","null"]},"url":{"type":["string","null"]},"channelName":{"type":["string","null"]},"publishedAt":{"type":["string","null"]},"status":{"type":"string","description":"success (charged) or a free status: no_captions, language_not_available, private, unavailable, blocked_suspect, invalid_url, failed, truncated (over the limit), skipped (outside the dates), unchanged (delivered by an earlier job of this account with the same URL set; text is null here, read it from that job, see GET /jobs)."},"label":{"type":"string","description":"Status in plain words."},"errorMessage":{"type":["string","null"]},"languageCode":{"type":["string","null"]},"trackKind":{"type":["string","null"],"description":"manual (creator captions) or asr (automatic)."},"text":{"type":["string","null"]},"segments":{"type":["array","null"],"items":{"type":"object","properties":{"startMs":{"type":"integer"},"endMs":{"type":"integer"},"text":{"type":"string"}}}},"files":{"type":"object","description":"Download links by format (text, srt, vtt, segments), for delivered videos.","additionalProperties":{"type":"string"}}}},"Results":{"type":"object","properties":{"id":{"type":"string"},"state":{"$ref":"#/components/schemas/JobState"},"complete":{"type":"boolean","description":"false while the job is still running."},"total":{"type":"integer"},"offset":{"type":"integer"},"limit":{"type":"integer"},"nextOffset":{"type":["integer","null"]},"videos":{"type":"array","items":{"$ref":"#/components/schemas/Video"}},"downloads":{"type":"object","properties":{"zip":{"type":"string"},"allText":{"type":"string"},"reportCsv":{"type":"string"}}}}},"JobList":{"type":"object","properties":{"total":{"type":"integer"},"offset":{"type":"integer"},"limit":{"type":"integer"},"nextOffset":{"type":["integer","null"]},"jobs":{"type":"array","items":{"$ref":"#/components/schemas/Job"}}}},"Balance":{"type":"object","properties":{"balance":{"type":"string","description":"USD, e.g. \"9.92\"."},"balanceMicros":{"type":"integer","description":"Millionths of a dollar."},"currency":{"type":"string","const":"USD"},"pricePerTranscript":{"type":"string"},"affordableTranscripts":{"type":"integer"}}}}}}