How Do You Drive GenLogin Profiles From Code?
The GenLogin API is a local HTTP service the desktop app exposes at http://localhost:55550/profiles. You list, start and stop profiles with plain HTTP calls, and each start returns a wsEndpoint that Selenium or Puppeteer attaches to over the DevTools protocol. The script then drives the running profile with its own cookies, proxy and fingerprint settings. Three things break most first attempts: the token, the 20-profile default page size, and stopping a profile before closing the browser.
📌 TL;DR Executive Summary
- Core Takeaway: The GenLogin API is two things — a JSON control plane on port 55550 and a per-profile DevTools endpoint. List, start, attach, work, close, stop.
- Key Risk/Challenge: Auth styles conflict between the current reference (Bearer token) and the 2022–2023 wrapper examples (empty key), and a profile stays in a running state if you stop it before closing the browser.
- Recommended Solution: Call the endpoints directly with requests or fetch, read the token from an environment variable, parse host and port out of
wsEndpointinstead of hardcoding localhost, and always stop in a finally block.
What the GenLogin API Actually Exposes
GenLogin’s Profile API reference documents an HTTP service the desktop app runs on your own machine. The base path is http://localhost:55550/profiles, and the lifecycle it covers is list, create, run, stop and delete. Because the service binds locally, whatever calls it must run on the same computer as the app. A script on a remote server cannot reach your laptop’s port 55550.
Control plane vs. session plane
Two layers sit behind that one URL. The control plane is ordinary JSON over HTTP: you send a request and get back a success flag plus a data object. The session plane appears only after a profile runs, when the response carries a wsEndpoint string. Selenium and Puppeteer never talk to port 55550 — they connect to that second endpoint and drive Chromium directly.
That split explains the most common ordering bug. Call stop while a CDP client is still attached and the profile can stay marked as running, so the next start for that profile fails or hangs. Close the browser connection first, then stop. The wrapper libraries do exactly that: the JavaScript SDK closes the browser in code before calling stopProfile().
GenLogin’s 3.0 documentation also recommends running automation separately per profile group, which is a sensible scheduling rule — one worker per group keeps a stuck profile from blocking the rest. If you want the same list/start/attach/stop shape mapped across other tools, our browser profile management API tutorial walks through it end to end.
Prerequisites
Fix the environment before you write a line of automation, because half the errors in this tutorial are setup errors wearing a disguise.
- GenLogin desktop app installed and signed in on Windows or macOS — the documented Python wrapper lists both. The app stays open for the whole run; it is the server.
- A profile you own or are authorised to manage. Every example below drives an account you already control, which is what platform terms expect.
- Python 3.9+ with
pip install requests selenium. - Node 18+ with
npm install puppeteer, if you attach Puppeteer instead of Selenium. - Your token exported as
GENLOGIN_TOKENin the shell that runs the script. - A target URL you own — a staging login page or an internal dashboard — for the smoke test.
How Send.win Helps With Genlogin Api
Send.win is a browser profile manager for professionals and teams: every account gets its own isolated browser profile.
- Isolated profiles – separate cookies, storage and sessions per profile
- Fingerprint protection – each profile keeps its own consistent browser fingerprint, so profiles never share one browser identity
- Desktop app + cloud browser – native app for Windows, macOS and Linux, or run profiles in the cloud with no install
- Residential proxies included – on Pro and Team, with timezone and locale matched to the proxy
- Team sharing – share a signed-in profile with a teammate without sharing the password
Try the free cloud demo — no install — or download the desktop app. Paid plans start at $19/month, and your first card purchase is covered by a 7-day money-back guarantee (see pricing).
Vendor marketing in this category leans toward bulk data collection. This tutorial stays on test flows: confirming a profile opens the session it saved, checking a page loads through the right proxy, verifying a build in a specific region. That is what the API is genuinely good at, and it is the work that does not collide with anyone’s terms of service.
Step 1 — Check That the Local Service Is Listening
Before debugging tokens or profile IDs, confirm something answers on 55550. Ten seconds here saves an hour of blaming your code.
import requests
BASE = "http://localhost:55550/profiles"
try:
r = requests.get(BASE, timeout=5)
print(r.status_code)
print(r.text[:300])
except requests.exceptions.ConnectionError:
print("Nothing is listening on 55550. Start the GenLogin app and try again.")
Paste the same URL into a browser tab if you prefer. A connection error means the app is closed, still starting, or the local API is switched off in your build’s settings. A 401 or 403 means the service is alive and rejecting the request at the auth layer — a much better problem, because the rest of the pipeline works.
Step 2 — Load the Token From the Environment
The docs and the community wrappers disagree about authentication. The current reference shows OAuth2 password authorization with a Bearer token on each request; the Python wrapper examples from 2023 pass an empty key. Both circulate, and which one your build enforces depends on its version. Send the header when you hold a token, and treat the empty-key style as legacy.
import os
import requests
TOKEN = os.environ.get("GENLOGIN_TOKEN", "")
SESSION = requests.Session()
SESSION.headers.update({
"Authorization": f"Bearer {TOKEN}",
"Content-Type": "application/json",
})
print("token loaded:", bool(TOKEN))
A requests.Session keeps the header on every call and reuses the TCP connection, which matters when you loop over dozens of profiles. If you have handled tokens for a comparable tool, the GoLogin API token setup follows the same pattern: fetch once, keep it out of source control, refresh when calls start returning 401. Never hardcode the value, and never commit a script with a real token in it.
Step 3 — List Profiles With Pagination
The list endpoint takes offset and limit. The limit is capped at 100 and defaults to 20, so a bare call quietly returns a fraction of your profiles. The response wraps everything in success plus data, with a lst_profile array and a pagination object inside.
BASE = "http://localhost:55550/profiles"
def list_profiles(offset=0, limit=100):
r = SESSION.get(BASE, params={"offset": offset, "limit": limit}, timeout=30)
r.raise_for_status()
payload = r.json()
data = payload.get("data", payload)
return data.get("lst_profile", []), data.get("pagination", {})
def all_profiles(page_size=100):
offset = 0
while True:
batch, pagination = list_profiles(offset, page_size)
yield from batch
if len(batch) < page_size:
break
offset += page_size
first_page, meta = list_profiles(0, 20)
print(len(first_page), meta)
for profile in all_profiles():
print(profile.get("id"), profile.get("name"))
Two defensive habits are built into that snippet. payload.get("data", payload) copes with builds that return the arrays at the top level. The loop stops when a page comes back shorter than the requested size instead of trusting a total count that may not be present. The profile list reference shows the sample response with both shapes — match your parser against it before you build anything on top.
Field names inside lst_profile vary by build: id, profile_id and name appear in different combinations. Print one object first and map the keys you actually receive.
Step 4 — Start a Profile and Capture the wsEndpoint
Starting a profile turns a saved configuration into a running Chromium process, and the part you need is the wsEndpoint. In the JavaScript wrapper, runProfile returns an object with id, user_id and profile_data, with the endpoint nested under data — which is why the parsing below checks two key spellings. Keep the paths as constants: they have shifted between 2.x and 3.0, so confirm the exact path and verb on the reference page for your build.
BASE = "http://localhost:55550/profiles"
# Verify these two paths against the Profile API reference for your build.
START_PATH = BASE + "/start/{pid}"
STOP_PATH = BASE + "/stop/{pid}"
def start_profile(profile_id):
r = SESSION.get(START_PATH.format(pid=profile_id), timeout=180)
r.raise_for_status()
data = r.json().get("data", {})
ws_endpoint = data.get("wsEndpoint") or data.get("ws_endpoint")
if not ws_endpoint:
raise RuntimeError(f"No wsEndpoint for profile {profile_id}: {data}")
return ws_endpoint
profiles, _ = list_profiles(0, 1)
first = profiles[0]
ws = start_profile(first.get("id") or first.get("profile_id"))
print(ws)
The 180-second timeout is deliberate. A profile behind a proxy has to negotiate that proxy before the browser is usable, and a cold exit node can add a long pause to the start. A 10-second timeout turns a working setup into a mystery failure. If the call returns success: false rather than raising, read the message field — it usually names the dead proxy or the port clash.
Step 5 — Attach Selenium Through debuggerAddress
Selenium launches nothing here. Setting debugger_address tells it to attach to a browser that is already running, over the DevTools protocol. That is the mechanism the Python wrapper uses, and it is why your script inherits the profile’s cookies and proxy instead of opening a clean browser.
from urllib.parse import urlparse
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
def attach_selenium(ws_endpoint):
parsed = urlparse(ws_endpoint)
host = parsed.hostname or "127.0.0.1"
port = parsed.port
if port is None:
raise ValueError(f"wsEndpoint has no port: {ws_endpoint}")
options = Options()
options.debugger_address = f"{host}:{port}"
return webdriver.Chrome(options=options)
driver = attach_selenium(ws)
driver.get("https://example.com/")
print(driver.title)
driver.quit()
The urlparse step is the part most tutorials skip. A wsEndpoint looks like ws://127.0.0.1:53412/devtools/browser/<uuid>, and Selenium wants only 127.0.0.1:53412. Hardcoding localhost works until the machine resolves it to IPv6 first while the profile listens on IPv4 — then you get “cannot connect to chrome” while the browser is plainly running. Parse the host the API gave you and let the string speak for itself.
If your team already runs Selenium against another profile manager, the attach code barely changes — the IxBrowser API with Python walkthrough uses the same debugger-address pattern on a different local port, which makes a useful side-by-side when you standardise on one tool.
Step 6 — Attach Puppeteer Through browserWSEndpoint
On the Node side you skip the host-and-port dance entirely, because puppeteer.connect() takes the full wsEndpoint string. The lib_Genlogin_js wrapper passes ignoreHTTPSErrors: true and defaultViewport: false, and both are worth copying: the first stops a self-signed internal certificate from breaking your test page, the second leaves the profile’s real window size alone.
const puppeteer = require('puppeteer');
async function attach(wsEndpoint) {
return puppeteer.connect({
browserWSEndpoint: wsEndpoint,
ignoreHTTPSErrors: true,
defaultViewport: false,
});
}
async function run(wsEndpoint) {
const browser = await attach(wsEndpoint);
const openPages = await browser.pages();
const page = openPages.length ? openPages[0] : await browser.newPage();
await page.goto('https://example.com/', { waitUntil: 'domcontentloaded' });
console.log(await page.title());
await browser.close();
}
const wsEndpoint = process.argv[2];
if (!wsEndpoint) {
console.error('Usage: node attach.js ws://127.0.0.1:PORT/devtools/browser/...');
process.exit(1);
}
run(wsEndpoint).catch(function (err) {
console.error(err);
process.exit(1);
});
browser.pages() returns the tabs the profile already had open. Reusing the first one keeps you inside the session the profile was saved with, which is usually what a login check needs. browser.close() shuts the Chromium process down; the stop call in the next step then clears the profile’s running flag. Do those in that order.
Step 7 — Stop the Profile in a finally Block
Stopping is the step people skip, and a skipped stop is what makes tomorrow’s run fail. Wrap the work so the stop fires whether the test passed, failed or threw.
def stop_profile(profile_id):
r = SESSION.get(STOP_PATH.format(pid=profile_id), timeout=60)
r.raise_for_status()
return r.json()
driver.quit() # release the CDP client first
stop_profile(profile_id) # then clear the running flag
driver.quit() closes the browser behind the debugger session, and only then should the stop endpoint fire. Reverse the order and the CDP client points at a browser the app is tearing down, so the profile can stay in a running state until you restart the desktop app. A one-second pause between the two calls gives the app time to release the profile’s port before the next start.
Version Drift: Why 2023 Tutorials Break on 3.0
GenLogin 3.0 phased out every version below 2.3 in February 2025, changing the technology and interface around automation. The reference pages now carry sample responses dated January 2026, including the OAuth2 token flow. The two community wrappers, lib_Genlogin_py and lib_Genlogin_js, date from 2023, and nothing in their repositories guarantees they match the current build. That is the strongest argument for calling the HTTP endpoints directly: paths and methods survive UI redesigns better than a wrapper’s internal assumptions.
When a 2023 snippet returns a 404 or a 405, the cause is usually a path or verb that changed, not a bug in your code. Check the current reference rather than patching around the error. Our GenLogin browser review covers what the tool is for and where the cost lands, which is worth reading before you build a workflow on top of it.
Tool choice matters here too. API-first managers hand you scripted control and put all profile hygiene on your code. Send.win goes the other way: Sendwin Browser and the cloud browser are built for people working inside profiles by hand, so nothing about a profile can be driven from code. What it does instead is sharing — a paid teammate opens a profile you shared and it is already signed in, with that profile’s own cookies, proxy and fingerprint settings.
Common Errors and What They Mean
| Symptom | Likely cause | Fix |
|---|---|---|
| Connection refused on 127.0.0.1:55550 | App closed, still starting, or local API disabled in settings | Open the desktop app, confirm the port in settings, retry |
| 401 or 403 on every call | Missing or expired token | Generate a fresh token, re-export GENLOGIN_TOKEN; the legacy empty-key examples no longer match |
| 404 or 405 on start/stop | Path or HTTP verb changed between 2.x and 3.0 | Check the current reference page for the endpoint you need |
success: false and no wsEndpoint |
Dead proxy, locked profile, or a port clash | Read the message field, fix the proxy, close any open copy of that profile |
| Selenium: cannot connect to chrome at 127.0.0.1:PORT | Hardcoded localhost, or the profile was already stopped | Parse host and port from wsEndpoint; start the profile immediately before attaching |
| session not created: driver supports a different Chrome version | ChromeDriver does not match the bundled Chromium build | Let Selenium Manager resolve the driver, or pin a matching version |
| Profile stuck in a running state | Stop called before the browser was closed | Quit the driver, pause, then stop — inside a finally block |
| Puppeteer protocol error on connect | Puppeteer version older than the browser it attaches to | Update Puppeteer and retry against the same wsEndpoint |
Most of these failures are cheaper to diagnose from the message field than from the status code. Log the whole JSON body for the first few runs, then tighten the logging once the happy path is stable.
The Full Script, End to End
Everything combined into one runnable file: it lists every profile, starts each one, attaches Selenium, loads a page you own, prints the title, then closes and stops in the right order — with both cleanup calls in a finally so a failed test never leaves a profile running.
import os
import time
from urllib.parse import urlparse
import requests
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
BASE = "http://localhost:55550/profiles"
TARGET_URL = "https://example.com/" # a page you own and are authorised to test
START_PATH = BASE + "/start/{pid}" # verify both paths in the current reference
STOP_PATH = BASE + "/stop/{pid}"
SESSION = requests.Session()
SESSION.headers.update({
"Authorization": "Bearer " + os.environ.get("GENLOGIN_TOKEN", ""),
"Content-Type": "application/json",
})
def list_profiles(offset=0, limit=100):
r = SESSION.get(BASE, params={"offset": offset, "limit": limit}, timeout=30)
r.raise_for_status()
payload = r.json()
data = payload.get("data", payload)
return data.get("lst_profile", []), data.get("pagination", {})
def all_profiles(page_size=100):
offset = 0
while True:
batch, _ = list_profiles(offset, page_size)
yield from batch
if len(batch) < page_size:
break
offset += page_size
def start_profile(profile_id):
r = SESSION.get(START_PATH.format(pid=profile_id), timeout=180)
r.raise_for_status()
data = r.json().get("data", {})
ws_endpoint = data.get("wsEndpoint") or data.get("ws_endpoint")
if not ws_endpoint:
raise RuntimeError(f"No wsEndpoint for profile {profile_id}: {data}")
return ws_endpoint
def stop_profile(profile_id):
r = SESSION.get(STOP_PATH.format(pid=profile_id), timeout=60)
r.raise_for_status()
return r.json()
def attach_selenium(ws_endpoint):
parsed = urlparse(ws_endpoint)
host = parsed.hostname or "127.0.0.1"
port = parsed.port
if port is None:
raise ValueError(f"wsEndpoint has no port: {ws_endpoint}")
options = Options()
options.debugger_address = f"{host}:{port}"
return webdriver.Chrome(options=options)
def smoke_test(profile_id):
ws_endpoint = start_profile(profile_id)
driver = None
try:
driver = attach_selenium(ws_endpoint)
driver.get(TARGET_URL)
WebDriverWait(driver, 20).until(
lambda d: d.execute_script("return document.readyState") == "complete"
)
print(profile_id, "->", driver.title)
except Exception as exc:
print(profile_id, "FAILED:", exc)
finally:
if driver is not None:
try:
driver.quit()
except Exception:
pass
try:
stop_profile(profile_id)
except Exception as exc:
print(profile_id, "stop failed:", exc)
time.sleep(1)
if __name__ == "__main__":
for profile in all_profiles():
profile_id = profile.get("id") or profile.get("profile_id")
if profile_id:
smoke_test(profile_id)
Run it with python smoke.py and read the output as a status board: one line per profile, with the page title confirming the session loaded. Two practical limits apply. Keep the desktop app running for the whole job, because it is the server. And scope the profile set before pointing this at a hundred profiles — filter by name or group first, then widen once you trust the output.
🏆 Send.win Verdict
Scripted control and shared access are different problems. GenLogin answers the first with a local API, at the cost of a token, pagination quirks, path changes between versions and a stop call you must never forget. Send.win solves the second: each isolated profile carries its own cookies, storage, sessions, fingerprint settings and proxy, in the desktop app or in the cloud browser running on Send.win’s server in Helsinki, and a profile shared with a paid teammate opens already signed in — no password changes hands. Send.win is not built for the first job at all: profiles are opened and worked in by hand, so if your work is code driving browsers, it is the wrong pick. It is the right pick if your job is people working inside client accounts without swapping credentials.
See Send.win plans — Pro at $19/mo or $83.88/yr with 150 profiles, 6 seats and 5 GB of proxy bandwidth a month; Team at $49/mo or $251.88/yr with 500 profiles and 16 seats; or try the free cloud demo in a browser tab, 10 minutes a day with nothing to install.
Frequently Asked Questions
How do I get a GenLogin API token?
The current reference documents OAuth2 password authorization, which issues a Bearer token you attach to every request. Check that page for the exact issuance step in your build, then export the value as an environment variable rather than pasting it into source files. Older wrapper examples pass an empty key, which reflects pre-3.0 behaviour and may no longer be accepted.
Do I need the desktop app running to use the API?
Yes. The Profile API is served by the desktop app on your own machine, so nothing answers on port 55550 when the app is closed. That also means the script and the app have to sit on the same computer — a remote server cannot reach your local port.
Why does localhost:55550 refuse the connection?
Either the app is closed or still starting, the local API is disabled in your build’s settings, or another process is holding the port. Check in a browser tab first: JSON means the service is alive, a connection error means it is not running at all.
How do I connect Selenium to a GenLogin profile?
Start the profile through the API, read the wsEndpoint from the response, and set debugger_address to the host and port inside that string. Selenium then attaches to the running browser instead of launching a new one, so the session keeps the profile’s cookies and proxy.
Does the GenLogin API work with Puppeteer or Playwright?
Puppeteer is documented: you pass the wsEndpoint to puppeteer.connect() as browserWSEndpoint. Playwright is not covered by GenLogin’s documentation, so treat a CDP-based connection as unsupported and test it thoroughly before depending on it. The safe bet is the tool the vendor actually documents.
How do I list all GenLogin profiles with the API?
Call the list endpoint with offset and limit, remembering that the limit caps at 100 and defaults to 20. Read the lst_profile array under data, then keep requesting the next offset until a page returns fewer entries than your page size.
How do I stop a profile after automation finishes?
Close the browser first — driver.quit() in Selenium, browser.close() in Puppeteer — and only then call the stop endpoint. Put both calls in a finally block so an exception mid-test cannot leave the profile stuck in a running state.
Is the GenLogin API free?
The Profile API ships with the desktop app rather than as a separately sold developer product, so the cost follows whichever plan you hold. Confirm the current plan and any API conditions on the vendor’s pricing page before you design a workflow around it.