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.
{ "movies": [{ "ids": {...} }] }
Whole show — every episode marked. “I finished this whole series.” Send status: "completed" with no seasons / episodes.
{ "shows": [{ "ids": {...}, "status": "completed" }] }
Whole season — every episode of one season. “I finished season 2.” Send seasons[] without an inner episodes.
{ "shows": [{ "ids": {...}, "seasons": [{ "number": 2 }] }] }
Specific episodes only — per-event scrobbling, manual tick-off.
{
"shows": [{
"ids": {...},
"seasons": [{
"number": 1,
"episodes": [{ "number": 1 }, { "number": 2 }]
}]
}]
}
Top-level episodes[] shorthand — auto-wraps to seasons: [{ number: 1, ... }]. Useful for anime sequential numbering and single-season shows.
{ "shows": [{ "ids": {...}, "episodes": [{ "number": 1 }] }] }
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:
{
"movies": [{
"ids": { "simkl": 53536 },
"status": "plantowatch",
"memo": { "text": "Remind me why I added this", "is_private": true }
}]
}
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 POST /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": {
"movies": <int count>,
"shows": <int count>,
"episodes": <int count>,
"statuses": [
{
"request": { /* echo of input item, with type added */ },
"response": {
"status": "completed",
"simkl_type": "tv" | "anime" | "movie",
"anime_type": "tv" | "movie" | "ova" | null
}
}
]
},
"not_found": {
"movies": [...],
"shows": [...],
"episodes": [...]
}
}
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.
OAuth 2.0 or PIN-flow access_token. Required for endpoints that read or modify the user's library, scrobble session, ratings, settings, or playbacks. See 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.