# Extract transcript



<MethodPage method="POST" path="/v1/transcripts" credits={1}>
  <MethodSignature name="extractTranscript" args={[{ name: "params" }, { name: "options", optional: true }]} returns="TranscriptResponse" />

  This is the primary transcript endpoint. Submit a JSON body with the video id and optional format, language, and track policy. `GET /v1/transcripts` runs the same extractor with query parameters.

  Successful requests consume 1 credit. Missing captions return `404` and are not billed.

  <Security permission="transcripts:read" />

  <SchemaGroup title="Request Body">
    <SchemaField name="video_id" type="string" location="body" required>
      11-character YouTube video ID or a full video URL.
    </SchemaField>

    <SchemaField name="format" type="string" optional location="body">
      Output format: `segments` (default), `word_timestamps`, `sentences`, `markdown`, `text`, `srt`, `vtt`, or `json3`.
    </SchemaField>

    <SchemaField name="word_level" type="boolean" optional location="body">
      Include millisecond word offsets. Only auto-generated (ASR) captions carry word timing; creator captions never do. With the default `track_policy=manual_first`, a video that has creator captions returns those, so pass `track_policy=asr_first` when you need `words`. Some auto-generated tracks have no word timing either; `has_word_level` tells you. Default `false`.
    </SchemaField>

    <SchemaField name="languages" type="string[]" optional location="body">
      Prioritized ISO language codes, for example `["en", "es"]`. `"*"` means the video's own language: the language spoken in it (its auto-generated track), or its only track when there is none. Default `["*"]`. For English when the video has it and its own language otherwise, pass `["en", "*"]`. The response `language` field says which track was returned.
    </SchemaField>

    <SchemaField name="track_policy" type="string" optional location="body">
      Track selection: `manual_first` (default) prefers creator captions and falls back to auto-generated; `asr_first` prefers auto-generated and falls back to creator captions; `exact_only` accepts only a track whose language code matches exactly (`en` does not match `en-GB`), creator captions first. Any other value returns `400 invalid_request`. Languages are tried in order first, then track kind within the matched language: if the English track is auto-generated only, `["en", "*"]` returns it rather than moving on. To see which languages and kinds a video has before spending a credit, call the free [basic info](/videos/basic-info) endpoint: each entry in `available_languages` has `kind` `manual` or `asr`. The response `track_kind` says which one you got.
    </SchemaField>
  </SchemaGroup>

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

    **When `format=word_timestamps`, `segments`, `sentences`, or `json3` (`application/json`):**

    ```jsonc
    {
      "video_id": string,            // Echoed YouTube video ID
      "language": string,            // Negotiated ISO language code (e.g. "en")
      "track_kind": string,          // "asr" (auto-generated) or "manual" (creator-uploaded)
      "duration_seconds": number,    // Total duration of caption track in seconds
      "has_word_level": boolean,     // Whether genuine word-level timing precision is present
      "segments": [                  // Array of caption segments
        {
          "text": string,            // Segment transcript text
          "start": number,           // Start offset in seconds
          "end": number,             // End offset in seconds
          "duration": number,        // Segment duration in seconds
          "words": [                 // Word-level timestamps (populated when word_level=true)
            {
              "word": string,        // Individual word
              "start": number,       // Word start offset
              "end": number          // Word end offset
            }
          ]
        }
      ]
    }
    ```

    **When `format=markdown`, `text`, `srt`, or `vtt`:**

    Returns the raw document directly with matching `Content-Type` (e.g. `text/markdown`, `text/plain`, `text/vtt`) without JSON wrapper overhead.
  </div>

  <MethodSamples>
    <LanguageSample language="TypeScript">
      ```ts
      const response = await fetch(
        "https://api.ytapi.dev/v1/transcripts",
        {
          method: "POST",
          headers: {
            Authorization: `Bearer ${process.env.YT_API_KEY}`,
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            video_id: "dQw4w9WgXcQ",
            format: "word_timestamps",
          }),
        },
      );

      const transcript = await response.json();
      console.log(transcript.language, transcript.segments);
      ```
    </LanguageSample>

    <LanguageSample language="Python">
      ```python
      import os
      import requests

      response = requests.post(
          "https://api.ytapi.dev/v1/transcripts",
          headers={"Authorization": f"Bearer {os.environ['YT_API_KEY']}"},
          json={
              "video_id": "dQw4w9WgXcQ",
              "format": "word_timestamps",
          },
      )

      data = response.json()
      print(data["language"], len(data["segments"]))
      ```
    </LanguageSample>

    <LanguageSample language="cURL">
      ```bash
      # JSON output with word timestamps
      curl -X POST https://api.ytapi.dev/v1/transcripts \
        -H "Authorization: Bearer $YT_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{
          "video_id": "dQw4w9WgXcQ",
          "format": "word_timestamps"
        }'

      # Direct Markdown output
      curl -X POST https://api.ytapi.dev/v1/transcripts \
        -H "Authorization: Bearer $YT_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{
          "video_id": "dQw4w9WgXcQ",
          "format": "markdown"
        }'
      ```
    </LanguageSample>

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

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

      func main() {
      	payload, _ := json.Marshal(map[string]any{
      		"video_id": "dQw4w9WgXcQ",
      		"format":   "word_timestamps",
      	})

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

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

      	body, _ := io.ReadAll(resp.Body)
      	fmt.Println(string(body))
      }
      ```
    </LanguageSample>
  </MethodSamples>
</MethodPage>
