Skip to main content

Documentation Index

Fetch the complete documentation index at: https://api.simkl.org/llms.txt

Use this file to discover all available pages before exploring further.

Scrobbling is how a media player reports real-time playback to Simkl. As the user starts, pauses, and finishes watching, your app sends events with the current progress. Simkl tracks the active session, shows the title in the user’s “Watching now” banner, and marks the item watched once playback completes. If you only need a “Mark as watched” button without real-time progress reporting, use Mark as watched instead.
Simkl user dashboard showing 'NOW WATCHING' Fallout S02E07 'The Handoff' with a 46% watched progress bar and 27 minutes left

The lifecycle at a glance

Four endpoints. The 80% threshold is the only “magic number” you have to remember.
EndpointCall whenEffect
startPlayback begins or resumesCreates an active session and shows the title in the user’s “Watching now” banner.
pauseUser pausesSaves current progress. The session is resumable from any device on the same account.
stopUser stops or playback endsprogress ≥ 80 → marked watched. progress < 80 → kept as a saved playback (resumable later).
checkinFire-and-forget alternativeOne call, no progress tracking. Server auto-completes shortly after the item’s runtime expiry — typically within ~2 minutes.
Send every identifier you have — title, year, and the full ids object.Simkl walks the ids object in priority order (simkl first when present, then external IDs like imdb, tmdb, tvdb, mal, anidb, …). If no ID resolves, it falls back to a title + year match, then to title-only as a last resort. Sending everything you know — title, year, plus every external ID your client has cached — maximizes the chance the right item gets credited. Extra fields are free; missing fields can cause a 404 id_err or, worse, a silent mismatch.You don’t need to search before writing. Endpoints like /scrobble/*, /sync/history, /sync/add-to-list, and /sync/ratings resolve IDs server-side — pass whatever you have directly, no /search/* round-trip required. Calling /search/id first to “resolve” a Simkl ID is wasted work that doubles your API quota for no gain.See Supported ID keys for the full list.

Which pattern do I need?

Do not poll /scrobble/* periodically. Send events only on real user actions — pressed Play, Paused, Stopped — with the current progress percentage. Simkl automatically advances progress between events using the item’s known runtime, so periodic re-posting wastes API quota and triggers rate limits.

Universal player-event mapping

This is the table to keep open while you’re wiring your player. It’s intentionally player-agnostic — the same mapping works for HTML5 <video>, AVPlayer, ExoPlayer, libVLC-based players, Kodi, Roku, Stremio addons, and anything else that emits playback events.
Player eventScrobble callWhen / why
Playback started (from beginning)POST /scrobble/start with progress: 0First time the user opens the title.
Playback resumed (from pause)POST /scrobble/start with current progressReplaces the existing session and clears prior pauses.
User pausesPOST /scrobble/pause with current progressServer saves the session for cross-device resume.
User stops or closes the playerPOST /scrobble/stop with current progressBelow 80 saves a paused session; >= 80 marks watched.
Playback ends naturally (credits / next-up)POST /scrobble/stop with progress: 100Server marks the title watched.
User seeks (any direction) within the same itemNo callUpdate your local progress variable. Reported on the next event.
User scrubs aggressivelyNo callSame rule. The 20-sec lock would reject overlapping calls anyway.
User changes subtitles or audio track(no call)Doesn’t affect progress.
User skips to next episodePOST /scrobble/stop (current item) → POST /scrobble/start (next)Two calls, both at the right progress.
Network error during a callretry on next eventDon’t loop on retries — the next real event covers it.

Handling seek and scrub events

When the user drags the playhead — fast-forwarding, rewinding, or scrubbing aggressively — don’t fire a scrobble call for the seek itself. Just update your local progress variable. The next real player event (play / pause / stop) reports the new position. Why? Each scrobble call counts against your client’s request budget, and the server’s 20-second per-user lock will reject overlapping in-flight calls. A user who scrubs three times per minute would burn 180 calls/hour for nothing — and miss the actual lifecycle events. Here’s a typical session with seeks, showing which events fire calls and which don’t: Progress can be non-monotonic between calls. A user can pause at 80%, rewind, and stop at 30% — the second progress: 30 is correct and your code shouldn’t try to “fix” it. The server stores whatever you send. The ≥80% auto-scrobble threshold only fires on stop, not pause. A user can scrub past 80% mid-playback dozens of times without triggering the watched mark — it only counts when they actually stop.
Start ≠ watched. A bare start only puts the title in the “Watching now” banner. The item is marked watched only when:

Reference scrobbler implementations

Below is the same minimal Scrobbler in four languages. Each one:
  • exposes start(item), pause(progress), stop(progress) and checkin(item),
  • attaches the standard auth header and required URL params,
  • shows how to wire it to a real player’s events.
Adapt the movie payload to TV / anime by replacing the movie field with show + episode or anime + episode (see Examples below).
# Generic Python scrobbler. Drop into any player that has play/pause/stop hooks.
# Tested shape: works as-is from a Kodi service add-on, mpv Lua bridge, or any
# Python-driven player.
import requests

API = "https://api.simkl.com"
PARAMS = {
    "client_id":   "YOUR_CLIENT_ID",
    "app-name":    "my-app-name",
    "app-version": "1.0",
}
HEADERS = {
    "Authorization": "Bearer USER_ACCESS_TOKEN",
    "User-Agent":    "my-app-name/1.0",
    "Content-Type":  "application/json",
}

class Scrobbler:
    def __init__(self, item):
        # item example: {"movie": {"title": "Inception", "year": 2010,
        #                          "ids": {"imdb": "tt1375666"}}}
        self.item = item

    def _post(self, action, progress=None):
        body = dict(self.item)
        if progress is not None:
            body["progress"] = progress
        r = requests.post(f"{API}/scrobble/{action}",
                          params=PARAMS, headers=HEADERS, json=body, timeout=10)
        r.raise_for_status()
        return r.json()

    def start(self, progress=0):    return self._post("start",   progress)
    def pause(self, progress):      return self._post("pause",   progress)
    def stop(self,  progress):      return self._post("stop",    progress)
    def checkin(self):              return self._post("checkin")

# Wire-up: call from your player's onPlaybackStarted / onPaused / onStopped hooks.
# scrob = Scrobbler({"movie": {"title": "Inception", "year": 2010,
#                              "ids": {"imdb": "tt1375666"}}})
# scrob.start(0)             # on play
# scrob.pause(45.2)          # on pause, with current progress %
# scrob.start(45.2)          # on resume
# scrob.stop(100)            # on natural end -> marks watched
The Scrobbler classes above intentionally skip retry/backoff and token refresh — those are app-level concerns. Wire your existing HTTP client and auth layer in their place; the scrobble lifecycle is just four POSTs.

Continue Watching across devices

Pause/stop on one device, resume on another. Any signed-in device with the same access_token can pick up where the user left off. The pattern:
1

Device A: pause

POST /scrobble/pause with the user’s token and current progress. Simkl saves the position as a playback.
2

Device B: gate the refetch on activities

POST /sync/activities returns a playback timestamp per media-type bucket. Compare it to the value you saved last sync — if it hasn’t moved, no new pause has happened and you can skip the next step.
3

Device B: discover

Only when the playback timestamp moved: GET /sync/playback returns all paused playbacks for this user (or narrow with /sync/playback/episodes / /sync/playback/movies if your UI only renders one kind). Save the new timestamp.
4

Device B: resume

POST /scrobble/start with the same item and the saved progress. Simkl continues the session; the prior pause is cleared automatically.
Don’t poll /sync/playback on a timer. Always gate refetches on the playback timestamp from /sync/activities — that endpoint is the cheap “is anything new?” check. Idle accounts won’t trip the gate, so quota stays free for active users. The same pattern applies to every Sync surface (history, watchlist, ratings); see the Sync guide for the full strategy.
Members can also browse and clean up their saved playbacks at the Playback progress manager.

Anime episode numbering

Anime and Western TV catalogs disagree about what an “episode” is. Simkl supports both numbering schemes and maps between them automatically — pick whichever IDs your source naturally exposes and scrobble with that. Simkl resolves to the same canonical record either way.
Pick the key that matches your data, or default to show / shows[] when unsure. Simkl looks every item up by ids and routes to the correct catalog (TV or anime) — so the wrapper you choose is for clarity, not routing. Scrobble accepts singular show or anime; sync writes (/sync/history, /sync/add-to-list, /sync/ratings) accept plural shows[] or anime[]. Either works; match the type when known for cleaner code and a more accurate not_found.shows counter.
Want the cultural/editorial reasoning behind anime cours vs. Western seasons? Read Anime vs. Western TV Seasons in the user-facing Simkl docs — it explains why each anime cour is its own title rather than a season of a parent show.

Two numbering models

CatalogWhat’s an item?How episodes are numbered
Anime-native — Simkl, AniDB, MAL, AniList, Kitsu, AniSearch, Anime-Planet, LiveChart, ANNEach anime “cour” (season/arc) is its own title with its own ID. Attack on Titan, Attack on Titan S2, Attack on Titan S3 are three separate records.Episodes restart at 1 within each title — no season field.
Western TV catalogs — TVDB, TMDB, IMDBOne franchise = one show with multiple seasons. Attack on Titan is one show, S1 / S2 / S3 are seasons of it.Episodes numbered as season + number (e.g. S2 E4).

Scrobble with whichever IDs you have

Anime-native (anidb / mal / anilist): pass the anime-cour ID and a plain episode number — no season needed.
POST /scrobble/start
{
  "progress": 42.2,
  "anime":   { "ids": { "anidb": 9541 } },
  "episode": { "number": 4 }
}
TVDB / TMDB style: pass the show-level ID and a season + episode number. Simkl looks up its internal mapping and identifies the exact AniDB-canonical episode you mean.
POST /scrobble/start
{
  "progress": 42.2,
  "anime":   { "ids": { "tvdb": 267440 } },
  "episode": { "season": 2, "number": 4 }
}
Episode-ID shortcut (last resort): if your integration genuinely doesn’t know season/number — e.g. a Plex-style file scraper that resolved to a single TVDB or AniDB episode ID — pass it via episode.ids. Prefer season + number whenever you can though: episode IDs can be re-issued when the source catalog merges duplicates or re-numbers a season, and a stale ID returns 404, while season + number is stable forever. See Standard media objects → Episode IDs.

How Simkl maps between them

Internally Simkl stores its own episode ID per anime title, mapped to the matching TVDB season + number (and AniDB episode ID where available). When you scrobble using Western catalog season numbers (TVDB, TMDB, IMDB), Simkl walks that mapping to find the right per-anime episode; when you scrobble using any anime-native ID (anidb, mal, anilist, kitsu, anisearch, animeplanet, livechart) with a plain episode number, it goes straight to the canonical record. Every scrobble response echoes both representations so your client can update either UI:
  • season / number — AniDB-canonical (per anime-cour numbering)
  • tvdb_season / tvdb_number — the TVDB-style equivalent
Example: Attack on Titan S2 E4 could resolve to:
FieldValue
season / number1 / 4 (per the Attack on Titan S2 anime title)
tvdb_season / tvdb_number2 / 4 (per the Attack on Titan TVDB show)
canonical Simkl recordthe same row either way
This is why episodes the user marks watched on a Plex/TVDB-style player and on an anime-tracker app land in the same place on their Simkl library.

Gotchas and FAQ

No. Send events only on real player events — play, pause, stop, ended. Simkl extrapolates progress between events using the item’s known runtime. Heartbeat polling burns through your rate limit and gets apps throttled.
No call needed. Just update your local progress; the next real event (pause / stop / ended) will reflect it.
Doesn’t matter — the ≥80% auto-scrobble rule only fires on /scrobble/stop, not on every progress report. A user can scrub past 80% mid-playback as many times as they want without triggering the watched mark. The server just notes “watched” at the moment of stop if progress >= 80. So:
  • User pauses at 90% → saved at 90%, not marked watched
  • User stops at 90% → action: scrobble, marked watched
  • User stops at 75%, then resumes and stops at 95% → action: scrobble, marked watched on the second stop
  • User stops at 95%, then later rewinds to 30% and stops there → previous watched state stays; the new pause at 30% is just a saved position
Resume on the next event. The 20-second per-user lock prevents double-fire if you accidentally retry while another call is in flight. You don’t need exponential backoff inside the scrobble loop — the next user action covers it.
The item couldn’t be matched. Don’t retry /search/id with the same external IDs you already sent — Simkl already tried those and failed. Realistic fallbacks:
  • Filename-based match — if your player knows the file path, call POST /search/file. Filename uses different signals (release-group tags, year-in-name, etc.) than external IDs and may resolve where IDs didn’t.
  • Title text search — if you have a title but no good IDs, call GET /search/{type} and let the user pick from results.
  • Cache the negative — if nothing resolves, remember it so you don’t keep retrying the same un-matchable item every playback.
  • Ask the user — surface a “couldn’t identify this title” UI so the user can correct it manually.
Some titles aren’t tracked. Cache the negative result for that filename / external ID so you don’t keep retrying. Optionally let the user manually identify the title via search UI.
pause is forgiving — the server treats it as a fresh session. Best practice is still to start first so the “Watching now” banner is correct.
Each start replaces the prior session for that user. No special cleanup needed — just call start on the new title.
Same scrobble cadence. The server uses the item’s runtime to compute auto-expiry, so a 3-hour movie just gets a longer expiry window. No client changes required.
Idempotent. Your subsequent stop won’t double-count — Simkl returns 409 if the same session has already been completed within the last hour.
Yes, if both devices use the same access token. The user pauses on TV; opens the iPad app; you call GET /sync/playback to fetch all saved playbacks (or narrow with /sync/playback/episodes / /sync/playback/movies if you only render one kind); you call /scrobble/start with the saved progress to resume.
Saved playbacks are also cleared (cascade). See Sync guide → continuous sync for how to detect deletions and reconcile your local state.
The patterns above are SDK-agnostic. If your SDK exposes player-event hooks (play / pause / ended), map them to the event table. If the SDK doesn’t surface stop events reliably, fall back to /scrobble/checkin for a fire-and-forget watching-now status.
Pick checkin when (a) you can detect that playback began but (b) you cannot reliably catch the stop — e.g. embedded players, casting flows, browser extensions where the page may close without a clean event. Simkl extrapolates progress from start time + runtime and auto-marks the item watched at 100%. You can still call /scrobble/stop later to override the auto-completion if you do catch a stop event.Auto-completion timing: When the computed progress reaches 100%, the title is auto-marked watched shortly after — typically within ~2 minutes. The delay is normal and consistent across all checkin sessions.
Anime-native catalogs (Simkl, AniDB, MAL, AniList, Kitsu, AniSearch, Anime-Planet, LiveChart, ANN) treat each cour as a separate title with episodes restarting at 1; Western TV catalogs (TVDB, TMDB, IMDB) roll all seasons under one show with season + number. Simkl supports both — see the Anime episode numbering section above for examples and the internal mapping.

Reference

Request bodies

progress is a float from 0 to 100 with up to 2 decimal places. Send 75, 75.0, 75.12, or 75.00 — responses normalize (e.g. 75 not 75.00).
POST /scrobble/start
{
  "progress": 12.5,
  "movie": {
    "title": "Inception",
    "year":  2010,
    "ids":   { "imdb": "tt1375666", "tmdb": 27205 }
  }
}

Action types in responses

actionReturned byMeaning
start/scrobble/startBeginning or resuming playback.
checkin/scrobble/checkinSimkl will auto-scrobble at 100% from the item’s runtime.
pause/scrobble/pause, or /scrobble/stop with progress < 80Session saved as a paused playback.
scrobble/scrobble/stop with progress >= 80Item marked as watched.

Session lifecycle details

Simkl stores one active scrobble session per show / movie / anime per user. Calling /scrobble/start (or /scrobble/checkin) for a new item replaces any existing session for that item and clears previous pauses.
  • Start and checkin sessions expire after the item’s remaining runtime.
  • Stop sessions expire 1 hour after the call (used for duplicate prevention).
  • Pause sessions expire immediately but persist as saved playbacks.
Saved playbacks are retained based on the user’s plan: Free 7 days · PRO 30 days · VIP 90 days.
One scrobble operation per user at a time, with a 20-second lock. If a request looks like a duplicate within 20s, expect 429-style throttling.
Stopping a session that’s already been completed (within the last hour) returns 409. This prevents accidental double-scrobbles. The 409 body contains watched_at and expires_at so you know when the protection ends.

Managing paused playbacks

The user can browse their saved playbacks at simkl.com/my/history/playback-progress-manager. Programmatically:

Get playback sessions

GET /sync/playback — list saved playbacks for resume UIs (or narrow with /sync/playback/:type where :type is episodes or movies).

Delete a playback

DELETE /sync/playback/:id — remove a saved playback.

See also

Start

POST /scrobble/start — show “Watching now”; resume a paused session.

Pause

POST /scrobble/pause — save progress so the user can resume.

Stop

POST /scrobble/stop — end session; >= 80 marks watched.

Checkin

POST /scrobble/checkin — auto-mark watched at 100% based on runtime.

Sync guide

History writes, deletion reconciliation, continuous sync.

Standard media objects

ids structure for movies, shows, anime, and episodes.