Errors

An error from this API is a request you can send. Ask for a five-second window at a level capped to three and the refusal carries a suggestion object holding the three-second call — ready to spread straight back into the retry. That is the difference between an agent that recovers and one that apologises.

Last updated

The shape of an error

Two fields are always present. The third is what makes an agent able to recover on its own.

A window that is too long for its leveljson
{
  "error": {
    "type": "window_too_long_for_detail",
    "message": "motion is limited to 3s windows; you asked for 5s.",
    "suggestion": { "start": 0, "end": 3, "detail": "motion" },
    "max_window_seconds": 3
  }
}
type
The machine-readable discriminator. Branch on this, never on the message — messages are written for a reader and will be improved.
message
What went wrong, written to be acted on. It names the limit and what was asked for, not just that a limit exists.
suggestion
Present when one concrete call would have worked. It is an arguments object, so the recovery is a spread rather than a parse.
alternative
Present when no single call is the answer — a different approach, in prose, for a reader to choose from.
Recovering, with the SDKts
import { WindowTooLong } from "@playhead/sdk";

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

Every type

Thirteen of them. The recover column is the point of the table — an error type without a fix is just a label.

Every error type the API returns
TypeStatusWhenWhat to do
window_too_long_for_detail400The window asked for is longer than that level allows.The response carries a suggestion object holding the call that would work. Spread it back into the request.
invalid_window400end is before start, or the window falls outside the video.Clamp against video.duration, which any earlier response reports.
unknown_detail400A detail level that does not exist.GET /v1/detail-levels returns the current list.
invalid_request400A malformed timestamp, or neither url nor upload_id.The message names the field. Timestamps take seconds or MM:SS.
unauthorized401No credential, a malformed one, or an expired token.Send Authorization: Bearer sk_live_…. Keys are created in the dashboard.
insufficient_credits402The wallet this credential spends is empty.Buy credit or wait for the monthly renewal. Not a retry — the balance has to change first.
insufficient_scope403An OAuth token that was never granted video:read.Reconnect the account and approve the scope.
tier_limit403The plan does not allow this — motion on free, or a video past the length cap.Use a lower level, a shorter window, or upgrade.
video_not_found404A video, job or upload id that names nothing you own.Ids are scoped to an organization. Check which key you are sending.
source_unavailable422The video could not be fetched: private, geo-blocked, deleted, or behind a login.Upload the file instead. The message distinguishes a login wall from a hard block, because they need different fixes.
no_video_stream422ffprobe cannot read the file as media, or it holds neither video nor audio.Camera raw is refused by name with the export to make instead. Everything else means a broken or truncated file.
rate_limited429Too many requests, or more jobs at once than the plan allows.Back off and retry. Honour Retry-After when it is sent.
processing_failed500A fault on our side, not something to fix in the request.Retrying is safe and is the right move.

Retrying

Two of the thirteen. The SDK does this for you; anything else building its own queue needs the same rule.

Which errors are worth retrying
RetryDo not retry
rate_limited (429) — back off, honouring Retry-Afterinsufficient_credits (402) — the balance has to change
processing_failed (500) — a fault on our side, safe to repeatwindow_too_long_for_detail (400) — send the suggestion instead
A connection that never landedsource_unavailable (422) — upload the file

Silence that is not an error

Two responses come back with a 200 and still mean “do not conclude anything from this”.

technical.skipped === true
The cut, freeze, black-frame and silence detectors never ran — they are skipped on windows over fifteen minutes. The empty lists beside this flag mean nothing at all, and reporting “no cuts” from them is the mistake this field exists to prevent.
speech.is_speech === false
The audio was music or noise rather than speech. A transcript is still returned, and it is fluent and entirely invented. Treat it as absent.
coverage.gaps.length > 0
Not an error either, but the seconds nobody looked at. coverage.truncated.next_start names where to resume when a level's window cap cut the request short.

The SDK turns each wire type into a class, so these become instanceof checks rather than string comparisons.

Questions

Which errors should I retry?
Only 429 and 5xx, plus a connection that never landed. Everything else needs something to change first — a shorter window, a different level, a topped-up balance — and retrying it burns the rate limit to arrive at the same refusal.
What is the suggestion field?
Arguments for a call that would work, not prose. When a window is too long for a detail level, suggestion holds the window that fits, ready to spread straight back into the request.
How do I tell a geo-block from a login wall?
The message distinguishes them, because they need different fixes: a login wall means upload the file, while a block means the video may be reachable from elsewhere. Both arrive as source_unavailable with a 422.
Why did an empty cut list come back for a long video?
Check technical.skipped. The detectors are skipped automatically on windows longer than fifteen minutes because a full decode costs more than the flags are worth on a survey. Without that flag, a video nobody measured looks identical to a video with no cuts.