API documentation
Base URL: https://api.skipdb.tv. All timestamps are in milliseconds (_ms suffix) unless noted otherwise. Data is licensed ODbL 1.0 + reciprocity unless you have explicit permission. Fields marked * are required.
Segment types
Every segment belongs to exactly one of these four segment_type values — this is the only set the API accepts for the type query param and the segment_type body field.
| segment_type | Meaning | Max length |
|---|---|---|
intro | Title sequence / opening credits. | 5 min |
recap | “Previously on…” catch-up before the episode proper starts. | 5 min |
outro | End credits. end_ms may be omitted — it defaults to duration_ms (credits run to the end of the stream). If provided, an end within 10s of the stream duration is snapped to it automatically. | 15 min |
preview | “Next time on…” teaser for the following episode, usually after the outro. | 15 min |
All segments must be at least 5s long, with one exception: submitting start_ms: 0, end_ms: 0is a sentinel meaning “confirmed — this episode has no segment of this type” (e.g. no recap this week). It always passes validation and is treated differently from a missing/unsubmitted segment.
Conventions
Auth
Reading is open and rate-limited (120 req/min). Writing needs one of:
- a logged-in session cookie (browser use), or
- Authorization: Bearer skdb_…, or
- X-API-Key: skdb_…
Writes are rate-limited to 30 req/min per account. Anonymous API keys (see below) can do everything a full account can except vote.
Errors
Errors are always { "error": "message", ... } with extra context where relevant (e.g. issues for validation, conflicting_segment_id for conflicts).
400 malformed request401 auth required403 not allowed404 not found409 conflict422 failed validation429 rate limitedEndpoints
/api/segmentsOpen · 120/minFetch the best segment of each type for a movie or episode, adjusted for the requester's stream duration when given.
Query params
| Name | Type | Notes |
|---|---|---|
imdb_id* | string | IMDb id, e.g. tt0903747. |
season | integer | Omit both season and episode for a movie. |
episode | integer | See season. |
type | intro | recap | outro | preview | Restrict to one segment type. Omit to fetch all four — see note below. |
duration | number (seconds) | Stream length. Strongly recommended — enables duration matching/shifting and improves CDN cache hit rates versus passing ms. |
adjust | "conservative" | "greedy" | "none" | Default conservative. How to shift timestamps when the stream duration is close but not identical: conservative only shifts earlier (never risks a late skip button), greedy shifts in either direction, none never shifts (still reports out-of-range). |
Response fields
| Name | Type | Notes |
|---|---|---|
segments | object | Always has all four keys (intro, recap, outro, preview). If type was passed, the other three keys are always null— that does not mean no data exists for them, just that they weren't requested. |
segments[type] | object | null | null means no approved data yet for that type. Otherwise: start_ms, end_ms, match (exact | shifted | agnostic | out-of-range), adjusted (bool, whether start/end were shifted), offset_ms (requested minus stored duration, 0 if not shifted), and confidence (0–1, from submission agreement + votes + match quality). |
intro_length_estimate_ms | number | null | Median intro length for the season, when 80% of episodes agree within 15s. Use to offer a “skip ~Xs” button (minus a few seconds of lead time) when no intro segment exists yet for this episode. |
match reference: exact (stream duration matched within 2s), shifted (within 15s, timestamps adjusted for an assumed extra/missing logo or scene at the start), agnostic (no duration supplied, so no comparison was possible), out-of-range (closest available data differs too much to shift reliably — treat as an uncertain match in your UI).
400 missing/invalid imdb_id, type, adjust, season, episode, or duration429 rate limit exceededcurl "https://api.skipdb.tv/api/segments?imdb_id=tt0903747&season=1&episode=1&duration=2820"
{
"imdb_id": "tt0903747", "season": 1, "episode": 1,
"segments": {
"intro": {
"start_ms": 61000, "end_ms": 91000,
"match": "exact", "adjusted": false, "offset_ms": 0,
"confidence": 0.93
},
"recap": null,
"outro": { "start_ms": 2760000, "end_ms": 2820000, "match": "shifted", ... },
"preview": { "start_ms": 2700000, "end_ms": 2760000, "match": "out-of-range", ... }
},
"intro_length_estimate_ms": 30000
}/api/segmentsSession or API key · 30/minSubmit a segment. Submitting implies agreement to publish under ODbL 1.0 + reciprocity (see terms). Resubmitting the same episode/type/duration within 24h edits your existing submission instead of creating a duplicate.
Body
| Name | Type | Notes |
|---|---|---|
imdb_id* | string | IMDb id. |
segment_type* | intro | recap | outro | preview | Fixed once submitted — edit the times via PATCH, not the type. |
season | integer | Omit both for a movie. |
episode | integer | See season. |
start_ms* | number | Milliseconds. |
end_ms | number | Required for every type except outro, where it defaults to duration_ms if omitted (so at least one of end_ms / duration_ms must be given for an outro). |
duration_ms | number | Stream length in ms. Recommended — improves matching for other requesters. duration_sec accepts the same thing in seconds or a clock string (e.g. "47:00"). |
401 no session/API key422 schema validation failed (issues[]) or bounds invalid (too short/long, end ≤ start, beyond duration)409 identical approved segment already exists (vote_url returned), or overlaps your own other submission (conflicting_segment_id)429 rate limit exceededcurl -X POST https://api.skipdb.tv/api/segments \
-H "Authorization: Bearer skdb_xxx" \
-H "Content-Type: application/json" \
-d '{
"imdb_id": "tt0903747",
"season": 1, "episode": 1,
"segment_type": "intro",
"start_ms": 61000, "end_ms": 91000,
"duration_ms": 2820000
}'
{ "id": 42, "status": "approved", "auto_approved": true,
"reasons": ["matches an existing approved segment (consensus)"],
"message": "Submission accepted and published.",
"license": "ODbL 1.0 + Service Provider Reciprocity ..." }status is one of approved, pending, rejected, or (on a 409) already_approved.
/api/segments/{id}Owner or moderator/adminEdit a submission's times. Re-runs review, so a previously auto-approved segment can drop back to pending if the new values no longer qualify.
Body (all optional — omitted fields keep their current value)
| Name | Type | Notes |
|---|---|---|
start_ms | number | Milliseconds. |
end_ms | number | Milliseconds. |
duration_ms | number | duration_sec also accepted. |
clear_duration | boolean | Drop the stored duration_ms. |
segment_type | string | Immutable — passing a different value than the segment already has returns a 422. Delete and resubmit instead. |
401 no session/API key403 not your submission and not staff404 segment not found422 bounds invalid or segment_type change attempted409 overlaps your own other submission{ "id": 42, "status": "pending", "auto_approved": false,
"reasons": [...], "message": "Submission updated and sent back to review." }/api/segments/{id}Owner or moderator/adminDelete a submission.
401 no session/API key403 not your submission and not staff404 segment not found{ "id": 42, "deleted": true }/api/segments/{id}/voteSession or registered API key (not anonymous)Vote a segment up or down. Requires a real, ownable identity — an anonymous key (below) can't vote — and you can't vote on your own submission.
Body
| Name | Type | Notes |
|---|---|---|
value* | 1 | -1 | 0 | 1 = good, -1 = bad, 0 = clear your existing vote. |
400 invalid segment id or JSON body401 auth required403 own segment, or segment disabled404 segment not found422 value must be 1, -1, or 0429 rate limit exceeded (60/min){ "segment_id": 42, "your_vote": 1,
"votes": { "up": 6, "down": 1, "score": 5 } }/api/titles/searchOpen · 30/minSearch by name or IMDb id. Proxies TMDB (when configured) and always also checks titles already in the database.
Query params
| Name | Type | Notes |
|---|---|---|
q* | string | Free-text name, or an IMDb id (e.g. tt0903747) for a direct lookup. |
Response fields
| Name | Type | Notes |
|---|---|---|
provider | "tmdb" | "local" | "none" | Which source results came from. |
results | array | TMDB (or fallback) matches. For an IMDb-id query with no metadata match, contains a single placeholder object with a note field — you can still submit segments for it. |
local | array | Titles already in SkipDB's database matching by name (name search only). |
curl "https://api.skipdb.tv/api/titles/search?q=Breaking%20Bad"/api/titles/{imdbId}Open · 30/minTitle metadata with the season/episode list and a per-episode coverage matrix (which segment types have data).
Response fields
| Name | Type | Notes |
|---|---|---|
name | string | |
year | number | null | |
media_type | "movie" | "series" | |
poster_url | string | null | |
seasons | array | Season list. |
totals | object | Aggregate coverage counts. |
episodes | array | Per-episode coverage matrix. |
400 invalid IMDb id429 rate limit exceeded/api/dumpOpen · 6/minThe full open data dump of every approved segment — no user data. Licensed ODbL 1.0 + reciprocity. This is the guarantee that the data stays free. Cached for an hour; may 302-redirect to a static mirror.
Response fields
| Name | Type | Notes |
|---|---|---|
segments | array | Each row: id, imdb_id, media_type, season, episode, segment_type, start_ms, end_ms, duration_ms, submitted_by (opaque user id, no PII), votes_up, votes_down, score, created_at, updated_at. |
count | number | Length of segments. |
generated_at | string (ISO 8601) |
/api/keysSession onlyGenerate or reset your API key. GET returns the active key's prefix (not the secret); POST returns the plaintext key once and revokes any previous key; DELETErevokes it. Not available via API key — you can't bootstrap key management from a key.
// POST response (201)
{ "key": "skdb_live_...", "prefix": "skdb_live_ab12",
"message": "Here is your API key. ..." }/api/keys/anonymousOpen (IP rate-limited)Get an API key without signing up. Creates a blank, login-less user behind the scenes and returns its key (plaintext, once) — there's no account to recover it from, so save it immediately. Rate-limited per IP (5/hour). DELETE with the same key (via Authorization or X-API-Key) revokes it.
Anonymous keys can submit, edit, and delete their own segments the same as a full account — the one thing they can't do is vote on other people's submissions, since voting feeds reputation and abuse resistance that depend on a real, ownable identity.
429 more than 5 keys from this IP in an hour (POST)401 missing key (DELETE)403 key belongs to a registered account — manage it from /account instead (DELETE)// POST response (201)
{ "key": "skdb_live_...", "prefix": "skdb_live_cd34",
"message": "Here is your anonymous API key. ..." }