TypeScript SDK
@playhead/sdk is a typed TypeScript client over the same REST API. It handles the parts you would otherwise write twice: waiting on long jobs, sending 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 call to send instead.
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
// No question means the whole video, second zero to the end.
const video = await playhead.ask({
url: "https://www.tiktok.com/@user/video/7300000000000000000",
});
console.log(video.answer); // the reading, with the second on every moment
console.log(video.seen_at); // [0, 1.2, 2.9, 4.0, …] the seconds it read
console.log(video.unanswered); // what the video does not show, named
console.log(video.credits_charged);
// The next question about this video is the cheap one.
const more = await playhead.ask({
session_id: video.session_id,
question: "What does the price on screen say?",
});Methods
Everything is typed, including the fields that are optional for a reason.
- Promise<AskResult>
- A question about a video, answered in words. Keep the session_id and pass it with the next one. Start here.
- Promise<AskResult>
- One second of the video, read at full resolution and answered in words.
- Promise<AskResult>
- ask() under the name the first version of this client used. Kept because code in the wild calls it.
- Promise<TranscriptResult | QueuedJob>
- The words, with the times they were said. No pictures are read.
- Promise<BatchStarted>
- Ask about several videos with one request. Use this instead of a loop over ask(). Every video is read on its own, so the answers are comparable.
- Promise<BatchResult>
- Wait for a batch and return every answer together. Each call holds the connection for up to 60 seconds, so short videos are usually one round trip.
- Promise<BatchResult>
- The batch as it stands. wait holds the connection for up to 60 seconds. Each item carries the ref you gave it.
- Promise<{ batches: BatchSummary[] }>
- Your recent batches, without the answers in them.
- Promise<{ batch_id, status }>
- Stop what has not started. What is already running finishes, and is charged.
- Promise<UploadResult>
- Send a file from your own machine. The bytes stream off disk, so a large file does not become memory. Returns an upl_… id you can use anywhere a url goes.
- Promise<UploadTicket>
- A signed upload address somebody else can send bytes to, with no credential of yours.
- Promise<Job>
- One read of a queued job.
- Promise<Job>
- Wait until the job settles. Throws JobFailed carrying the job's own error body if it does not.
- Promise<CreditsResult>
- Balance, plan and the ledger behind both.
- Promise<DeleteResult>
- Erase a video and everything read 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 seconds you asked for are more than this call allows. Carries suggestion and maxWindowSeconds. | No |
SourceUnavailable | The video could not be reached: private, deleted, or behind a sign-in. Upload the file instead. | 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, or neither a url nor an upload id. | 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.ask({ url, question, window: { from: 0, to: 900 } });
} catch (error) {
if (error instanceof WindowTooLong && error.suggestion) {
// The refusal carries the call that would have worked.
await playhead.ask({ url, question, ...error.suggestion });
} else {
throw error;
}
}All 13 types that arrive over the wire are on the errors page, with what to do about each one.
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.ask({
url: upload.upload_id,
question: "Is there a black frame anywhere, and does the logo hold to the end?",
});
console.log(video.answer);const started = await playhead.batches.create({
question: "What is the hook, and when does the call to action appear?",
items: ads.map((ad) => ({ url: ad.url, ref: ad.id })),
});
const batch = await playhead.batches.wait(started.batch_id);
for (const item of batch.items) {
// Match by ref, never by position: a video that failed keeps its place.
console.log(item.ref, item.error ?? item.answer);
}const { text, segments, speech } = await playhead.transcript({ url, words: true });
if (!speech?.is_speech) {
// Music comes back as fluent sentences nobody said. This is the field
// that tells you so before you act on them.
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, so Python, Go, Rust or anything else works with the HTTP client you already use. There is nothing this package can do that a plain request cannot.
- Does the SDK need Node?
- It targets Node 20 and above. Everything else it does is a plain fetch and would run anywhere, but sending a file off your disk is what sets the version floor.
- What is the difference between ask() and watch()?
- Nothing. watch() is the name the first version of this client used, and it still works so that code in the wild keeps running. New code should call ask().
- Which failures does the SDK retry?
- A connection that never landed, a 429, and any 5xx. It backs off between attempts and honours Retry-After when the server sends one. Nothing else is retried: a 402 or a malformed request would only burn the rate limit to arrive at the same refusal.