Bypass

Chrome X-Browser-Validation Header: Bypass Guide 2026

Chrome's X-Browser-Validation header is an undocumented integrity check that can instantly block your scraping bots. This base64-encoded SHA-1 hash verifies whether your User-Agent actually matches the browser you claim to be running.

In this guide, you'll learn how to reverse-engineer and generate valid X-Browser-Validation headers, plus multiple alternative bypass techniques when Google tightens the screws.

What is Chrome's X-Browser-Validation Header?

Chrome's X-Browser-Validation header is a base64-encoded SHA-1 hash that combines a platform-specific API key with your full User-Agent string. Google uses this fingerprint to detect user agent spoofing attempts.

Chrome sends these headers with requests to Google services:

{
  "x-browser-channel": "stable",
  "x-browser-copyright": "Copyright 2026 Google LLC. All rights reserved.",
  "x-browser-validation": "6h3XF8YcD8syi2FF2BhuE2KllQo=",
  "x-browser-year": "2026"
}

The x-browser-validation value is what catches scrapers off guard. Without matching this hash, Google services can instantly identify that you're spoofing Chrome without actually running it.

How the Validation Header Works

Understanding the hash generation is critical for bypassing it. The process is straightforward once you know the formula.

The Hash Formula

Chrome generates the validation header using this simple algorithm:

DATA = API_KEY + USER_AGENT
HASH = SHA-1(DATA)
HEADER = Base64(HASH)

Here's what happens under the hood:

  1. Chrome retrieves a platform-specific API key hardcoded in the browser binary
  2. It concatenates this key with the full User-Agent string
  3. The combined string is hashed using SHA-1
  4. The 20-byte digest is base64-encoded

Platform-Specific API Keys

Google hardcodes different API keys for each operating system. These keys were extracted from Chrome binaries through reverse engineering:

API_KEYS = {
    'Windows': 'AIzaSyA2KlwBX3mkFo30om9LUFYQhpqLoa_BNhE',
    'Linux': 'AIzaSyBqJZh-7pA44blAaAkH6490hUFOwX0KCYM',
    'macOS': 'AIzaSyDr2UxVnv_U85AbhhY8XSHSIavUW0DC-sY'
}

These keys haven't changed since the feature launched, but Google could rotate them at any time. Keep an eye on Chromium commits for updates.

Method 1: Generate Valid Headers with Python

The most direct approach is generating valid headers yourself. This method works for simple HTTP requests where TLS fingerprinting isn't an issue.

Install the Toolkit

First, grab the open-source toolkit from GitHub:

git clone https://github.com/dsekz/chrome-x-browser-validation-header.git
cd chrome-x-browser-validation-header
pip install -e .

For production environments, pin to a specific commit hash:

pip install git+https://github.com/dsekz/chrome-x-browser-validation-header.git@<commit-hash>

This prevents breaking changes from affecting your scrapers.

Generate Headers

The toolkit makes header generation trivial:

from xbv import generate_validation_header

# Use a current Chrome 140/141 User-Agent for 2026
ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36"

header_value = generate_validation_header(ua)
print(f"X-Browser-Validation: {header_value}")

The function automatically detects your platform and selects the correct API key. You can also specify the key explicitly:

# Force Windows key regardless of your actual platform
windows_key = "AIzaSyA2KlwBX3mkFo30om9LUFYQhpqLoa_BNhE"
header_value = generate_validation_header(ua, api_key=windows_key)

Manual Implementation

If you prefer not using external libraries, here's the raw implementation:

import hashlib
import base64

