Bulk jobs
Use bulk jobs when one request needs many transcripts. Submit a channel or playlist URL (or a list of video IDs), poll for progress, then download the results as a ZIP. Supports cancellation with prorated refund.
Lifecycle
- Submit: POST a URL or video_ids array with an Idempotency-Key. Receive a
job_idimmediately. - Poll: GET
/jobs/:idevery 2 to 5 seconds untilstatusis terminal. - Inspect (optional): GET
/jobs/:id/videosfor per-video status and transcripts. - Export: GET
/jobs/:id/exportto download all completed transcripts as a ZIP. - Cancel (optional): DELETE
/jobs/:idat any time before terminal state to halt processing. Refunds every video that didn't deliver a successful transcript: still-pending videos plus any that had failed during processing but weren't yet refunded.
Submit a job
/api/v1/jobsHeaders
Authorization: Bearer yvt_live_...
Idempotency-Key: <required>
Content-Type: application/jsonBody (URL form)
{
"url": "https://www.youtube.com/@channelname",
"format": "txt"
}Body (video IDs form)
For power users who already enumerated the channel themselves.
{
"video_ids": ["abc12345678", "def12345678", "..."],
"format": "txt"
}Body fields
| Name | Type | Description |
|---|---|---|
url | string | A YouTube channel, playlist, or video URL. Same parser as /resolve. Either url or video_ids is required. |
video_ids | string[] | Explicit list of 11-char video IDs. Either url or video_ids is required. |
format | string | Default export format. Can be overridden per-call at export time. See Export formats. Defaults to txt. |
Plan limits
| Plan | Max active bulk jobs | Max videos per job |
|---|---|---|
| Free | 0 (bulk denied) | None |
| Starter | 1 | 500 |
| Pro | 2 | 2,000 |
| Business | 3 | 10,000 |
Channel and playlist resolution stops once the request exceeds your plan's per-job video cap. Very large channels can take a few minutes to resolve, so keep the same Idempotency-Key when retrying the same submission.
Response 201 Created
{
"job_id": "9f3a8b2e-1d4c-4e7a-b9c1-2f6e8d5a3c7b",
"status": "queued",
"total_videos": 487,
"status_url": "/api/v1/jobs/9f3a8b2e-1d4c-4e7a-b9c1-2f6e8d5a3c7b",
"results_url": "/api/v1/jobs/9f3a8b2e-1d4c-4e7a-b9c1-2f6e8d5a3c7b/videos"
}Errors
| Status | Type | When |
|---|---|---|
| 400 | bad_request | Missing Idempotency-Key; both url and video_ids missing; invalid format; URL unrecognized; bad video_id in array; resolution returned zero videos |
| 402 | insufficient_quota | Not enough credits for the resolved video count |
| 403 | forbidden | Free plan |
| 409 | idempotency_conflict | Same Idempotency-Key reused with a different body, or concurrent retry while first is in flight |
| 413 | request_too_large | Exceeded the per-job video cap for your plan |
| 429 | rate_limited | Either per-minute rate limit OR active-bulk-job cap reached. Check error.message to disambiguate. |
| 503 | service_degraded | Bulk job submissions are temporarily paused because transcript fetching is degraded. Retry in a few minutes. Sync transcript requests are unaffected. |
Poll status
/api/v1/jobs/:idThis endpoint is intended for polling. A reasonable interval is every 2 to 5 seconds while status is processing.
Response 200
{
"id": "9f3a8b2e-1d4c-...",
"status": "processing",
"format": "txt",
"source_url": "https://www.youtube.com/@channelname",
"source_type": "channel",
"total_videos": 487,
"videos_completed": 342,
"videos_failed": 5,
"created_at": "2026-05-21T12:34:56.789Z",
"updated_at": "2026-05-21T12:39:12.456Z",
"status_url": "/api/v1/jobs/...",
"results_url": "/api/v1/jobs/.../videos"
}Status values
pending: accepted, processing has not started yet.processing: at least one video has started.completed: all videos in terminal state (completed or failed).canceled: API-side cancel viaDELETE /jobs/:id.failed: job ended without completing all videos.
videos_completed + videos_failed reaches total_videos when the job is terminal.
Errors
404 not_found if the job doesn't exist or doesn't belong to your account.
Per-video results
/api/v1/jobs/:id/videosCursor-paginated list of per-video status (and optionally transcripts).
Query parameters
| Name | Type | Description |
|---|---|---|
cursor | string | Opaque cursor from the previous response's next_cursor. Omit on the first call. |
limit | number | Default 100, max 500. |
status | string | Filter to one status: pending, processing, completed, failed. |
include | string | Pass transcript to include per-video transcript arrays inline. Otherwise the transcript field is absent from each row. |
Response 200
{
"videos": [
{
"video_id": "abc12345678",
"title": "Video Title",
"status": "completed",
"language": "en",
"error_message": null,
"transcript": [{ "text": "...", "start": 0.0, "duration": 1.5 }]
},
{
"video_id": "def12345678",
"title": null,
"status": "failed",
"language": null,
"error_message": "Video def12345678 is not playable: This video is unavailable"
}
],
"next_cursor": "eyJ0IjoiMjAyNi0wNS0..."
}next_cursor: null means the last page.
Export as ZIP
/api/v1/jobs/:id/exportDownload all completed transcripts as a ZIP archive. Only available after the job reaches a terminal state. Failed videos are excluded; only completed rows are included.
Query parameters
| Name | Type | Description |
|---|---|---|
format | string | Override the job's stored format. One of txt, srt, json, csv, docx, markdown. An unrecognized value falls back to the job's stored format. Use /transcript/:id if you want strict validation. |
Response 200
Content-Type: application/zipContent-Disposition: attachment; filename="transcripts-<8-char-job-id>.zip"- Each entry in the ZIP is
<sanitized-title-or-videoId>.<ext>. Duplicate titles get(2),(3), etc. suffixes. - The JSON shape inside the ZIP differs from the sync transcript response. See Export formats.
Errors
| Status | Type | When |
|---|---|---|
| 404 | not_found | Job doesn't exist, doesn't belong to you, or has zero completed videos |
| 409 | conflict | Job is still processing. It must reach a terminal state first |
| 410 | gone | Export expired (30 days after job became terminal). |
| 403 | forbidden | Free plan requesting a non-txt/non-json format |
Cancel
/api/v1/jobs/:idCancel a running or queued bulk job. Refunds every video that is not a successfully delivered transcript: still-pending videos plus any videos that had already failed during processing but had not been refunded yet.
Response 200
{
"id": "9f3a8b2e-...",
"status": "canceled",
"total_videos": 487,
"videos_completed": 342,
"videos_failed": 145,
"refunded": 145
}refunded is the total credits returned to your quota by this cancel: it equals the number of pending videos at cancellation time plus the number of videos that had failed during processing without yet being refunded. In the example above, 342 completed, 5 failed early in processing, 140 were still pending, so cancel refunds 5 + 140 = 145.
- Job becomes downloadable via
/exportimmediately. Partial results are available. - A video already being fetched may continue in the background, but it is not included in the canceled job unless it had already completed.
- Two concurrent DELETE calls serialize cleanly: only the winning call returns
refunded > 0. The other returns 409.
Errors
| Status | Type | When |
|---|---|---|
| 404 | not_found | Job doesn't exist or doesn't belong to you |
| 409 | conflict | Job is already in a terminal state (completed, failed, or canceled) |
Full example
KEY=yvt_live_...
HOST=https://api.youtubevideotranscript.io
# Submit
JOB=$(curl -sS -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"url": "https://www.youtube.com/@channelname", "format": "txt"}' \
"$HOST/api/v1/jobs")
JOB_ID=$(echo "$JOB" | jq -r .job_id)
# Poll until done
while true; do
STATUS=$(curl -sS -H "Authorization: Bearer $KEY" "$HOST/api/v1/jobs/$JOB_ID" | jq -r .status)
echo "status=$STATUS"
case "$STATUS" in
completed|failed|canceled) break ;;
esac
sleep 5
done
# Download
curl -OJ -H "Authorization: Bearer $KEY" "$HOST/api/v1/jobs/$JOB_ID/export"