Connect a script to a running profile
This guide shows you how to start a profile from a script and drive it with Puppeteer, Playwright or Selenium. By the end, your script will control a profile with its own fingerprint, proxy and saved session.
This is the desktop app’s local API. It only works on the computer running the app.
What you need
Section titled “What you need”- The ExitNode desktop app open and signed in.
- A paid plan. On the Free plan, starting a profile returns
tier_or_permission_required. - The Automation API switch on your access. See Members and roles.
- Your API token.
- The ID of the profile you want to drive.
Get your token
Section titled “Get your token”- Click Settings in the sidebar.
- Find the Local automation API card. It should say Enabled.
- Next to Token, click Copy. Show reveals it first.
- Note the Endpoint. It’s normally
http://127.0.0.1:48111. Click it to copy. - Click Test connection. A tick and listening on the port means the API is running.
The token is also saved in a file:
- On a Mac:
~/.exitnode/local_api_token - On Windows:
.exitnode\local_api_tokenin your user folder
Treat it like a password. Send it with every request:
Authorization: Bearer <token>Replace the token
Section titled “Replace the token”Click Regenerate, then confirm. Scripts using the old token stop working until you give them the new one.
If the card says Disabled
Section titled “If the card says Disabled”The card tells you to set local_api_enabled in config.json and restart the app.
Find a profile’s ID
Section titled “Find a profile’s ID”From the app: right-click the profile, click Copy, then Copy profile ID.
From your script:
curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:48111/api/profiles/allThis lists every profile you can access, with its id, name, status, folder and running. Add ?org=<id> to limit it to one organization.
Start a profile
Section titled “Start a profile”curl -X POST -H "Authorization: Bearer $TOKEN" \ http://127.0.0.1:48111/api/profiles/<ID>/startThe call waits until the browser is ready, for up to 45 seconds. It answers with:
| Field | What it’s for |
|---|---|
ok |
true if it worked. |
ws_endpoint |
The address Puppeteer and Playwright connect to. |
debugger_address |
The address Selenium connects to. |
debug_port |
The port number. |
browser_pid |
The browser’s process ID. |
already_running |
true if the profile was already open. |
The profile appears as running in the app, as if you’d clicked Start.
Connect Puppeteer
Section titled “Connect Puppeteer”const res = await fetch(`http://127.0.0.1:48111/api/profiles/${ID}/start`, { method: 'POST', headers: { Authorization: `Bearer ${TOKEN}` },}).then((r) => r.json());
const browser = await puppeteer.connect({ browserWSEndpoint: res.ws_endpoint });const page = await browser.newPage();await page.goto('https://example.com');Connect Playwright
Section titled “Connect Playwright”const { ws_endpoint } = await (await fetch(`http://127.0.0.1:48111/api/profiles/${ID}/start`, { method: 'POST', headers: { Authorization: `Bearer ${TOKEN}` },})).json();
const browser = await chromium.connectOverCDP(ws_endpoint);Connect Selenium
Section titled “Connect Selenium”Selenium uses the debugger address, not the WebSocket address.
import requestsfrom selenium import webdriver
headers = {"Authorization": f"Bearer {TOKEN}"}r = requests.post(f"http://127.0.0.1:48111/api/profiles/{ID}/start", headers=headers).json()
options = webdriver.ChromeOptions()options.add_experimental_option("debuggerAddress", r["debugger_address"])driver = webdriver.Chrome(options=options)driver.get("https://example.com")Stop the profile
Section titled “Stop the profile”curl -X POST -H "Authorization: Bearer $TOKEN" \ http://127.0.0.1:48111/api/profiles/<ID>/stopThis closes the browser and saves the session, the same as clicking Stop.
All the endpoints
Section titled “All the endpoints”| Method | Path | What it does |
|---|---|---|
GET |
/api/status |
Shows whether the app is running and signed in, its version and how many profiles are open. |
GET |
/api/profiles |
Lists profiles running now, with their connection details. |
GET |
/api/profiles/all |
Lists every profile you can access. |
POST |
/api/profiles/<ID>/start |
Starts a profile, or returns its details if it’s open. You can send { "org": "<id>" }. |
POST |
/api/profiles/<ID>/stop |
Stops a profile. |
If something goes wrong
Section titled “If something goes wrong”A failed call answers with an error code. Some add a message saying what to do.
| Code | HTTP | What to do |
|---|---|---|
unauthorized |
401 | The token is wrong or missing. Copy it again. |
app_not_signed_in |
401 | Sign in to the desktop app. |
tier_or_permission_required |
403 | Your plan or role doesn’t include the API. See Billing and plans. |
forbidden_origin |
403 | Call the API from a script, not from a web page. |
profile_not_found |
404 | Check the ID, and that you have access to the profile. |
not_active |
404 | You tried to stop a profile that isn’t running here. |
launch_failed |
409 | The profile couldn’t start. It may be in use elsewhere. |
browser_exited_or_no_binary |
502 | The browser closed while starting. Read message. |
browser_not_found |
503 | The browser isn’t installed yet. Open Settings and click Install browser. |
timeout_waiting_for_cdp |
504 | The browser took too long. Try again. |
A connection error means the app isn’t open, or the API is Disabled.
If a profile’s proxy has a problem, the call fails with browser_exited_or_no_binary, and message gives the reason. See Launch, stop and monitor profiles for what each reason means.
Stuck on something? Contact support.