def generate_xbv_header(user_agent: str, platform: str = 'Windows') -> str:
    """
    Generate X-Browser-Validation header manually.
    
    Args:
        user_agent: Full Chrome User-Agent string
        platform: 'Windows', 'Linux', or 'macOS'
    
    Returns:
        Base64-encoded validation header
    """
    api_keys = {
        'Windows': 'AIzaSyA2KlwBX3mkFo30om9LUFYQhpqLoa_BNhE',
        'Linux': 'AIzaSyBqJZh-7pA44blAaAkH6490hUFOwX0KCYM',
        'macOS': 'AIzaSyDr2UxVnv_U85AbhhY8XSHSIavUW0DC-sY'
    }
    
    api_key = api_keys.get(platform, api_keys['Windows'])
    data = api_key + user_agent
    
    # SHA-1 hash then base64 encode
    sha1_hash = hashlib.sha1(data.encode()).digest()
    return base64.b64encode(sha1_hash).decode()

This code is portable and has zero dependencies beyond Python's standard library.

Complete Request Example

Here's how to use the generated headers with requests:

import requests
from xbv import generate_validation_header

# Current Chrome 141 User-Agent (update quarterly)
ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36"

headers = {
    'User-Agent': ua,
    'X-Browser-Validation': generate_validation_header(ua),
    'X-Browser-Channel': 'stable',
    'X-Browser-Year': '2026',
    'X-Browser-Copyright': 'Copyright 2026 Google LLC. All rights reserved.',
    # Client hints for Chrome 141
    'sec-ch-ua': '"Chromium";v="141", "Not(A:Brand";v="24", "Google Chrome";v="141"',
    'sec-ch-ua-mobile': '?0',
    'sec-ch-ua-platform': '"Windows"',
    'sec-ch-ua-platform-version': '"15.0.0"',
    'sec-fetch-dest': 'document',
    'sec-fetch-mode': 'navigate',
    'sec-fetch-site': 'none',
    'sec-fetch-user': '?1',
    'upgrade-insecure-requests': '1',
    'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
    'accept-language': 'en-US,en;q=0.9',
    'accept-encoding': 'gzip, deflate, br',
}

response = requests.get('https://example.com', headers=headers)
print(response.status_code)

Important: This method only handles the X-Browser-Validation header. For sites with TLS fingerprinting, you'll need Method 2.

Method 2: Use curl_cffi for TLS Fingerprint Matching

The requests library has a distinct TLS fingerprint that anti-bot systems recognize instantly. curl_cffi solves this by impersonating real browser TLS handshakes.

Why TLS Fingerprinting Matters

When your scraper initiates an HTTPS connection, it sends a "Client Hello" message containing:

  • TLS version
  • Supported cipher suites
  • TLS extensions
  • Elliptic curves

This creates a JA3/JA4 fingerprint. Python's default libraries produce fingerprints that scream "bot" to any sophisticated detection system.

Install curl_cffi

pip install curl_cffi

The library bundles pre-compiled curl-impersonate binaries for Windows, macOS, and Linux.

Basic Usage

from curl_cffi import requests
from xbv import generate_validation_header

ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36"

# impersonate="chrome" uses the latest supported Chrome fingerprint
response = requests.get(
    "https://tls.browserleaks.com/json",
    impersonate="chrome",
    headers={
        'X-Browser-Validation': generate_validation_header(ua),
        'X-Browser-Channel': 'stable',
        'X-Browser-Year': '2026',
    }
)

print(response.json())

The impersonate="chrome" parameter handles TLS fingerprint matching automatically. Your JA3 hash will match a real Chrome browser.

Pin Specific Chrome Versions

For consistent fingerprints across your scraper fleet:

# Pin to Chrome 136 fingerprint
response = requests.get(
    "https://example.com",
    impersonate="chrome136"
)

# Available versions as of late 2025:
# chrome99, chrome100, chrome101, chrome104, chrome107, chrome110,
# chrome116, chrome119, chrome120, chrome123, chrome124, chrome131,
# chrome133a, chrome136

The a suffix (like chrome133a) indicates alternative fingerprints observed from A/B testing.

Session Management with curl_cffi

Maintain cookies and sessions across requests:

from curl_cffi import requests

session = requests.Session()

# First request sets cookies
session.get(
    "https://example.com/login",
    impersonate="chrome"
)

# Subsequent requests include cookies automatically
response = session.get(
    "https://example.com/protected",
    impersonate="chrome"
)

print(session.cookies)

Async Support

