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.
npm install @playhead/sdkimport { 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.
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.
| Class | When | Retried automatically |
|---|---|---|
WindowTooLong | The window exceeds the level's cap. Carries suggestion and maxWindowSeconds. | No |
SourceUnavailable | Geo-blocked, private, deleted, or behind a login. | No |
AuthenticationError | Missing, wrong or expired credential. | No |
PermissionError | Authenticated, but not allowed to do this. | No |
InsufficientCredits | Out of credits. | No |
RateLimited | Too many requests or too many at once. Carries retryAfter. | Yes |
NotFound | A job, video or upload id that names nothing. | No |
InvalidRequest | Malformed — a bad timestamp, no source, an unknown level. | No |
ConnectionError | The request never completed. | Yes |
JobFailed | A queued job settled as failed. Carries the job's own error body. | No |
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.
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 }]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),
});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.