watching, plantowatch, hold, dropped, completed) that the API has always exposed. A title can be on the user’s Watchlist with a status AND on one or more Custom Lists at the same time, independently.
Reading Custom Lists is now available, in beta. Two endpoints are live: one for a user’s lists, one for a list’s contents. Creating, editing and deleting lists, and adding or removing items, remain web-only for now.Being beta, the response shape may still change. Anything that would break a client is announced on the changelog and in the Discord
#api channel.Still on AUTH V1? Migrate first
Custom Lists is the first Simkl API a V1
client_id cannot reach at all, so migrating is the prerequisite rather than a later cleanup. Register a separate V2 app, run both side by side, and move users across as they reconnect — your V1 app keeps working throughout.The two endpoints
GET /lists/user/{userId}
Every list belonging to one user, paginated. Another user’s account returns their public lists; your own additionally returns unlisted and private ones, plus a
pinned flag.GET /lists/{id}
One list’s metadata and its items, with sorting and paging.
Read a custom list
Fetch the user's lists
description, display_number, user, top_items and timestamps.A list’s size is counts.items, not total_items. The total_items at the top level belongs to the pagination object and counts lists on this page, not titles inside them.Viewing your own account adds a pinned flag to every entry and includes your private and unlisted lists.Every entry says how the user relates to it, in role: owner, collaborator or follower. By default you only get lists they own; add followed=true and collaborants=true to include the others. Followed and collaborated lists both carry someone else’s user, so role is the way to tell them apart — for a “Your lists / Following / Shared with you” layout, group on it.One subtlety: collaborator is only reported when collaborants=true is set. A list the user both collaborates on and follows reads follower if you only asked for followed=true.Fetch one list's items
Take an The list’s own metadata comes back alongside
id from the previous response:items, at the top level — the same fields you saw in step 1, merged in rather than nested under a list key. One difference: top_items is always empty here, because you’re getting the real items instead.Omit sort to get the order the list owner chose. Add extended=full for an overview on each item.Item shape varies by media type: series carry total_episodes, network and next_episode_date, TV adds total_seasons, movies carry budget, box_office and dvd_date, and anime carries anime_type and mapped_tvdb_seasons. Since a list holds one media type, branch once on media_type rather than per item. The external ratings key follows suit — mal on an anime list, imdb everywhere else.Page through, if the list is long
Use
total_pages from the response rather than counting yourself, and stop when you reach it.Remember the ceiling: page times limit cannot exceed 10000, so at limit=500 the last reachable page is 20. A list larger than 10,000 items cannot be read to the end through pagination alone.pagination object, so one paging helper works for both endpoints. Neither is a plain array — the items live under items on one and lists on the other.
Lists that belong together
Some lists are parts of one collection — a franchise kept as separate Movies and TV Shows lists, or a ranking split by decade. On simkl.com they appear as buttons above the list. Users build a collection on the website by giving their lists the same title, each ending in a different label in parentheses —Star Wars Franchise (Movies) and Star Wars Franchise (TV Shows). The label can be anything: a media type, a year, a genre. See Custom List Collections in the Simkl help center for the full how-to.
GET /lists/{id} reports the collection in collection:
collection.nameis the shared title; each entry’snameis its label — the text in parentheses, not the list’s full name.- The list you requested is in
liststoo, so you can render the tabs straight from this array and mark the current one byid. - A favourites list’s collection is the owner’s other favourites lists, one per media type.
collectionisnullwhen the list stands alone — the common case, so check before rendering tabs.
GET /lists/{id} using its id. Only the one the user opens needs fetching — see Fetch lazily, cache locally.
Fetch lazily, cache locally
Custom Lists are the easiest endpoint on the API to over-call, because the natural UI — a screen of lists, each opening into its items — invites a fetch per screen. Three rules keep an app that browses lists constantly down to a handful of requests a day.1. Render the lists screen without fetching any list
GET /lists/user/{userId} already returns everything a list card needs: name, counts.items, and top_items — up to five poster previews, in list order.
So do not call GET /lists/{id} for each list to build the index. One request renders the whole screen. Fetch a list’s items only when the user actually opens it.
2. Cache each list, and serve the cache on back-navigation
The pattern that generates the most wasted calls is ordinary browsing:id plus whatever sort, direction and paging you requested, and render from it until you have a reason to believe it is stale. A cache that only survives until the next screen transition is not a cache.
3. Refetch only when something says the list changed
Two signals, in increasing precision:
So the cheap loop is: poll activities → if the bucket moved, refetch the index → compare each list’s
updated_at against your cached copy → refetch only the lists whose timestamp moved, and only when the user opens them.
Beyond that, refresh on an explicit pull-to-refresh and nothing else. There is no need for a timer.
The 200 that is not a list
This is the bug most likely to reach production, so handle it before you write anything else. A caller whose account is not PRO or VIP does not get a403. They get HTTP 200 carrying a premium_only body instead of list data:
error key on a 200 before reading items:
!res.ok branch is not boilerplate here. Because premium_only arrives as a 200, it is tempting to treat every non-200 as impossible and skip the check — but 401, 403, 404 and 400 are all reachable on these endpoints, and none of them carries items either.
Three details that each break a naive client:
erroris lowercasepremium_only.- There is no
itemsarray and nopaginationobject. This is a different shape, not a degraded one, soitemsisundefinedrather than[]. A client that maps over it throws; a client that checkslengthsilently renders nothing. itemis singular and renderable — real title, description and poster URL. Rendering it as an ordinary card is the simplest way to explain the empty list to the user.
A list that does not exist returns the same
premium_only body. For a free or anonymous caller the tier check runs before the list is looked up, so a bad list ID is indistinguishable from a paywalled one. With a PRO or VIP token you get a normal 404.Paging and sorting
Both endpoints takelimit (default 50, max 500), page, and direction.
The two endpoints validate sort differently, which is deliberate but easy to trip over:
So a typo in
sort fails loudly on one endpoint and silently on the other.
On GET /lists/{id}, omitting sort entirely is usually right — it uses the order the list owner chose, in the direction they chose.
Two viewer-relative tokens get rewritten before use, and the rewritten form is what comes back: my-rating becomes user-rating, and last-watched becomes user-last-watched.
?followed=true and ?collaborants=true must be the literal string true. 1, yes and on are ignored without error, so a filter that seems to do nothing is usually this.What privacy actually controls
unlisted means reachable by link, but not enumerable. Anyone given the list ID can read it, but it will not show up when you list that user’s lists — only the owner sees their own unlisted lists there.
The practical consequence for a library view: reading another user’s lists returns their public ones, while reading the signed-in user’s own returns public, unlisted and private together, each tagged with its privacy value. If you render someone else’s library you cannot accidentally expose an unlisted list, because it is never in the response.
private is the most restricted: it appears only in the owner’s own library and to collaborators, and reading it by ID as anyone else is a 403 private_list.
Two more behaviours worth knowing on
GET /lists/{id}.?extended=full adds an overview field to every item. It is PRO-only on both sides — a free caller does not get overview even if the parameter is present, and the response is cached as a separate variant, so mixing the two does not poison either.Sorts that depend on watch data resolve against the list owner, not the caller. That is deliberate — it means a shared list looks identical to everyone who opens it, rather than reshuffling per viewer.Account limits
These are limits on what a user can build. They are enforced when a list is saved on the website, so the read API never returns them — but they explain why a list stops accepting items.
There are three separate ceilings and a user can hit any of them independently: too many items in one list, too many lists, or too many items across everything.
Still web-only
- Creating, renaming and deleting lists
- Adding and removing items
- Reordering, notes, collaborators and privacy changes
Common questions
Should I use Custom Lists or the Watchlist?
Should I use Custom Lists or the Watchlist?
Different jobs. The five-status Watchlist is where every tracked title lives, with watch state — that is what you want for “show me the user’s library” and for anything involving progress. Custom Lists are user-made groupings on top, and most users have few or none.If you are building a library view, start with
GET /sync/all-items. Add Custom Lists when you specifically want to surface the user’s own curation.Why does an anonymous call return 200 instead of 401?
Why does an anonymous call return 200 instead of 401?
Because the response carries content rather than just refusing: a renderable card an app can show in place of the list, without special-casing anything. That is convenient if you handle it and a trap if you do not, which is why it has its own section above.A genuine missing-token error looks different —
401 with user_token_required, documented in Errors.Can I scrape simkl.com/lists/ pages instead?
Can I scrape simkl.com/lists/ pages instead?
No. Scraping simkl.com is not allowed.Everyone doing it will be permanently blocked from the website and their account terminated.
Why does one auto list return 400?
Why does one auto list return 400?
It is set to random order. An auto list can be told to shuffle its results on every view, and the API does not serve those:
GET /lists/{id} answers 400 with the message “Random auto lists are not supported by the API”.The reason is caching. List responses are cached for hours, so the API would hand every caller the same “random” sample until the cache expired — not random at all. Rather than pretend, it refuses.Treat it like any other list you cannot show: skip it, or link the user to the list on simkl.com, where it shuffles as intended.Do Custom Lists count against my rate limit?
Do Custom Lists count against my rate limit?
Yes, the same as any other endpoint — see Rate limits. List reads are not in the exempt set, and the allowance they spend is the user’s, shared with every other app they run.Fetch lazily, cache locally has the three rules that matter: render the index from
top_items without opening any list, serve the cache on back-navigation, and refetch only when updated_at says a list actually changed.Poll /sync/activities rather than the lists themselves. It carries custom_lists.lists.all, which moves whenever any of the user’s lists changes — see Custom Lists have their own timestamps. One cheap call tells you whether a refetch is needed at all, instead of pulling lists on a timer to find out nothing changed.It also breaks down by kind — favorites, recommendations, regular, auto — so a user favouriting one show costs you one list refresh rather than a full re-read. If your UI only surfaces favourites, watch that one bucket and ignore the rest.Can I read another user's public lists?
Can I read another user's public lists?
Yes, by their user ID, as long as your token is PRO or VIP — the tier check is on the caller, not the list owner. You will see their public and unlisted lists; private ones are visible only to the owner and collaborators, and return
403 private_list otherwise.Reference
Custom lists API
Endpoint overview, shared parameters, and the
premium_only response.AUTH V2
How to get the user token these endpoints require.
Sync guide
Two-phase model,
date_from semantics, deletion reconciliation.Watchlist statuses
The five status values and per-type rules.