Simkl is the central place where users store all their watch history and watchlists like Watching, Plan to Watch, On Hold, Dropped, Completed. The Sync API lets you read and write that data so every app and device stays in sync.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.
GET /sync/activities
GET /sync/all-items
{type} and {status} segments are optional — narrow as needed.Rewatches guide — separate viewing sessions for items the user already finished
?allow_rewatch=yes), display session counts, mirror the simkl.com “Rewatches” panel, or read per-episode session data, the dedicated Rewatches guide has the full walkthrough — session lifecycle, flag combinations for reading sessions back, ready-made code for UI patterns, and the precautions you must implement before enabling the flag.The two-phase model
| Phase | When | Endpoint | date_from? |
|---|---|---|---|
| Phase 1 — Initial sync | First time the user signs in | /sync/all-items/shows, /sync/all-items/movies, /sync/all-items/anime | No — pull the full library |
| Phase 2 — Continuous sync | Every poll afterwards | /sync/all-items?date_from=… (multi-type apps) or /sync/all-items/:type?date_from=… (single-type apps) | Yes — pass your saved timestamp |
date_from you didn’t have on the first run.”
Useful query params
The flags below shape what/sync/all-items returns and what /sync/history does — they’re optional, but most non-trivial integrations need at least one or two. Layer them onto the calls in Phase 1 / Phase 2 below as needed:
| Param | On | What it does |
|---|---|---|
extended=simkl_ids_only | GET /sync/all-items | Returns just ids.simkl per item — perfect for the deletion-reconciliation diff in Phase 2. |
extended=ids_only | GET /sync/all-items | Same, plus external IDs (imdb, tmdb, tvdb, mal, …). |
extended=full | GET /sync/all-items | Adds posters, overview, fanart, ratings — full metadata per item. Always pair with date_from. |
extended=full_anime_seasons | GET /sync/all-items (anime) | At the show level, adds mapped_tvdb_seasons: [n,…] mapping each Simkl/AniDB season to a TVDB season. At the episode level, adds a tvdb: \{ season, episode \} block on every anime episode. Anime apps that index against TVDB don’t need a second lookup. |
episode_watched_at=yes | GET /sync/all-items | Adds per-episode watched_at timestamps to every episode in the response. Modifier only — episodes must already be loaded (see include_all_episodes and extended=full). Always pair with date_from — significantly larger response. |
include_all_episodes=yes (or =original) | GET /sync/all-items | Loads canonical seasons[].episodes[] for items in every watchlist status — including completed and dropped, which skip episode loading by default. yes also synthesizes virtual episode rows (with air-date timestamps) for completed entries that have no per-episode data. original loads real per-episode rows only — no virtual synthesis. |
next_watch_info=yes | GET /sync/all-items | For items with status watching that have a “next to see” episode, attaches a next_to_watch_info object (title, season, episode, date). Anime entries omit season. Items without a next episode (or with other statuses) don’t get the field. |
episode_tvdb_id=yes | GET /sync/all-items | Adds ids.tvdb_id on each episode in episode-bearing responses. |
memos=yes | GET /sync/all-items | Includes the user’s per-item memo object (text capped at 140 chars, plus is_private). Note: field name is singular memo (not memos); empty memos render as \{\}. |
anime_type=… | GET /sync/all-items | Filter anime entries by anime type (tv, movie, ova, ona, special, music video). |
language=en | GET /sync/all-items | Force English titles instead of the user’s profile language. |
skip_auto_watching=yes | POST /sync/history | Suppresses the implicit episode auto-fill that happens when you POST a show without seasons/episodes. Use when your client manages episode-level state itself. |
allow_rewatch=yes | POST /sync/history, GET /sync/all-items | Opt into rewatch tracking. On POST /sync/history, recording a watch on an already-Completed item creates a separate rewatch session instead of being a no-op. On GET /sync/all-items, any item with saved rewatch sessions appears multiple times — its normal entry, plus one extra entry per rewatch session. Simkl Pro / VIP only — non-Pro callers see no effect even with the flag set. |
Phase 1: Initial sync
The first time a user signs in, you don’t have a saved timestamp, so you can’t ask for a delta — you have to pull the full library.Decide which types you need
- Multi-type apps (Plex/Jellyfin/Kodi plugins, full trackers) — pull all three: shows, movies, anime.
- Single-type apps (anime-only tracker, movie scrobbler, TV-show watchlist) — pull only the type you care about.
Pull each type sequentially
/sync/all-items/shows, then /sync/all-items/movies, then /sync/all-items/anime one after the other — not in parallel. Initial libraries can be massive, and back-to-back parallel pulls of three full payloads spike CPU on both ends.movies, then anime. Single-type apps call only their one endpoint.Save the bootstrap timestamp
/sync/activities once and save activities.all locally as last_sync. From now on every sync is Phase 2.Phase 2: Continuous sync
Every subsequent poll runs this loop:Check /sync/activities
watching and hold — see Watchlist statuses. The settings block bumps when the user changes their profile timezone, date / time format, or any other account-level preference — gate POST /users/settings re-fetches on settings.all, see Dates and timezones → User timezone preference.)Compare against your saved timestamp
activities.all === local_state.last_sync, stop here — nothing changed, no follow-up call needed. This is the cheap path that runs on the vast majority of polls.Fetch only the delta
activities.all moved, fetch the changed items with date_from set to your saved timestamp:- Multi-type apps:
/sync/all-items?date_from=YOUR_SAVED_TIMESTAMP— single request, all three types and every status, only items modified since. - Single-type apps:
/sync/all-items/{type}?date_from=YOUR_SAVED_TIMESTAMP(replace{type}withshows,movies, oranime) — same delta semantics, scoped so you don’t transfer types you don’t render.
date_from value exactly as /sync/activities returned it (ISO 8601 UTC). Don’t reformat it locally.date_from call returns summary fields only (status, counters, last_watched/next_to_watch markers) — no seasons[].episodes[] array. If your tracker app maintains an episode-level local cache, add extended=full&episode_watched_at=yes to the URL:watching/plantowatch/hold items get seasons[].episodes[].watched_at so you can apply per-episode diffs. For completed/dropped items also include include_all_episodes=yes (their episode arrays are gated separately to keep the default payload small). For rewatch sessions (?allow_rewatch=yes) the same rule applies — without extended=full, the rewatch entry comes back as a summary-only row with watched_episodes_count: 0 as a sentinel.Merge and update
date_from only returns deltas). Then save the new activities.all as last_sync for the next poll.Detecting deletions. date_from only surfaces items that were added or modified — not removed. When activities shows removed_from_list moved, refetch the full library with extended=simkl_ids_only (or ids_only if you also want external IDs) and diff against your local cache. Items missing from the new ID-only response have been removed from the user’s library — also clear any local rating you stored for them, since Simkl wipes the rating when an item is removed.Automatic moves on rate. When a user rates an item that isn’t in any list yet, Simkl auto-files it based on the item’s airing status: a released movie → Completed, an unreleased / upcoming movie → Plan to Watch, a single-episode show → Completed, any other show or anime → Watching. The auto-move bumps the corresponding list timestamp, so the rated item shows up in the next date_from delta even though the user only rated it. Treat the delta as authoritative — don’t try to second-guess why an item moved.Z suffix). Don’t reformat date_from locally — pass it back to Simkl exactly as /sync/activities returned it.watched_at near 1970-01-01T00:00:01Z is the “Very long time ago / I don’t remember” placeholder, not a corrupt date. Use it on writes when the user marks something watched without remembering when; render it on reads as “Very long time ago” (simkl.com’s own label) — never display the literal 1970-01-01. Full convention at Dates and timezones → “Very long time ago” placeholder.When to actually run sync
The sync loop above is cheap, but only when you trigger it on user-visible events. Don’t run unconditional background timers.| Platform | Recommended triggers |
|---|---|
| Mobile / TV apps | App launch · Wake from background · Pull-to-refresh. Throttle to once every 15–30 min to prevent rapid-switch spam. |
| Media servers (Plex / Kodi / Jellyfin / Emby) | Hook into library-scan-completed events, or run when a playback session ends and the user returns to the home screen. |
| All platforms | Always allow a manual override (a refresh button, pull-to-refresh, or a menu item). |
| Never | Unconditional polling timers without active user interaction — no setInterval(sync, 60_000) in the background. |
Edge cases and gotchas
Real users do unusual things, clients have bugs, and networks drop. The behaviours below are what to expect when sync meets the messy real world — none of them break your integration, but each one can trip up a parser, a UI assumption, or a retry loop.One sync write per user at a time — 20-second lock
One sync write per user at a time — 20-second lock
POST /sync/history while the original is still being processed), the second request blocks until the first finishes or the 20-second timeout fires. On timeout, the second call returns 400 rate_limit:rate_limit 400, wait a few seconds and retry once. The lock covers /sync/history, /sync/history/remove, /sync/add-to-list, /sync/ratings, /sync/ratings/remove, and /sync/watched. Read endpoints (/sync/activities, /sync/all-items) are not gated by this lock.Long offline gaps are safe — `date_from` accepts any past timestamp
Long offline gaps are safe — `date_from` accepts any past timestamp
GET /sync/all-items?date_from=<your old saved timestamp> and the server returns the cumulative delta of everything that changed since. There is no maximum age on date_from — older timestamps simply return a larger response.You never need to fall back to a full Phase 1 sync unless your local cache is gone or corrupted. The only thing that can actually go stale across long gaps is the user’s access token — see Tokens for how revocation works.Items can be reclassified across types — read `simkl_type` and `anime_type`
Items can be reclassified across types — read `simkl_type` and `anime_type`
shows or anime. When you POST an item with an external ID and the response says it landed in a category you didn’t expect, that’s not a bug — it’s Simkl correcting your classification.Every POST /sync/history response includes a simkl_type (movie / tv / anime) and anime_type (tv / special / ova / movie / music video / ona) on each added.statuses[].response entry. Store these locally so a later deletion of “the anime Akira” targets the right type, even if you originally POSTed it as a TMDB movie.Same when reading /sync/all-items: the top-level key the item lands under (shows vs movies vs anime) tells you Simkl’s classification — your local store needs to follow it.Re-added items reappear in deltas, with watch-state preserved
Re-added items reappear in deltas, with watch-state preserved
date_from delta as a fresh write. The corresponding watchlist timestamp on /sync/activities (e.g. tv_shows.plantowatch) bumps; removed_from_list already bumped at the deletion. Your client should treat a re-appearing simkl_id as a current entry — overwrite any local “removed” flag.History (watched episodes, watched_at timestamps) survives the remove/re-add cycle. Ratings do not — Simkl wipes the user-set rating when an item is removed from the list, so if a re-added item comes back unrated, that’s expected.`/sync/activities` is per-category — drill down for cheaper polls
`/sync/activities` is per-category — drill down for cheaper polls
tv_shows.all instead of the top-level all — and skip the call entirely when movies or anime moved but shows didn’t. Same trick for narrower surfaces: a “Continue Watching” rail only cares about tv_shows.playback / movies.playback / anime.playback; a ratings screen only cares about *.rated_at. Saving and comparing the narrower timestamp halves your API calls for apps with a focused UI.Playback sessions auto-hide once the item is marked watched
Playback sessions auto-hide once the item is marked watched
GET /sync/playback returns only open sessions — items the user has paused and hasn’t finished. As soon as a watch event lands for that item after the pause time, Simkl filters the session out of GET responses.No action needed on your side — this is exactly what you want for a “Continue Watching” rail: once the user finishes the episode, it disappears from the resume list automatically.Playback retention varies by subscription tier
Playback retention varies by subscription tier
| Tier | Retention |
|---|---|
| Free | 7 days |
| Simkl Pro | 30 days |
| Simkl VIP | 90 days |
Playback `progress` rounds to integer on read
Playback `progress` rounds to integer on read
progress: 75.5 is valid on /scrobble/start, /scrobble/pause, /scrobble/stop, and /scrobble/checkin. The server stores it accurately, but reads round to the nearest integer:current_position and runtime instead of trusting the round-tripped progress field.`409 Conflict` on `/scrobble/stop` for recently-watched items
`409 Conflict` on `/scrobble/stop` for recently-watched items
/scrobble/stop on an item that was already marked watched within the last hour, the server rejects the call with 409 Conflict to prevent duplicate scrobbles. The response body includes the original watched_at and an expires_at showing when the 1-hour duplicate-window closes:`extended=full` without `date_from` will hurt — quantifiably
`extended=full` without `date_from` will hurt — quantifiably
GET /sync/all-items?extended=full (no date_from) returns the user’s entire library with overview text, genres, ratings, posters, runtime, and per-season episode arrays for every item — often several megabytes per call. Combine with episode_watched_at=yes on a heavy completed watchlist and the payload can hit tens of megabytes.Always pair extended=full and episode_watched_at=yes with date_from so the response is just the changed slice. On Phase 1 (the only legit no-date_from call), don’t add either flag — fetch the minimal payload first, then enrich per-item with the dedicated /movies/{id}, /tv/{id}, /anime/{id} calls as the user opens them.Common write operations
Mark items watched
Mark items watched
POST /sync/history accepts movies, shows, anime, and episodes arrays. For shows / anime you can specify which seasons or episodes to mark. Anime entries are equally valid under shows[] or anime[] — Simkl resolves the catalog by ids either way (see Anime in shows[] or anime[] for details and the not_found.shows caveat).Record a rewatch — Simkl Pro / VIP only
Record a rewatch — Simkl Pro / VIP only
POST /sync/history calls. To record an additional viewing as its own session, set ?allow_rewatch=yes on the request:- Plan gate. Only Simkl Pro / VIP accounts record rewatches. Free-tier callers get a silent no-op even with
?allow_rewatch=yes. - Up to 50 rewatches per item (movie, show, or anime).
- 2-day minimum gap between watch events on the same item. Movies and individual episodes — a new rewatch closer than 48 hours to the previous watch of the same item collapses into the same session. It’s a rewatch, not a rewind 😄. Re-watching different episodes back-to-back is fine.
- Sleep-and-resume. User starts an episode at midnight, falls asleep, finishes it the next morning. That’s one viewing, not two — but the two timestamps the client reports can be 6–10 hours apart and trivially look like distinct watches.
- Wrong timezones in clients. Apps frequently label local time as UTC (or vice versa) on the
watched_atfield, producing a 1–12-hour drift on every write. The 2-day buffer absorbs the worst-case drift without spawning fake rewatch sessions. - DST transitions. Naive datetime libraries miscalculate by an hour twice a year, in the days surrounding the spring-forward / fall-back boundary. Same buffer covers them.
- Clock drift on offline-capable apps. Mobile, set-top, and console clients that batch writes after coming back online often back-date events using the current device clock minus a rough offset, not the actual playback time. Multi-hour drift is common.
- Scrobble pause/resume noise. Some media players re-fire
/scrobble/startand/scrobble/stoparound a pause (bathroom break, doorbell, phone call). Without a gap, the resume reads like a second viewing. - Multi-device duplicates. A user’s phone, TV, and home-theatre receiver can all see the same file open and each report a play event. Same item, near-identical timestamps — the gap collapses them into one session.
- Network retry storms. A flaky connection causes a client to retry
POST /sync/historyseveral times for one watch event. The gap collapses the retries instead of pretending each retry was a separate rewatch. - Importer re-runs. Users importing history from another source often re-run the import to catch missed items, re-submitting the same
watched_atvalues. Without the gap, every re-run inflates the rewatch count. - Background play. A TV left on for noise can auto-loop the same episode, or a chromecast can re-cast the same title on idle. The user isn’t really rewatching it.
- Buggy progress reporting. A media player that miscalculates
progresscan hit 80%+ multiple times during a single play (especially on seeks), each of which clients sometimes translate into a freshPOST /sync/history. The gap collapses these.
Rewatches guide — full walkthrough
active / completed / closed), per-item rewatch fields (rewatch_id, rewatch_status, last_watched_at, is_rewatch), reading sessions back from GET /sync/all-items, episode-level tracking, and ready-made code for the UI patterns simkl.com uses on every movie / show / anime detail page (rewatch indicator, “mark next episode rewatched”, resume, close, history list, stats).Remove items from history
Remove items from history
POST /sync/history/remove — same shape as /sync/history, but removes the items.Move an item to a Watchlist status
Move an item to a Watchlist status
POST /sync/add-to-list with to set to one of watching, plantowatch, hold, dropped, completed (see Watchlist statuses — movies skip watching and hold):Add ratings
Add ratings
POST /sync/ratings — pass a 1–10 rating per item.Always batch writes
Always batch writes
Supported ID keys
When you POST items to/sync/history, /sync/add-to-list, or /sync/ratings, the ids object can match on any of simkl, imdb, tmdb, tvdb, mal, anidb, anilist, kitsu, livechart, anisearch, animeplanet, netflix, letterboxd, traktslug, crunchyroll, hulu. Sending more than one is fine — Simkl walks the IDs in order and falls back to title/year matching. See the full table with types and examples in Standard media objects → Supported ID keys.
Reference implementation
Ratings
The Sync API handles user-set ratings (the 1-10 scores the user has personally assigned). For Simkl’s average ratings (the community score), the data is included on every detail-endpoint response under theratings field — call GET /movies/{id}, GET /tv/{id}, or GET /anime/{id}. Detail endpoints don’t need a token and are Cloudflare-cached. If you only have an external ID, resolve it first via GET /redirect. User-set ratings (the Sync side) also support date_from for incremental sync via GET /sync/ratings?date_from=….
Sync API reference
Every endpoint this guide touches, jump-linked:GET /sync/activities
GET /sync/all-items
{type} and {status} are optional — multi-type, single-type, or single-bucket.POST /sync/history
POST /sync/history/remove
POST /sync/add-to-list
POST /sync/ratings
POST /sync/ratings/remove
What about real-time playback?
Sync handles watchlist state and history. To report playback as it happens (start / pause / stop with progress), use the Scrobble guide. To read saved pause points (e.g. for a “Continue Watching” rail), useGET /sync/playback (or narrow with /sync/playback/:type) — see How playbacks work for the full picture.