Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/faraasaaay/innertube-v1/llms.txt

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

YouTube’s BotGuard system requires a Proof-of-Origin Token (PoToken) for certain YouTube clients. The token proves that a genuine browser environment generated the request rather than an automated tool. In the InnerTube SDK this proof is computed locally by running Google’s BotGuard JavaScript inside an Android WebView — no external server is involved. Two YouTubeClient flags control whether a PoToken is needed:
FlagMeaning
useWebPoTokens = trueThe client accepts a PoToken and will embed it in serviceIntegrityDimensions if one is provided
requirePoToken = trueThe client will fail without a valid PoToken (set only on TVHTML5_SIMPLY)
Clients with useWebPoTokens = true include WEB_REMIX, WEB_CREATOR, TVHTML5, and TVHTML5_SIMPLY.

PoTokenGenerator

PoTokenGenerator manages the Android WebView lifecycle for token generation. Instantiate it once and reuse it throughout the application lifetime.

getWebClientPoToken(videoId, sessionId)

This is the primary method. It returns a PoTokenResult?null if the WebView is unavailable or generation times out.
import com.metrolist.music.utils.potoken.PoTokenGenerator

// Create once — typically a singleton in your DI graph
val poTokenGenerator = PoTokenGenerator()

// Call from a coroutine, e.g. before constructing a player request
val visitorData: String = YouTube.visitorData ?: return  // sessionId must be set first

val result = poTokenGenerator.getWebClientPoToken(
    videoId = "dQw4w9WgXcQ",
    sessionId = visitorData,
)

if (result != null) {
    println("Player request token: ${result.playerRequestPoToken}")
    println("Streaming data token: ${result.streamingDataPoToken}")
} else {
    println("PoToken unavailable — falling back to non-PoToken clients")
}
visitorData (the session ID) must be set on YouTube.visitorData before calling getWebClientPoToken. The streaming PoToken is generated from sessionId and must be obtained first — the generator enforces this ordering automatically when it creates a new WebView instance.

PoTokenResult

PoTokenResult carries two distinct tokens returned from a single generator call:
FieldTypeDescription
playerRequestPoTokenStringSession-level token derived from sessionId. Generated once per session and reused across multiple player calls with the same visitorData. This is the value passed to YouTube.player() as the poToken argument.
streamingDataPoTokenStringPer-video token derived from videoId. Appended to adaptive stream URLs as the pot= query parameter by YTPlayerUtils during URL resolution.
import com.metrolist.innertube.YouTube
import com.metrolist.innertube.models.YouTubeClient

suspend fun playerWithPoToken(videoId: String) {
    val sessionId = YouTube.visitorData ?: return
    val poTokenResult = poTokenGenerator.getWebClientPoToken(videoId, sessionId)

    YouTube.player(
        videoId = videoId,
        playlistId = null,
        client = YouTubeClient.WEB_REMIX,
        poToken = poTokenResult?.playerRequestPoToken,  // null-safe: falls back gracefully
    ).onSuccess { response ->
        println("Got player response for $videoId")
    }
}

Lifecycle management

PoTokenGenerator maintains a single internal PoTokenWebView instance. A new WebView is created (and the old one closed) when any of the following conditions are detected at the start of getWebClientPoToken:

First call

webPoTokenGenerator == null — no WebView exists yet.

Session changed

webPoTokenSessionId != sessionId — the visitorData has rotated (e.g. after sign-in or sign-out).

Token expired

webPoTokenGenerator.isExpired — the BotGuard token lifetime has elapsed.

Renderer killed

webPoTokenGenerator.isDead — the WebView renderer process was killed by the OS (low-memory event).
When the WebView is recreated, the generator:
  1. Closes the old WebView (PoTokenWebView.close() hops to the Main dispatcher).
  2. Clears the cached webPoTokenStreamingPot and webPoTokenSessionId.
  3. Creates a fresh PoTokenWebView via PoTokenWebView.getNewPoTokenGenerator(context).
  4. Generates a streaming PoToken from sessionId before any per-video tokens are produced.

Timeout handling

