cdpfleet / 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.
- 1 tokenper country, no proxy account
- Tier 1–2device-OS targeting
- 15 / 15Camoufox geoip checks agreed with the IP (case 74)
Choose the country
- A token instead of a proxy account:
cdpfleet-resi-country-deexits 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 inproxy_rules(case 25, 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).
- 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). - 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); 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).
Camoufox through a sticky German residential IP, timezone and language from that IP
// npm install [email protected]
// 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
# 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
// 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
}
}
}
// 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 get github.com/playwright-community/[email protected]
//
// 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). It draws the language per launch the way residents are spread, so a German exit gave German in 2 of 5 launches anden-DE,es-DE,el-DEin the others; setlocalewhen the test needs the country's main language. - Chrome needs the context set: headful Chrome reported
UTCanden-USthrough every exit until the context gotlocaleandtimezoneIdfrom a first lookup of the exit (case 74). For maps and store finders addgeolocationand the permission too — granting the permission alone leftgetCurrentPositiontiming out (case 35). - Formats follow the locale:
de-DEformats1.234.567,891and1.10.2026, 17:30:00,ja-JP2026/10/2 0:30:00, and the same instant shows as local time in each country (case 15). - 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).
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).
- Prices are formatted, not converted: a euro price showed as
€49.90to the US and Japanese personas and49,90 €to the German one — the currency is the site's choice (case 15). - Languages and redirects: the same URL can answer in another language or redirect to a country site; record the final URL and
document.documentElement.langnext to the screenshot. - Or just look, no code: a live tab 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. Geo compare does several countries side by side in one call (case 87).
- Ad checks: load the page from the market you buy for, keep the session sticky while ads load, and keep a recording of the run as evidence of what was shown.