Skip to main content

Documentation Index

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

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

YouTube flags anonymous traffic that does not present valid browser signals. This guide explains how InnerTube mitigates bot detection through guest-session rotation and WebView-based Proof-of-Origin Token (PoToken) generation.

What Is Bot Detection?

When YouTube determines that a request does not originate from a real browser or signed-in user, it blocks playback at the player or CDN level. Common symptoms in guest sessions:
  • PlayerResponse.playabilityStatus.status is LOGIN_REQUIRED
  • Stream URLs return HTTP 403 even though the format list was populated
  • playabilityStatus.reason contains phrases like "Sign in to confirm you're not a bot" or "Error 2000"
Bot detection signals are distinct from geo-restrictions, which produce NOT_AVAILABLE_IN_THIS_COUNTRY or similar messages. InnerTube will not attempt session rotation for geo-errors because rotating the session cannot change the content’s regional availability.

BotDetectionMitigator

BotDetectionMitigator is a singleton that tracks guest playback failures and coordinates visitorData rotation. It is imported from com.music.vivi.utils.

Reporting Failures and Successes

Call notifyPlaybackFailure() whenever a stream error occurs in a guest session. The method returns true if the error looks like a bot-detection signal (meaning rotation may help) and false if it is a geo-restriction or the user is logged in. The errorMessage parameter is optional and defaults to null.
val shouldRotate = BotDetectionMitigator.notifyPlaybackFailure(
    isLoggedIn = YouTube.cookie != null,
    errorMessage = errorReason   // optional; e.g. "Sign in to confirm you're not a bot"
)
Call notifyPlaybackSuccess() when a track begins playing successfully to reset the internal failure counter:
BotDetectionMitigator.notifyPlaybackSuccess()

Error Classification Helpers

Two helper functions are available if you need to inspect error messages before calling notifyPlaybackFailure:
// Returns true for geographic restriction messages
BotDetectionMitigator.isGeoError("NOT_AVAILABLE_IN_THIS_COUNTRY")  // → true

// Returns true for bot-detection signals
BotDetectionMitigator.isBotDetectionError("Sign in to confirm")    // → true
isGeoError matches strings containing any of: "not available in your country", "not available in your region", "not available in this country", "not available in this region", "geo-restricted", "GEO_RESTRICTED", "NOT_AVAILABLE_IN_THIS_COUNTRY", "only available in certain countries", "country restriction", or "region restriction" (case-insensitive). isBotDetectionError matches "Sign in to confirm", "confirm you're not a bot", "automated queries", "Error 2000", "403", or "This content isn't available on this device" (case-insensitive).

Rotating the Guest Session

rotateGuestSession() is a suspend function that refreshes visitorData while preserving the user’s locale, so the new token is issued for the correct region:
// Called automatically by YTPlayerUtils on first playback failure
BotDetectionMitigator.rotateGuestSession()
Internally it:
  1. Snapshots YouTube.locale (region and language).
  2. Sets YouTube.visitorData = null.
  3. Calls YouTube.refreshVisitorData() to obtain a new token.
  4. Persists the new token to DataStore via VisitorDataKey.
  5. Resets the failure counter.
If refreshVisitorData() fails, the locale is restored so that subsequent attempts use the correct region.

Integrating with Playback

The following pattern mirrors how YTPlayerUtils.playerResponseForPlayback() uses BotDetectionMitigator:
suspend fun resolveWithBotMitigation(videoId: String): PlaybackData {
    val firstAttempt = resolvePlaybackData(videoId)

    if (firstAttempt.isFailure && YouTube.cookie == null) {
        // Guest session failed — rotate visitorData and retry once
        BotDetectionMitigator.rotateGuestSession()

        val retry = resolvePlaybackData(videoId)
        retry.onSuccess { BotDetectionMitigator.notifyPlaybackSuccess() }
        return retry.getOrThrow()
    }

    firstAttempt.onSuccess { BotDetectionMitigator.notifyPlaybackSuccess() }
    return firstAttempt.getOrThrow()
}

PoToken Generation

Some YouTube clients (WEB_REMIX and TVHTML5) require a Proof-of-Origin Token to accompany player requests. The PoToken proves that the request was initiated by a real browser or WebView environment running Google’s BotGuard JavaScript.

PoTokenResult

PoTokenResult is a simple class holding two tokens:
class PoTokenResult(
    val playerRequestPoToken: String,   // sent in the player request body
    val streamingDataPoToken: String,   // appended to stream URLs as &pot=...
)

PoTokenGenerator

PoTokenGenerator is a high-level manager that owns a PoTokenWebView instance and handles its lifecycle automatically. Create one instance per session and reuse it across player requests.
val poTokenGenerator = PoTokenGenerator()

// Generate tokens for a videoId and session identifier
val sessionId = YouTube.visitorData ?: return
val poToken: PoTokenResult? = poTokenGenerator.getWebClientPoToken(videoId, sessionId)
getWebClientPoToken() returns null if:
  • The device does not have a functional Android WebView (e.g. running on a plain JVM).
  • The WebView implementation is broken (detected automatically via BotGuard console errors).
Internally, PoTokenGenerator:
  1. Acquires a mutex to ensure only one PoTokenWebView exists at a time.
  2. Creates a new PoTokenWebView if none exists, if the current one has expired, or if the sessionId has changed.
  3. Generates the streamingDataPoToken once per session (passed the sessionId).
  4. Generates a fresh playerRequestPoToken for each videoId.
  5. On failure, retries once with a freshly recreated PoTokenWebView.

PoTokenWebView

PoTokenWebView is the low-level component that runs BotGuard in a hidden Android WebView. You do not normally need to interact with it directly.
  • Loads BotGuard JavaScript from the app’s assets/po_token.html.
  • Makes HTTP requests to jnn/v1/Create and jnn/v1/GenerateIT via OkHttp to obtain and exchange a BotGuard challenge.
  • Obeys YouTube.proxy — all OkHttp requests from the WebView go through the configured proxy.
  • isExpired: Booleantrue after the integrity token’s expiry window (minus a 10-minute safety margin).
  • close() — must be called on the main thread to cleanly destroy the WebView and cancel all pending coroutines.

How to Use PoTokens with player()

val sessionId = if (YouTube.cookie != null) YouTube.dataSyncId else YouTube.visitorData
    ?: return

// Step 1: Obtain the signature timestamp (required for WEB_REMIX)
val signatureTimestamp = NewPipeExtractor.getSignatureTimestamp(videoId).getOrNull()

// Step 2: Generate PoToken
val poToken: PoTokenResult? = poTokenGenerator.getWebClientPoToken(videoId, sessionId)

// Step 3: Call player() with both tokens
val response = YouTube.player(
    videoId = videoId,
    playlistId = null,
    client = YouTubeClient.WEB_REMIX,
    signatureTimestamp = signatureTimestamp,
    poToken = poToken?.playerRequestPoToken
).getOrThrow()

// Step 4: Append the streaming PoToken to every stream URL
val rawUrl = response.streamingData?.adaptiveFormats
    ?.filter { it.isAudio && it.isOriginal }
    ?.maxByOrNull { it.bitrate }
    ?.url ?: error("No audio URL")

val streamUrl = "${rawUrl}&pot=${poToken?.streamingDataPoToken}"
PoToken generation requires a real Android WebView. It will not work in pure JVM environments such as unit tests or server-side Kotlin. In those environments, PoTokenGenerator.getWebClientPoToken() returns null. Use a client that does not require PoTokens (such as ANDROID_VR_1_43_32) in environments without WebView support.

Build docs developers (and LLMs) love