For high-volume scraping:

import asyncio
from curl_cffi import AsyncSession

async def scrape_urls(urls):
    async with AsyncSession() as session:
        tasks = []
        for url in urls:
            task = session.get(url, impersonate="chrome")
            tasks.append(task)
        
        responses = await asyncio.gather(*tasks)
        return responses

urls = ["https://example.com/page1", "https://example.com/page2"]
results = asyncio.run(scrape_urls(urls))

Add Proxy Support

from curl_cffi import requests

proxies = {
    "http": "http://user:pass@proxy.example.com:8080",
    "https": "http://user:pass@proxy.example.com:8080"
}

# SOCKS5 also supported
socks_proxies = {
    "http": "socks5://user:pass@proxy.example.com:1080",
    "https": "socks5://user:pass@proxy.example.com:1080"
}

response = requests.get(
    "https://example.com",
    impersonate="chrome",
    proxies=proxies
)

Residential proxies from providers like Roundproxies significantly improve success rates. Datacenter IPs are increasingly blocklisted.

Method 3: Browser Automation with Stealth Plugins

When JavaScript execution is required, browser automation becomes necessary. However, vanilla Puppeteer and Playwright leak dozens of detectable signals.

Puppeteer Stealth (JavaScript)

Install the stealth plugin:

npm install puppeteer puppeteer-extra puppeteer-extra-plugin-stealth

Configure for maximum stealth:

const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');

puppeteer.use(StealthPlugin());

(async () => {
    const browser = await puppeteer.launch({
        headless: 'new',
        args: [
            '--no-sandbox',
            '--disable-setuid-sandbox',
            '--disable-blink-features=AutomationControlled',
            '--disable-features=IsolateOrigins,site-per-process'
        ]
    });
    
    const page = await browser.newPage();
    
    // Set viewport to common resolution
    await page.setViewport({ width: 1920, height: 1080 });
    
    // Navigate and interact
    await page.goto('https://example.com', { waitUntil: 'networkidle2' });
    
    const content = await page.content();
    console.log(content);
    
    await browser.close();
})();

The stealth plugin patches multiple detection vectors:

  • Removes navigator.webdriver property
  • Fixes chrome.runtime inconsistencies
  • Patches navigator.plugins and navigator.mimeTypes
  • Handles navigator.languages properly
  • Removes "HeadlessChrome" from User-Agent

Playwright Stealth (Python)

Install the Python stealth library:

pip install playwright playwright-stealth

Use the context manager for automatic stealth:

import asyncio
from playwright.async_api import async_playwright
from playwright_stealth import Stealth

async def main():
    async with Stealth().use_async(async_playwright()) as p:
        browser = await p.chromium.launch(headless=True)
        page = await browser.new_page()
        
        # navigator.webdriver returns None instead of True
        webdriver_status = await page.evaluate("navigator.webdriver")
        print(f"webdriver: {webdriver_status}")  # Output: None
        
        await page.goto("https://example.com")
        content = await page.content()
        print(content[:500])
        
        await browser.close()

asyncio.run(main())

Selenium Wire with Header Injection

For Selenium users who need to modify headers:

from seleniumwire import webdriver
from xbv import generate_validation_header

def interceptor(request):
    ua = request.headers.get('user-agent')
    if ua and 'Chrome' in ua:
        request.headers['X-Browser-Validation'] = generate_validation_header(ua)
        request.headers['X-Browser-Channel'] = 'stable'
        request.headers['X-Browser-Year'] = '2026'
        request.headers['X-Browser-Copyright'] = 'Copyright 2026 Google LLC. All rights reserved.'

driver = webdriver.Chrome()
driver.request_interceptor = interceptor

driver.get('https://example.com')
print(driver.page_source[:500])

driver.quit()

NoDriver: The Successor to undetected-chromedriver

NoDriver eliminates Selenium and WebDriver dependencies entirely, using direct browser communication:

pip install nodriver
import nodriver as uc

