---
title: "Get the words, with the times they were said"
method: POST
path: "/v1/transcript"
tags: ["Video"]
---

# Get the words, with the times they were said

`POST /v1/transcript`

## Request

- Base URL: `https://api.tryplayhead.com`
- URL: `https://api.tryplayhead.com/v1/transcript`
- Auth: HTTP bearer

Text only — no images, so **no vision tokens at all**. This is the call to make whenever the question is about what is said rather than what is shown; it is the cheapest endpoint in the API by a wide margin.

The uploader's own captions are preferred when they exist, because a human typed the names; otherwise the audio is transcribed. The response says which of the two happened.

Set `language` when the audio is mostly music, crowd or engine noise: detection misfires there and a wrong language returns fluent nonsense. Every response also reports whether the audio was speech at all, so music scored over a video is not handed back as dialogue.

## Request body

- union: Exactly one of `url` or `upload_id` is required.
  - object
    - `url` string, required: A link to the video. YouTube, TikTok, Instagram, X, Facebook (including Meta Ad Library), Vimeo, Twitch, Dailymotion and Reddit are recognised by name; any other page or direct media URL is attempted generically. An `upl_…` id from POST /v1/uploads is also accepted here.
    - `upload_id` string: The id returned by POST /v1/uploads. Use instead of `url`.
    - `language` string
    - `words` boolean: Include per-word timings.
    - `start` union: Seconds (`12.5`) or a clock (`0:12.5`, `1:03:20`).
      - number
      - string
    - `end` union: Seconds (`12.5`) or a clock (`0:12.5`, `1:03:20`).
      - number
      - string
    - `format` 'json' | 'srt' | 'vtt' | 'text'
    - `sync` boolean: Run the work inside this request. Honored on a one-machine deployment only: a worker-fleet deployment ignores it, answers `202` with a `job_id`, and the job is polled at `GET /v1/jobs/{jobId}`. The accepted row is durable before the answer, so a disconnect loses nothing, and a repeat with the same `idempotency_key` returns the original job.
    - `webhook_url` string, uri
    - `idempotency_key` string
  - object
    - `url` string: A link to the video. YouTube, TikTok, Instagram, X, Facebook (including Meta Ad Library), Vimeo, Twitch, Dailymotion and Reddit are recognised by name; any other page or direct media URL is attempted generically. An `upl_…` id from POST /v1/uploads is also accepted here.
    - `upload_id` string, required: The id returned by POST /v1/uploads. Use instead of `url`.
    - `language` string
    - `words` boolean: Include per-word timings.
    - `start` union: Seconds (`12.5`) or a clock (`0:12.5`, `1:03:20`).
      - number
      - string
    - `end` union: Seconds (`12.5`) or a clock (`0:12.5`, `1:03:20`).
      - number
      - string
    - `format` 'json' | 'srt' | 'vtt' | 'text'
    - `sync` boolean: Run the work inside this request. Honored on a one-machine deployment only: a worker-fleet deployment ignores it, answers `202` with a `job_id`, and the job is polled at `GET /v1/jobs/{jobId}`. The accepted row is durable before the answer, so a disconnect loses nothing, and a repeat with the same `idempotency_key` returns the original job.
    - `webhook_url` string, uri
    - `idempotency_key` string

## Response `200`

The transcript, or the queued job.

- union
  - TranscriptResult
    - `language` string, nullable
    - `source` 'captions' | 'transcribed': `captions` when the uploader wrote them, `transcribed` when we heard them. They differ in reliability and that is the caller's business.
    - `text` string
    - `segments` object[]
      - `start` number
      - `end` number
      - `text` string
      - `confidence` number
      - `words` object[]: Present when `words` was true.
        - `start` number
        - `end` number
        - `word` string
        - `confidence` number
    - `speech` object: Whether the audio was speech at all. Music scored over a video transcribes into fluent sentences nobody said; this says so rather than handing back lyrics as dialogue.
      - `is_speech` boolean
      - `confidence` number
      - `reason` string
    - `low_confidence` object[]: Passages the model was unsure of. Settle these against the frames.
      - `start` number
      - `end` number
      - `text` string
      - `confidence` number
    - `job_id` string
    - `status` string
    - `credits_charged` number, nullable
    - `cache_hit` boolean
    - `video` Video
      - `id` string
      - `platform` string: `youtube`, `tiktok`, `instagram`, `x`, `facebook`, `vimeo`, `reddit`, `twitch`, `dailymotion`, `web` for a generically extracted page, or `upload`.
      - `duration` number: Seconds.
      - `width` integer
      - `height` integer
      - `orientation` string
      - `src_fps` number
      - `title` string, nullable
      - `language` string, nullable
      - `engagement` object, nullable: The platform's own public numbers for the post, read when the link was resolved. A count the platform does not give is null, never 0: Instagram gives no views without a login. Null for an upload.
        - `views` integer, nullable
        - `likes` integer, nullable
        - `comments` integer, nullable
        - `reposts` integer, nullable
        - `posted_at` string, nullable: When the post went up: a date and time, or a date when the platform gives only the day.
        - `read_at` string, date-time: When these numbers were read.
      - `content_box` object, nullable: The picture inside the frame when the source was letterboxed. Present means the tiles show this region, not the full frame.
        - `x` integer
        - `y` integer
        - `width` integer
        - `height` integer
      - `carousel` object, nullable: Present when the post is a photo carousel — N stills with no timeline. One second is one slide: second 0 is slide 1. The number is an index wearing the units the rest of the API speaks, not a duration.
        - `slides` integer
        - `audio_seconds` number, nullable
      - `start_timecode` string, nullable: SMPTE timecode of the source's first frame, e.g. `14:36:27:07`. Present on camera and deck formats, absent on everything from the web. A different clock from the elapsed seconds everywhere else in this response — both are true, and neither should be converted into the other.
      - `interlaced` boolean
      - `audio_tracks` integer: Streams, not channels. A camera master routinely has eight.
    - `subtitles` string: Present when `format` was `srt` or `vtt`.
  - QueuedJob: What an asynchronous create returns, with status `202`: the work is accepted and durable, not done. Poll `GET /v1/jobs/{jobId}`. A worker crash retries the job with a growing delay up to its attempt budget, and the terminal state then carries a typed `error`. Each job also has a deadline; an attempt cannot outlive it.
    - `job_id` string
    - `status` 'queued'
    - `queue` string
    - `queued_at` string, date-time

## Other responses

- `400`: The request cannot be run as written.
- `401`: Missing, malformed or expired credential.
- `402`: Out of credits.
- `422`: The video could not be fetched.
- `429`: Rate limited.

---

[API](https://skmtc.dev/tryplayhead/apis/playhead-api.md) · [All operations](https://skmtc.dev/tryplayhead/apis/playhead-api/llms.txt) · [OpenAPI document](https://skmtc.dev/tryplayhead/apis/playhead-api/revisions/21dfb775b852?raw)
