This is the short reference for the hosted registry at https://openseek.duckdns.org. One lookup returns a WebVTT cue track; each cue names a 320×180 tile inside a JPEG sheet. Start at OpenSeek, manage keys on the dashboard, and read the client lifecycle on the SDK page.
One title takes three requests. The lookup is keyed; the VTT and the sheets are open.
Look the title up. Send the metadata id and the runtime the player reports, with the key in an X-API-Key header:
curl -H 'X-API-Key: <your key>' \
'https://openseek.duckdns.org/v1/sprites?imdb_id=tt0111161&duration_ms=8280000'
The response carries the cue track URL, which version was matched, and how far coverage reaches:
{
"media_type": "movie",
"title": "The Shawshank Redemption",
"imdb_id": "tt0111161",
"vtt_url": "https://openseek.duckdns.org/s/tt0111161-v1/thumbnails.vtt",
"version": 1,
"source_duration_ms": 8280000,
"scale": 1.0,
"status": "complete",
"covered_until_ms": 8280000
}
Fetch the VTT. No key is needed for this request:
curl 'https://openseek.duckdns.org/s/tt0111161-v1/thumbnails.vtt'
Crop the tile. Each cue payload has the form sheet-320x180-0.jpg#xywh=320,0,320,180: a sheet name, then the tile box inside it. Resolve the sheet name against the directory the vtt_url came from, fetch the sheet, and crop the box described by x,y,w,h. The tile is 320×180, the only size served.
| Endpoint | Gate | Note |
|---|---|---|
GET /v1/sprites | keyed | The lookup. 401 without a valid X-API-Key header. |
GET /v1/keys/validate | open | Key self-check: present the key, get 200 {valid, name} or 401 {valid: false}. Never counts quota. |
GET /s/… | open | VTT and sheets. Sheets carry long cache headers; they are immutable once written. |
GET /v1/status, GET /v1/catalog, GET /v1/titles, GET /v1/requests | open | Catalogue metadata only. The requests read stays open but is permanently empty. |
POST /v1/contribute, POST /v1/register, contribution verify / promote / get, jobs lease / ack / fail | keyed | Writes and worker routes. The jobs routes are the operator's own and are not part of the public API. |
Signing in with a Google account issues a key for the hosted registry, up to four active keys per account. The key is shown once and only its sha256 plus a six-character prefix is stored. See and revoke your keys on the dashboard. Self-hosting needs no account: mint keys on your own host; see self-hosting.
Two 401 shapes, and only these: {"error": "api key required (X-API-Key)"} when the header is missing, {"error": "invalid api key"} when it is unknown or revoked. A revoked account's keys answer the same way.
imdb_id or tmdb_id — identifies a film. Supply one id family, not both.show_imdb_id or show_tmdb_id plus season and episode — identifies an episode.duration_ms — required. The runtime the player reports, in milliseconds. The registry matches the nearest indexed version within ±240 s of it.version — optional. Pins one indexed version instead of matching by duration.A successful lookup answers:
{
"media_type": "movie",
"title": "The Shawshank Redemption",
"imdb_id": "tt0111161",
"vtt_url": "https://openseek.duckdns.org/s/tt0111161-v1/thumbnails.vtt",
"version": 1,
"source_duration_ms": 8280000,
"scale": 1.0,
"status": "complete",
"covered_until_ms": 8280000
}
version is the indexed version chosen; source_duration_ms is that copy's runtime and scale is the requested duration divided by it, so a player reporting a slightly different runtime still lands on the right cue. status and covered_until_ms say how far previews reach (see next section). vtt_url is absolute and may point at object storage rather than the registry: resolve sheet names against its directory, never rebuild them from the origin.
The cue track is WebVTT: a WEBVTT header, then one cue per frame:
WEBVTT
00:00:00.000 --> 00:00:10.000
sheet-320x180-0.jpg#xywh=0,0,320,180
00:00:10.000 --> 00:00:20.000
sheet-320x180-0.jpg#xywh=320,0,320,180
A full bundle is positional — cue i covers second i × interval_s — but a bundle captured part-way through a title starts where it started, so never index cues by position. For a scrub position, take the last cue whose start is less than or equal to the position, after checking coverage first: when status is pending, previews exist only up to covered_until_ms, and a floor lookup past that point returns a stale tail tile instead of nothing. Past the one-hour mark timestamps start with 01:, so parse both two- and three-component times.
Two empty states, and they look different:
404 {"error": "not indexed"}. There is no VTT to fetch; show no preview.status: pending but the position is past covered_until_ms. There are cues, just none for this position; show no preview.| Status | Body | When |
|---|---|---|
| 400 | {"error": "duration_ms required"} | Lookup without a runtime. |
| 400 | {"error": "tmdb_id|imdb_id|show_* required"} | Lookup without any metadata id. |
| 400 | {"error": "bad json"} / {"error": "media_type+title+duration_ms required"} | Register with an unreadable or incomplete body. |
| 400 | {"error": "title-block JSON field required"} and kindred multipart errors | Contribute without the title block, the VTT, or any sheet. |
| 401 | {"error": "api key required (X-API-Key)"} | Keyed route, header missing. |
| 401 | {"error": "invalid api key"} | Keyed route, key unknown or revoked. |
| 401 | {"valid": false} | Validate with a missing or unknown key. |
| 404 | {"error": "not indexed"} | No title, or no version within ±240 s. |
| 404 | {"error": "version not found"} | Pinned version does not exist on the title. |
| 404 | {"error": "public intake is closed - POST previews you captured with POST /v1/contribute"} | POST /v1/jobs or POST /v1/requests. Deliberate and unauthed, so it never reads as a key problem. |
| 409 | {"error": "version_no taken"} | Register pins a version slot already held. |
| 413 | {"error": "body too large (50MB cap)"} | Contribute body over 50 MB. |
| 429 | {"error": "quota exceeded"} | Not returned today: quota is counted, never enforced. Reserved for the enforcing flag. |
Sign-in has three of its own: 404 {"error": "auth disabled"} while it is unconfigured, 429 after ten starts per address per ten minutes, and 503 when the store is unavailable. The full client lifecycle is on the SDK page; every endpoint with its gate is on the API page.
OpenSeek · Dashboard · SDK · API · Self-hosting · openseek-sdk · NuvioMobile · openseek-site. MIT licensed.