async def main():
    browser = await uc.start(
        headless=False,  # Headful mode is less detectable
        browser_args=['--disable-blink-features=AutomationControlled']
    )
    
    page = await browser.get('https://nowsecure.nl')
    
    # NoDriver provides direct CDP access
    await page.sleep(3)
    
    content = await page.get_content()
    print(content[:500])
    
    await browser.close()

if __name__ == '__main__':
    uc.loop().run_until_complete(main())

NoDriver offers better WAF resistance than undetected-chromedriver because it doesn't send the telltale Runtime.enable CDP command.

Method 4: CDP Connection to Real Chrome

The most undetectable approach: connect to a genuine Chrome instance rather than a controlled browser.

Launch Chrome with Remote Debugging

Start Chrome manually or via subprocess:

# macOS
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-profile

# Linux
google-chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-profile

# Windows
"C:\Program Files\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9222 --user-data-dir=C:\temp\chrome-profile

Connect with Playwright

import subprocess
import time
from playwright.sync_api import sync_playwright

# Launch Chrome (adjust path for your system)
chrome_process = subprocess.Popen([
    '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
    '--remote-debugging-port=9222',
    '--user-data-dir=/tmp/chrome-profile-scraping'
])

time.sleep(3)  # Wait for Chrome to start

with sync_playwright() as p:
    # Connect to existing Chrome instance
    browser = p.chromium.connect_over_cdp('http://localhost:9222')
    
    # Use existing context or create new one
    context = browser.contexts[0] if browser.contexts else browser.new_context()
    page = context.new_page()
    
    page.goto('https://example.com')
    print(page.content()[:500])
    
    # Don't close the browser - just disconnect
    browser.close()

# Optionally terminate Chrome
chrome_process.terminate()

This approach produces zero automation signals because Chrome isn't being controlled through typical automation protocols.

Docker-Based Chrome Instance

For reproducible environments:

FROM zenika/alpine-chrome:latest

RUN apk add --no-cache python3 py3-pip

# Expose debugging port
EXPOSE 9222

CMD ["chromium-browser", "--headless", "--no-sandbox", "--disable-gpu", "--remote-debugging-address=0.0.0.0", "--remote-debugging-port=9222"]
docker build -t chrome-debug .
docker run -d -p 9222:9222 chrome-debug

Connect from your scraper:

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.connect_over_cdp('http://localhost:9222')
    page = browser.new_page()
    page.goto('https://example.com')

Method 5: HTTP-Only Approach with Proper Header Ordering

For static content that doesn't require JavaScript, a pure HTTP approach with careful header ordering often succeeds.

Why Header Order Matters

Chrome sends headers in a specific order. Randomizing or alphabetizing headers raises red flags.

Use httpx with HTTP/2

import httpx
from xbv import generate_validation_header

ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36"

# HTTP/2 client for more realistic connections
client = httpx.Client(http2=True)

# Headers in Chrome's exact order
headers = [
    ('host', 'example.com'),
    ('connection', 'keep-alive'),
    ('sec-ch-ua', '"Chromium";v="141", "Not(A:Brand";v="24", "Google Chrome";v="141"'),
    ('sec-ch-ua-mobile', '?0'),
    ('sec-ch-ua-platform', '"Windows"'),
    ('upgrade-insecure-requests', '1'),
    ('user-agent', ua),
    ('x-browser-validation', generate_validation_header(ua)),
    ('x-browser-channel', 'stable'),
    ('x-browser-year', '2026'),
    ('accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8'),
    ('sec-fetch-site', 'none'),
    ('sec-fetch-mode', 'navigate'),
    ('sec-fetch-user', '?1'),
    ('sec-fetch-dest', 'document'),
    ('accept-encoding', 'gzip, deflate, br'),
    ('accept-language', 'en-US,en;q=0.9'),
]

response = client.get('https://example.com', headers=headers)
print(response.status_code)

client.close()

Build a Header Factory

Automate header generation for different scenarios:

from typing import Dict, List, Tuple
from xbv import generate_validation_header

