XDLogin documentation
Set up your first profiles, then drive hundreds of them from the local REST API, the Chrome DevTools Protocol, or an AI agent over MCP. Everything here works on the free plan.
From install to a running profile
XDLogin is local-first: profiles are folders on your disk, each launched as its own real Chrome process with its own fingerprint, cookies and proxy. The account only carries your plan.
-
Install and sign in
Grab the build for your platform from the download page and create a free account inside the app. On Windows, Linux and macOS, profiles launch in your installed Google Chrome, Edge, Brave or Chromium; XDLogin finds it automatically.
-
Create a profile
Click New profile, name it, and choose the device it should look like: Windows, macOS, Linux, or an Android phone such as a Pixel 7. Every fingerprint signal, from
navigator.platformand client hints to the GPU string, screen and touch points, is derived from that one device model, so nothing contradicts anything. Templates save a setup you want to repeat. -
Attach a proxy
Paste an HTTP, HTTPS, SOCKS4 or SOCKS5 proxy with its credentials. The proxy test shows the exit IP, country and latency, and WebRTC only ever exposes the proxied address. Set the profile's timezone and language to match that country yourself — the profile health check tells you when they disagree. Proxies are unmetered: bring any provider.
-
Launch and verify
Click Launch. The profile opens as a separate browser and the fingerprint is installed before the first page loads, in every tab and popup. Use the built-in Fingerprint test to see exactly what sites see.
-
Organise
Group profiles in folders, tag them, and search or filter the list. The activity log records launches, edits and deletions, and exports as CSV.
-
Cookies and backups
The cookie editor imports and exports cookies per profile. Backup and restore writes your profiles to a JSON file you control. Both work on every plan.
-
Sync across machines (Pro and above)
Cloud sync seals each profile with AES-256-GCM on your device before upload, using a passphrase that never leaves your machines; the server only stores ciphertext. Sync moves only what changed and resolves conflicts deterministically. It is off until you turn it on.
Endpoints on port 31337
Enable the API in Settings → API. It binds to 127.0.0.1:31337 (the port is configurable), answers { "success": true, "data": … } on every endpoint except health, and is available on every plan including Free. Profile ids are UUIDs: list them, or read data.id from a create call.
If you set an API key in Settings → API, every request needs it — /api/health included — as the header Authorization: Bearer <key>. Anything else, or nothing, gets 401 Unauthorized. With no key set the API is open to any program on your machine, so set one.
Liveness check: answers { "status": "ok", "version": … }. Use it to confirm the API is enabled before scripting against it.
Every profile with its id, name, device model, proxy summary, tags, folder and running state.
Create a profile. Accepts the same fields as the profile editor: name, device model, proxy, tags, folder and notes. The new profile comes back with its id in data.id.
Read, update or delete one profile. PATCH takes any subset of the create fields. DELETE removes the profile and its on-disk data.
Start the profile's browser with its fingerprint and proxy. Returns as soon as the launch is accepted.
The automation attach point for a running profile. Returns 409 until the browser has finished booting, so poll it after launch.
// 200 OK { "success": true, "data": { "wsEndpoint": "ws://127.0.0.1:49152/devtools/browser/9a4f2c9e", "httpEndpoint": "http://127.0.0.1:49152", "debuggerAddress": "127.0.0.1:49152", "port": 49152, "browser": "Chrome/139.0.0.0" } }
Close the browser and flush cookies and storage to disk.
Duplicate a profile's settings into a new profile with a fresh id and an empty session.
Your current plan and limits, and a summary of how many profiles exist and are running.
Puppeteer, Playwright, Python and cURL
Launch a profile through the API, wait for its DevTools endpoint, then attach whichever framework you already use. Selenium works the same way: pass data.debuggerAddress as Chrome's debuggerAddress option.
import puppeteer from 'puppeteer-core'; const BASE = 'http://127.0.0.1:31337'; // enable in Settings → API const id = 'YOUR-PROFILE-ID'; // from GET /api/profiles // Drop `headers` if you have not set an API key in Settings → API. const headers = { Authorization: 'Bearer YOUR-API-KEY' }; async function connectEndpoint() { // /connect answers 409 until the browser has booted for (;;) { const r = await fetch(`${BASE}/api/profiles/${id}/connect`, { headers }); if (r.status === 200) return (await r.json()).data; await new Promise(res => setTimeout(res, 500)); } } await fetch(`${BASE}/api/profiles/${id}/launch`, { method: 'POST', headers }); const data = await connectEndpoint(); const browser = await puppeteer.connect({ browserWSEndpoint: data.wsEndpoint }); const page = await browser.newPage(); await page.goto('https://whoer.net'); console.log(await page.title()); await browser.disconnect(); // leave the profile running, or: await fetch(`${BASE}/api/profiles/${id}/stop`, { method: 'POST', headers });
import { chromium } from 'playwright'; const BASE = 'http://127.0.0.1:31337'; const id = 'YOUR-PROFILE-ID'; const headers = { Authorization: 'Bearer YOUR-API-KEY' }; // omit if no key is set await fetch(`${BASE}/api/profiles/${id}/launch`, { method: 'POST', headers }); let data; while (!data) { // 409 until the browser is ready const r = await fetch(`${BASE}/api/profiles/${id}/connect`, { headers }); if (r.status === 200) data = (await r.json()).data; else await new Promise(res => setTimeout(res, 500)); } const browser = await chromium.connectOverCDP(data.wsEndpoint); const context = browser.contexts()[0]; // the profile's own context const page = context.pages()[0] ?? await context.newPage(); await page.goto('https://whoer.net'); console.log(await page.title());
import time, requests from playwright.sync_api import sync_playwright BASE = "http://127.0.0.1:31337" PROFILE = "YOUR-PROFILE-ID" HEADERS = {"Authorization": "Bearer YOUR-API-KEY"} # omit if no key is set requests.post(f"{BASE}/api/profiles/{PROFILE}/launch", headers=HEADERS) while True: # 409 until the browser is ready r = requests.get(f"{BASE}/api/profiles/{PROFILE}/connect", headers=HEADERS) if r.status_code == 200: data = r.json()["data"] break time.sleep(0.5) with sync_playwright() as p: browser = p.chromium.connect_over_cdp(data["wsEndpoint"]) page = browser.contexts[0].pages[0] page.goto("https://whoer.net") print(page.title()) # Selenium instead? chrome_options.add_experimental_option( # "debuggerAddress", data["debuggerAddress"])
# drop the -H lines if you have not set an API key AUTH="Authorization: Bearer YOUR-API-KEY" # is the API on? curl -H "$AUTH" http://127.0.0.1:31337/api/health # list profiles and pick an id curl -H "$AUTH" http://127.0.0.1:31337/api/profiles # launch it curl -H "$AUTH" -X POST http://127.0.0.1:31337/api/profiles/YOUR-PROFILE-ID/launch # poll for the DevTools endpoint (409 until ready) curl -H "$AUTH" http://127.0.0.1:31337/api/profiles/YOUR-PROFILE-ID/connect # stop it and flush cookies to disk curl -H "$AUTH" -X POST http://127.0.0.1:31337/api/profiles/YOUR-PROFILE-ID/stop
MCP server for agents
The desktop app includes a Model Context Protocol server, off by default. Turn it on in Settings → MCP: it listens on http://127.0.0.1:8931 and speaks MCP's JSON-RPC 2.0 over HTTP POST. Configure your client with the streamable HTTP transport and that URL — the older HTTP+SSE transport is not served, so a client set to SSE gets a 405. If you set an API key, send it as Authorization: Bearer <key>. Point any MCP-capable agent at it and it can run profiles the way you would.
Tools exposed to the agent, available on every plan:
- profile_list
- profile_get
- profile_create
- profile_clone
- profile_launch
- profile_stop
- profile_delete
- browser_navigate
- browser_screenshot
- fingerprint_get
- proxy_test
- cookies_export
- cookies_import
- health
- stats
Profiles are also exposed as resources, so an agent can read a profile's configuration by id before acting on it.