TypeScript SDK

@playhead/sdk is a typed TypeScript client over the same REST API. It handles the parts you would otherwise write twice: queueing and polling long jobs, streaming a local file up without loading it into memory, retrying only the failures a retry can fix, and turning a refusal into a typed error that carries the replacement call.

Last updated

Install

One package, no peer dependencies, Node 20 or newer.

Installbash
npm install @playhead/sdk
Analyse a whole videots
import { Playhead } from "@playhead/sdk";

const playhead = new Playhead();          // reads PLAYHEAD_API_KEY

const video = await playhead.watch({
  url: "https://www.tiktok.com/@user/video/7300000000000000000",
});

console.log(video.coverage.is_full_video);   // true — the whole video, by default
console.log(video.technical?.scene_cuts);    // [1.24, 2.88, 4.02, …]
console.log(video.transcript?.text);

for (const sheet of video.sheets) {
  console.log(sheet.t0, "→", sheet.url);
}

Methods

Ten methods over eight endpoints. Everything is typed, including the response fields that are optional for a reason.

watch(params, wait?)Promise<FramesResult>
Contact sheets for a video. Queues, polls and returns the result — or returns a synchronous answer untouched. Start here.
frames(params)Promise<FramesResult | QueuedJob>
The raw POST /v1/frames call. Returns the result when sync is true, a job when it is false.
transcript(params)Promise<TranscriptResult | QueuedJob>
Words with timings. No images, so no vision tokens.
uploads.create(pathOrBlob)Promise<UploadResult>
Send a local file. Bytes stream off disk, so file size does not become heap. Returns an upl_… id usable anywhere a url is.
uploads.ticket()Promise<UploadTicket>
A signed upload URL somebody else can POST to with no credential of yours.
jobs.get(jobId)Promise<Job>
One poll of a queued job.
waitFor(jobId, options?)Promise<Job>
Poll until the job settles. Throws JobFailed carrying the job's own error body if it does not.
credits(options?)Promise<CreditsResult>
Balance, plan and the ledger behind both.
detailLevels()Promise<DetailLevelsResult>
The six levels, straight from the engine rather than from a constant.
videos.delete(videoId)Promise<DeleteResult>
Erase a video and everything derived from it.

Configuration

Every option has a default that is right for a server-side integration.

Client optionsts
new Playhead({
  apiKey: process.env.PLAYHEAD_API_KEY,       // required; falls back to the env var
  baseUrl: "https://api.tryplayhead.com",     // or PLAYHEAD_API_URL
  timeout: 15 * 60 * 1000,                    // per attempt
  maxRetries: 2,                              // retryable failures only
  headers: { "x-team": "growth" },            // merged into every request
});

Errors

Every failure is a PlayheadError or a subclass, so a catch branches on a type rather than on a string.

Error classes exported by the SDK
ClassWhenRetried automatically
WindowTooLongThe window exceeds the level's cap. Carries suggestion and maxWindowSeconds.No
SourceUnavailableGeo-blocked, private, deleted, or behind a login.No
AuthenticationErrorMissing, wrong or expired credential.No
PermissionErrorAuthenticated, but not allowed to do this.No
InsufficientCreditsOut of credits.No
RateLimitedToo many requests or too many at once. Carries retryAfter.Yes
NotFoundA job, video or upload id that names nothing.No
InvalidRequestMalformed — a bad timestamp, no source, an unknown level.No
ConnectionErrorThe request never completed.Yes
JobFailedA queued job settled as failed. Carries the job's own error body.No
Recovering from a refusalts
import { WindowTooLong } from "@playhead/sdk";

try {
  await playhead.watch({ url, detail: "motion", start: 0, end: 5 });
} catch (error) {
  if (error instanceof WindowTooLong && error.suggestion) {
    // { start: 0, end: 3, detail: "motion" }
    await playhead.watch({ url, ...error.suggestion });
  } else {
    throw error;
  }
}

The full list of server-side types, including the 13 that arrive over the wire, is on the errors page.

Recipes

The three patterns that come up in almost every integration.

Your own footage, without publishing itts
const upload = await playhead.uploads.create("./exports/cut-v4.mov");
const video = await playhead.watch({ url: upload.upload_id, detail: "overview" });

console.log(video.video.start_timecode);   // "14:36:27:07" on a camera master
console.log(video.technical?.blacks);      // [{ start: 252.1, end: 252.2 }]
A long video, without holding a request opents
const { job_id } = await playhead.frames({
  url,
  detail: "overview",
  sync: false,
  idempotency_key: `weekly-${week}`,   // a scheduler that fires twice pays once
});

const job = await playhead.waitFor(job_id, {
  interval: 5_000,
  onPoll: (job) => console.log(job.status),
});
Only the words, when the question is about what was saidts
const { text, segments, speech } = await playhead.transcript({ url, words: true });

if (!speech?.is_speech) {
  // Music transcribes into fluent sentences nobody said.
  throw new Error("this audio is not speech");
}

More at Examples, each one a real question with the call that answers it.

Questions

Is there a Python SDK?
Not yet. The REST API is plain JSON over HTTP and there is an OpenAPI 3.1 spec at /openapi.json, so a generated client in Python — or Go, Rust, or anything else — takes one command with your generator of choice.
Does the SDK need Node?
It targets Node 20 and above, because uploading a local file streams it off disk rather than reading it into memory. Everything else is fetch and would run anywhere, but the upload path is what the version floor is for.
What does watch() do that frames() does not?
It handles both shapes the server can answer with. Queued work is polled to completion and the result returned; a synchronous answer is returned immediately without a single extra request. Unless you are managing your own queue, watch() is the method to reach for.
Which failures does the SDK retry?
A connection that never landed, a 429, and any 5xx — with exponential backoff and jitter, honouring Retry-After when the server sends one. Nothing else. Retrying a 402 or a bad window only burns the rate limit to arrive at the same refusal.