How Does Selenium Attach to a Multilogin Profile?
You connect Selenium to a Multilogin profile by signing in to the Multilogin API, starting the profile with automation_type=selenium, reading the local port out of the JSON response, and passing that port to webdriver.Remote(). Multilogin Selenium automation works this way because Selenium 4 speaks only the W3C WebDriver protocol, and the launcher exposes each running profile as an endpoint that conforms to it. Get the port wrong, or leave the desktop app closed, and nothing starts at all.
📌 TL;DR Executive Summary
- Core Takeaway: A working Multilogin Selenium run is four calls: sign in, start the profile with
automation_type=selenium, attachwebdriver.Remote()to127.0.0.1:PORT, and stop it in afinallyblock so the next run can start. - Key Risk/Challenge: The desktop app must stay open, the port has to be read fresh from each start response, and long runs die from stale elements, click overlays and stray
driver.quit()calls rather than from the fingerprint layer. - Recommended Solution: Re-read the port every run, use explicit waits with a re-locate loop, wrap teardown in
try/finally— or run the same pattern on Send.win, where the local Automation API ships with the Team plan and profiles come with built-in residential proxies.
What You Need Before the First Run
The official flow assumes a working environment, not a fresh account you are still setting up. Every missing item below surfaces later as an error that points somewhere else.
- The Multilogin desktop app installed and running. Multilogin’s own API documentation lists a closed app as the first cause of a profile that refuses to start. The API controls the local app; it does not replace it.
- One saved browser profile, already created in the interface. The start call needs a profile ID and the ID of the folder it lives in.
- Both IDs copied from the app. They are visible in the Multilogin interface and cannot be guessed.
- Python 3.9 or newer in a virtual environment you can throw away, plus
requestsandselenium4.x. - Account email and password available to the script, ideally through environment variables rather than string literals.
- A proxy already attached to the profile. Fix this in the app first; the fingerprint and the exit IP need to agree before you log into anything.
- Realistic expectations about plan limits. Multilogin’s live pricing page lists a permanent Free tier with five cloud-only browser profiles and API access, while Pro tiers cover 10, 20 and 50 local profiles. If you are weighing whether the licensing fits your workload, it is worth reading a breakdown of Multilogin alternatives compared alongside the official numbers, because profile counts, not features, usually drive the bill.
Step 1 — Install the Libraries and Move Credentials Out of the Script
Two dependencies cover the whole Multilogin Selenium setup. Install them inside a virtual environment so a Selenium upgrade in one project cannot break another.
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install requests selenium
Then keep secrets out of the file. Environment variables survive a git push; string literals do not.
import os
EMAIL = os.environ["MLX_EMAIL"]
PASSWORD = os.environ["MLX_PASSWORD"]
FOLDER_ID = os.environ["MLX_FOLDER_ID"]
PROFILE_ID = os.environ["MLX_PROFILE_ID"]
Export them in your shell before running anything. On a server, set them through the process manager instead, so the values stay out of a shared shell profile.
Step 2 — Sign In and Keep the Bearer Token
Authentication is a single POST to the API base. Multilogin’s documented example sends the email plus an MD5 hash of the password and reads the bearer token out of the response, then reuses it in the Authorization header of every later call.
import hashlib
import requests
API_BASE = "https://api.multilogin.com"
def sign_in(email: str, password: str) -> str:
payload = {
"email": email,
"password": hashlib.md5(password.encode()).hexdigest(),
}
response = requests.post(f"{API_BASE}/user/signin", json=payload, timeout=30)
response.raise_for_status()
return response.json()["data"]["token"]
Two details matter. First, raise_for_status() turns a silent 401 into an exception you can read; without it you index into ["data"]["token"] on an error payload and get a KeyError that tells you nothing. Second, sign in at the top of every job instead of caching a token in a file. An expired token produces the same “profile does not start” symptom as a closed app.
The MD5 step is request shaping, not security advice — it is simply the input format that endpoint expects.
Step 3 — Start the Profile with automation_type=selenium
Starting a profile is a GET against the launcher, scoped to the folder and profile IDs, with the automation type passed as a query parameter. The launcher runs on a separate host from the main API, and the v2 path is the one Multilogin marks as recommended for launching profiles.
LAUNCHER = "https://launcher.mlx.yt:45001/api/v2"
def start_profile(token: str, folder_id: str, profile_id: str) -> int:
headers = {"Authorization": f"Bearer {token}"}
url = f"{LAUNCHER}/profile/f/{folder_id}/p/{profile_id}/start"
response = requests.get(
url,
params={"automation_type": "selenium"},
headers=headers,
timeout=120,
)
response.raise_for_status()
return int(response.json()["data"]["port"])
The returned port is the whole point of the call. It is a localhost port tied to that running profile, so read it from the response every time and never store it between runs. A dead port produces a connection error that looks like a network problem and is actually a bookkeeping problem.
The generous 120-second timeout is deliberate. A warm profile answers quickly, but a start call that has to bring up a cold browser and connect a new proxy can take much longer, and patience here costs nothing when the call succeeds early.
Step 4 — Attach WebDriver to the Port the Launcher Returned
Two attach styles are in circulation, and they are not interchangeable. Pick one and stay with it for the whole script.
Option A: WebDriver.Remote against the profile port
This is the pattern in Multilogin’s official Selenium example. WebDriver.Remote accepts any endpoint that conforms to the W3C WebDriver protocol, which is exactly what the launcher exposes. For a Mimic profile you pass ChromiumOptions().
from selenium import webdriver
from selenium.webdriver.chromium.options import ChromiumOptions
def attach(port: int):
options = ChromiumOptions()
return webdriver.Remote(
command_executor=f"http://127.0.0.1:{port}",
options=options,
)
For a Stealthfox profile, swap ChromiumOptions() for Selenium’s Firefox Options() class. The rest of the call is identical, because the difference lives in the browser engine, not in the connection.
Option B: debuggerAddress attach
The alternative points ChromeDriver at the already-running Chromium through its DevTools address, instead of asking the launcher to speak WebDriver for you.
from selenium.webdriver.chrome.options import Options
options = Options()
options.debugger_address = f"127.0.0.1:{port}"
driver = webdriver.Chrome(options=options)
This version needs a ChromeDriver build that matches the Chromium inside the profile, because the driver does the protocol translation locally — a versioning problem Option A does not have. The hard rule for debugger attach is one profile per WebDriver session, with no unmanaged ChromeDriver running against the same account. Two drivers on one profile produce detached sessions and half-completed form fills.
| Factor | WebDriver.Remote (Option A) | debuggerAddress (Option B) |
|---|---|---|
| Extra driver binary needed | No — the launcher handles it | Yes — matching ChromeDriver |
| Works with Stealthfox | Yes, via Firefox Options() | Effectively Chromium only |
| Version drift risk | Low | Moderate to high |
| Best for | Scripted jobs and CI runs | Attaching to a browser you started by hand |
| Failure signature | Connection refused on the port | SessionNotCreatedException |
Both styles end with the same object: a driver bound to a real profile browser. Everything after that is standard Selenium, and the constraints are standard Selenium constraints — which is where production runs actually break. When you move from one profile to a batch, the connection bookkeeping gets harder, and the techniques in this guide to run multiple browser profiles with Selenium cover the queueing side of that problem.
Step 5 — Survive Stale Elements, Overlays and Dead Sessions
Selenium’s error reference names three failures as the usual suspects in long-running scripts: StaleElementReferenceException when the DOM re-renders under you, ElementClickInterceptedException when a banner or animation covers the target, and InvalidSessionIdException after a tab closes or the driver quits.
The documented fixes are equally specific. Re-locate the element after any navigation instead of holding a reference across it. Wait explicitly for element_to_be_clickable rather than sleeping on a fixed interval. And audit the script for stray close() or quit() calls, because an InvalidSessionIdException at minute nine is almost always a teardown that fired early.
import time
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import (
StaleElementReferenceException,
ElementClickInterceptedException,
)
def click_when_ready(driver, locator, timeout: int = 25):
"""Click a target, surviving re-renders and overlays."""
deadline = time.time() + timeout
while time.time() < deadline:
try:
element = WebDriverWait(driver, 5).until(
EC.element_to_be_clickable(locator)
)
element.click()
return element
except StaleElementReferenceException:
time.sleep(0.3) # re-locate on the next pass
except ElementClickInterceptedException:
driver.execute_script("window.scrollBy(0, 140);")
raise TimeoutError(f"never became clickable: {locator}")
Notice what the loop does not do. It does not catch a broad Exception, and it does not retry forever. A wrapper that swallows everything turns a broken selector into a 25-second pause followed by a vague timeout, which is worse than an immediate traceback pointing at the failing line.
The same discipline applies to page state. After every navigation, wait for an element that proves the new page rendered rather than for document.readyState. Single-page apps flip that flag long before the framework hydrates the form you are about to type into.
Step 6 — Stop the Profile in a finally Block
Stopping is a plain GET to the v1 launcher endpoint with the profile ID. The official demo waits five seconds, then stops the profile — a habit worth copying, because it gives in-flight requests a moment to settle.
LAUNCHER_V1 = "https://launcher.mlx.yt:45001/api/v1"
def stop_profile(token: str, profile_id: str) -> None:
headers = {"Authorization": f"Bearer {token}"}
url = f"{LAUNCHER_V1}/profile/stop/p/{profile_id}"
requests.get(url, headers=headers, timeout=60)
The important part is not the call, it is where it lives. Every start must be paired with a stop that runs even when the job raises — a stale element at step four, a keyboard interrupt, a dropped proxy connection. A profile left running blocks the next start on that profile, and the error will not tell you why.
token = sign_in(EMAIL, PASSWORD)
port = start_profile(token, FOLDER_ID, PROFILE_ID)
driver = None
try:
driver = attach(port)
# ... your actual work ...
finally:
if driver is not None:
driver.quit()
stop_profile(token, PROFILE_ID)
time.sleep(5)
Call the stop endpoint even when quit() appeared to succeed. The API call is the documented way to release the profile, and it is the only one that guarantees the next run gets a clean start instead of a “profile already in use” response.
Check Fingerprint and Proxy Parity Before the First Authenticated Run
Automation is the part you can debug with a traceback. The fingerprint layer fails silently, and it fails at the worst moment: the first authenticated session on a new profile. Run this checklist once per profile before pointing a script at it.
- Exit IP matches the profile’s declared region. Load an IP-check page inside the profile and confirm country, city and ASN. A profile claiming a German timezone with an exit IP in a US datacentre is visibly wrong on the first request.
- Timezone and locale follow the proxy. They should move with the exit IP automatically; verify it rather than assuming it.
- WebRTC is not leaking the host address. A local IP appearing in the WebRTC candidate list while the page sees a proxy IP is a classic pairing error.
- Fingerprint values are stable across restarts. Open the same profile twice and compare canvas and WebGL output. If they differ between sessions, the site sees a device that changes hardware on every visit.
Automate Multilogin Selenium With Send.win
Send.win pairs isolated, fingerprint-managed browser profiles with a full Automation API, so your scripts run in profiles that look and behave like real, separate users:
- Selenium, Puppeteer & Playwright support – drive any profile programmatically (Team plan)
- Isolated profiles – each with its own fingerprint, cookies, and storage
- Built-in residential proxies – with automatic timezone, locale, and WebRTC matching
- Desktop app for Windows, macOS & Linux – plus cloud sessions when you don’t want a local install
Try the instant cloud browser demo — no install, straight from your browser. Then compare plans: a 30-day free trial with no credit card, and paid plans from $6.99/month billed annually.
If you plan to run Multilogin Selenium automation across dozens of profiles, the fingerprint questions get harder than the API questions. The mechanics of keeping canvas, WebGL and hardware signals coherent across an automated fleet are covered in this walkthrough of Selenium fingerprint isolation, and they apply whether you drive profiles through Multilogin or anything else.
Common Errors and What Actually Causes Them
Almost every Multilogin Selenium failure maps to one of eight causes. The table is ordered by how often each one appears, not by severity.
| Symptom | Real cause | Fix |
|---|---|---|
| Profile does not start, launcher times out | Desktop app not running | Open the Multilogin app and leave it running for the whole job |
| 401 on the start call | Missing or expired bearer token | Re-run sign_in() at the top of every job |
Connection refused on 127.0.0.1:PORT |
Stale port, or attaching too early | Read port from the start response; retry the attach a few times |
SessionNotCreatedException |
debuggerAddress attach with a mismatched ChromeDriver | Match the driver build or switch to WebDriver.Remote |
StaleElementReferenceException |
DOM re-rendered after navigation | Re-locate inside the retry loop, never across a get() |
ElementClickInterceptedException |
Cookie banner, modal or animation over the target | Wait for clickable, then scroll the element into view |
InvalidSessionIdException |
Stray close()/quit(), or the profile was stopped mid-run |
Audit teardown; keep exactly one quit path in finally |
| Start fails on the second run | Previous run never stopped the profile | Stop in finally, with a five-second settle |
Two notes sit outside the table. Multilogin’s help centre keeps a dedicated WebDriver connection troubleshooting page, which is worth opening when the error text is genuinely ambiguous. And if you are automating the cloud-phone side of the product, Selenium is the wrong tool: native Android instances are driven through ADB or Appium, not WebDriver.
The Full Script
Everything above assembled into one file: sign in, start, attach, run a short job with a resilient click, and tear down in finally. Replace the IDs and the target URL with your own.
"""Run a job inside a Multilogin profile through Selenium."""
import hashlib
import os
import time
import requests
from selenium import webdriver
from selenium.webdriver.chromium.options import ChromiumOptions
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import (
StaleElementReferenceException,
ElementClickInterceptedException,
)
API_BASE = "https://api.multilogin.com"
LAUNCHER_V1 = "https://launcher.mlx.yt:45001/api/v1"
LAUNCHER_V2 = "https://launcher.mlx.yt:45001/api/v2"
EMAIL = os.environ["MLX_EMAIL"]
PASSWORD = os.environ["MLX_PASSWORD"]
FOLDER_ID = os.environ["MLX_FOLDER_ID"]
PROFILE_ID = os.environ["MLX_PROFILE_ID"]
TARGET_URL = "https://example.com/login"
def sign_in() -> str:
payload = {
"email": EMAIL,
"password": hashlib.md5(PASSWORD.encode()).hexdigest(),
}
response = requests.post(f"{API_BASE}/user/signin", json=payload, timeout=30)
response.raise_for_status()
return response.json()["data"]["token"]
def start_profile(token: str) -> int:
headers = {"Authorization": f"Bearer {token}"}
url = f"{LAUNCHER_V2}/profile/f/{FOLDER_ID}/p/{PROFILE_ID}/start"
response = requests.get(
url,
params={"automation_type": "selenium"},
headers=headers,
timeout=120,
)
response.raise_for_status()
return int(response.json()["data"]["port"])
def stop_profile(token: str) -> None:
headers = {"Authorization": f"Bearer {token}"}
url = f"{LAUNCHER_V1}/profile/stop/p/{PROFILE_ID}"
requests.get(url, headers=headers, timeout=60)
def attach(port: int):
options = ChromiumOptions()
return webdriver.Remote(
command_executor=f"http://127.0.0.1:{port}",
options=options,
)
def click_when_ready(driver, locator, timeout: int = 25):
deadline = time.time() + timeout
while time.time() < deadline:
try:
element = WebDriverWait(driver, 5).until(
EC.element_to_be_clickable(locator)
)
element.click()
return element
except StaleElementReferenceException:
time.sleep(0.3)
except ElementClickInterceptedException:
driver.execute_script("window.scrollBy(0, 140);")
raise TimeoutError(f"never became clickable: {locator}")
def run() -> None:
token = sign_in()
port = start_profile(token)
driver = None
try:
driver = attach(port)
driver.get(TARGET_URL)
click_when_ready(driver, (By.ID, "signin"))
WebDriverWait(driver, 20).until(
EC.presence_of_element_located((By.CSS_SELECTOR, "[data-testid=account]"))
)
print("landed on:", driver.current_url)
finally:
if driver is not None:
driver.quit()
stop_profile(token)
time.sleep(5)
if __name__ == "__main__":
run()
The script is deliberately single-profile. Scaling a Multilogin Selenium setup means a queue of profile IDs and one worker process per profile, each with its own token, port and teardown — not threads inside a single process.
If you would rather not run a separate vendor app on every machine, Send.win includes a local Automation API for Selenium, Puppeteer and Playwright on its Team plan, so the same sign-in, attach and teardown shape works with connection details copied from a profile’s automation settings instead of a launcher host. Every Send.win profile already has a residential proxy behind it, so the parity checklist is a setup step rather than a separate purchase. This comparison of Send.win vs Multilogin covers where the two overlap and where the licensing model differs before you migrate a working script.
🏆 Send.win Verdict
The Multilogin pattern above — sign in, start with automation_type=selenium, attach to the local port, stop in finally — maps cleanly onto Send.win. What differs is everything around the browser: residential proxies are built in on every plan, so the proxy parity checklist is configuration rather than an extra purchase, and the local Automation API for Selenium, Puppeteer and Playwright is included on Team at $49/mo, or $20.99/mo billed annually. Paid plans also add cloud sync (20 profiles on Pro, 100 on Team) and profile sharing with login, so a teammate can pick up the same account from another device.
Try Send.win free today — 30 days for $0 today, card required, cancel anytime.
Frequently Asked Questions
How do I connect Selenium to a Multilogin profile?
Sign in to the Multilogin API to get a bearer token, start the profile with automation_type=selenium, read the local port from the JSON response, and pass http://127.0.0.1:PORT to webdriver.Remote(). That gives you a normal Selenium driver bound to the profile’s browser. Everything after that is standard Selenium code.
Which port does Multilogin give Selenium to attach to?
The port comes back in the data.port field of the start response, and it is a localhost port that belongs to that running profile. Read it fresh from each start response instead of storing it. Reusing a port from an earlier session is a frequent cause of connection-refused errors.
Why does my Multilogin profile fail to start from Selenium?
A closed desktop app is the first cause listed in Multilogin’s own documentation — the API controls the local app, it does not replace it. After that, check for an expired token, a wrong profile or folder ID, and whether the profile is already running. A previous run that never stopped the profile blocks the next start too.
Do I need the Multilogin desktop app running for the API?
Yes, for local profile automation. The launcher endpoints are served on your machine, so the app has to stay open for the whole job. Multilogin also sells cloud-hosted instances, but the classic Selenium attach flow assumes a locally running app with the profile pointed at a proxy you configured.
Should I use WebDriver.Remote or debuggerAddress?
Prefer WebDriver.Remote for scripted jobs. It needs no extra driver binary, works for both Mimic and Stealthfox profiles, and avoids ChromeDriver version drift. The debugger attach suits binding to a browser you launched by hand, but it requires a matching ChromeDriver and limits you to one profile per WebDriver session.
Is the API the same for Mimic and Stealthfox profiles?
The connection flow is identical; only the options class changes. Use ChromiumOptions() for a Mimic profile and Selenium’s Firefox Options() for Stealthfox. Sign-in, start, attach and stop calls do not differ between the two engines.
How do I stop the Multilogin profile after the script ends?
Send a GET to the v1 launcher stop endpoint with the profile ID, and call it from a finally block so it runs even when the job raises. Multilogin’s own demo waits five seconds before stopping. Call it even if driver.quit() appeared to succeed, because the API call is what releases the profile for the next run.
How do I handle stale element and session errors in a long profile run?
Catch StaleElementReferenceException and re-locate the element on the next loop pass instead of holding a reference across a navigation. Catch ElementClickInterceptedException and scroll or dismiss the overlay. For InvalidSessionIdException, audit the script for stray close() or quit() calls — a session rarely dies on its own.