PoToken generation is guarded by an 8-second timeout (POTOKEN_TIMEOUT_MS = 8_000L). This accounts for the cold-start cost of WebView spin-up and BotGuard JavaScript execution (~2–5 seconds on a healthy device) while leaving a margin for slow hardware before the fallback chain takes over. When the timeout fires:
  1. The generator logs a warning and closes the current WebView.
  2. All cached state (webPoTokenGenerator, webPoTokenStreamingPot, webPoTokenSessionId) is cleared under the mutex.
  3. getWebClientPoToken returns null.
  4. YTPlayerUtils.playerResponseForPlayback falls through to non-PoToken clients such as ANDROID_VR.
// POTOKEN_TIMEOUT_MS is an internal constant — 8 000 ms
// You do not configure it directly; it applies automatically on every call.
If generation times out consistently on a device, the WebView sandbox may be under OS memory pressure. The fallback to ANDROID_VR clients ensures playback continues, but stream quality may differ. Consider calling YTPlayerUtils.prewarmPoToken() at app start to amortise the cold-start cost before the user presses play.

BadWebViewException

If the device’s system WebView implementation is broken or incompatible, PoTokenWebView.getNewPoTokenGenerator() throws a BadWebViewException. When PoTokenGenerator catches this exception it sets the internal webViewBadImpl = true flag, and all subsequent calls to getWebClientPoToken return null immediately without attempting to spin up a WebView again.
// This is handled internally — you do not need to catch BadWebViewException yourself.
// The generator returns null for all subsequent calls on a device with a bad WebView.

val result = poTokenGenerator.getWebClientPoToken(videoId, sessionId)
if (result == null) {
    // Either WebView is unavailable, broken, or timed out.
    // Fall back to ANDROID_VR or other non-PoToken clients.
}

Pre-warming the generator

YTPlayerUtils.prewarmPoToken() warms the generator at app start using a stable dummy video ID ("jNQXAC9IVRw"). Because PoToken generation is a local WebView computation with no YouTube network request for the warm-up video itself, this is cheap to call. The result is discarded — the goal is simply to have the WebView and BotGuard JS already loaded when the user first taps play.
import com.metrolist.music.utils.YTPlayerUtils
import com.metrolist.innertube.YouTube

// Call this once visitorData is available, typically after app launch / sign-in
suspend fun onAppReady() {
    // visitorData must be set before prewarmPoToken() does anything
    YouTube.visitorData = fetchVisitorData()

    // Fire-and-forget warm-up — failures are swallowed internally
    YTPlayerUtils.prewarmPoToken()
}

Integration with the player

When a client has useWebPoTokens = true, the playerRequestPoToken is embedded in the player request body as serviceIntegrityDimensions.poToken. The InnerTube.player() method handles this automatically when you pass a non-null poToken argument. The streamingDataPoToken is separately appended to stream URLs as pot= by YTPlayerUtils during URL resolution.
import com.metrolist.innertube.YouTube
import com.metrolist.innertube.models.YouTubeClient
import com.metrolist.music.utils.potoken.PoTokenGenerator

val generator = PoTokenGenerator()

suspend fun playWithBotGuard(videoId: String) {
    val sessionId = YouTube.visitorData ?: return

    // 1. Obtain tokens
    val tokens = generator.getWebClientPoToken(
        videoId = videoId,
        sessionId = sessionId,
    )

    // 2. Pass playerRequestPoToken to the player call (session-level token for the request body)
    val response = YouTube.player(
        videoId = videoId,
        playlistId = null,
        client = YouTubeClient.WEB_REMIX,
        poToken = tokens?.playerRequestPoToken,
    ).getOrThrow()

    // 3. Extract the stream URL
    val streamUrl = response.streamingData
        ?.adaptiveFormats
        ?.filter { it.mimeType?.startsWith("audio/") == true }
        ?.maxByOrNull { it.bitrate ?: 0 }
        ?.url

    println("Stream URL: $streamUrl")
}
Clients with requirePoToken = true (currently TVHTML5_SIMPLY) will return an error or empty streamingData when called without a valid PoToken. Do not select TVHTML5_SIMPLY from the ContentAwareFallbackStrategy chain unless PoTokenGenerator.getWebClientPoToken() has returned a non-null result for the current session. If the generator returns null (WebView unavailable or timed out), skip requirePoToken = true clients and proceed with the remaining fallbacks.

Build docs developers (and LLMs) love