class ChromeHeaderFactory:
    """Generate realistic Chrome headers for different versions and platforms."""
    
    VERSIONS = {
        '141': {
            'sec-ch-ua': '"Chromium";v="141", "Not(A:Brand";v="24", "Google Chrome";v="141"',
            'year': '2026'
        },
        '140': {
            'sec-ch-ua': '"Chromium";v="140", "Not(A:Brand";v="24", "Google Chrome";v="140"',
            'year': '2025'
        },
        '139': {
            'sec-ch-ua': '"Chromium";v="139", "Not(A:Brand";v="24", "Google Chrome";v="139"',
            'year': '2025'
        }
    }
    
    def __init__(self, version: str = '141', platform: str = 'Windows'):
        self.version = version
        self.platform = platform
        self.ua = self._build_user_agent()
    
    def _build_user_agent(self) -> str:
        platforms = {
            'Windows': 'Windows NT 10.0; Win64; x64',
            'macOS': 'Macintosh; Intel Mac OS X 10_15_7',
            'Linux': 'X11; Linux x86_64'
        }
        platform_str = platforms.get(self.platform, platforms['Windows'])
        return f"Mozilla/5.0 ({platform_str}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{self.version}.0.0.0 Safari/537.36"
    
    def get_headers(self) -> Dict[str, str]:
        version_data = self.VERSIONS.get(self.version, self.VERSIONS['141'])
        
        return {
            'User-Agent': self.ua,
            'X-Browser-Validation': generate_validation_header(self.ua),
            'X-Browser-Channel': 'stable',
            'X-Browser-Year': version_data['year'],
            'X-Browser-Copyright': f"Copyright {version_data['year']} Google LLC. All rights reserved.",
            'sec-ch-ua': version_data['sec-ch-ua'],
            'sec-ch-ua-mobile': '?0',
            'sec-ch-ua-platform': f'"{self.platform}"',
            'sec-fetch-dest': 'document',
            'sec-fetch-mode': 'navigate',
            'sec-fetch-site': 'none',
            'sec-fetch-user': '?1',
            'upgrade-insecure-requests': '1',
            'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
            'accept-language': 'en-US,en;q=0.9',
            'accept-encoding': 'gzip, deflate, br',
        }

# Usage
factory = ChromeHeaderFactory(version='141', platform='Windows')
headers = factory.get_headers()

import requests
response = requests.get('https://example.com', headers=headers)

Method 6: Residential Proxies with Header Injection

Some proxy providers support header injection at the network level, allowing you to modify requests without client-side code changes.

Configure Proxy Headers

import requests
from xbv import generate_validation_header

ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36"

# Residential proxy from Roundproxies or similar
proxy = {
    'http': 'http://username:password@residential.roundproxies.com:8080',
    'https': 'http://username:password@residential.roundproxies.com:8080'
}

# Some proxies support X-Proxy-Header-* prefix for injection
headers = {
    'User-Agent': ua,
    'X-Browser-Validation': generate_validation_header(ua),
    'X-Browser-Channel': 'stable',
    'X-Browser-Year': '2026',
}

response = requests.get(
    'https://example.com',
    headers=headers,
    proxies=proxy,
    timeout=30
)

print(response.status_code)

Rotate IP with Each Request

import requests
from itertools import cycle
from xbv import generate_validation_header

# List of residential proxies
proxies_list = [
    'http://user:pass@proxy1.roundproxies.com:8080',
    'http://user:pass@proxy2.roundproxies.com:8080',
    'http://user:pass@proxy3.roundproxies.com:8080',
]

proxy_cycle = cycle(proxies_list)

def scrape_with_rotation(url: str) -> str:
    ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36"
    
    proxy_url = next(proxy_cycle)
    proxy = {'http': proxy_url, 'https': proxy_url}
    
    headers = {
        'User-Agent': ua,
        'X-Browser-Validation': generate_validation_header(ua),
        'X-Browser-Channel': 'stable',
        'X-Browser-Year': '2026',
    }
    
    response = requests.get(url, headers=headers, proxies=proxy, timeout=30)
    return response.text

