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.
null in a Simkl response is never noise — it’s one of five specific signals. This page is the master reference; schema descriptions link back here by type.
Type 1
Never happened yet — event hasn’t fired
Type 2
Doesn’t apply — key omitted entirely
Type 3
Empty result — whole response null
Type 4
Unknown — data not on file
Type 5
End state — journey complete
Type 1 — Never happened yet
Where you’ll see it| Endpoint | Field | When it’s null |
|---|---|---|
GET /sync/activities | every bucket timestamp | User has never had activity in that bucket |
GET /sync/all-items | last_watched_at | Plantowatch items — user hasn’t watched yet |
GET /sync/all-items | user_rated_at, user_rating | Items the user hasn’t rated |
GET /sync/all-items | last_watched | Shows the user hasn’t started |
Type 2 — Doesn’t apply to this type
Where you’ll see it| Endpoint | Type | What’s omitted |
|---|---|---|
GET /sync/activities | movies block | watching and hold keys (movies can’t be in those statuses) |
GET /sync/all-items | Movie records | Episode-related fields (seasons, last_watched, etc.) |
| Movie / TV records | — | Anime-only IDs (mal, anidb, anilist, kitsu) and anime_type |
GET /tv/episodes/{id} | TV specials | season AND episode both omitted on type: "special" items |
GET /anime/episodes/{id} | Every anime episode | season omitted (AniDB numbers anime sequentially, no per-season concept) |
GET /anime/{id} | TV-classified shows | relations array omitted entirely (anime catalog only — TV records skip this field) |
GET /sync/all-items without ?memos=yes | every entry | memo key omitted (gated on the query param) |
GET /sync/all-items without ?next_watch_info=yes | every entry | next_to_watch_info key omitted |
GET /sync/all-items without ?allow_rewatch=yes | rewatch-session rows | is_rewatch, rewatch_id, rewatch_status keys omitted entirely; only the canonical entry returns |
GET /sync/all-items without ?extended=full_anime_seasons | anime entries | mapped_tvdb_seasons array omitted |
GET /sync/all-items without ?episode_tvdb_id=yes | episode rows | tvdb block on each episode omitted |
/sync/all-items/anime entries wrap in show:, by design. Each
item in the anime array nests its media object under a key called
show:, not anime:. This is intentional cross-catalog compatibility:
apps that source their data from TMDB or TVDB treat anime as shows
(those catalogs have no separate “anime” concept), so the show:
wrapper keeps Simkl’s sync responses drop-in compatible with code
written against TMDB/TVDB-shaped data. Anime-only fields (anime_type,
mal/anidb/anilist/kitsu IDs) sit alongside on the outer item and on
show.ids respectively.Type 3 — Empty result set
The whole response is
null or an empty array — the endpoint accepted the request but there are no items to return. The URL is valid; the call returns 200; the body is just empty.| Endpoint | When it’s empty |
|---|---|
GET /sync/all-items/movies/watching | Always — movies can’t have this status, the route is reachable but the data set is structurally empty |
GET /sync/all-items/movies/hold | Same |
GET /sync/all-items/{type}/{status} | Any bucket the user simply has no items in |
POST /search/random | Returns {"error": "not_found"} when no item matches the filters |
Type 4 — Unknown / data not on file
Field present, value
null — the data point is supposed to have a value but Simkl genuinely doesn’t have it yet. Render a placeholder; don’t display the literal null.| Endpoint | Field | When it’s null |
|---|---|---|
GET /tv/airing | date | Air date not on file yet |
GET /tv/episodes/{id} | description | Episode synopsis unknown |
GET /tv/episodes/{id} | img | Episode still not on file |
GET /movies/{id} | ratings.imdb, etc. | No external rating known |
GET /anime/{id} | en_title | Localized English title not on file (e.g. Death Note returns null) |
GET /tv/{id}, /anime/{id} | network | Mirrored from the upstream TVDB record. null when TVDB has no network on file (mostly recent anime). |
GET /tv/{id}, /anime/{id} | airs | Recurring schedule not on file (older shows, anime films) |
GET /tv/{id}, /anime/{id} | status | Catalog Status column blank — rare; status is normally one of tba, ended, airing |
| Catalog endpoints | runtime | Runtime not tracked |
network reflects the upstream catalog (TVDB). The value is what
TVDB classifies the record under — for streaming-only shows TVDB lists
the platform as the “network” ("Prime Video", "Netflix", "Apple TV+"),
for traditional broadcast it’s the broadcaster ("HBO", "AMC", "CBS").
Don’t infer distribution model from this field; treat it as an opaque
display string from the source catalog.memo: {} is a Type 4 variant. On
GET /sync/all-items?memos=yes,
items the user has set a memo on return:null, NOT missing — {}. The empty-object marker keeps the key
shape consistent (client code can always do item.memo.text || ''
without an existence check or a null-coalesce). When ?memos=yes is
NOT set, the memo key is omitted entirely (Type 2).Type 5 — End state reached
Field present, value
null — the value used to be meaningful but the workflow has ended; the field is intentionally cleared. Render the “complete” state in your UI.| Endpoint | Field | When it’s null |
|---|---|---|
GET /sync/all-items | next_to_watch | Show in completed — no further episodes |
GET /sync/all-items ?next_watch_info=yes | next_to_watch_info | Same — completed show, nothing remaining |
Empty / Missing response shape — by endpoint
The API uses five different shapes for “this request was valid but there’s no data”. The table below tells you what to expect at each endpoint so a single parsing path can handle them all.The five shapes you’ll meet
200 [ ]
Empty array. Safe to iterate.
200 null
Top-level
null. r.json() returns None.200 { }
Empty object.
len() is 0.301 redirect
Answer in
Location: header. No body.200 fallthrough
Server returned unrelated discover data instead of an error.
Per-endpoint shape matrix
| Endpoint | Trigger | Status | Body shape | How to handle |
|---|---|---|---|---|
GET /movies/{id} | unknown numeric Simkl ID | 200 | [] | Empty array (Type 3). Safe to iterate. |
GET /tv/{id} | unknown numeric Simkl ID | 200 | [] | Same. |
GET /anime/{id} | unknown numeric Simkl ID | 200 | [] | Same. |
GET /tv/episodes/{id} | unknown parent show ID | 200 | [] | Same. |
GET /anime/episodes/{id} | unknown parent anime ID | 200 | [] | Same. |
GET /tv/episodes/{id} | non-numeric id | 200 | [] | Same. No 404 for non-numeric id. |
GET /anime/episodes/{id} | non-numeric id | 200 | [] | Same. |
GET /movies/{id} | non-numeric id | 404 | {error, code, message?} | JSON error envelope. |
GET /tv/{id} | non-numeric id | 200 | object{...} | Fallthrough — server returns discover data (top_aired_fanarts, etc.) instead of an error — NOT a ShowDetail shape. |
GET /anime/{id} | non-numeric id | 200 | object{...} | Same fallthrough behavior. |
GET /tv/episodes/ | empty id (trailing slash) | 400 | {error: "empty_id", code: 400} | JSON error envelope. |
GET /anime/episodes/ | empty id | 400 | {error: "empty_id", code: 400} | Same. |
GET /sync/activities | fresh account (no activity yet) | 200 | object{} w/ all-null timestamps | Type 1 nulls inside, never null at top. |
GET /sync/all-items | brand-new user (no library, no type filter) | 200 | {} | Empty object. Iterating keys yields nothing. |
GET /sync/all-items/{type} | empty bucket | 200 | object{type: []} | Object wrapper present; inner array is empty. |
GET /sync/all-items/movies/watching | movies can’t be watching | 200 | {} | Empty object — not null, not []. Iterating keys yields nothing. |
GET /sync/all-items/movies/hold | movies can’t be hold | 200 | {} | Same. |
GET /sync/all-items?memos=yes | item with no memo set | 200 | { "memo": {} } per item | Empty object inside the per-item shape — NOT null, NOT missing. Type 4 variant. |
GET /sync/all-items/badtype | bad type segment | 200 | object{...} | Fallthrough — server returns a default response with cross-type data. No 400/404. |
GET /sync/all-items/{type}/badstatus | bad status segment | 200 | object{type: []} | Falls back to “all of that type”, just empty. No 400/404. |
GET /sync/ratings/{type}/{1-10} | user has no ratings at this score | 200 | {} | Empty object, NOT empty array. |
GET /sync/ratings/{type}/0 | rating < 1 (out of range) | 200 | {} | Same — no validation error. |
GET /sync/ratings/{type}/11 | rating > 10 (out of range) | 200 | {} | Same. |
GET /sync/ratings/badtype/{N} | bad type | 200 | {} | Same. |
GET /sync/playback/{type} | empty | 200 | array[...] | Returns array (possibly empty). |
GET /sync/playback/badtype | bad type | 404 | {error: "url_failed", code: 404} | 404 here, but 200 fallthrough on /sync/all-items/badtype — be aware of the asymmetry. |
GET /search/{type} | missing q parameter | 200 | [] | Empty array. |
GET /search/badtype | bad type | 200 | null | Top-level null. r.json() returns None. |
GET /search/id | unknown external id | 200 | [] | Empty array. |
GET /search/id | no id param at all | 200 | [] | Same. |
POST /search/random | empty body | 200 | object{...} | Returns a random item — never empty. |
POST /search/random | filters yield no match | 200 | {error: "not_found", ...} | Status is 200 but body is the error envelope. Branch on 'error' in body, not on status. |
POST /search/file | empty body | 200 | null | Top-level null. |
GET /ratings | bad params, no id | 200 | null | Top-level null. |
GET /ratings | unknown imdb id | 200 | null | Same. |
GET /ratings/{type} | missing user_watchlist param | 200 | null | Same — even WITHOUT a Bearer token (auth gate is on the param). |
POST /users/settings | normal | 200 | object{user, account, connections} | Always a populated object for the authed user. |
POST /users/{user_id}/stats | unknown user_id | 200 | object{...} w/ zero stats | Returns a zero-counts record, NOT 404. (Cross-account access intentional.) |
GET /users/recently-watched-background/{user_id} | numeric unknown user | 200 | null | Top-level null for a numeric-but-unknown id. |
GET /users/recently-watched-background/{user_id} | non-numeric user_id | 404 | {error: "user_id_failed", code: 404} | JSON error envelope. |
GET /redirect | no id at all | 301 | empty body, Location: //simkl.com | 301 redirect — body is empty; answer is in the header. Don’t r.json(). |
GET /redirect | unknown external id | 301 | empty body, Location: //simkl.com | Same — fallback to simkl.com root. |
GET /redirect | valid id | 301 | empty body, Location: //simkl.com/<type>/<id>/<slug> | Always 301 — never returns JSON. |
GET /oauth/pin/{user_code} | unknown user_code | 200 | object{...} (pending status) | Returns the pending-PIN shape, NOT 404. |
GET /calendar/{type}.json (CDN) | normal | 200 | array[...] | — |
GET /calendar/{year}/{month}/{type}.json (CDN) | far-future month | 404 | plain string body | r.json() will raise. Wrap the parse. |
GET /calendar/badtype.json (CDN) | bad type | 404 | plain string body | Same. |
GET /discover/trending/{file}.json (CDN) | normal | 200 | array[...] or object{...} (combined) | — |
GET /discover/trending/badcombo.json (CDN) | unknown filename | 404 | plain string body | Same — CDN errors are not JSON. |
Defensive parser pattern
If you’re writing a generic Simkl client wrapper, this defensive pattern handles every shape above:Pinning a shape in your code
If your integration depends on a specific row of this table, pin it as a comment in your code. When the table changes on a future doc release, your in-code reference makes the diff easier to spot during a routine dependency-update review.Quick decision table
| Symptom | Type | Means | Do |
|---|---|---|---|
Timestamp null on activities | 1 | Never happened yet | Don’t show “last activity” |
| Key isn’t in the object | 2 | Doesn’t apply to type | 'key' in obj check, or skip silently |
Whole response is null | 3 | Bucket is empty | Render empty state |
Description/image is null | 4 | Data not on file | Render placeholder (“TBA”, ”—“) |
next_to_watch null on a show | 5 | Show is completed | Render “all caught up” |
Related
Watchlist statuses
Which statuses are valid per type. The reason
watching and hold are omitted from movie blocks (Type 2).Standard media objects
Field-by-field shape of Movie / Show / Anime / Episode — which fields can be null, which are always present.
Dates and timezones
ISO-8601, the
Z suffix, and the 1970-01-01T00:00:01Z “very long time ago” placeholder.Errors
How error envelopes differ from null responses, and when you’ll see each.