TL;DR Use Firefox cookies for YouTube auth in 2026 — Chrome 127+ app-bound encryption breaks all external extraction tools. [1] The core auth chain is
SAPISID → SAPISIDHASH headerfor InnerTube API calls, now augmented by mandatory PO tokens that must be generated by BotGuard/DroidGuard. [5] Watch Later (list=WL) works with yt-dlp ⭐ 169k + cookies, but is read-mostly via the official Data API: list and insert work; update, delete, and reorder do not. [3]
Cookie Map
YouTube auth state lives in a cluster of .youtube.com / .google.com cookies. The critical ones for InnerTube API access:
| Cookie | Lifetime | Role |
|---|---|---|
SAPISID |
2 yr | Source for SAPISIDHASH; required for authenticated InnerTube requests [4] |
__Secure-3PAPISID |
2 yr | Secure-context APISID variant; used when available, preferred over SAPISID by InnerTube clients [4] |
SID |
2 yr | Session identifier; paired with HSID for replay-attack defence [12] |
HSID |
2 yr | HTTP-only session token; contains signed Google Account ID + last-login time [12] |
SSID |
2 yr | Secure (HTTPS-only) variant of SID [12] |
VISITOR_INFO1_LIVE |
6 mo | Encodes the Visitor ID; maps to visitor_data in InnerTube requests; anchors PO tokens for unauthenticated sessions [9] |
YSC |
session | Per-tab session token; anti-CSRF |
LOGIN_INFO |
2 yr | Account-chooser state; not involved in request authentication |
PREF |
2 yr | User preferences (language, autoplay, etc.); not auth-critical |
The maintainer of YoutubeExplode ⭐ 3.7k confirmed that APISID/SAPISID (or the __Secure-3PAPISID variant) are the required cookies; the others appear in real browser sessions but are not strictly required for playback. [4]
Authentication Flow: SAPISIDHASH
YouTube’s InnerTube API — the private JSON API used by yt-dlp, YoutubeExplode, and third-party clients — authenticates via a derived Authorization header, not OAuth. The formula: [6][13]
Authorization: SAPISIDHASH {ts}_{SHA1("{ts} {SAPISID} https://www.youtube.com")}
Where {ts} is a fresh Unix epoch timestamp per request. Required alongside:
Cookieheader with the full session cookie setX-Origin: https://www.youtube.com
OAuth is dead for third-party tools. --username/--password and device-code login no longer work — YouTube deprecated them for non-Google clients. Browser cookies are now the only authentication path. [2]
PO Token System
Since 2024–2025, YouTube enforces Proof of Origin (PO) tokens — attestation tokens verifying that requests originate from a genuine browser or device. Missing or invalid tokens → HTTP 403. [5]
| Context | Client | PO token required? |
|---|---|---|
| GVS (video stream) | web |
✓ (waived for YouTube Premium) |
| GVS | mweb |
✓ |
| Player (format URL) | web |
✓ |
| Subs (subtitles) | web |
✓ |
| Player | android |
✓ (DroidGuard) |
| Player | ios |
✓ (iOSGuard) |
- DATASYNC_ID — authenticated GVS requests (account session)
- VISITOR_DATA — unauthenticated GVS requests (sourced from
VISITOR_INFO1_LIVE) - VIDEO_ID — Player and Subs contexts regardless of auth status
Using yt-dlp ⭐ 169k: [14]
# Manual token injection
yt-dlp --extractor-args "youtube:player-client=web,default;po_token=web+TOKEN_VALUE" URL
# Plugin-based automatic generation (recommended)
pip install bgutil-ytdlp-pot-provider
yt-dlp URL # plugin hooks in automatically
Active PO token plugins: bgutil-ytdlp-pot-provider ⭐ 555 (uses LuanRT’s BotGuard library; runs as HTTP server or CLI subprocess) [15] and yt-dlp-get-pot ⭐ 112 (abstract provider framework for custom integrations). [16]
Rate limits: authenticated session → ~2,000 videos/hr; guest/visitor session → ~300 videos/hr. [2]
Cookie Extraction in 2026
Chrome is broken. Chrome 127 (July 2024) shipped app-bound encryption — the AES decryption key is bound to the Chrome binary itself. [1] Every external extraction path now fails:
- DPAPI decryption — fails even with admin rights
rookiepyRust library —decrypt_encrypted_value failed- Chrome DevTools Protocol (port 9222) — port-binding issues on Windows
Firefox works. Firefox stores cookies unencrypted in SQLite at %APPDATA%\Mozilla\Firefox\Profiles\*\cookies.sqlite (Linux: ~/.mozilla/firefox/*/cookies.sqlite). [1]
import sqlite3, shutil, glob, os
profiles = os.path.join(os.environ["APPDATA"], "Mozilla", "Firefox", "Profiles")
db = max(glob.glob(f"{profiles}/*/cookies.sqlite"), key=os.path.getsize)
shutil.copy2(db, "tmp.sqlite") # copy to avoid lock
conn = sqlite3.connect("tmp.sqlite")
rows = conn.execute(
"SELECT host, name, value, path, expiry, isSecure "
"FROM moz_cookies WHERE host LIKE '%youtube%' OR host LIKE '%google%'"
).fetchall()
# → 60–70 rows in Netscape format ready for yt-dlp --cookies
Or directly: yt-dlp --cookies-from-browser firefox URL
Rotation caveat: Cookies expire in ~2 weeks. [7] To preserve a working session: log in via a private/incognito window, export cookies immediately, then close the tab — leaving an authenticated tab open triggers background rotation scripts that invalidate the exported cookies. [2]
Watch Later Mechanics
Identity
Watch Later is a system-managed RelatedPlaylist identified by the short token WL. Its full ID is user-specific and dynamic — never hardcode it. Retrieve via the Data API: [3]
svc = build("youtube", "v3", credentials=creds)
resp = svc.channels().list(part="contentDetails", mine=True).execute()
wl_id = resp["items"][0]["contentDetails"]["relatedPlaylists"]["watchLater"]
Browser URL: https://www.youtube.com/playlist?list=WL (resolves via session cookie).
Data API Capabilities
| Operation | YouTube Data API v3 | Notes |
|---|---|---|
| List items | ✓ playlistItems.list |
Requires OAuth youtube.readonly scope |
| Insert item | ✓ playlistItems.insert |
Requires OAuth youtube scope |
| Update item | ✗ | 403: “The playlist cannot be modified” [3] |
| Delete item | ✗ | 403 or 404 [3] |
| Reorder items | ✗ | Impossible via API |
| Rename / change settings | ✗ | Impossible via API |
YouTube’s backend enforces special auto-management behavior on Watch Later (e.g., removing watched videos via the UI) that would conflict with arbitrary API writes. [3]
Workaround for full CRUD: create a custom playlist. It grants complete insert/update/delete/reorder control unavailable on the native Watch Later. [3]
“Remove Watched” Button
YouTube’s web UI includes a “Remove watched videos” button on the Watch Later page. It is manual, not automatic — it bulk-removes all videos where any playback progress was recorded, including partially watched ones. [11] No programmatic API equivalent exists.
Content restriction: “Made for kids” videos cannot be added to Watch Later at all. [11]
Downloading with yt-dlp
Watch Later is a private playlist; cookies are required: [10]
yt-dlp --cookies-from-browser firefox "https://www.youtube.com/playlist?list=WL"
⚠ Known issue: using yt-dlp as a Python module (not CLI subprocess) can trigger "YouTube said: The playlist does not exist" for WL even with valid cookies. Root cause: deleted/unavailable videos in the queue produce a response the module path mishandles. Workaround: invoke via subprocess.run(["yt-dlp", ...]). [10]