Skip to content
Cascading Labs QScrape VoidCrawl Yosoi

Geolocation & Emulation

A page reveals where it thinks you are through three independent signals: the geolocation API, the JS locale plus Accept-Language header, and the timezone. VoidCrawl can override all three per tab through Chrome’s CDP Emulation domain, so the same browser can present as a visitor in New York, Paris, or Tokyo without a proxy.

These are low-level Page / PooledTab methods. They are part of the Python SDK and the Rust core, not the MCP tool surface.

The three levers

MethodCDP callWhat shifts
set_geolocation(lat, lon, accuracy=None)Emulation.setGeolocationOverridenavigator.geolocation.getCurrentPosition
set_locale(locale)Emulation.setLocaleOverrideIntl, navigator.language, Accept-Language
set_timezone(timezone_id)Emulation.setTimezoneOverrideDate, Intl.DateTimeFormat().resolvedOptions().timeZone

Geolocation

set_geolocation takes latitude and longitude, with an optional accuracy in metres that defaults to 50. VoidCrawl grants the geolocation permission for you first, because headless Chrome auto-denies navigator.geolocation otherwise and the override would never be read.

import asyncio
from voidcrawl import BrowserConfig, BrowserSession
async def main() -> None:
async with BrowserSession(BrowserConfig()) as browser:
page = await browser.new_page("https://example.com") # secure context
await page.set_geolocation(40.758, -73.9855) # Times Square
coords = await page.eval_js(
"new Promise(r => navigator.geolocation.getCurrentPosition("
"p => r([p.coords.latitude, p.coords.longitude])))"
)
print(coords) # [40.758, -73.9855]
await page.close()
asyncio.run(main())

Locale

set_locale takes a BCP 47 tag such as "fr-FR". It drives both Intl resolution and the Accept-Language request header, which is the lever that actually shifts region-aware content like localized pricing or Google Maps results.

await page.set_locale("fr-FR")
lang = await page.eval_js("Intl.DateTimeFormat().resolvedOptions().locale")
print(lang) # "fr-FR"

Timezone

set_timezone takes an IANA time zone id like "America/New_York", not a UTC offset. It rewrites Date and Intl for the tab, so any server-side probe that reads the rendered clock sees the spoofed zone.

await page.set_timezone("America/New_York")
tz = await page.eval_js("Intl.DateTimeFormat().resolvedOptions().timeZone")
print(tz) # "America/New_York"

Teleporting a tab

Region-aware sites cross-check signals, so a believable visitor sets all three together. Apply them before navigating to the target so the first request already carries the right Accept-Language.

async def teleport(page, lat, lon, locale, tz):
await page.set_locale(locale)
await page.set_timezone(tz)
await page.set_geolocation(lat, lon)
# Present as a visitor in Paris
await teleport(page, 48.8566, 2.3522, "fr-FR", "Europe/Paris")
await page.goto("https://your-target.example")

When per-tab state resets

Overrides are per-tab CDP state. In the pool, a recycled tab keeps whatever emulation it last held, but a fresh acquire() may hand you a different tab. If you need deterministic geo per scrape, re-apply the overrides right after each acquire, or use a dedicated BrowserSession per region to keep cookie jars isolated as well.

Examples

FAQs

Does set_geolocation work on any page?

navigator.geolocation reads require a secure context, so https:// pages or localhost, not data: URLs. The override is applied at the CDP layer and the geolocation permission is granted for you, but the page must still call navigator.geolocation.getCurrentPosition for the spoofed coordinates to surface.

I set a New York geolocation but Google Maps still shows my real city. Why?

Many “near me” services geolocate from your request IP and Accept-Language header, not the browser geolocation API. set_geolocation only moves the navigator.geolocation reading. To shift IP-driven results you also need set_locale, and often a proxy that exits in the target region.

What format does set_timezone expect?

An IANA time zone id such as America/New_York or Europe/Paris, not a UTC offset. It overrides Date and Intl for the tab, so any server probe that reads the rendered clock sees the spoofed zone.

Do these overrides survive tab recycling in the pool?

No. Overrides are per-tab CDP state. When the pool recycles a tab back to about:blank the emulation persists on that tab object, but a fresh acquire() may hand you a different tab. Re-apply the overrides after each acquire if you need them deterministic.

References

CDP Emulation domain. Chrome DevTools Protocol. Reference for setGeolocationOverride, setLocaleOverride, and setTimezoneOverride. https://chromedevtools.github.io/devtools-protocol/tot/Emulation/

Geolocation API. W3C. Specification for navigator.geolocation and secure-context requirements. https://www.w3.org/TR/geolocation/

Time Zone Database. IANA. Canonical list of time zone ids accepted by setTimezoneOverride. https://www.iana.org/time-zones

BCP 47. IETF. Tags for identifying languages, the format set_locale expects. https://www.rfc-editor.org/info/bcp47