Navegadores alojados
Inicia un navegador Clearcote en nuestros servidores con una sola llamada a la API y contrólalo por el Chrome DevTools Protocol desde Playwright, Puppeteer o cualquier cliente CDP. No tienes que instalar ni ejecutar nada por tu cuenta. Por defecto, el tráfico sale por IPs residenciales y pagas por GB con un saldo prepago.
- IPs residenciales, no de centro de datos. Los sitios web ven una conexión doméstica real de un proveedor de internet para hogares, no una dirección de hosting o de la nube.
- Hardware real, no un VPS. Los navegadores corren en nuestros propios servidores físicos dedicados, no en máquinas virtuales compartidas en la nube.
GratisObtén €5 de tráfico gratis al conectar GitHub. Sin tarjeta.
Una sola vez, uno por cuenta de GitHub. La cuenta de GitHub debe tener al menos 30 días de antigüedad.
Precios
- €1.00 por GB, proxy residencial incluido. Por defecto, cada sesión sale a internet por una IP residencial: una conexión doméstica real de un proveedor de internet para hogares, no una dirección de centro de datos o de hosting. Lo que pagas con esos €1.00 es ese tráfico; no hay una factura aparte por el proxy.
- El tráfico se mide entre el navegador e internet, sumando subida y bajada (1 GB = 109 bytes). Consulta qué cuenta como tráfico.
- No se cobra por tiempo, sesiones ni mensajes CDP.
- Prepago: recarga saldo en el panel. Un navegador necesita al menos €0.50 para iniciar, y cada sesión tiene como tope lo que el saldo puede pagar en el momento en que arranca, repartido entre los navegadores que tengas en ejecución (si tu propio
maxGbes menor, se aplica ese). Un navegador en ejecución se detiene cuando el saldo llega a cero (el consumo se reporta aproximadamente cada 15 segundos, así que el último reporte puede dejarlo un poco por debajo de cero).
Qué cuenta como tráfico
Cada byte que el navegador envía a un sitio web o recibe de él se contabiliza tal como pasa por la red, igual que lo hace un proveedor de proxies. En una página típica, eso significa:
- Se cuenta: la página en sí y todo lo que carga: scripts, hojas de estilo, imágenes, fuentes, video, llamadas a APIs, anuncios y trackers, WebSockets, además de los encabezados de las solicitudes, las cookies y el overhead del cifrado (TLS) de cada conexión. Las páginas siguen cargando mientras esperas, así que el polling en segundo plano y la analítica también cuentan.
- No se cuenta: la conexión CDP entre tu código y el navegador (comandos, resultados, capturas de pantalla, PDFs, el contenido de la página que lees), la vista en vivo del panel y todo lo que el navegador nunca descarga (solicitudes que bloqueas, archivos que sirve desde su propia caché).
Como referencia aproximada, una página liviana de texto cuesta bastante menos de 1 MB, una página típica de noticias o de una tienda entre 2 y 5 MB, y una SPA pesada o cualquier cosa con video, 10 MB o más. A €1.00 por GB, 1000 páginas de 3 MB cada una son unos 3 GB. Tu propia sesión muestra sus cifras reales: el panel muestra el tráfico de cada sesión y sus 20 sitios principales por bytes, y maxGb le pone un tope a la sesión para que una página descontrolada no se coma tu saldo.
Cómo reducir el tráfico
La mayor parte del peso de una página suele ser cosas que un script no necesita. Los mayores ahorros, en orden:
- Bloquea imágenes, multimedia y fuentes. Muchas veces son la mitad de la página o más. Bloquéalas por patrón de URL en el navegador y esas solicitudes nunca salen de él, así que nunca se facturan, y el navegador conserva su caché:
// Playwright: block by URL pattern over CDP (keeps the browser cache on)
const cdp = await context.newCDPSession(page);
await cdp.send("Network.enable");
await cdp.send("Network.setBlockedURLs", {
urls: ["*.png", "*.jpg", "*.jpeg", "*.gif", "*.webp", "*.svg", "*.woff", "*.woff2", "*.ttf", "*.mp4", "*.webm"],
});
// Puppeteer: the same, through its CDP session
const client = await page.createCDPSession();
await client.send("Network.enable");
await client.send("Network.setBlockedURLs", { urls: ["*.png", "*.jpg", "*.jpeg", "*.gif", "*.webp", "*.woff2"] });No uses page.route() / context.route() ni la intercepción de solicitudes de Puppeteer solo para descartar solicitudes: desactivan la caché del navegador, así que cada página vuelve a descargar sus scripts y estilos, y eso cuesta más que las imágenes que ahorraste. Úsalos solo cuando necesites modificar solicitudes. La opción adblock también bloquea anuncios y trackers por ti.
- Bloquea anuncios, analítica y trackers. Cancela las solicitudes a dominios de terceros que no necesitas. La lista de sitios principales de la sesión en el panel te muestra cuáles te cuestan más.
- No esperes más de lo necesario.
waitUntil: "networkidle"espera a todo lo que carga una página, incluidos los anuncios. Mejor usa"domcontentloaded"y luego espera solo el elemento que de verdad necesitas. - Cierra el navegador en cuanto termines. Una página abierta sigue haciendo polling en segundo plano. Baja
idleTimeoutSecpara que una sesión olvidada se cierre sola. - Reutiliza un navegador para muchas páginas. Gracias a su caché, los scripts y estilos que comparten las páginas de un mismo sitio se descargan una sola vez, no en cada página. Navega dentro de la misma sesión en lugar de iniciar una nueva para cada URL.
- Llama a la API del sitio cuando puedas. Una vez que el navegador tiene una sesión funcionando, un
fetch()desde dentro de la página para obtener el JSON que necesitas pesa una fracción de lo que pesa volver a cargar la página. - Ponle un tope. Define
maxGben cada sesión para que una página inesperadamente pesada se detenga en lugar de vaciar tu saldo.
El bloqueo funciona en la mayoría de los sitios, pero algunos verifican que las imágenes o las fuentes realmente se hayan cargado. Si un sitio se comporta distinto con el bloqueo activado, vuelve a permitir ese tipo de recurso para ese sitio.
¿Quieres verlo funcionar primero? El Playground ejecuta un script en un navegador en la nube directamente desde tu panel, con la vista en vivo, la salida de la consola y las capturas de pantalla lado a lado.
1. Obtén una clave de API
Crea una en la página “API keys”. Empieza con cc_live_ y se envía como token Bearer. Mantenla en secreto: cualquiera que la tenga puede gastar tu saldo.
2. Inicia un navegador y conéctate
POST /api/v1/browsers devuelve una connectUrl: una URL de WebSocket de un solo uso para ese navegador. Conéctate dentro de los dos minutos siguientes; no se puede usar dos veces.
// Node.js + Playwright
import { chromium } from "playwright";
const res = await fetch("https://www.clearcotelabs.com/api/v1/browsers", {
method: "POST",
headers: { authorization: "Bearer cc_live_...", "content-type": "application/json" },
body: JSON.stringify({ identity: "account-1", country: "us" }),
});
const { connectUrl, id, error } = await res.json();
if (error) throw new Error(error);
const browser = await chromium.connectOverCDP(connectUrl);
const context = browser.contexts()[0];
const page = context.pages()[0] ?? (await context.newPage());
await page.goto("https://example.com");
await browser.close(); // ends the session// Puppeteer: the same connectUrl
const browser = await puppeteer.connect({ browserWSEndpoint: connectUrl, defaultViewport: null });# Python + Playwright
import requests
from playwright.sync_api import sync_playwright
r = requests.post("https://www.clearcotelabs.com/api/v1/browsers",
headers={"authorization": "Bearer cc_live_..."},
json={"identity": "account-1", "country": "de"}).json()
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(r["connectUrl"])
page = browser.contexts[0].new_page()
page.goto("https://example.com")
browser.close()La llamada de creación responde 201 con todo lo que tu código necesita para conectarse y para saber el precio:
{
"id": "bs_…", // use it with GET / DELETE /api/v1/browsers/<id>
"connectUrl": "wss://…/v1/connect/bs_…?token=…",
"expiresAt": "2026-09-24T10:02:00.000Z", // connect before this (two minutes)
"worker": "w_…",
"pricing": { "eurPerGb": 1, "eurPerHour": 0 },
"limits": { "maxSeconds": 14400, "idleSeconds": 300 } // plus maxBytes when capped
}Opciones
Todas son opcionales. Envíalas como cuerpo JSON de la llamada de creación.
| Campo | Tipo | Significado |
|---|---|---|
identity | string | One label per account you run: the same device fingerprint AND the same residential IP, for as long as that IP stays online. |
fingerprint | string | Device seed only (no IP pinning). The same seed gives the same device profile every time; with lightStealth on it picks from a small set of metadata profiles. |
lightStealth | boolean | Default true: varies only the metadata axes the host can back up. Set false to turn it off. |
platform | windows | macos | linux | android | Operating system the persona presents. |
brand | Chrome | Edge | Opera | Vivaldi | Browser brand the persona presents. |
timezone | IANA name | e.g. America/New_York. Use geoip instead to follow the exit IP. |
locale | string | Accept-Language, e.g. en-US,en. |
geoip | boolean | Timezone and language follow the exit IP. Default true unless you set timezone or locale yourself. |
proxy | "managed" | { server, username?, password? } | Omitted = managed residential pool. Or your own proxy: http://, socks5:// or socks5h:// (rules below). |
country | 2-letter code | Managed pool: exit country, e.g. us, de, gb. |
state | region code | Managed pool: exit state/region, e.g. ca, ny. Needs country. |
city | city name | Managed pool: exit city, e.g. "los angeles". Needs state. |
proxySession | string | Managed pool: sticky label. The same label returns the same exit IP later (kept for 24 hours). |
timeoutSec | number | Hard limit on the session length, in seconds (10 up to the account maximum below). |
idleTimeoutSec | number | End the session after this long without a CDP command (10–1800). |
maxGb | number | Stop the session after this much traffic (0.001–1000). |
headless | boolean | Default true. |
keepAlive | boolean | Default false. Keep the browser running when your client disconnects, until you end it (DELETE, or the CDP command Browser.close) or a limit does; reconnect with POST /api/v1/browsers/<id>/connect. |
version | string | Run a specific Clearcote release, e.g. "152.0.7977.82-r21" or "r21". Omitted = the current release. See "Pinning a release". |
profile | "name" | { name, persist? } | Load a saved profile (cookies + site storage). With persist: true, save it back when the session ends. See "Profiles". |
url | http(s) URL | Opened in the first tab before you connect: you find it already loading. |
adblock | boolean | Refuse known ad and tracker hosts before they load, so they are never billed. Default false. |
note | string | Your label for the session (at most 256 characters). Shown in the dashboard; filter by it in the list. |
worker | string | Place the session on the same server as an earlier one (its worker). 503 if that server is full. |
Configuración stealth predeterminada e identidades
Cada sesión parte de los ajustes de la página de configuración recomendada: lightStealth activado, un seed, y zona horaria e idioma que siguen a la IP de salida. Con lightStealth (el valor por defecto), el seed elige el perfil de dispositivo dentro de un conjunto pequeño que varía los núcleos de CPU, la memoria y el pixel ratio; canvas, WebGL y audio son los de la máquina donde corre la sesión. Define lightStealth: false para tener una persona completa por seed: las lecturas de canvas y WebGL reciben ruido por sitio derivado del seed, y las cadenas de la GPU, la pantalla y la configuración de audio (frecuencia de muestreo, latencia) siguen a la persona. Sin identity ni fingerprint, cada sesión recibe un seed aleatorio y una IP nueva.
Pasa identity: "account-42" para volver en sesiones posteriores con el mismo perfil de dispositivo, y con la misma IP mientras esa IP residencial siga en línea, que es lo que espera una cuenta con la sesión iniciada. Las identidades son privadas de tu cuenta: otro cliente que use la misma etiqueta recibe su propio seed y su propia IP. Por sí sola, una identidad no conserva cookies: para eso, usa un perfil.
Perfiles: inicia sesión una sola vez
Un perfil guarda las cookies, el localStorage y el IndexedDB de una sesión bajo un nombre, así la siguiente sesión con ese nombre arranca ya autenticada. Pasa profile: { name: "shop-account", persist: true } para cargarlo y volver a guardarlo cuando termine la sesión, o simplemente profile: "shop-account" para cargarlo en modo de solo lectura. La primera sesión con un nombre nuevo empieza vacía y lo crea.
// Every run: the same body. The first one starts signed out; sign in, then close the browser
// and the session saves the cookies and site storage. Every later run starts signed in.
const res = await fetch("https://www.clearcotelabs.com/api/v1/browsers", {
method: "POST",
headers: { authorization: "Bearer cc_live_...", "content-type": "application/json" },
body: JSON.stringify({ profile: { name: "shop-account", persist: true }, country: "de" }),
});- Mismo dispositivo, misma IP. Un perfil trae su propia identidad (
profile:<name>), así que el sitio ve la misma huella digital, y la misma IP residencial mientras esa IP siga en línea, como un cliente que regresa. Si prefieres otra cosa, pasa tú mismoidentityofingerprint, y mantén el mismo país entre ejecuciones. - Un solo escritor a la vez. Solo una sesión en ejecución puede guardar en un perfil; una segunda con
persist: truerecibe409 PROFILE_IN_USE. Las sesiones de solo lectura pueden correr en paralelo y ven el último estado guardado. - Se guarda cuando termina la sesión, ya sea que cierres el navegador, te desconectes, lo detengas o lo termine un límite. Si el navegador se cae, se conserva el estado guardado anterior en lugar de reemplazarlo por uno parcial. Las cookies de sesión (las que no tienen vencimiento) se descartan, igual que las descarta un navegador real al reiniciarse.
- Hasta unos 3.5 MB comprimidos. Si el IndexedDB de un sitio lo hace más grande, el IndexedDB queda fuera; las cookies y el localStorage se siguen guardando.
- Privado. Un perfil es solo tuyo (el “shop-account” de otro cliente es un perfil distinto), se almacena cifrado y solo se entrega al servidor que ejecuta tu sesión. Lístalos con
GET /api/v1/browsers/profiles, borra uno conDELETE /api/v1/browsers/profiles/<name>o usa el panel.
IPs de salida: rotativas, sticky y por ubicación
- Por defecto: cada sesión de navegador recibe su propia IP residencial de salida y la conserva durante toda la sesión.
- Sticky: pasa la misma etiqueta
proxySession(por ejemplo, una por cada cuenta que administras) para recuperar la misma IP de salida en una sesión posterior. Las etiquetas son privadas de tu cuenta. Una IP residencial sigue disponible mientras su peer esté en línea, normalmente varias horas; cuando se desconecta, recibes otra IP de la misma red. - Ubicación:
countryy, opcionalmente,stateycity. Cuanto más acotado el objetivo, más pequeño el pool. - Tu propio proxy:
proxy: { server: "http://host:port", username, password }, osocks5://host:port(los nombres se resuelven de nuestro lado) osocks5h://host:port(los nombres los resuelve tu proxy). La solicitud se valida de forma estricta: un puerto explícito, las credenciales enusername/password(una URLuser:pass@hostda un 400; como máximo 255 bytes cada una), ycountry,state,cityyproxySessionse rechazan en lugar de ignorarse, porque describen el pool administrado. La dirección del propio proxy tiene que ser pública. El tráfico se factura de la misma forma.
Por defecto, la zona horaria y el idioma siguen a la IP de salida (geoip). Pasa timezone / locale para elegirlos tú, o geoip: false para desactivarlo.
Fijar una versión
Las sesiones ejecutan la versión actual de Clearcote. Para ejecutar una anterior, pasa version: la versión completa ("152.0.7977.82-r21"), solo la recompilación ("r21"), una versión de Chromium o solo su versión mayor ("152" elige el build más reciente de esa versión), o "latest". Son las mismas versiones que descarga la opción version del SDK, así que una sesión alojada y una ejecución local fijadas a la misma versión usan el mismo build.
const res = await fetch("https://www.clearcotelabs.com/api/v1/browsers", {
method: "POST",
headers: { authorization: "Bearer cc_live_...", "content-type": "application/json" },
body: JSON.stringify({ identity: "account-1", version: "152.0.7977.82-r21" }),
});
const { connectUrl, engine, warnings } = await res.json();
// engine -> { version: "152.0.7977.82", revision: "r21", pinned: true }
// warnings -> [ "This session runs 152.0.7977.82-r21, older than the current ... " ]- Revisa
warnings. Una versión anterior no tiene lo que agregaron las versiones posteriores. Las opciones y correcciones que llegaron después pueden faltar, o ignorarse en silencio en lugar de rechazarse, así que un ajuste que funciona en la versión actual puede no hacer nada, sin avisar, en una versión fijada. - El
enginede la respuesta siempre indica qué versión ejecuta la sesión, esté fijada o no. - Una versión que no existe da un
400con el códigoUNKNOWN_VERSION, y el mensaje lista las versiones que puedes elegir. - La primera sesión con una versión que nuestros servidores todavía no han usado puede tardar hasta un minuto más en iniciar mientras se descarga el build. Las siguientes sesiones con esa versión arrancan tan rápido como cualquier otra.
Página de inicio y bloqueo de anuncios
urlabre una página en la primera pestaña antes de que te conectes, así que ya está cargando cuando tu script se conecta.adblock: truerechaza las solicitudes a hosts conocidos de anuncios, de verificación de anuncios y de analítica antes de que se hagan, así que nunca se facturan. La lista es deliberadamente conservadora (no toca los gestores de etiquetas, las herramientas de consentimiento, los SDKs de login ni los CAPTCHAs), pero algunos sitios notan que faltan anuncios; déjalo desactivado donde eso importe.
Vista en vivo: observar, tomar el control, compartir
En el panel, haz clic en una sesión para ver a qué sitios fue su tráfico y para verla en vivo. Pulsa “Take control” para hacer clic, escribir, desplazarte, pegar y navegar tú mismo, por ejemplo para iniciar sesión o superar una verificación que tu script no puede. Tu script sigue conectado todo el tiempo, así que ponlo en pausa mientras actúas. La interacción humana cuenta como actividad, así que una sesión que estás controlando no se cierra por inactividad. “Share” genera un enlace que cualquiera puede abrir sin cuenta, solo para ver o con control, válido de 15 minutos a 4 horas y nunca más allá del final de la sesión.
A través de la API:
# a live-view WebSocket for a running session (open it within 60 s)
# binary messages are JPEG frames, text messages are {"url","title","tabs"}
curl -H "authorization: Bearer cc_live_..." https://www.clearcotelabs.com/api/v1/browsers/<id>/live
# with control: the answer says "interactive": true when it was granted
curl -H "authorization: Bearer cc_live_..." "https://www.clearcotelabs.com/api/v1/browsers/<id>/live?control=1"
# a share link: control optional, 1 to 240 minutes (default 30)
curl -X POST -H "authorization: Bearer cc_live_..." -H "content-type: application/json" -d '{"control": false, "minutes": 60}' https://www.clearcotelabs.com/api/v1/browsers/<id>/shareCon control, envía mensajes de texto JSON por el mismo WebSocket. Las coordenadas son fracciones (de 0 a 1) del fotograma que estás viendo; todo lo demás se ignora.
| Mensaje | Qué hace |
|---|---|
{"t":"mouse", | Pulsar (down), soltar (up) o mover (move); n es la cantidad de clics y m, los modificadores (Alt 1, Ctrl 2, Meta 4, Shift 8). |
{"t":"wheel", | Desplazarse una cantidad de píxeles en un punto. |
{"t":"key", | Una tecla que se presiona o se suelta, tal como la envía un teclado. |
{"t":"text", | Insertar texto como si se escribiera (hasta 5000 caracteres). |
{"t":"nav",, forward, reload o {"t":"nav", | Historial, recargar o abrir una dirección http(s). |
GET /api/v1/browsers/<id> incluye traffic: los 20 sitios principales por bytes de esa sesión.
Administrar sesiones
# one session: status, traffic, seconds, cost so far
curl -H "authorization: Bearer cc_live_..." https://www.clearcotelabs.com/api/v1/browsers/<id>
# stop it (a running browser closes within about 15 seconds)
curl -X DELETE -H "authorization: Bearer cc_live_..." https://www.clearcotelabs.com/api/v1/browsers/<id>
# balance + your 20 most recent sessions
curl -H "authorization: Bearer cc_live_..." https://www.clearcotelabs.com/api/v1/browsers
# filtered by status and note text, up to 100; page back with before=<a createdAt you got>
curl -H "authorization: Bearer cc_live_..." "https://www.clearcotelabs.com/api/v1/browsers?status=active,ended¬e=shop-de&limit=50"
# label a session (null clears it)
curl -X PATCH -H "authorization: Bearer cc_live_..." -H "content-type: application/json" -d '{"note": "shop-de nightly"}' https://www.clearcotelabs.com/api/v1/browsers/<id>Ponle una note a la sesión al crearla para encontrarla después en la lista y en el panel. Para iniciar una sesión en el mismo servidor que una anterior (cachés ya calientes, la misma máquina), pasa el worker de esa sesión; si ese servidor está lleno, recibes un 503, no otro servidor.
Una sesión también termina cuando cierras el navegador o te desconectas, y cuando se alcanza alguno de los límites que se describen más abajo. Una solicitud de detención termina de inmediato una sesión a la que nadie se conectó; un navegador en ejecución lo cierra su servidor dentro de un intervalo de reporte, unos 15 segundos. GET responde con:
{
"id": "bs_…",
"status": "active", // see the table below
"proxy": "managed", // or "custom"
"createdAt": "…", "startedAt": "…", "endedAt": null,
"endReason": null, // set once ended, e.g. "user", "balance", "launch_failed"
"stopRequested": false,
"usage": { "bytesUp": 120334, "bytesDown": 4812009, "gb": 0.0049, "seconds": 41 },
"traffic": [ { "site": "example.com", "bytesUp": 20400, "bytesDown": 3100000 }, … ], // top 20 sites
"costEur": 0.0050,
"pricing": { "eurPerGb": 1, "eurPerHour": 0 }
}| status | Significado |
|---|---|
pending | Created; nobody has connected yet. Counts towards the concurrency limit until it starts or expires. |
active | A browser is running and reporting usage. |
lost | No usage report for 5 minutes. Billed up to the last report; a late report puts it back to active. |
ended | Closed: you disconnected, stopped it, or a limit or the balance ended it. endReason says which. |
expired | Nobody connected within two minutes of creating it. Never billed. |
La llamada de listado, GET /api/v1/browsers, devuelve { balanceEur, sessions: [...] } con los mismos objetos de sesión, de la más reciente a la más antigua.
Mantener un navegador abierto y reconectarse
Por defecto, una sesión termina cuando tu cliente se desconecta. Iníciala con keepAlive: true y el navegador sigue en ejecución, con sus pestañas, cookies e IP de salida, para que un script posterior (o el mismo, después de un fallo o de cerrar la laptop) retome donde se quedó el anterior:
# a new single-use connect URL for a running keepAlive session (connect within two minutes)
curl -X POST -H "authorization: Bearer cc_live_..." https://www.clearcotelabs.com/api/v1/browsers/<id>/connect- Para dejarlo en ejecución, desconéctate: con
browser.close()de Playwright (sobreconnectOverCDPsolo desconecta), conbrowser.disconnect()de Puppeteer o simplemente terminando tu proceso. - Para terminarlo, usa
DELETE /api/v1/browsers/<id>o envía el comando CDPBrowser.close: elbrowser.close()de Puppeteer lo hace, y en Playwright,await (await browser.newBrowserCDPSession()).send("Browser.close"). Hasta entonces, sigue ocupando su lugar en tu límite de concurrencia. - Un cliente a la vez: una reconexión mientras hay otro cliente conectado se rechaza con
409, igual que una para una sesión iniciada sinkeepAlive. - Los límites siguen aplicándose mientras nadie está conectado:
idleTimeoutSec(súbelo, hasta 1800, si piensas volver a ese navegador),timeoutSec,maxGby tu saldo. Una página que queda abierta sigue cargando su tráfico en segundo plano, que se factura como cualquier otro.
Límites
- 24 navegadores en ejecución o iniciándose a la vez por cuenta.
- Las sesiones duran como máximo 4 horas.
- Una sesión sin ningún comando CDP durante 5 minutos se cierra (puedes cambiarlo con
idleTimeoutSec). - Cada sesión empieza con un perfil de navegador nuevo, que se borra cuando termina la sesión, a menos que uses un perfil con nombre, que conserva las cookies y el almacenamiento de los sitios entre sesiones.
- Por seguridad, el navegador no puede abrir archivos locales (
file://), subir archivos desde el servidor, acceder a redes privadas o internas, ni enviar correo por el puerto 25. - Subir un archivo funciona desde Playwright:
setInputFiles()envía el archivo desde tu máquina (hasta 50 MB). EluploadFile()de Puppeteer se rechaza. Las descargas se quedan en nuestro servidor y se borran junto con la sesión; para conservar un archivo, obtenlo desde dentro de la página y devuelve su contenido. - No se pueden cargar extensiones de Chrome en los navegadores alojados.
- El navegador no reenvía los mensajes de consola ni los errores de página, así que
page.on("console")no recibe nada. Recoge lo que necesites dentro de la página y léelo conevaluate.
Errores
| Estado | code | Significado |
|---|---|---|
| 400 | — | The body is not JSON, or an option is invalid; the message says which. |
| 400 | UNKNOWN_VERSION | No release matches version; the message lists the ones you can pick. |
| 401 | — | Missing, malformed or revoked API key. |
| 402 | INSUFFICIENT_BALANCE | Balance below the minimum. Top up in the dashboard. |
| 404 | NOT_FOUND | No session with that id on your account. |
| 409 | NOT_RUNNING | Live view or a reconnect asked for before the browser started or after it ended. |
| 409 | NOT_KEEPALIVE | This session cannot be reconnected. Start it with keepAlive: true. |
| 409 | PROFILE_IN_USE | Another session is already saving to that profile. Stop it, or open the profile with persist: false. |
| 429 | CONCURRENCY_LIMIT | Too many browsers running or starting at once. Close one first. |
| 429 | — | More than 60 create calls in a minute from one address. Slow down. |
| 503 | NO_CAPACITY | No free browser slot right now. Retry after a few seconds. |
| 503 | NO_WORKER | The server running that session is not reachable at the moment. |
| 503 | NOT_CONFIGURED | Hosted browsers are not configured on this server. |
| 503 | NOT_AVAILABLE | Notes or profiles are not enabled on this server yet. |
Los errores son JSON: { "error": "...", "code": "..." }. Si se rechaza la conexión WebSocket en sí, crea una sesión nueva: las URLs de conexión son de un solo uso y vencen a los dos minutos. Un upgrade de WebSocket rechazado responde con un estado HTTP y un error en JSON: 409 cuando la URL ya se usó, la sesión se canceló, no se inició con keepAlive o ya tiene un cliente conectado; 401 cuando la URL venció.
Qué reintentar
- Reintenta con backoff:
503 NO_CAPACITYy503 NO_WORKER(espera 1, 2, 4… segundos con algo de jitter y abandona después de unos pocos intentos), y un429sin código (el límite de tasa por dirección). - Reintenta unas pocas veces con backoff: otras respuestas
5xx, y una conexión que se rechazó antes de que arrancara tu script (con una sesión nueva: la URL de conexión anterior ya se usó). - Nunca en bucle:
400(corrige la solicitud),401,402(agrega crédito),429 CONCURRENCY_LIMIT(cierra primero un navegador) y409 PROFILE_IN_USE. Tiene que intervenir una persona; reintentar solo quema solicitudes.
Buenas prácticas
- Conéctate; nunca lances un navegador. Usa
connectOverCDPopuppeteer.connectcon laconnectUrl;chromium.launch()inicia un navegador en tu propia máquina. - Usa lo que ya existe. Toma
browser.contexts()[0]y su primera página en lugar de crear un contexto nuevo: un contexto nuevo empieza sin las cookies ni el almacenamiento del perfil, y Playwright le asigna un viewport emulado de 1280×720 que no coincide con la ventana del navegador. - Define la persona al crear la sesión, no desde el script. El país, la zona horaria y el idioma van en la llamada de creación. Sobrescribir el user agent, el viewport o las propiedades de navigator desde un script genera justo las inconsistencias que busca la detección.
- Mantén los hooks de CDP al mínimo. Los listeners amplios, la intercepción de todas las solicitudes y los init scripts son la huella propia de la automatización. Playwright y Puppeteer sin modificar funcionan tal cual: el motor mantiene los efectos secundarios de
Runtime.enablefuera de la página, así que un driver parcheado como Patchright es opcional, no necesario. - Una sesión, muchas páginas. Iniciar un navegador es la parte lenta; navega dentro de él. Inicia sesión una sola vez con un perfil en lugar de hacerlo en cada ejecución.
- Detenlo siempre. Cierra el navegador en un
finally, y ajustamaxGbyidleTimeoutSecal trabajo, para que un bug no pueda dejar un navegador corriendo a costa de tu saldo. - Mira antes de agregar código. Cuando un sitio se comporta de forma extraña, obsérvalo en la vista en vivo (o toma el control) antes de agregar esperas y parches.
Frameworks
Todo lo que se conecte a Chrome por CDP funciona con la connectUrl. Es de un solo uso, así que un framework que se reconecta por su cuenta necesita una sesión nueva para cada conexión. El navegador arranca cuando te conectas, normalmente en pocos segundos; no hay ningún estado que consultar antes.
// Patchright (optional; a Playwright fork): npm i patchright
import { chromium } from "patchright";
const browser = await chromium.connectOverCDP(connectUrl);
const page = browser.contexts()[0].pages()[0];# Browser Use
from browser_use import Agent, Browser
agent = Agent(task="Find the cheapest flight to Lisbon next Friday", llm=llm, browser=Browser(cdp_url=connect_url))
await agent.run()
# Crawl4AI
from crawl4ai import AsyncWebCrawler, BrowserConfig
config = BrowserConfig(browser_mode="custom", cdp_url=connect_url, use_managed_browser=True)
async with AsyncWebCrawler(config=config) as crawler:
result = await crawler.arun("https://example.com")// Stagehand v4
import { localBrowser, Stagehand } from "@browserbasehq/stagehand";
const stagehand = await Stagehand.create({ browser: await localBrowser.connect({ cdpUrl: connectUrl }) });Un helper para empezar
Crear la sesión con los reintentos de arriba, conectarse y detenerla siempre, en una sola función:
// clearcote-hosted.ts
import { chromium, type Browser } from "playwright"; // or "patchright"
const API = "https://www.clearcotelabs.com/api/v1/browsers";
const AUTH = { authorization: "Bearer " + process.env.CLEARCOTE_API_KEY };
const RETRY_CODES = new Set(["NO_CAPACITY", "NO_WORKER"]);
export async function createSession(options: Record<string, unknown> = {}, attempts = 5) {
for (let i = 0; ; i++) {
const res = await fetch(API, {
method: "POST",
headers: { ...AUTH, "content-type": "application/json" },
body: JSON.stringify(options),
});
const body = await res.json().catch(() => ({}));
if (res.ok) return body as { id: string; connectUrl: string; worker: string };
const retry = RETRY_CODES.has(body.code) || (res.status === 429 && !body.code) || [500, 502, 504].includes(res.status);
if (!retry || i + 1 >= attempts) throw new Error([res.status, body.code, body.error].filter(Boolean).join(" "));
await new Promise((r) => setTimeout(r, Math.min(15_000, 1000 * 2 ** i) * (0.5 + Math.random())));
}
}
export async function withBrowser<T>(options: Record<string, unknown>, work: (browser: Browser) => Promise<T>) {
const session = await createSession(options);
try {
const browser = await chromium.connectOverCDP(session.connectUrl);
try {
return await work(browser);
} finally {
await browser.close().catch(() => {});
}
} finally {
// Ends the session if closing the browser did not (a no-op otherwise).
await fetch(API + "/" + session.id, { method: "DELETE", headers: AUTH }).catch(() => {});
}
}
// await withBrowser({ profile: { name: "shop", persist: true }, country: "de" }, async (browser) => {
// const page = browser.contexts()[0].pages()[0];
// await page.goto("https://example.com");
// });El Playground
El Playground del panel ejecuta un script contra uno de estos navegadores con la vista en vivo, la consola y las capturas de pantalla al lado, y la sección “Use in your code”, justo debajo, muestra las mismas opciones de sesión que el código de arriba. Su page es un pequeño helper que habla directamente por CDP, no Playwright, así que un script del Playground es un boceto que hay que traducir, no un archivo para pegar:
| Helper | Qué hace |
|---|---|
page.goto(url, { timeout? }) | Navegar y esperar el evento load. |
page.click(sel) · page.type(sel, text) · page.press(key) | Entrada real de mouse y teclado, con desplazamiento previo hasta el elemento. |
page.evaluate(fn, ...args) | Ejecutar una función en la página y recibir su resultado en JSON. |
page.waitForSelector(sel, { timeout? }) · page.waitForNavigation() | Esperar un elemento o la siguiente carga de página. |
page.scroll(px) · page.screenshot({ fullPage? }) | Desplazamiento con la rueda del mouse; un JPEG que aparece en la pestaña “Screenshots”. |
page.title() · page.url() · page.content() | El título, la dirección y el HTML del documento. |
log(...values) · sleep(ms) | Imprimir en la consola (los objetos se muestran formateados); pausar. |
cdp(method, params) | Un comando CDP directo al navegador (Target.*, Browser.*, Storage.*). |
page.cdp(method, params) | Un comando CDP directo a la página (Page.*, Runtime.*, DOM.*, Network.*). |
Los errores indican la línea del script de la que provienen. “Share” copia un enlace que lleva el script y sus opciones de sesión en la URL, así que no se guarda nada de nuestro lado; quien lo abra lo ejecuta con su propio saldo.