# Scrape multiple pages
urls = ['https://example.com/page1', 'https://example.com/page2']
results = [scrape_with_rotation(url) for url in urls]

Residential and mobile proxies from providers like Roundproxies have better IP reputation than datacenter proxies, significantly reducing blocks.

Chrome Version Reference Table 2025-2026

Keep your User-Agent strings current. Outdated versions are a major red flag.

Version Release Date User-Agent String
Chrome 141 Q4 2025 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36
Chrome 140 Q3 2025 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36
Chrome 139 Q2 2025 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36
Chrome 138 Q1 2025 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36

Mobile User-Agents

Platform User-Agent String
Android Chrome Mozilla/5.0 (Linux; Android 15; Pixel 9) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36
iOS Chrome Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/141.0.0.0 Mobile/15E148 Safari/604.1

Update frequency: Refresh your User-Agent strings quarterly to match the latest stable Chrome release.

TLS Fingerprinting: The Hidden Layer

X-Browser-Validation is just one piece of the puzzle. TLS fingerprinting catches scrapers before HTTP headers are even examined.

Understanding JA3 and JA4

JA3 creates a fingerprint from five TLS handshake fields:

  1. TLS version
  2. Cipher suites offered
  3. TLS extensions
  4. Elliptic curves
  5. EC point formats

These values are concatenated and MD5 hashed:

JA3 = MD5(TLSVersion,Ciphers,Extensions,EllipticCurves,ECFormats)

Example JA3 hash: dbe0907495f5e986a232e2405a67bed1

JA4 improves on JA3 by sorting extensions alphabetically (defeating randomization) and adding ALPN and SNI information.

Why Python requests Gets Blocked

Python's default TLS fingerprint is instantly recognizable:

# Python requests fingerprint (easily detected)
771,4867-4866-4865-49196-49200-159-52393-52392-52394-49195-49199-158-49188-49192-107-49187-49191-103-49162-49172-57-49161-49171-51-157-156-61-60-53-47-255,0-11-10-35-22-23-13-43-45-51,29-23-30-25-24,0-1-2

Compare to Chrome's fingerprint:

# Chrome fingerprint (expected by most sites)
771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513-21,29-23-24,0

Anti-bot systems maintain databases of known JA3/JA4 hashes. Any mismatch between your claimed User-Agent and TLS fingerprint triggers instant blocks.

Test Your TLS Fingerprint

Verify your fingerprint matches Chrome:

from curl_cffi import requests

response = requests.get(
    "https://tls.browserleaks.com/json",
    impersonate="chrome"
)

data = response.json()
print(f"JA3 Hash: {data.get('ja3_hash')}")
print(f"JA4 Hash: {data.get('ja4')}")

You can also check https://scrapfly.io/web-scraping-tools/ja3-fingerprint for a visual test.

Common Pitfalls to Avoid

1. Header Inconsistency

Your X-Browser headers must match your User-Agent's Chrome version.

Wrong:

# User-Agent says Chrome 141, but year is 2024
ua = "...Chrome/141.0.0.0..."
headers = {
    'X-Browser-Year': '2024'  # Mismatch!
}

Correct:

ua = "...Chrome/141.0.0.0..."
headers = {
    'X-Browser-Year': '2026'  # Chrome 141 released in 2025/2026
}

2. Missing sec-ch-ua Headers

Chrome Client Hints are increasingly checked. Missing them is suspicious.

# Include all relevant Client Hints
headers = {
    'sec-ch-ua': '"Chromium";v="141", "Not(A:Brand";v="24", "Google Chrome";v="141"',
    'sec-ch-ua-mobile': '?0',
    'sec-ch-ua-platform': '"Windows"',
    'sec-ch-ua-platform-version': '"15.0.0"',
    'sec-ch-ua-full-version-list': '"Chromium";v="141.0.0.0", "Google Chrome";v="141.0.0.0"',
}

3. TLS Fingerprint Mismatch

Generating valid X-Browser headers with Python requests still fails because the TLS fingerprint betrays you.

