youtube · git:20260717.c271e80 · 2026-07-17 · sha256 7bd735deaf3bd4c4
youtube git:20260717.c271e80A
Immutable. This exact content never changes and is served at /api/v1/blob/7bd735deaf3bd4c4.
---
name: youtube
description: >-
Drive the user's authenticated YouTube via the browser skill: channels,
videos, comments + commenters, search via the InnerTube API; gated
like/comment/subscribe. Use for YouTube research/automation as the user.
metadata:
tags: browser, youtube, internal-api, recon, social-graph
---
# YouTube — recon plan (InnerTube API)
**Internal API:** InnerTube —
`POST https://www.youtube.com/youtubei/v1/<endpoint>?key=<INNERTUBE_API_KEY>&prettyPrint=false`,
body `{ context: <INNERTUBE_CONTEXT>, ...params }`. The web app uses this for
everything.
## Config + auth (from the page)
- `INNERTUBE_API_KEY` + `INNERTUBE_CONTEXT` live in `ytcfg`. Read in-page:
```js
;() => ({
key: ytcfg.get("INNERTUBE_API_KEY"),
ctx: ytcfg.get("INNERTUBE_CONTEXT"),
})
```
- Public reads: often work with just key + context (no auth).
- Personalized / write (subscriptions, comments, like): need **SAPISIDHASH**
auth:
- `Authorization: SAPISIDHASH <ts>_<SHA1(ts + " " + SAPISID + " " + origin)>`
where SAPISID = the `SAPISID` (or `__Secure-3PAPISID`) cookie, origin =
`https://www.youtube.com`, ts = unix seconds. Compute SHA-1 in-page
(SubtleCrypto). Also send `X-Goog-AuthUser: 0`, `Origin`, cookies (auto).
In-page fetch:
```js
;async (endpoint, body) => {
const key = ytcfg.get("INNERTUBE_API_KEY")
const r = await fetch(
`/youtubei/v1/${endpoint}?key=${key}&prettyPrint=false`,
{
method: "POST",
headers: {
"content-type": "application/json" /* + SAPISIDHASH for writes */,
},
body: JSON.stringify({
context: ytcfg.get("INNERTUBE_CONTEXT"),
...body,
}),
}
)
return await r.json()
}
```
## Endpoints to test
| Purpose | endpoint | params |
| ----------------------------------- | ------------------------------------------------- | ---------------------------------- |
| Channel / home / playlist | `browse` | `browseId` (channel UC…), `params` |
| Watch page → video + comments token | `next` | `videoId` |
| **Comments** (+ who/likes) | `next` (continuation token from the watch `next`) | `continuation` |
| Search | `search` | `query`, `params` |
| Video metadata | `player` | `videoId` |
| WRITE: comment | `comment/create_comment` / `create_comment_reply` | `createCommentParams` |
| WRITE: like/dislike | `like/like` / `like/dislike` / `like/removelike` | `target.videoId` |
| WRITE: subscribe | `subscription/subscribe` / `unsubscribe` | `channelIds` |
## Entity model
`Channel{browseId, title, subscriberCount}` ·
`Video{videoId, title, author, viewCount, likeCount}` ·
`Comment{author, text, likeCount, replyCount}`. Responses are deeply nested
**renderer trees** (`*Renderer`) — walk to the renderer that holds the field
(e.g. `commentRenderer`, `videoRenderer`).
## Capture method
Open youtube.com → read ytcfg (above) → `list_network_requests` for
`/youtubei/v1/` → see the endpoint + the body params + continuation tokens.
## Actions to test (read-first; writes gated)
1. read ytcfg key+context.
2. `search` a topic → videos.
3. `next(videoId)` → video + first comments → **who commented + likes** (→
graph).
4. `browse(channelId)` → a creator's videos/about.
5. (gated, needs SAPISIDHASH) comment / like / subscribe — explicit confirm.
## Gotchas
- SAPISIDHASH required for any authenticated/write call — compute in-page from
the SAPISID cookie (SubtleCrypto SHA-1). Read-only public data may skip it.
- Renderer trees are verbose; extract by walking for the specific `*Renderer`.
- Comments come via a continuation token from the `next` response, not a flat
list.