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

  1. Submit: POST a URL or video_ids array with an Idempotency-Key. Receive a job_id immediately.
  2. Poll: GET /jobs/:id every 2 to 5 seconds until status is terminal.
  3. Inspect (optional): GET /jobs/:id/videos for per-video status and transcripts.
  4. Export: GET /jobs/:id/export to download all completed transcripts as a ZIP.
  5. Cancel (optional): DELETE /jobs/:id at 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

POST/api/v1/jobs

Headers

http
Authorization: Bearer yvt_live_...
Idempotency-Key: <required>
Content-Type: application/json

Body (URL form)

json
{
  "url": "https://www.youtube.com/@channelname",
  "format": "txt"
}

Body (video IDs form)

For power users who already enumerated the channel themselves.

json
{
  "video_ids": ["abc12345678", "def12345678", "..."],
  "format": "txt"
}

Body fields

NameTypeDescription
urlstringA YouTube channel, playlist, or video URL. Same parser as /resolve. Either url or video_ids is required.
video_idsstring[]Explicit list of 11-char video IDs. Either url or video_ids is required.
formatstringDefault export format. Can be overridden per-call at export time. See Export formats. Defaults to txt.

Plan limits

PlanMax active bulk jobsMax videos per job
Free0 (bulk denied)None
Starter1500
Pro22,000
Business310,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

json
{
  "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

StatusTypeWhen
400bad_requestMissing Idempotency-Key; both url and video_ids missing; invalid format; URL unrecognized; bad video_id in array; resolution returned zero videos
402insufficient_quotaNot enough credits for the resolved video count
403forbiddenFree plan
409idempotency_conflictSame Idempotency-Key reused with a different body, or concurrent retry while first is in flight
413request_too_largeExceeded the per-job video cap for your plan
429rate_limitedEither per-minute rate limit OR active-bulk-job cap reached. Check error.message to disambiguate.
503service_degradedBulk job submissions are temporarily paused because transcript fetching is degraded. Retry in a few minutes. Sync transcript requests are unaffected.

Poll status

GET/api/v1/jobs/:id

This endpoint is intended for polling. A reasonable interval is every 2 to 5 seconds while status is processing.

Response 200

json
{
  "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 via DELETE /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

GET/api/v1/jobs/:id/videos

Cursor-paginated list of per-video status (and optionally transcripts).

Query parameters

NameTypeDescription
cursorstringOpaque cursor from the previous response's next_cursor. Omit on the first call.
limitnumberDefault 100, max 500.
statusstringFilter to one status: pending, processing, completed, failed.
includestringPass transcript to include per-video transcript arrays inline. Otherwise the transcript field is absent from each row.

Response 200

json
{
  "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

GET/api/v1/jobs/:id/export

Download 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

NameTypeDescription
formatstringOverride 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/zip
  • Content-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

StatusTypeWhen
404not_foundJob doesn't exist, doesn't belong to you, or has zero completed videos
409conflictJob is still processing. It must reach a terminal state first
410goneExport expired (30 days after job became terminal).
403forbiddenFree plan requesting a non-txt/non-json format

Cancel

DELETE/api/v1/jobs/:id

Cancel 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

json
{
  "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 /export immediately. 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

StatusTypeWhen
404not_foundJob doesn't exist or doesn't belong to you
409conflictJob is already in a terminal state (completed, failed, or canceled)

Full example

bash
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"

We use Google Analytics cookies and note which site referred you, so we know how people find us. Nothing personal, nothing sold. See our Privacy Policy.