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) via a profile passed on every browserless_agent call — dropping the profile on a follow-up call lands you in a logged-out 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.
Run the whole flow (warm-up → search → open film → toggle → confirm) inside one browserless_agent call's commands array with a residential proxy (proxy: { proxy: "residential" }) and the user's cookie profile. Letterboxd fronts pages with a Cloudflare "Performing security verification" interstitial (Turnstile) on the first hit; the humanlike-fingerprint default clears it automatically in ~5–10s (see Gotchas). If it lingers, a solve { type: "cloudflare" } command targets Turnstile.
Warm up (first commands), with the user's cookie profile already attached.
{ "method": "goto", "params": { "url": "https://letterboxd.com/", "waitUntil": "load", "timeout": 45000 } } { "method": "waitForTimeout", "params": { "time": 8000 } }The
waitForTimeoutlets the Cloudflare Turnstile interstitial clear. (The user's session cookies must be loaded via the profile /cookie-syncbefore this call — see prerequisite.)Search for the film by title. Use the films-scoped search URL — it returns only films (no members/lists/reviews noise):
{ "method": "goto", "params": { "url": "https://letterboxd.com/search/films/the%20matrix/", "waitUntil": "load", "timeout": 45000 } } { "method": "waitForTimeout", "params": { "time": 3000 } }The 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.
{ "method": "goto", "params": { "url": "https://letterboxd.com/film/the-matrix/", "waitUntil": "load", "timeout": 45000 } } { "method": "waitForTimeout", "params": { "time": 3000 } }Extract the title from
h1.headline-1.primarynameand the year from.releaseyear a(or anya[href*="/films/year/"]) with atext/evaluate. 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:
{ "method": "snapshot" } { "method": "click", "params": { "selector": "<ref-or-selector whose accessible name contains \"watchlist\">" } } { "method": "waitForTimeout", "params": { "time": 1500 } }Read the accessible name from the
snapshot; the toggle is an AJAX POST, so no full navigation follows. If 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), read via a fresh
snapshot, or - loading the watchlist page and checking the film appears:The 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.
{ "method": "goto", "params": { "url": "https://letterboxd.com/<username>/watchlist/", "waitUntil": "load", "timeout": 45000 } } { "method": "waitForTimeout", "params": { "time": 3000 } }
- the control's accessible name flipping to "Remove this film from your watchlist" (its active/added state), read via a fresh
No session-release step. There is nothing to release — but the session does not die on return: it persists across separate calls, keyed by the call's
proxy/profile. Keeping the user's cookie profile (and proxy) on every call reconnects you to the same session with its cleared Turnstile, cookies, and film-page state intact; dropping or changing that config lands you in a different, logged-out session. Batching steps 1–6 into one call'scommandsarray is the simplest way to stay in one session.
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
browserless_agentwith a residential proxy (proxy: { proxy: "residential" }); the humanlike-fingerprint default is on. Letterboxd serves a Cloudflare "Performing security verification" (Turnstile) interstitial (RootWebArea: Just a moment…) on the first navigation from a fresh session. The stealth default clears it automatically — awaitForTimeoutof 8000ms after the firstgotois enough; the page then resolves to the realLetterboxd • Social film discoverycontent. If it sticks, add asolve { type: "cloudflare" }command. A degraded (non-stealth) fingerprint 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 stealthbrowserless_agentsession (including asolve { type: "hcaptcha" }attempt) did not clear 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>/."
}