Add to History
Record watch events. The unit is the watch event — adding “I watched The Walking Dead S01E01 at 8pm” — not list membership (use POST /sync/add-to-list for that, or set status per-item here to do both at once).
Granularity — when does the server expand “implicit all”?
The body shape determines whether you mark a single episode, a season, or a whole show.
One movie — single movie completion.
Whole show — every episode marked. “I finished this whole series.” Send status: "completed" with no seasons / episodes.
Whole season — every episode of one season. “I finished season 2.” Send seasons[] without an inner episodes.
Specific episodes only — per-event scrobbling, manual tick-off.
Top-level episodes[] shorthand — auto-wraps to seasons: [{ number: 1,... }]. Useful for anime sequential numbering and single-season shows.
The response always reports the actual count of episodes affected in added.episodes, so apps can verify the server expanded correctly.
Memo-only updates (and “add to watchlist + memo” in one call)
This endpoint is the only way to set a memo on an item. POST /sync/add-to-list accepts the memo field in the request body and echoes it back in the response, but does not persist it — silently discarded.
To set or update a memo without recording a watch event, send ids + status + memo:
Two behaviors worth knowing:
- Memos attach to watchlist items only. The item has to be in one of the five statuses (
watching/plantowatch/hold/dropped/completed) for the memo to stick. Sendingstatusmakes this explicit. - The endpoint auto-adds items. If the item isn’t on the user’s watchlist yet, this same call creates the watchlist row at the specified
statusAND saves the memo in one shot.added.movies/added.showsreports the count of newly-added items (0when the item was already there and you only changed memo/status).
To read memos back, call GET /sync/all-items?memos=yes — see the Sync guide.
Per-item options
| Field | Type | Notes |
|---|---|---|
watched_at | ISO-8601 string | Pin the watch event to a specific time. Defaults to request time. |
added_at | ISO-8601 string | Override when the item was added to the watchlist (rarely used outside backups). |
status | string | Set the watchlist status (watching/plantowatch/hold/completed/dropped) in the same call. Combine with rating to do “watched + rate + status” in one request. |
rating | int 1-10 | Rate the item alongside the watch event. Same effect as a separate POST /sync/ratings call. |
memo | { "text": string, "is_private": bool } | User memo, max 140 chars. is_private: false shows the memo on the user’s public profile + activity feed; true keeps it self-only. Read-back requires /sync/all-items?memos=yes. |
is_rewatch | bool | Force the rewatch path on this item even if the server can’t auto-detect (used by backup/restore tools). Requires ?allow_rewatch=yes query param to take effect. |
use_tvdb_anime_seasons | bool (anime-only, optional) | Default false (AniDB sequential — flat single-season). Set true to interpret season/number as TVDB per-season numbering. Only needed when your source uses TVDB-style numbering AND the title is multi-season in TVDB (e.g. Demon Slayer S2 Entertainment District). Single-season anime and AniDB-sequential inputs work without this flag. Use when syncing from Plex/Sonarr/Kodi/Jellyfin. |
Rewatches
Re-posting an already-watched episode is a no-op by default — the server detects the duplicate and skips. To insert a rewatch session, send ?allow_rewatch=yes as a query parameter. The server then creates a separate rewatch row that doesn’t double-count the original watch.
Simkl PRO / VIP only. Free-tier callers with ?allow_rewatch=yes get a silent no-op (added: { movies: 0, shows: 0, episodes: 0 }) that still consumes a rate-limit slot. Check account.type from GET /users/settings at sign-in and gate the flag on "pro" / "vip". Cache the value; refetch only when activities.settings.all from GET /sync/activities bumps. Full pattern in Rewatches guide → PRO / VIP gate.
For backup/restore tools that always want to insert (even when the auto-detect heuristic can’t fire), set is_rewatch: true per-item AND pass the query param.
Response: added and not_found
added.statuses[*].response.status is the resolved Watchlist status the server placed the item on — e.g. a "completed" write on a still-airing show is silently downgraded to "watching" and reflected here. You don’t need a follow-up POST /sync/add-to-list — this call already moves the item; chaining would just overwrite the server’s smarter decision.
added.statuses[*].response.simkl_type tells you which catalog the item resolved to (useful when you sent ambiguous IDs — TMDB IDs can be either movie or tv on Simkl). Always inspect to know what got created.
not_found carries the verbatim input for items the resolver couldn’t match (typo, fuzzy-title miss, ID not in Simkl’s catalog yet). Apps should:
- Show “we couldn’t track: …” UI for these
- Offer manual ID-entry fallback
- Don’t infer success from the 201 status alone — branch on
not_found.movies.length === 0 && not_found.shows.length === 0 && not_found.episodes.length === 0
Errors
400 empty_field if a per-item required field is missing. 400 wrong_parameter for invalid enum values. Empty body {} returns 201 with zero counts (NOT 400) — that’s a known asymmetry vs /scrobble/start which 400s on empty body.
Sync guide — full walkthrough
Initial-pull-then-delta-loop pattern, date_from semantics, deletion reconciliation, Trakt/Letterboxd migration recipes.
When to use /sync/history vs /sync/add-to-list
| Goal | Endpoint |
|---|---|
| User finished watching → mark watched + rate + memo | /sync/history (carries all three in one shape) |
| User clicked “Add to Plan to Watch” button | /sync/add-to-list (status-only, no watch event) |
| Backfill from Trakt/Letterboxd/IMDb (events with timestamps) | /sync/history with watched_at per item |
| Bulk import a watchlist (no watch events) | /sync/add-to-list |
| Remove an item from the user’s library | /sync/history/remove |
Authorizations
Preferred form: your client_id as a URL query parameter on every request. Self-describing in logs and curl commands. See Headers and required parameters.
A user access_token, sent as Authorization: Bearer .... Required for endpoints that read or modify the user's library, scrobble session, ratings, settings, or playbacks. Every flow returns the same kind of token. See Set up authentication.
Headers
Descriptive identifier for your app, ideally name/version. Examples: PlexMediaServer/1.43.1.10540, kodi-simkl/0.9.2, MyApp/2.4.1 (https://myapp.com).
Query Parameters
Opt into rewatch tracking. When yes, POST /sync/history records an additional rewatch session instead of being a no-op for already-watched items, and GET /sync/all-items returns one extra entry per saved rewatch session alongside the item's normal entry. Available to Simkl PRO and VIP users — non-PRO callers see no effect even with the flag set.
⚠️ Do not enable this flag until you've read the Rewatches guide end-to-end and implemented the precautions. Used carelessly (on retries, on every scrobble event, on importer re-runs, without pinning rewatch_id after the first write), it will pollute the user's history stats and rewatches panel with phantom sessions. The flag should be gated behind explicit user intent — a dedicated "Rewatch" button — never on background or automated flows. Also expose a per-user Track rewatches toggle in your app's settings (default off) — not every user wants the rewatch-session complexity.
Limits: up to 50 rewatches per item (movie, show, or anime), and any two watch events on the same item (movie or episode) must be at least 2 days apart — a new rewatch within 48 hours of the previous watch of that same item collapses into the same session (it's a rewatch, not a rewind 😄). Full walkthrough — session lifecycle (active / completed / closed with bidirectional transitions), episode-level tracking, reading sessions back from GET /sync/all-items, and ready-made code for simkl.com-style UI patterns — in the Rewatches guide.
yes, no When yes, suppresses the implicit episode auto-fill that happens when you POST a show without explicit seasons/episodes. Use when the client manages episode-level state itself.
yes Your client_id from your Simkl developer settings. Required on every request.
Short, lowercase identifier for your app (e.g. plex-scrobbler, kodi-bridge). Helps Simkl identify which apps are using the API.
Your app's current version (e.g. 1.0, 2.4.1). Helps Simkl debug issues you report.
Body
Body for /sync/history and /sync/history/remove. Items go under movies[], shows[], or anime[] — Simkl resolves anime titles correctly under either shows[] or anime[], so match the field to your data type when known.
Top-level convenience array for marking episode-level history without nesting. The server wraps each entry into a synthetic single-season show. Each item carries the same shape as items under shows[].seasons[].episodes[] plus the parent show reference.
Array of anime entries (same shape as shows[]).
Response
Success
Response from POST /sync/history. Note: not_found.shows includes any anime entries that failed to resolve, regardless of whether they were sent under anime[] or shows[] — there is no separate not_found.anime bucket.