How Do You Drive an AdsPower Profile With Selenium?
AdsPower Selenium automation works in two moves. First you ask the AdsPower Local API to start a profile and it answers with a driver path plus a debugger address. Then you attach ChromeDriver to that address with options.debugger_address, so Selenium drives the real profile instead of launching a blank Chrome. Everything else — pacing, cleanup order, fingerprint checks — exists to keep those two calls honest.
📌 TL;DR Executive Summary
- Core Takeaway: Start the profile over the Local API, read
data.webdriveranddata.ws.seleniumfrom the JSON response, then attach Selenium withdebuggerAddress. Never let ChromeDriver launch its own browser. - Key Risk/Challenge: The Local API is rate-limited (1 request/sec baseline, rising per profile count on newer builds), the port can change, and a skipped stop call leaves the profile locked for the next run.
- Recommended Solution: Wrap start, attach and stop in one context manager, call
driver.quit()before the stop request, and read the API address from settings instead of hard-coding 50325.
Prerequisites: What You Need Before the First API Call
The Local API listens on the machine where the desktop client runs. That single fact rules out a few setups before you write a line of code, so check the list first.
- AdsPower desktop client installed and running. The API does not answer while the app is closed, and it responds on local addresses only, so a container or WSL session needs host networking to reach it.
- Selenium 4 for Python:
pip install selenium. Selenium 4 dropped Desired Capabilities in favour of the browser options classes, which matters for the attach step below. - A saved profile and its
user_id. Copy the ID from the profile list in the client; it is the only identifier the start endpoint needs. - Local API enabled. In the client, open Account Management → Settings → Local API and note the address shown there.
- A plan check. The community Python wrapper states the Local API is available on paid AdsPower subscriptions, and AdsPower’s own repository notes that the team collaboration version includes API access. Confirm what your account shows before building a pipeline on it.
- The
requestslibrary for the HTTP calls:pip install requests.
If you have not attached an automation driver to a fingerprint-managed browser before, the mechanics are the same for every vendor — you can read the general pattern in connect Selenium to an antidetect browser first and then come back to the AdsPower specifics.
Step 1 — Read the Local API Port Instead of Hard-Coding 50325
AdsPower’s default Local API address is http://local.adspower.net:50325/, and http://localhost:50325/ reaches the same service. The catch is documented in the vendor’s own repository: the port can change, so scripts should read it rather than assume it. AdsPower writes the live address into a file called local_api inside the Cache folder of each profile, which gives you a programmatic source of truth.
import re
from pathlib import Path
import requests
DEFAULT_BASE = "http://local.adspower.net:50325" # confirm in Settings -> Local API
def read_api_base(cache_dir=None):
"""The port can change. AdsPower writes the live address to the profile's
Cache/local_api file; fall back to the value shown in the settings page."""
if cache_dir:
try:
raw = Path(cache_dir, "local_api").read_text(encoding="utf-8")
match = re.search(r"https?://[^\s\"']+", raw)
if match:
return match.group(0).rstrip("/")
except OSError:
pass
return DEFAULT_BASE
def api_is_up(base_url):
try:
requests.get(base_url, timeout=3)
return True # any HTTP answer means the desktop client is listening
except requests.RequestException:
return False
base_url = read_api_base("/path/to/profile/Cache")
print(base_url, api_is_up(base_url))
The health check deliberately does not depend on an endpoint path. It only asks whether something is listening on that address, which separates “the client is closed” from “my request is malformed” when you are debugging. The same AdsPower localAPI repository is worth keeping open while you work, because it documents the settings location and the cache file in one place.
Step 2 — Start the Profile Through the Local API
The start call is a GET with the user_id as a parameter, plus an API key if your client exposes one. A successful response carries a code of 0 and a data object with three things you need: the path to the matching driver binary, a ws.selenium debugger address for Selenium, and a ws.puppeteer endpoint for Playwright and Puppeteer.
import requests
START_PATH = "/api/v1/browser/start" # verify the path against your client build's docs
def start_profile(base_url, user_id, api_key=None):
params = {"user_id": user_id}
if api_key:
params["api_key"] = api_key
response = requests.get(base_url + START_PATH, params=params, timeout=60)
response.raise_for_status()
payload = response.json()
if payload.get("code") != 0:
raise RuntimeError("start failed: {0} (code {1})".format(
payload.get("msg"), payload.get("code")))
data = payload["data"]
return {
"webdriver": data.get("webdriver"), # driver build for this profile
"selenium_ws": data["ws"]["selenium"], # debugger address for Selenium
"puppeteer_ws": data["ws"].get("puppeteer"), # CDP endpoint for Playwright
}
Two details save time later. The webdriver value is the driver build that matches the profile’s Chromium, so using it avoids the version mismatch that shows up as an empty DevToolsActivePort error. The endpoint paths are the part you should verify once per build: the Local API pages that document the Selenium sample date from March 2023, while the current v2 profile start endpoint is documented separately and returns the same ws.selenium and ws.puppeteer fields. Put both path constants at the top of your script so a client update is a one-line fix.
Step 3 — Attach ChromeDriver With debuggerAddress
This is the step people get wrong. If you create the driver without a debugger address, Selenium launches a fresh, brand-new Chrome and your carefully configured profile sits unused. Setting options.debugger_address flips ChromeDriver into attach mode: it connects to the already-running browser and drives it.
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
def attach_driver(webdriver_path, debugger_address):
options = Options()
options.debugger_address = debugger_address # attach, do not launch
options.page_load_strategy = "eager" # stop at DOMContentLoaded
service = Service(executable_path=webdriver_path) if webdriver_path else Service()
driver = webdriver.Chrome(service=service, options=options)
driver.set_window_size(1440, 900)
return driver
page_load_strategy maps to document.readyState: normal waits for complete, eager returns at interactive, and none does not block the driver at all. The setting applies to the entire session, so decide before you create the driver rather than mid-script. The Selenium project keeps this documented on its browser options page.
Because ChromeDriver is not launching anything, options that only apply at launch — binary location, user-data-dir, extension loading — have no effect here. Window size is a good example: set it after the attach with set_window_size(), because the option form is ignored on an existing session.
--disable-blink-features=AutomationControlled or a custom user agent to the attach options. On an attach they either do nothing or contradict the profile’s own settings, and a user agent that disagrees with the profile’s client hints is exactly the kind of mismatch fingerprint checkers flag.
Step 4 — Keep the Session From Reading as Automation
AdsPower’s own guide is candid that Selenium leaves traces: fixed browser attributes plus fast, regular operation patterns that anti-fraud systems can classify as automation. The fingerprint work is done by the profile — the vendor advertises 20+ masking options and 50+ customizable parameters — but your script can undo it in three ways.
Timing. A loop that clicks at perfectly even intervals is a machine signature. Real sessions have variance: a pause before a form field, a scroll that goes too far, a slower response after a slow network call. Add random jitter to your waits and vary action order between runs.
Consistency between fingerprint layers. Anti-fraud systems compare the user agent, plugin list, canvas and WebGL signatures, time zone, language, screen resolution, CPU, memory and font list against each other and against the exit IP. A profile with a US time zone behind a European proxy fails the comparison even though every individual value looks plausible. Free checkers such as PixelScan and IPhey report this kind of mismatch, including WebRTC leaks, and are worth running once per profile before you automate anything.
Flags that fight the profile. Anything you would normally add to hide automation is redundant here and sometimes harmful. If your profile already declares a coherent fingerprint, your job is to not overwrite it. The wider problem is covered in automation detection in Selenium, and the proxy side has its own checklist in AdsPower proxy setup.
Step 5 — Close in the Right Order
Cleanup has a specific sequence: end the WebDriver session first, then tell the API to stop the browser. Skip the stop call and the profile stays flagged as open inside the client, so the next start attempt fails. Do it in the other order and you close the browser while your WebDriver client still expects it to answer, which produces a confusing timeout instead of a clean exit.
STOP_PATH = "/api/v1/browser/stop" # confirm this path on your build, same as START_PATH
def stop_profile(base_url, user_id, api_key=None):
params = {"user_id": user_id}
if api_key:
params["api_key"] = api_key
response = requests.get(base_url + STOP_PATH, params=params, timeout=30)
response.raise_for_status()
return response.json()
# inside your worker, after the last action:
driver.quit() # 1. end the WebDriver session
stop_profile(base_url, user_id) # 2. release the profile
Scaling Past One Profile: Rate Limits and a Session Manager
AdsPower’s Local API documentation describes a frequency limit that scales with the number of profiles on your account on patch v2.8.2.1 and later: 2 requests per second for 0–200 profiles, 5/sec for 200–5000, and 10/sec above 5000, with some endpoints fixed at 1 per second. Older builds are capped at 1 request per second regardless. The safe default is one call per second, then loosen it once you know your patch version.
import time
from contextlib import contextmanager
MIN_INTERVAL = 1.0 # 1 req/sec is the baseline that works on every build
@contextmanager
def adspower_session(base_url, user_id, api_key=None):
info = start_profile(base_url, user_id, api_key)
driver = attach_driver(info["webdriver"], info["selenium_ws"])
try:
yield driver
finally:
driver.quit()
stop_profile(base_url, user_id, api_key)
time.sleep(MIN_INTERVAL) # pace the next start call
The context manager guarantees the stop call even when an exception fires mid-page, which is the most common source of locked profiles in practice. If you are running several sessions in parallel, the same pacing applies per call, not per script — a thread pool that starts twenty profiles at once will trip the limit no matter how tidy the code is. For the broader pattern of running profiles in parallel without collisions, see run multiple browser profiles.
Playwright, Puppeteer, or the Python Wrapper?
The same start response feeds other drivers. The v2 endpoint returns ws.puppeteer, which Playwright consumes through chromium.connect_over_cdp and Puppeteer through browserWSEndpoint. One subtlety: when Playwright is connected to an existing browser, browser.close() disconnects the client without closing the browser, so the profile keeps running until you send the stop request.
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(info["puppeteer_ws"])
context = browser.contexts[0]
page = context.pages[0] if context.pages else context.new_page()
page.goto("https://example.com")
browser.close() # disconnects the client; the profile is still open
stop_profile(base_url, user_id) # this is what actually closes it
If you prefer not to hand-roll the HTTP layer, there is a community wrapper: the MIT-licensed adspower package by Croco Factory, which offers synchronous and asynchronous API calls plus Selenium and Playwright helpers, and exposes ProxyConfig and FingerprintConfig objects for profile creation. It can create an anonymous profile that is deleted after the last statement in its context manager, which is handy for throwaway scraping runs.
| Approach | What you add | Best for | Main caveat |
|---|---|---|---|
| Direct HTTP + Selenium | requests plus the driver path from the start response |
Full control, no third-party code in your stack | You own retries, pacing and cleanup |
adspower wrapper package |
pip install adspower with a [selenium] or [playwright] extra |
Fast prototyping, async calls, profile creation objects | Community-maintained; public documentation dates to 2023 |
| Playwright / Puppeteer over CDP | The ws.puppeteer value from the same start response |
Auto-waiting APIs and network interception | browser.close() only disconnects the client |
pip install adspower
pip install "adspower[selenium]"
pip install "adspower[playwright]"
Two caveats before you build on it. It is community-maintained rather than an official AdsPower product, and its published version dates to 2023, so verify it against your current client before putting it in production. Its README is also the source of the claim that the Local API requires a paid subscription — useful context, but confirm it against your own account rather than a third-party readme. The direct HTTP approach has no dependency to rot and takes about forty lines.
Common AdsPower Selenium Errors and What Causes Them
| Symptom | What is happening | Fix |
|---|---|---|
| ChromeDriver cannot reach the debugger address | The profile is already open in another client window, or your script is talking to a different AdsPower instance than the one that started it | Close the profile in the UI, confirm the API base address, then start it again |
code is non-zero with a “too many requests” style message |
You exceeded the per-second frequency limit | Pace calls at one per second; only raise it if your patch version is v2.8.2.1 or later and your profile count allows it |
| Profile stuck as open after a crash | driver.quit() ran but the stop request never fired |
Wrap the session in try/finally or a context manager so stop always runs |
| Selenium opens a blank Chrome window | debugger_address was never set on the options object |
Set it before constructing the driver, not after |
| Your options are silently ignored | Attach mode does not launch a browser, so launch-time options do nothing | Resize and configure through the driver after attach, or in the profile itself |
| Connection refused from Docker or WSL | The API answers on local addresses of the host machine | Run the container with host networking, or point at the host address rather than 127.0.0.1 |
Why does a profile stay locked?
Locking is a state flag inside the client, not a crash. The client marks a profile open when the start call succeeds and only clears it on a stop call. Any code path that skips the stop request — an exception, a sys.exit(), a killed terminal — leaves the flag set. The context manager in Step 5 is the whole fix.
Why does the same script work today and fail tomorrow?
Three moving parts: the API port (documented as changeable), the client patch version (which changes the frequency limit), and the profile state (open or closed). Log the base URL, the patch version and the profile state at the top of every run, and most “random” failures stop being random.
The Full Final Script
This combines everything above into one runnable file. Replace the profile ID, point the cache directory at your own profile folder, and adjust the selectors to your target page.
"""Drive an AdsPower profile with Selenium and close it cleanly."""
import time
from contextlib import contextmanager
import requests
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
API_BASE = "http://local.adspower.net:50325" # read the live value from Settings
USER_ID = "your_profile_id" # profile id from the AdsPower list
API_KEY = None # only if your client shows one
START_PATH = "/api/v1/browser/start" # verify on your build
STOP_PATH = "/api/v1/browser/stop" # verify on your build
MIN_INTERVAL = 1.0 # requests per second, safe baseline
def api_is_up(base_url):
try:
requests.get(base_url, timeout=3)
return True
except requests.RequestException:
return False
def start_profile(base_url, user_id, api_key=None):
params = {"user_id": user_id}
if api_key:
params["api_key"] = api_key
response = requests.get(base_url + START_PATH, params=params, timeout=60)
response.raise_for_status()
payload = response.json()
if payload.get("code") != 0:
raise RuntimeError("start failed: {0}".format(payload.get("msg")))
data = payload["data"]
return {
"webdriver": data.get("webdriver"),
"selenium_ws": data["ws"]["selenium"],
"puppeteer_ws": data["ws"].get("puppeteer"),
}
def stop_profile(base_url, user_id, api_key=None):
params = {"user_id": user_id}
if api_key:
params["api_key"] = api_key
response = requests.get(base_url + STOP_PATH, params=params, timeout=30)
response.raise_for_status()
return response.json()
def attach_driver(webdriver_path, debugger_address):
options = Options()
options.debugger_address = debugger_address
options.page_load_strategy = "eager"
service = Service(executable_path=webdriver_path) if webdriver_path else Service()
driver = webdriver.Chrome(service=service, options=options)
driver.set_window_size(1440, 900)
return driver
@contextmanager
def adspower_session(base_url, user_id, api_key=None):
info = start_profile(base_url, user_id, api_key)
driver = attach_driver(info["webdriver"], info["selenium_ws"])
try:
yield driver
finally:
driver.quit()
stop_profile(base_url, user_id, api_key)
time.sleep(MIN_INTERVAL)
def main():
if not api_is_up(API_BASE):
raise SystemExit("AdsPower client is not running, or the API base is wrong")
with adspower_session(API_BASE, USER_ID, API_KEY) as driver:
driver.get("https://example.com/login")
field = WebDriverWait(driver, 20).until(
EC.presence_of_element_located((By.CSS_SELECTOR, "input[type=email]"))
)
field.send_keys("you@example.com")
time.sleep(1.5) # jitter beats a perfectly regular loop
print(driver.title)
if __name__ == "__main__":
main()
Run it once with a page you control before pointing it at anything that matters, and watch the client’s profile list while it runs. Seeing the profile flip to open and back to closed confirms the whole loop, including the part that most tutorials skip.
🏆 Send.win Verdict
If the Local API plumbing above looks like work you would rather not own, Send.win approaches the same problem from the other end: the Automation API ships inside the browser instead of running as a separate service you wire up. It is a local API for Selenium, Puppeteer and Playwright, profiles come with built-in residential proxies on every plan, and timezone, locale, WebRTC and geolocation follow the proxy exit IP automatically — which reduces the per-profile consistency checks you would otherwise run by hand. AdsPower’s Local API remains a reasonable route if you want explicit control and your plan includes it; just be honest about the trade. Send.win’s Automation API is a Team-plan feature, and the browser runs on Windows, macOS and Linux as a desktop app, or in the cloud browser if you would rather not install anything.
Try Send.win free today — a 30-day trial costs $0 today, includes 10 isolated profiles with unique fingerprints and 1 GB of residential proxy traffic (card required), and you can cancel anytime.
Frequently Asked Questions
How do I connect Selenium to the AdsPower Local API?
Send a GET request to the local start endpoint with your user_id, read data.webdriver and data.ws.selenium from the JSON response, then create a ChromeDriver instance with options.debugger_address set to that second value. Selenium attaches to the running profile instead of launching its own browser. The whole sequence is about ten lines plus error handling.
Why does debuggerAddress fail with my profile?
Almost always because the profile is already open somewhere, or because the script is talking to a different API address than the client is listening on. The debugger port belongs to the running browser, so a second start attempt cannot reuse it. Close the profile in the client UI, confirm the base address from Settings → Local API, and start it again.
Does the AdsPower Local API work on the free plan?
The community Python wrapper’s documentation states that the Local API is only available on paid AdsPower subscriptions, and AdsPower’s own repository notes that the team collaboration version includes API access. There is no dated pricing page confirming either way in the material available today, so check the API section in your own client before you build a pipeline around it.
How do I stop hitting the AdsPower API rate limit?
Pace every call at one per second as a baseline, because older builds are capped there. On patch v2.8.2.1 and later the documented limit rises with profile count — 2 requests per second up to 200 profiles, 5 up to 5000, 10 above that — but some endpoints stay fixed at 1/sec. A single context manager with a sleep in the finally block handles most of it.
Can I use Playwright connect_over_cdp with AdsPower profiles?
Yes. The v2 start endpoint returns a ws.puppeteer value, and Playwright consumes it through chromium.connect_over_cdp. Remember that calling browser.close() on a connected browser only disconnects the client — the profile stays open until you send the stop request to the Local API.
Why does my AdsPower Selenium script still get detected as a bot?
AdsPower’s own guide points at fixed browser attributes and fast, regular operation patterns, and those come from your script rather than the profile. Even timing intervals, identical action order between runs and extra anti-detection flags that contradict the profile’s own user agent are the usual culprits. Run each profile through a consistency checker and add jitter to your waits.
Is there a Python library for the AdsPower API?
Yes — the MIT-licensed adspower package by Croco Factory offers synchronous and asynchronous calls, Selenium and Playwright helpers, and ProxyConfig and FingerprintConfig objects for profile creation. Install it with pip install adspower, plus the [selenium] or [playwright] extra. It is community-maintained rather than official, so test it against your current client build.
Should I close the driver or the profile first?
Always the driver. Call driver.quit() to end the WebDriver session, then send the stop request to release the profile. In the other order the browser disappears while the WebDriver client still expects it to answer, and skipping the stop call leaves the profile flagged as open so the next start fails.
Automate Adspower 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.