Solution: Use curl_cffi or browser automation.

4. Static IP with High Volume

Sending 10,000 requests from a single IP address looks automated regardless of perfect headers.

Solution: Rotate residential proxies.

5. Forgetting JavaScript Checks

Some sites verify browser consistency through JavaScript:

// Sites may check these
navigator.webdriver        // Should be undefined/false
navigator.plugins.length   // Should be > 0
navigator.languages        // Should match Accept-Language

Solution: Use stealth plugins or real browser connections.

6. Incorrect Platform Key

Using Windows API key with a macOS User-Agent:

# Wrong - key doesn't match platform in User-Agent
ua = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)..."
key = "AIzaSyA2KlwBX3mkFo30om9LUFYQhpqLoa_BNhE"  # This is Windows key!

The validation header won't match what Google expects for macOS Chrome.

Troubleshooting Guide

Problem: Still Getting Blocked Despite Valid Headers

Diagnosis Steps:

  1. Check TLS fingerprint at https://tls.browserleaks.com/json
  2. Verify all headers are present and consistent
  3. Test with a residential proxy
  4. Check if JavaScript challenges are being served

Solution: Switch to curl_cffi with impersonate="chrome" parameter.

Problem: Headers Work Initially, Then Stop

Cause: Google may have rotated API keys or updated detection.

Solution:

  1. Check the GitHub toolkit for updates
  2. Verify current Chrome version numbers
  3. Test header generation manually with known-good User-Agents

Problem: CAPTCHA Challenges Despite Everything

Cause: Behavioral analysis triggered, not just header/fingerprint checks.

Solution:

  1. Reduce request rate
  2. Add realistic delays (random 2-5 second waits)
  3. Vary navigation patterns
  4. Use residential IPs instead of datacenter

Problem: Works in Testing, Fails in Production

Cause: Often related to scale or IP reputation.

Solution:

  1. Implement exponential backoff on failures
  2. Rotate User-Agents and proxies together
  3. Monitor success rates per proxy
  4. Consider time-of-day patterns

FAQ

How often do the API keys change?

As of late 2025, Google hasn't rotated the API keys since introducing the X-Browser-Validation header. However, they could change at any time. Monitor Chromium commits and update your implementation when necessary.

Does this work for all Google services?

The X-Browser-Validation header is primarily used on Google properties. Third-party sites using Cloudflare, DataDome, or PerimeterX have their own detection methods that require different approaches.

Can I use Firefox instead to avoid this?

Yes. Firefox and Safari don't send X-Browser-Validation headers. However, you'll need to match Firefox's distinct TLS fingerprint and headers. curl_cffi supports Firefox impersonation with impersonate="firefox".

What's the detection risk of each method?

Method Detection Risk Complexity Speed
Python + XBV Toolkit High (TLS exposed) Low Fast
curl_cffi Low Low Fast
Puppeteer Stealth Medium Medium Slow
NoDriver Low Medium Medium
Real Chrome CDP Very Low High Slow

The legality depends on your jurisdiction and intended use. Accessing public data for research purposes is generally permitted. However, violating Terms of Service or accessing private data may have legal consequences. Always consult legal counsel for your specific situation.

How do I know if my fingerprint matches Chrome?

Test against these services:

  • https://tls.browserleaks.com/json
  • https://bot.sannysoft.com
  • https://pixelscan.net
  • https://browserscan.net

Compare your results against a real Chrome browser.

Next Steps

The X-Browser-Validation header is one layer in Chrome's evolving fingerprinting system. To stay ahead:

  1. Monitor Chromium commits for changes to header generation
  2. Update User-Agents quarterly to match current Chrome releases
  3. Test your fingerprint regularly against detection services
  4. Combine multiple techniques (curl_cffi + residential proxies + header factory)
  5. Consider alternative browsers when targeting non-Google sites

The cat-and-mouse game between scrapers and anti-bot systems continues. What works today may fail tomorrow. Build flexibility into your scraping infrastructure so you can adapt quickly.