Automating Browser Profiles with a Local API
Start a profile from a script, get back a debugging endpoint, and attach Playwright or Puppeteer to a browser that already has its identity and proxy attached.
Automating an antidetect browser is usually presented as a hard problem. It is not, provided the tool hands you the one thing your automation library already knows how to use: a debugging endpoint.
The shape of it
PowerOps runs an HTTP API bound to the loopback address. Your script asks it to start a profile; it launches the browser with that profile's identity, storage and proxy already attached, and returns the WebSocket endpoint of the running browser. Your script connects to that endpoint with Playwright, Puppeteer or anything else that speaks the DevTools protocol.
The automation library never has to know what a fingerprint is. By the time it connects, the environment is already established.
Starting a profile
curl -s -H "Authorization: Bearer $POWEROPS_API_KEY" \
http://127.0.0.1:50326/statusEvery route except the status check requires a bearer key. Starting a browser returns the endpoint to attach to:
curl -s -X POST \
-H "Authorization: Bearer $POWEROPS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"profileId": "..."}' \
http://127.0.0.1:50326/browser/startAttaching your library
import { chromium } from "playwright";
const res = await fetch("http://127.0.0.1:50326/browser/start", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.POWEROPS_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ profileId }),
});
const { ws } = await res.json();
// The profile's identity, storage and proxy are already in place.
const browser = await chromium.connectOverCDP(ws);
const [context] = browser.contexts();
const page = context.pages()[0] ?? await context.newPage();
await page.goto("https://example.com");The important line is the comment. There is no fingerprint configuration in your script, because configuring the environment is not your script's job. It belongs to the profile, and it stays the same whether the profile is opened by a person clicking a row or by a scheduled job at three in the morning.
What the API covers
- The status route is a health check, and the only one that needs no key.
- The browser routes start, stop and query the browsers currently running.
- The profile routes create, update, query and delete profiles.
- The proxy routes manage the proxy library and assign routes.
- The group routes organise profiles in bulk.
- The extension routes import and assign extensions.
How it is kept local
An API that drives browsers holding signed-in accounts deserves a word about its boundaries.
- It binds 127.0.0.1. There is no setting that makes it listen on another interface.
- A bearer key is required on every route except the status check.
- It validates the Host header and sends no CORS headers, so a web page open in your everyday browser cannot reach it.
- Secrets are write-only: profile passwords, two-factor secrets and proxy passwords are never returned by a query route.
A pattern worth copying
Query the profiles in a group, start each one, do the work, stop it. Run them a few at a time rather than all at once. The constraint is memory on your own machine, since each profile is a real browser process.
const { profiles } = await api("/profiles?groupId=" + groupId);
for (const batch of chunk(profiles, 5)) {
await Promise.all(batch.map(async (profile) => {
const { ws } = await api("/browser/start", { profileId: profile.id });
try {
await doTheWork(ws);
} finally {
await api("/browser/stop", { profileId: profile.id });
}
}));
}Stopping the profile in a `finally` matters more than it looks: closing cleanly is what writes the browser state out, and a profile killed mid-session can lose the session it just established.
Try it on your own machine
PowerOps runs the profiles described here. Every one of them keeps its own browser directory, device identity and network route, all from one desktop application.
Download for Windows