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
| Method | CDP call | What shifts |
|---|---|---|
set_geolocation(lat, lon, accuracy=None) | Emulation.setGeolocationOverride | navigator.geolocation.getCurrentPosition |
set_locale(locale) | Emulation.setLocaleOverride | Intl, navigator.language, Accept-Language |
set_timezone(timezone_id) | Emulation.setTimezoneOverride | Date, 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 asynciofrom 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 Parisawait 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
examples/geolocation_teleport.py— move one browser through four cities and verify the spoof three ways.examples/geolocation_pizza_search.py— run a “pizza near me” query per city with isolated cookie jars.
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