SkipDBSkipDB

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_typeMeaningMax length
introTitle sequence / opening credits.5 min
recap“Previously on…” catch-up before the episode proper starts.5 min
outroEnd 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 limited

Endpoints

GET/api/segmentsOpen · 120/min

Fetch the best segment of each type for a movie or episode, adjusted for the requester's stream duration when given.

Query params

NameTypeNotes
imdb_id*stringIMDb id, e.g. tt0903747.
seasonintegerOmit both season and episode for a movie.
episodeintegerSee season.
typeintro | recap | outro | previewRestrict to one segment type. Omit to fetch all four — see note below.
durationnumber (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

NameTypeNotes
segmentsobjectAlways 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 | nullnull 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_msnumber | nullMedian 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 exceeded
curl "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
}
POST/api/segmentsSession or API key · 30/min

Submit 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

NameTypeNotes
imdb_id*stringIMDb id.
segment_type*intro | recap | outro | previewFixed once submitted — edit the times via PATCH, not the type.
seasonintegerOmit both for a movie.
episodeintegerSee season.
start_ms*numberMilliseconds.
end_msnumberRequired 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_msnumberStream 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 exceeded
curl -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.

PATCH/api/segments/{id}Owner or moderator/admin

Edit 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)

NameTypeNotes
start_msnumberMilliseconds.
end_msnumberMilliseconds.
duration_msnumberduration_sec also accepted.
clear_durationbooleanDrop the stored duration_ms.
segment_typestringImmutable — 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." }
DELETE/api/segments/{id}Owner or moderator/admin

Delete a submission.

401 no session/API key403 not your submission and not staff404 segment not found
{ "id": 42, "deleted": true }
POST/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

NameTypeNotes
value*1 | -1 | 01 = 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 } }
GET/api/titles/searchOpen · 30/min

Search by name or IMDb id. Proxies TMDB (when configured) and always also checks titles already in the database.

Query params

NameTypeNotes
q*stringFree-text name, or an IMDb id (e.g. tt0903747) for a direct lookup.

Response fields

NameTypeNotes
provider"tmdb" | "local" | "none"Which source results came from.
resultsarrayTMDB (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.
localarrayTitles already in SkipDB's database matching by name (name search only).
curl "https://api.skipdb.tv/api/titles/search?q=Breaking%20Bad"
GET/api/titles/{imdbId}Open · 30/min

Title metadata with the season/episode list and a per-episode coverage matrix (which segment types have data).

Response fields

NameTypeNotes
namestring
yearnumber | null
media_type"movie" | "series"
poster_urlstring | null
seasonsarraySeason list.
totalsobjectAggregate coverage counts.
episodesarrayPer-episode coverage matrix.
400 invalid IMDb id429 rate limit exceeded
GET/api/dumpOpen · 6/min

The 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

NameTypeNotes
segmentsarrayEach 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.
countnumberLength of segments.
generated_atstring (ISO 8601)
POST/api/keysSession only

Generate 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. ..." }
POST/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. ..." }