Add a Film to Your Letterboxd Watchlist
Purpose
From an already-authenticated Letterboxd session, search for a film by title, open the correct film page (disambiguating by release year), and add it to the signed-in user's watchlist. Returns the matched film's title, year, and confirmation that the watchlist toggle flipped to its "added" state. This is a write action (it mutates the user's watchlist) but it is non-destructive and reversible (the same control removes the film). It is not a purchase or payment of any kind.
There is no public write API for the watchlist — the toggle is an internal, CSRF-guarded, session-bound AJAX POST. A browser session is the only reliable surface, so this skill is browser-driven.
When to Use
- "Add The Matrix to my Letterboxd watchlist."
- A "save this film for later" action triggered from another agent that resolved a title.
- Bulk-queueing several films a user wants to watch (run once per title).
- Any flow where the user is logged in to Letterboxd (cookies present) and wants a film saved to Watchlist — not logged (watched), liked, or rated. Those are separate controls.
Workflow
Prerequisite — authenticated session. This skill operates on the user's own logged-in account. Supply the user's Letterboxd session cookies (see cookie-sync) on a stealth browser session. Do not attempt to create an account programmatically — Letterboxd's sign-up is hCaptcha-gated (see Gotchas). The watchlist controls described below are rendered only when the session is authenticated; on a logged-out session the film page shows a "Sign in to log, rate or review" prompt and the actions panel is absent.
Use a stealth session (--verified --proxies). Letterboxd fronts pages with a Cloudflare "Performing security verification" interstitial (Turnstile) on the first hit; a verified session clears it automatically in ~5–10s (see Gotchas).
-
Open a verified, proxied session and warm up.
sid=$(browse cloud sessions create --keep-alive --verified --proxies \ | node -e "let s='';process.stdin.on('data',c=>s+=c).on('end',()=>process.stdout.write(JSON.parse(s).id))") export BROWSE_SESSION="$sid" browse open "https://letterboxd.com/" --remote browse wait timeout 8000 --remote # let the Cloudflare Turnstile interstitial clear(Inject the user's session cookies onto this session before proceeding —
cookie-sync.) -
Search for the film by title. Use the films-scoped search URL — it returns only films (no members/lists/reviews noise):
browse open "https://letterboxd.com/search/films/$(printf %s "the matrix" | jq -sRr @uri)/" --remote browse wait timeout 3000 --remoteThe general endpoint
https://letterboxd.com/search/<url-encoded query>/also works but mixes in non-film results. Results live inul.results; each film link is.film-title-wrapper a(or.headline-2 a) and itshrefis the canonical film slug, e.g./film/the-matrix/. The result label includes the year, e.g.The Matrix (1999). -
Pick the right result by title + year. Match on both. Letterboxd disambiguates same-title films with a year-suffixed slug:
/film/the-matrix/(1999) vs/film/the-matrix-2004/(2004). Choose thehrefwhose adjacent year matches the requested year; if no year was requested, prefer the top result. -
Open the film page.
browse open "https://letterboxd.com/film/the-matrix/" --remote browse wait timeout 3000 --remoteExtract the title from
h1.headline-1.primarynameand the year from.releaseyear a(or anya[href*="/films/year/"]). Verify these match the request before mutating anything. -
Add to watchlist via the actions panel. On an authenticated film page, the right-hand actions panel exposes three toggles — watched (eye), like (heart), and watchlist. Locate the watchlist control by its accessible name / title text (it reads roughly "Add this film to your watchlist") rather than a brittle CSS class, then click it:
browse snapshot --remote # find the ref whose accessible name contains "watchlist" browse click @<ref-for-watchlist> --remote browse wait timeout 1500 --remote # the toggle is an AJAX POST — no full navigationIf the control already reads "Remove … from your watchlist" / shows a filled/active icon, the film is already on the watchlist — treat as success (idempotent), do not click (clicking would remove it).
-
Confirm. The toggle is an in-page AJAX action; confirm by either:
- the control's accessible name flipping to "Remove this film from your watchlist" (its active/added state), or
- loading the watchlist page and checking the film appears:
browse open "https://letterboxd.com/<username>/watchlist/" --remote browse wait timeout 3000 --remote # film poster with the matched slug should be present in the grid
Prefer the watchlist-page check when you need hard confirmation — the in-page state flip alone is sufficient for the common case.
-
Release the session.
browse cloud sessions update "$sid" --status REQUEST_RELEASE
Never click Watch (log/watched), Like, Rate, or Review — those are adjacent controls in the same panel and are not the watchlist. Adding to a watchlist is the only mutation this skill performs.
Site-Specific Gotchas
- Stealth is required. Run with
--verified --proxies. Letterboxd serves a Cloudflare "Performing security verification" (Turnstile) interstitial (RootWebArea: Just a moment…) on the first navigation from a fresh session. A verified session clears it automatically —browse wait timeout 8000after the firstopenis enough; the page then resolves to the realLetterboxd • Social film discoverycontent. A bare (non-verified) session can get stuck on this interstitial. - The watchlist control only exists when logged in. On a logged-out session the film page contains no
watchlistmarkup at all (verified: 0 occurrences of "watchlist" in the logged-out film-page HTML) — only a "Sign in to log, rate or review" prompt. There is nothing to click without a valid session. Provide the user's cookies viacookie-syncfirst. - No public write API — and the internal one is session/CSRF-bound. The watchlist toggle fires an internal AJAX POST guarded by the page's
__csrftoken and the session cookie. There is no documented, reusable public endpoint; do not try to reconstruct or replay it out-of-band. Drive the UI toggle instead. (Read-only data like film metadata is available via the public pages and TMDB, but writing to a watchlist has no public API.) - Account creation is hard-walled by hCaptcha — do not automate signup. The
create-accountmodal (https://letterboxd.com/create-account/, which redirects to/?register=true) is protected by an hCaptcha "I am human" checkbox that escalates to an image grid; thetokenhidden field stays empty and the form silently no-ops until solved. A verified Browserbase session did not auto-solve the image challenge within 60s. This is irrelevant to the skill in practice because users bring their own authenticated session (cookie-sync) — but it means you cannot bootstrap a throwaway test account here. Don't burn iterations trying. - Duplicate hidden forms. The homepage/film pages embed many copies of the sign-up/sign-in forms (one is spawned per film-poster hover widget, ~20 instances). Selectors like
input[name="emailAddress"]match all of them; only one is on-screen. If you ever must interact with these, filter by a non-zerogetBoundingClientRect()(the on-screen instance), not byoffsetParent(the modal uses fixed positioning, sooffsetParentisnulleven when visible). - Field names, if you ever touch auth forms: the real signup inputs are
emailAddress,username,password,termsAndAge,acceptPrivacyPolicy(notemail/termsAccepted). The sign-in form is.signin-formwithusername+password. - Same-title disambiguation is year-based. Slugs append a year/qualifier on collisions (
/film/the-matrix/vs/film/the-matrix-2004/,/film/inception/vs/film/inception-1980/). Always match the year, not just the title. - Watchlist add is AJAX, not a navigation. After clicking, the URL does not change; confirm via the control's state flip or by loading
/{username}/watchlist/. Don't wait for a page load that never comes. - Idempotency. If the film is already on the watchlist, the control shows the "Remove…" state. Re-clicking removes it. Check state before clicking and treat already-present as success.
Expected Output
// Added successfully
{
"success": true,
"film_title": "The Matrix",
"year": 1999,
"film_slug": "/film/the-matrix/",
"added_to_watchlist": true,
"already_present": false,
"watchlist_confirmation": "Watchlist control flipped to 'Remove this film from your watchlist'; film present at /<username>/watchlist/",
"error_reasoning": null
}
// Already on the watchlist (idempotent no-op)
{
"success": true,
"film_title": "The Matrix",
"year": 1999,
"film_slug": "/film/the-matrix/",
"added_to_watchlist": true,
"already_present": true,
"watchlist_confirmation": "Control already in 'Remove…' state; not re-clicked",
"error_reasoning": null
}
// Not authenticated — no watchlist control rendered
{
"success": false,
"film_title": "The Matrix",
"year": 1999,
"added_to_watchlist": false,
"error_reasoning": "No authenticated session: film page shows 'Sign in to log, rate or review' and no watchlist control. Supply user cookies via cookie-sync."
}
// Film not found / ambiguous title
{
"success": false,
"film_title": null,
"year": null,
"added_to_watchlist": false,
"error_reasoning": "No film result matched the requested title/year on /search/films/<query>/."
}