# Submit batch job



<MethodPage method="POST" path="/v1/batch">
  <MethodSignature name="submitBatch" args={[{ name: "params" }, { name: "options", optional: true }]} returns="BatchSubmitResponse" />

  Submit a batch of up to 100 video tasks (transcripts or basic metadata) for asynchronous execution. Tasks are queued and executed concurrently across a background worker pool (default 10, maximum 20 concurrent workers). The call returns immediately with **`202 Accepted`** containing a unique `id`. Poll [`GET /v1/batch/{id}`](/batch/get) to retrieve task results once processing finishes.

  <Security permission="transcripts:read" />

  <SchemaGroup title="Headers">
    <SchemaField name="Idempotency-Key" type="string" location="header" optional>
      Makes the submit safe to retry. Send any unique string of 1 to 255 printable ASCII characters, such as a UUID. Reuse it when you retry the same request after a timeout or network error. For 24 hours, the same key with the same body returns the original job (`202` with header `Idempotent-Replayed: true`) instead of starting and billing a second one. The same key with a different body returns `409 idempotency_conflict`. Keys are per account.
    </SchemaField>
  </SchemaGroup>

  <SchemaGroup title="Request Body">
    <SchemaField name="tasks" type="BatchTask[]" location="body" required>
      Array of up to 100 task objects to execute in the batch job.

      <SchemaField name="id" type="string" optional>
        Caller-defined task identifier echoed in the result.
      </SchemaField>

      <SchemaField name="type" type="string" required>
        Task type: `transcript` or `basic_info`.
      </SchemaField>

      <SchemaField name="video_id" type="string" required>
        11-character YouTube video ID.
      </SchemaField>

      <SchemaField name="format" type="string" optional>
        Transcript format when `type` is `transcript` (`segments`, `word_timestamps`, `markdown`, etc.). Default `segments`.
      </SchemaField>
    </SchemaField>

    <SchemaField name="concurrency" type="number" optional location="body">
      Number of concurrent workers executing tasks in the batch (default 10, maximum 20).
    </SchemaField>
  </SchemaGroup>

  <div id="returns" className="mt-8">
    Response (HTTP 202) [#response-http-202]

    ```jsonc
    {
      "id": string,                  // Unique batch job identifier (e.g. "batch_1789995131572858354_84b12de0")
      "status": string,              // Initial job status ("pending")
      "total": number,               // Total number of submitted tasks
      "estimated_credits": number,   // Maximum estimated credits reserved for this batch (1 credit / task)
      "created_at": string           // ISO 8601 creation timestamp
    }
    ```

    | Response header             | When                                                                                                                                                                                                   |
    | :-------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
    | `Idempotent-Replayed: true` | The request repeated an `Idempotency-Key` from the last 24 hours with the same body. The response describes the job created by the first request; no new job was started and no credits were reserved. |
  </div>

  <Callout type="success" title="Fair credit accounting & upfront reservation">
    Your account balance is checked against `estimated_credits` upon submission (1 credit reserved per task). In batch execution, every successfully completed task consumes 1 credit (both for `transcript` and `basic_info`). Please note: while individual single `GET /v1/videos/{id}/basic-info` calls are free, batch processing of `basic_info` is billed at 1 credit per successful task due to high-concurrency server cluster capacity. Unused reserved credits are released immediately when the job finishes. Failed tasks cost 0 credits.
  </Callout>

  <MethodSamples>
    <LanguageSample language="TypeScript">
      ```ts
      // 1. Submit batch job
      const submitRes = await fetch("https://api.ytapi.dev/v1/batch", {
        method: "POST",
        headers: {
          Authorization: `Bearer ${process.env.YT_API_KEY}`,
          "Content-Type": "application/json",
          // Reuse the same key if you retry this request.
          "Idempotency-Key": crypto.randomUUID(),
        },
        body: JSON.stringify({
          tasks: [
            { id: "task_1", type: "transcript", video_id: "dQw4w9WgXcQ", format: "segments" },
            { id: "task_2", type: "basic_info", video_id: "jNQXAC9IVRw" },
          ],
          concurrency: 10,
        }),
      });

      const job = await submitRes.json();
      console.log(`Submitted batch ${job.id} (${job.total} tasks, status: ${job.status})`);

      // 2. Poll for completion
      while (true) {
        await new Promise((r) => setTimeout(r, 1000));
        const pollRes = await fetch(`https://api.ytapi.dev/v1/batch/${job.id}`, {
          headers: { Authorization: `Bearer ${process.env.YT_API_KEY}` },
        });
        const data = await pollRes.json();
        if (data.status === "completed" || data.status === "failed") {
          console.log(`Finished: ${data.successful}/${data.total} succeeded in ${data.duration_ms}ms`);
          for (const res of data.results) {
            console.log(res.id, res.status, res.status === 200 ? "OK" : res.error?.message);
          }
          break;
        }
      }
      ```
    </LanguageSample>

    <LanguageSample language="Python">
      ```python
      import time
      import uuid
      import requests

      api_key = "YOUR_API_KEY"
      headers = {
          "Authorization": f"Bearer {api_key}",
          "Content-Type": "application/json",
      }

      # 1. Submit batch job
      # Reuse the same Idempotency-Key if you retry this request.
      resp = requests.post(
          "https://api.ytapi.dev/v1/batch",
          headers={**headers, "Idempotency-Key": str(uuid.uuid4())},
          json={
              "tasks": [
                  {"id": "t1", "type": "transcript", "video_id": "dQw4w9WgXcQ", "format": "segments"},
                  {"id": "t2", "type": "basic_info", "video_id": "jNQXAC9IVRw"},
              ],
              "concurrency": 10,
          },
      )
      job = resp.json()
      print(f"Batch submitted: {job['id']}, status: {job['status']}")

      # 2. Poll for completion
      while True:
          time.sleep(1.0)
          poll_resp = requests.get(f"https://api.ytapi.dev/v1/batch/{job['id']}", headers=headers)
          data = poll_resp.json()
          if data.get("status") in ("completed", "failed"):
              print(f"Job completed: {data['successful']}/{data['total']} successful")
              for res in data.get("results", []):
                  print(res["id"], res["status"])
              break
      ```
    </LanguageSample>

    <LanguageSample language="cURL">
      ```bash
      # 1. Submit batch job
      curl -X POST https://api.ytapi.dev/v1/batch \
        -H "Authorization: Bearer $YT_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{
          "tasks": [
            {
              "id": "task_1",
              "type": "transcript",
              "video_id": "dQw4w9WgXcQ",
              "format": "segments"
            },
            {
              "id": "task_2",
              "type": "basic_info",
              "video_id": "jNQXAC9IVRw"
            }
          ],
          "concurrency": 10
        }'

      # 2. Poll status with the returned job ID
      curl https://api.ytapi.dev/v1/batch/batch_1789995131572858354_84b12de0 \
        -H "Authorization: Bearer $YT_API_KEY"
      ```
    </LanguageSample>

    <LanguageSample language="Go">
      ```go
      package main

      import (
      	"bytes"
      	"encoding/json"
      	"fmt"
      	"net/http"
      	"os"
      	"time"
      )

      type SubmitResponse struct {
      	ID     string `json:"id"`
      	Status string `json:"status"`
      	Total  int    `json:"total"`
      }

      type BatchJobResponse struct {
      	Status     string `json:"status"`
      	Successful int    `json:"successful"`
      	Total      int    `json:"total"`
      }

      func main() {
      	apiKey := os.Getenv("YT_API_KEY")
      	payload, _ := json.Marshal(map[string]any{
      		"tasks": []map[string]any{
      			{"id": "t1", "type": "transcript", "video_id": "dQw4w9WgXcQ"},
      			{"id": "t2", "type": "basic_info", "video_id": "jNQXAC9IVRw"},
      		},
      	})

      	req, _ := http.NewRequest("POST", "https://api.ytapi.dev/v1/batch", bytes.NewBuffer(payload))
      	req.Header.Set("Authorization", "Bearer "+apiKey)
      	req.Header.Set("Content-Type", "application/json")

      	resp, err := http.DefaultClient.Do(req)
      	if err != nil {
      		panic(err)
      	}
      	defer resp.Body.Close()

      	var submit SubmitResponse
      	_ = json.NewDecoder(resp.Body).Decode(&submit)
      	fmt.Printf("Batch submitted: %s (status: %s)\n", submit.ID, submit.Status)

      	for {
      		time.Sleep(1 * time.Second)
      		getReq, _ := http.NewRequest("GET", "https://api.ytapi.dev/v1/batch/"+submit.ID, nil)
      		getReq.Header.Set("Authorization", "Bearer "+apiKey)
      		getResp, _ := http.DefaultClient.Do(getReq)

      		var job BatchJobResponse
      		_ = json.NewDecoder(getResp.Body).Decode(&job)
      		getResp.Body.Close()

      		if job.Status == "completed" || job.Status == "failed" {
      			fmt.Printf("Completed %d/%d tasks\n", job.Successful, job.Total)
      			break
      		}
      	}
      }
      ```
    </LanguageSample>
  </MethodSamples>
</MethodPage>
