[cdpfleet](https://cdpfleet.com/) / Geo testing

For localization, growth and ad teams

# One script, *every country* your users are in.

Pick the exit country with one proxy token, let the browser take its timezone and language from that IP, and check what each country really gets: the language, the prices, the cookie banner, the ads. Measured, with the mismatches a site can spot.

[The residential proxy](https://cdpfleet.com/residential-proxy)[Case: three countries](https://cdpfleet.com/docs/cases/three-countries)

-   **1 token**per country, no proxy account
-   **Tier 1–2**device-OS targeting
-   **15 / 15**Camoufox geoip checks agreed with the IP (case 74)

## Choose the country

-   **A token instead of a proxy account:** `cdpfleet-resi-country-de` exits from a residential IP in Germany; add `-session-<name>` to keep the same IP and `-lifetime-<minutes>` to say how long. The same token works in every language and in `proxy_rules` ([case 25](https://cdpfleet.com/docs/cases/residential-without-a-proxy), [residential proxy](https://cdpfleet.com/residential-proxy)).
-   **Rotating or sticky:** three lookups through a rotating token gave three different IPs; two sessions with the same session name got the same IP, even in different browsers. Use a sticky session when a check spans several pages or a city-level result matters ([case 25](https://cdpfleet.com/docs/cases/residential-without-a-proxy)).
-   **Device OS of the IP:** Tier 1 and 2 tokens can also ask for the operating system of the device behind the IP, e.g. `-os-windows`, to match a Windows persona ([resi-os-persona recipe](https://cdpfleet.com/docs/examples#resi-os-persona)).
-   **Only the requests that need it:** send the site through the residential exit and everything else through your own proxy with `proxy_rules` ([resi-for-one-host recipe](https://cdpfleet.com/docs/examples#resi-for-one-host)); residential traffic is billed per GB.
-   **Your own exits work too:** a proxy per country of your own, or on dedicated plans your own VPN server with `vpn` ([proxies](https://cdpfleet.com/proxies)).

Camoufox through a sticky German residential IP, timezone and language from that IP

 

```node
// npm install playwright@1.60.0
// Browsers run on cdpfleet, so no `npx playwright install` is needed.
import { firefox } from 'playwright';

const KEY = process.env.CDPFLEET_API_KEY;

// 1. Launch the browser
const res = await fetch('https://starter.cdpfleet.com/camoufox/session', {
  method: 'POST',
  headers: { 'x-api-key': KEY, 'content-type': 'application/json' },
  body: JSON.stringify({
    proxy: 'cdpfleet-resi-country-de-session-shop1-lifetime-30',
    geoip: true,
  }),
});
if (!res.ok) throw new Error(`launch failed: ${res.status} ${await res.text()}`);
const { wsUrl } = await res.json();

// 2. Connect and drive it
const browser = await firefox.connect(wsUrl, { headers: { 'x-api-key': KEY } });
const page = await browser.newPage();
await page.goto('https://example.com');
console.log(await page.title());
await browser.close(); // ends the session and stops billing
```

```python
# pip install playwright==1.60.0 requests
# Browsers run on cdpfleet, so no `playwright install` is needed.
import os
import requests
from playwright.sync_api import sync_playwright

KEY = os.environ["CDPFLEET_API_KEY"]

# 1. Launch the browser
res = requests.post(
    "https://starter.cdpfleet.com/camoufox/session",
    headers={"x-api-key": KEY},
    json={
        "proxy": "cdpfleet-resi-country-de-session-shop1-lifetime-30",
        "geoip": True,
    },
    timeout=60,
)
res.raise_for_status()
ws_url = res.json()["wsUrl"]

# 2. Connect and drive it
with sync_playwright() as p:
    browser = p.firefox.connect(ws_url, headers={"x-api-key": KEY})
    page = browser.new_page()
    page.goto("https://example.com")
    print(page.title())
    browser.close()  # ends the session and stops billing
```

```java
// Maven: com.microsoft.playwright:playwright:1.60.0  and  com.google.code.gson:gson:2.11.0
// Run with PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1: browsers run on cdpfleet.
import com.google.gson.JsonParser;
import com.microsoft.playwright.*;
import java.net.URI;
import java.net.http.*;
import java.util.Map;

public class Example {
  public static void main(String[] args) throws Exception {
    String key = System.getenv("CDPFLEET_API_KEY");

    // 1. Launch the browser
    String body = """
        {
          "proxy": "cdpfleet-resi-country-de-session-shop1-lifetime-30",
          "geoip": true
        }
        """;
    HttpResponse<String> res = HttpClient.newHttpClient().send(
        HttpRequest.newBuilder(URI.create("https://starter.cdpfleet.com/camoufox/session"))
            .header("x-api-key", key)
            .header("content-type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build(),
        HttpResponse.BodyHandlers.ofString());
    if (res.statusCode() != 200) {
      throw new RuntimeException("launch failed: " + res.statusCode() + " " + res.body());
    }
    String wsUrl = JsonParser.parseString(res.body()).getAsJsonObject().get("wsUrl").getAsString();

    // 2. Connect and drive it
    try (Playwright playwright = Playwright.create()) {
      Browser browser = playwright.firefox().connect(wsUrl,
          new BrowserType.ConnectOptions().setHeaders(Map.of("x-api-key", key)));
      Page page = browser.newPage();
      page.navigate("https://example.com");
      System.out.println(page.title());
      browser.close(); // ends the session and stops billing
    }
  }
}
```

```csharp
// dotnet add package Microsoft.Playwright --version 1.60.0
// Browsers run on cdpfleet, so no `playwright install` is needed.
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using Microsoft.Playwright;

var key = Environment.GetEnvironmentVariable("CDPFLEET_API_KEY")!;

// 1. Launch the browser
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("x-api-key", key);
var body = """
    {
      "proxy": "cdpfleet-resi-country-de-session-shop1-lifetime-30",
      "geoip": true
    }
    """;
var res = await http.PostAsync("https://starter.cdpfleet.com/camoufox/session",
    new StringContent(body, Encoding.UTF8, "application/json"));
if (!res.IsSuccessStatusCode)
    throw new Exception($"launch failed: {(int)res.StatusCode} {await res.Content.ReadAsStringAsync()}");
var wsUrl = (await res.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("wsUrl").GetString()!;

// 2. Connect and drive it
using var playwright = await Playwright.CreateAsync();
var browser = await playwright.Firefox.ConnectAsync(wsUrl, new BrowserTypeConnectOptions
{
    Headers = new Dictionary<string, string> { ["x-api-key"] = key },
});
var page = await browser.NewPageAsync();
await page.GotoAsync("https://example.com");
Console.WriteLine(await page.TitleAsync());
await browser.CloseAsync(); // ends the session and stops billing
```

```go
// go get github.com/playwright-community/playwright-go@v0.6000.0
//
// One-time driver setup (needs Node.js 18+): playwright-go can no longer download the
// 1.60.0 driver itself, so build it from npm and point PLAYWRIGHT_DRIVER_PATH at it:
//   mkdir -p ~/.cache/pw-driver-1.60.0 && cd ~/.cache/pw-driver-1.60.0 &&
//     curl -sL https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz | tar xz &&
//     ln -sf "$(command -v node)" node
//   export PLAYWRIGHT_DRIVER_PATH=~/.cache/pw-driver-1.60.0
// Browsers run on cdpfleet, so no browsers are downloaded (SkipInstallBrowsers).
package main

import (
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"strings"

	"github.com/playwright-community/playwright-go"
)

func main() {
	key := os.Getenv("CDPFLEET_API_KEY")

	// 1. Launch the browser
	body := `{
	  "proxy": "cdpfleet-resi-country-de-session-shop1-lifetime-30",
	  "geoip": true
	}`
	req, _ := http.NewRequest("POST", "https://starter.cdpfleet.com/camoufox/session", strings.NewReader(body))
	req.Header.Set("x-api-key", key)
	req.Header.Set("content-type", "application/json")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		msg, _ := io.ReadAll(res.Body)
		log.Fatalf("launch failed: %s %s", res.Status, msg)
	}
	var session struct {
		WsURL string `json:"wsUrl"`
	}
	if err := json.NewDecoder(res.Body).Decode(&session); err != nil {
		log.Fatal(err)
	}

	// 2. Connect and drive it (Install fetches only the Playwright driver, once)
	opts := &playwright.RunOptions{SkipInstallBrowsers: true}
	if err := playwright.Install(opts); err != nil {
		log.Fatal(err)
	}
	pw, err := playwright.Run(opts)
	if err != nil {
		log.Fatal(err)
	}
	defer pw.Stop()
	browser, err := pw.Firefox.Connect(session.WsURL, playwright.BrowserTypeConnectOptions{
		Headers: map[string]string{"x-api-key": key},
	})
	if err != nil {
		log.Fatal(err)
	}
	page, err := browser.NewPage()
	if err != nil {
		log.Fatal(err)
	}
	if _, err := page.Goto("https://example.com"); err != nil {
		log.Fatal(err)
	}
	title, _ := page.Title()
	fmt.Println(title)
	browser.Close() // ends the session and stops billing
}
```

## Make the browser agree with its IP

-   **Camoufox does it for you:** with `geoip` (on by default) its timezone, language, `Accept-Language`, Intl formats and WebRTC agreed with the exit IP in all 15 cells — New York, Berlin and Tokyo, five runs each ([case 74](https://cdpfleet.com/docs/cases/timezone-locale-ip-consistency)). It draws the language per launch the way residents are spread, so a German exit gave German in 2 of 5 launches and `en-DE`, `es-DE`, `el-DE` in the others; set `locale` when the test needs the country's main language.
-   **Chrome needs the context set:** headful Chrome reported `UTC` and `en-US` through every exit until the context got `locale` and `timezoneId` from a first lookup of the exit ([case 74](https://cdpfleet.com/docs/cases/timezone-locale-ip-consistency)). For maps and store finders add `geolocation` and the permission too — granting the permission alone left `getCurrentPosition` timing out ([case 35](https://cdpfleet.com/docs/cases/geolocation-and-permissions)).
-   **Formats follow the locale:** `de-DE` formats `1.234.567,891` and `1.10.2026, 17:30:00`, `ja-JP` `2026/10/2 0:30:00`, and the same instant shows as local time in each country ([case 15](https://cdpfleet.com/docs/cases/three-countries)).
-   **Geo-IP gets the country right, not always the city:** databases disagree about residential ranges, so check location with the service the site itself uses ([case 15](https://cdpfleet.com/docs/cases/three-countries)).

## What changes by country, measured

-   **The site you get:** from Germany all five news sites in our test showed a consent banner in four of five runs; from the US two of them never showed one, and the US visitor carried up to 291 third-party cookies without a click. One site answered 403 to the US exit in three of five runs and always loaded from Germany ([case 37](https://cdpfleet.com/docs/cases/cookie-consent-by-country)).
-   **Prices are formatted, not converted:** a euro price showed as `€49.90` to the US and Japanese personas and `49,90 €` to the German one — the currency is the site's choice ([case 15](https://cdpfleet.com/docs/cases/three-countries)).
-   **Languages and redirects:** the same URL can answer in another language or redirect to a country site; record the final URL and `document.documentElement.lang` next to the screenshot.
-   **Or just look, no code:** a [live tab](https://cdpfleet.com/functions/livetab) opens a real browser tab in the country you pick, used from your own browser — see what a site shows there without a VPN; free for 3 minutes, longer from the dashboard or [one API call](https://cdpfleet.com/docs/functions/livetab). Geo compare does several countries side by side in one call ([case 87](https://cdpfleet.com/docs/cases/geo-compare)).
-   **Ad checks:** load the page from the market you buy for, keep the session sticky while ads load, and keep a [recording](https://cdpfleet.com/recordings) of the run as evidence of what was shown.

## Phones and mobile layouts

-   **Mobile layouts on desktop browsers:** Playwright device emulation changes the user agent, viewport and touch, through the same country exit ([case 02](https://cdpfleet.com/docs/cases/mobile-emulation)).
-   **Real phone browsers:** Chrome, Samsung Internet and other browsers on hardware Android phones and iOS WebView are in early access ([mobile](https://cdpfleet.com/mobile)).

## Related

Cases, recipes, posts and comparisons about this — updated as new ones are published.

### Reference docs

The details, options and errors.

-   [Proxies](https://cdpfleet.com/docs/proxies)Own proxies, pools and proxy\_rules.
-   [Recipe: sticky US identity](https://cdpfleet.com/docs/examples#resi-sticky)The same IP for 30 minutes.
-   [Recipe: Windows persona](https://cdpfleet.com/docs/examples#resi-os-persona)Country, device OS and geoip together.
-   [Recipe: lean persona](https://cdpfleet.com/docs/examples#camoufox-lean-persona)macOS, geoip and fewer bytes.
-   [Geo testing examples](https://cdpfleet.com/docs/cases#for-geo)Examples and benchmarks for this use, filtered.

### Cases 6

Real problems solved end to end, run on production in five languages.

-   [Case 87: Geo compare: the same pages from five countries in one call, and reading the diff](https://cdpfleet.com/docs/cases/geo-compare)One `POST /v1/functions/geo` per page loads it from the US, Germany, the UK, Japan and Brazil at once and returns each country's Markdown and HTML plus a diff…[Code on GitHub](https://github.com/cdpfleet/cdpfleet/tree/main/cases/87-geo-compare)
-   [Case 74: Does the browser agree with its IP? Timezone, language and WebRTC through exits in three countries](https://cdpfleet.com/docs/cases/timezone-locale-ip-consistency)Five setups through residential exits in the US, Germany and Japan: which ones keep timezone, language, Accept-Language, Intl formats and the WebRTC address…[Code on GitHub](https://github.com/cdpfleet/cdpfleet/tree/main/cases/74-timezone-locale-ip-consistency)
-   [Case 37: Cookie banners by country: the same five sites from Germany and from the US](https://cdpfleet.com/docs/cases/cookie-consent-by-country)The Guardian, BBC, AP News, The Verge and HuffPost through a German and a US exit: which show a consent banner, which consent API they expose, what loads before…[Code on GitHub](https://github.com/cdpfleet/cdpfleet/tree/main/cases/37-cookie-consent-by-country)
-   [Case 35: A German IP with a UTC clock: making location, timezone and permissions agree](https://cdpfleet.com/docs/cases/geolocation-and-permissions)What a page learns about where you are — geolocation, its permission, timezone and language — through a German proxy, with defaults and with a Berlin context.…[Code on GitHub](https://github.com/cdpfleet/cdpfleet/tree/main/cases/35-geolocation-and-permissions)
-   [Case 25: No proxy of your own: rotating, country and sticky residential IPs with one token](https://cdpfleet.com/docs/cases/residential-without-a-proxy)Four launches on the cdpfleet residential proxy — any country, Germany, and one sticky US identity carried from one browser session to the next — with nothing but a…[Code on GitHub](https://github.com/cdpfleet/cdpfleet/tree/main/cases/25-residential-without-a-proxy)
-   [Case 15: Three countries, one script — and reading values the way the site does](https://cdpfleet.com/docs/cases/three-countries)Camoufox personas for the US, Germany and Japan: timezone, language, local dates and numbers, geolocation, all consistent with the exit country.[Code on GitHub](https://github.com/cdpfleet/cdpfleet/tree/main/cases/15-three-countries)

## Read next

[Residential proxy →](https://cdpfleet.com/residential-proxy)[Proxies →](https://cdpfleet.com/proxies)[Web scraping →](https://cdpfleet.com/web-scraping)[Cross-browser testing →](https://cdpfleet.com/cross-browser-testing)

---
Source: https://cdpfleet.com/geo-testing · Markdown: https://cdpfleet.com/geo-testing.md · All docs for AI tools: https://cdpfleet.com/llms.txt
