Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/faraasaaay/inner/llms.txt

Use this file to discover all available pages before exploring further.

YouTube’s service integrity system requires proof-of-origin tokens (PoTokens) for certain web and TV clients to verify that requests originate from a legitimate browser or device context. Inner includes a pure-Kotlin PoTokenGenerator that produces deterministic tokens from a visitor or account identifier — no WebView, BotGuard JavaScript execution, or native code required.
These tokens are a best-effort implementation and may not work for all use cases or future YouTube API changes. If stream playback fails with 403 errors, try switching to a mobile client (IOS, ANDROID_MUSIC) which does not require PoTokens.

When PoTokens are needed

Service integrity enforcement is applied to web and TV clients. Inner’s PlaybackAuthState.needsServiceIntegrity(client) returns true for the following clientName values:
ClientNeeds service integrity
WEB
WEB_REMIX
WEB_CREATOR
MWEB
WEB_EMBEDDED_PLAYER
TVHTML5
TVHTML5_SIMPLY_EMBEDDED_PLAYER
TVHTML5_SIMPLY
IOS / IOS_MUSIC
ANDROID / ANDROID_MUSIC
ANDROID_VR
If you exclusively use IOS, IOS_MUSIC, MOBILE, or ANDROID_MUSIC for playback, you can leave webClientPoTokenEnabled = false and skip token generation entirely.

Token types

Stored in PlaybackAuthState.poToken. Used as a fallback when neither poTokenPlayer nor poTokenGvs is set. Both resolvePlayerPoToken() and resolveGvsPoToken() fall back to this value.
Stored in PlaybackAuthState.poTokenPlayer. Placed in the serviceIntegrityDimensions.poToken field of player request bodies when the target client needsServiceIntegrity. This is the token YouTube uses to verify the player call itself.
Stored in PlaybackAuthState.poTokenGvs. Appended as the pot query parameter to Google Video Server (GVS) stream URLs: https://rr*.googlevideo.com/…?pot=<token>. YouTube’s media servers use this to validate stream access.

Generating tokens

PoTokenGenerator exposes three methods. generateSessionToken and generateContentToken are convenience wrappers that call generateColdStartToken with a specific clientState value:
import com.tamed.music.innertube.utils.PoTokenGenerator

// Cold-start token — pass visitorData or dataSyncId as the identifier,
// and any client-state string (empty string is the default).
val coldStart = PoTokenGenerator.generateColdStartToken(
    identifier = YouTube.visitorData ?: "",
    clientState = ""
)

// Session token — equivalent to generateColdStartToken(identifier, "session").
val sessionToken = PoTokenGenerator.generateSessionToken(
    identifier = YouTube.visitorData ?: ""
)

// Content-specific token — equivalent to generateColdStartToken(identifier, videoId).
// Recommended for per-stream requests.
val contentToken = PoTokenGenerator.generateContentToken(
    identifier = YouTube.visitorData ?: "",
    videoId = "dQw4w9WgXcQ"
)
Use dataSyncId instead of visitorData as the identifier when the user is authenticated (YouTube.hasLoginCookie() == true), since dataSyncId is the authenticated session identifier. You can also use PlaybackAuthState.sessionId, which automatically returns dataSyncId when available and falls back to visitorData.

Applying tokens

Once you have generated tokens, configure them on the YouTube object. Setting webClientPoTokenEnabled = true is required — without it all token resolution is suppressed regardless of which token fields are set.
YouTube.webClientPoTokenEnabled = true
YouTube.poTokenPlayer = PoTokenGenerator.generateSessionToken(YouTube.visitorData ?: "")
YouTube.poTokenGvs = PoTokenGenerator.generateColdStartToken(YouTube.visitorData ?: "")
Or set the entire state atomically:
import com.tamed.music.innertube.PlaybackAuthState

YouTube.authState = YouTube.authState.copy(
    webClientPoTokenEnabled = true,
    poTokenPlayer = PoTokenGenerator.generateSessionToken(YouTube.visitorData ?: ""),
    poTokenGvs = PoTokenGenerator.generateColdStartToken(YouTube.visitorData ?: ""),
)

How Inner uses tokens

1

Player token resolution

When YouTube.player(videoId, playlistId, client) is called, it invokes resolvePlayerPoToken(client, explicitPoToken, authState).The resolution logic in PlaybackAuthState:
  1. If an explicit poToken argument was passed to player(), use it directly.
  2. If webClientPoTokenEnabled is false, return null.
  3. If needsServiceIntegrity(client) is false, return null.
  4. Otherwise return poTokenPlayer ?: poToken.
The resolved token is placed in the serviceIntegrityDimensions.poToken field of the player request body.
2

GVS stream token resolution

When YouTube.registerPlayback(...) is called — which hits the playback tracking endpoint after stream selection — the GVS token is resolved via resolveGvsPoToken(authState):
  1. If a client argument is provided and needsServiceIntegrity(client) is false, return null.
  2. If webClientPoTokenEnabled is false, return null.
  3. Otherwise return poTokenGvs ?: poToken.
3

Stream URL decoration

The appendGvsPoToken(url, client, authState) helper appends ?pot=<token> (or &pot=<token>) to a stream URL. It only appends if the URL does not already contain pot= and if resolveGvsPoToken returns a non-null value. This is called when constructing the final media URL passed to the player.

Token internals

The token structure follows SmartTube’s PoTokenService approach. PoTokenGenerator uses four constants:
ConstantValueRole
MAGIC_HEADER0x0AOpens the outer payload; protobuf field tag for the random key
TOKEN_VERSION0x22Field tag separating the key from the encrypted identifier
INNER_TAG0x38Opens the inner payload; field tag for the client-state string
TIMESTAMP_TAG0x02Field tag for the timestamp within the inner payload
Token construction proceeds as follows:
  1. 16-byte random key — generated fresh for each token via Random.nextBytes(16).
  2. XOR-encrypted identifier — the UTF-8 bytes of visitorData (or dataSyncId) are XOR-encrypted byte-by-byte against the random key, cycling the key for identifiers longer than 16 bytes.
  3. Inner payloadINNER_TAG + varint length + clientState bytes + TIMESTAMP_TAG + varint length + little-endian timestamp bytes (current milliseconds since epoch, with trailing zero bytes stripped).
  4. Outer payloadMAGIC_HEADER + varint(16) + key bytes + TOKEN_VERSION + varint(encryptedId.size) + encrypted identifier bytes + inner payload.
  5. Encoding — the full outer payload is base64url-encoded without padding (= characters are trimmed).
The result is a URL-safe string that can be placed in an HTTP query parameter or a JSON field without additional encoding.

Build docs developers (and LLMs) love