Pular para o conteúdo

Navegadores hospedados

Inicie um navegador Clearcote nos nossos servidores com uma única chamada de API e controle-o pelo Chrome DevTools Protocol, a partir do Playwright, do Puppeteer ou de qualquer cliente CDP. Nada para instalar nem para rodar por conta própria. Por padrão, o tráfego sai por IPs residenciais, e você paga por GB a partir de um saldo pré-pago.

  • IPs residenciais, não de datacenter. Os sites veem uma conexão de internet doméstica real, de um provedor para consumidores, e não um endereço de hospedagem ou de nuvem.
  • Hardware real, não uma VPS. Os navegadores rodam nos nossos próprios servidores físicos dedicados, e não em máquinas virtuais compartilhadas na nuvem.

GrátisGanhe €5 de tráfego grátis ao conectar o GitHub. Sem cartão.

Uma única vez, um por conta do GitHub. A conta do GitHub precisa ter pelo menos 30 dias.

Resgatar €5

Preços

  • €1.00 por GB, proxy residencial incluído. Por padrão, toda sessão sai para a internet por um IP residencial: uma conexão doméstica real de um provedor de internet para consumidores, e não um endereço de datacenter ou de hospedagem. É esse tráfego que os €1.00 pagam; não existe cobrança separada de proxy.
  • O tráfego é medido entre o navegador e a internet, somando upload e download (1 GB = 109 bytes). Veja o que conta como tráfego.
  • Não há cobrança por tempo, por sessão nem por mensagem CDP.
  • Pré-pago: adicione saldo no painel. Um navegador precisa de pelo menos €0.50 para iniciar, e cada sessão tem como teto o que o saldo consegue pagar no momento em que ela inicia, dividido entre os navegadores que você tem em execução (o seu próprio maxGb vale se for menor). Um navegador em execução é encerrado quando o saldo chega a zero (o uso é reportado a cada 15 segundos, mais ou menos, então o último relatório pode deixar o saldo um pouco abaixo de zero).

O que conta como tráfego

Cada byte que o navegador envia a um site ou recebe dele é contado na rede, do mesmo jeito que um provedor de proxy conta. Numa página típica, isso significa:

  • Conta: a própria página e tudo o que ela carrega: scripts, folhas de estilo, imagens, fontes, vídeo, chamadas de API, anúncios e rastreadores, WebSockets, além dos cabeçalhos das requisições, dos cookies e do overhead de criptografia (TLS) de cada conexão. As páginas continuam carregando enquanto você espera, então polling em segundo plano e analytics também contam.
  • Não conta: a conexão CDP entre o seu código e o navegador (comandos, resultados, screenshots, PDFs, conteúdo da página que você extrai), a visualização ao vivo no painel e tudo o que o navegador nunca chega a buscar (requisições que você bloqueia, arquivos que ele serve do próprio cache).

Como referência aproximada: uma página leve, só de texto, fica bem abaixo de 1 MB; uma página típica de notícias ou de loja, entre 2 e 5 MB; e uma single-page app pesada ou qualquer coisa com vídeo, 10 MB ou mais. A €1.00 por GB, 1.000 páginas de 3 MB cada dão cerca de 3 GB. A sua própria sessão mostra os números reais: o painel lista o tráfego de cada sessão e os 20 sites que mais consumiram bytes nela, e o maxGb limita uma sessão para que uma página descontrolada não consuma todo o seu saldo.

Como reduzir o tráfego

A maior parte do peso de uma página costuma ser coisa de que um script não precisa. As maiores economias, em ordem:

  1. Bloqueie imagens, mídia e fontes. Muitas vezes são metade da página ou mais. Bloqueie-as por padrão de URL no navegador e elas nem saem dele, então nunca são cobradas — e o navegador mantém o cache:
javascript
// 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"] });

Evite page.route() / context.route() e a interceptação de requisições do Puppeteer só para descartar requisições: elas desligam o cache do navegador, então cada página baixa os scripts e estilos de novo, o que custa mais do que as imagens que você economizou. Use-as só quando precisar alterar requisições. A opção adblock também bloqueia anúncios e rastreadores para você.

  1. Bloqueie anúncios, analytics e rastreadores. Aborte as requisições para domínios de terceiros de que você não precisa. A lista dos principais sites da sessão, no painel, mostra quais custam mais.
  2. Não espere mais do que o necessário. waitUntil: "networkidle" espera tudo o que a página carrega, inclusive anúncios. Prefira "domcontentloaded" e depois espere só pelo elemento de que você realmente precisa.
  3. Feche o navegador assim que terminar. Uma página aberta continua fazendo polling em segundo plano. Diminua o idleTimeoutSec para que uma sessão esquecida feche sozinha.
  4. Reutilize um navegador para muitas páginas. Graças ao cache, scripts e estilos compartilhados entre páginas do mesmo site são baixados uma vez só, e não a cada página. Navegue na mesma sessão em vez de abrir uma nova para cada URL.
  5. Chame a API do site quando puder. Depois que o navegador tem uma sessão funcionando, um fetch() de dentro da página para buscar o JSON de que você precisa pesa uma fração do que pesa carregar a página de novo.
  6. Defina um teto. Configure maxGb em toda sessão para que uma página inesperadamente pesada pare em vez de drenar o seu saldo.

O bloqueio funciona na maioria dos sites, mas alguns verificam se as imagens ou as fontes realmente carregaram. Se um site se comportar de outro jeito com o bloqueio ativado, libere de novo aquele tipo de recurso para esse site.

Quer ver funcionando antes? O Playground roda um script num navegador na nuvem direto do seu painel, com a visualização ao vivo, a saída do console e os screenshots lado a lado.

1. Obtenha uma chave de API

Crie uma na página API keys. Ela começa com cc_live_ e é enviada como bearer token. Guarde-a em segredo: qualquer pessoa que tiver a chave pode gastar o seu saldo.

2. Inicie um navegador e conecte-se

POST /api/v1/browsers retorna uma connectUrl: uma URL WebSocket de uso único para aquele navegador. Conecte-se em até dois minutos; ela não pode ser usada duas vezes.

javascript
// 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
javascript
// Puppeteer: the same connectUrl
const browser = await puppeteer.connect({ browserWSEndpoint: connectUrl, defaultViewport: null });
python
# 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()

A chamada de criação responde 201 com tudo o que o seu código precisa para se conectar e saber o preço:

json
{
  "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
}

Opções

Todas são opcionais. Envie-as como corpo JSON da chamada de criação.

CampoTipoSignificado
identitystringOne label per account you run: the same device fingerprint AND the same residential IP, for as long as that IP stays online.
fingerprintstringDevice 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.
lightStealthbooleanDefault true: varies only the metadata axes the host can back up. Set false to turn it off.
platformwindows | macos | linux | androidOperating system the persona presents.
brandChrome | Edge | Opera | VivaldiBrowser brand the persona presents.
timezoneIANA namee.g. America/New_York. Use geoip instead to follow the exit IP.
localestringAccept-Language, e.g. en-US,en.
geoipbooleanTimezone 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).
country2-letter codeManaged pool: exit country, e.g. us, de, gb.
stateregion codeManaged pool: exit state/region, e.g. ca, ny. Needs country.
citycity nameManaged pool: exit city, e.g. "los angeles". Needs state.
proxySessionstringManaged pool: sticky label. The same label returns the same exit IP later (kept for 24 hours).
timeoutSecnumberHard limit on the session length, in seconds (10 up to the account maximum below).
idleTimeoutSecnumberEnd the session after this long without a CDP command (10–1800).
maxGbnumberStop the session after this much traffic (0.001–1000).
headlessbooleanDefault true.
keepAlivebooleanDefault 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.
versionstringRun 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".
urlhttp(s) URLOpened in the first tab before you connect: you find it already loading.
adblockbooleanRefuse known ad and tracker hosts before they load, so they are never billed. Default false.
notestringYour label for the session (at most 256 characters). Shown in the dashboard; filter by it in the list.
workerstringPlace the session on the same server as an earlier one (its worker). 503 if that server is full.

Padrões de stealth e identidades

Toda sessão parte das configurações da página de configurações recomendadas: lightStealth ativado, uma seed, e fuso horário e idioma que acompanham o IP de saída. Com lightStealth (o padrão), a seed escolhe o perfil de dispositivo dentro de um conjunto pequeno que varia núcleos de CPU, memória e pixel ratio; canvas, WebGL e áudio são os da máquina em que a sessão roda. Defina lightStealth: false para ter uma persona completa por seed: as leituras de canvas e WebGL recebem ruído por site derivado da seed, e as strings de GPU, a tela e as configurações de áudio (taxa de amostragem, latência) seguem a persona. Sem identity nem fingerprint, cada sessão recebe uma seed aleatória e um IP novo.

Passe identity: "account-42" para voltar em sessões futuras com o mesmo perfil de dispositivo e no mesmo IP, enquanto esse IP residencial continuar online, que é o que uma conta logada espera. As identidades são privadas da sua conta: outro cliente que use o mesmo rótulo recebe a própria seed e o próprio IP. Sozinha, uma identidade não guarda cookies: para isso, use um perfil.

Perfis: faça login uma vez só

Um perfil guarda os cookies, o localStorage e o IndexedDB de uma sessão sob um nome, para que a próxima sessão com esse nome já comece logada. Passe profile: { name: "shop-account", persist: true } para carregá-lo e salvá-lo de volta quando a sessão terminar, ou apenas profile: "shop-account" para carregá-lo em modo somente leitura. A primeira sessão com um nome novo começa vazia e cria o perfil.

javascript
// 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" }),
});
  • Mesmo dispositivo, mesmo IP. Um perfil traz a própria identidade (profile:<name>), então o site vê o mesmo fingerprint e, enquanto esse IP continuar online, o mesmo IP residencial, como um cliente que volta. Passe identity ou fingerprint você mesmo para escolher outra coisa, e mantenha o mesmo país entre as execuções.
  • Só uma sessão grava por vez. Apenas uma sessão em execução pode salvar num perfil; uma segunda com persist: true recebe 409 PROFILE_IN_USE. Sessões somente leitura podem rodar em paralelo e veem o último estado salvo.
  • Salvo quando a sessão termina, seja porque você fechou o navegador, desconectou ou parou a sessão, seja porque um limite a encerrou. Se o navegador tiver travado, o estado salvo anterior é mantido em vez de ser substituído por um parcial. Cookies de sessão (os que não têm data de expiração) são descartados, como um navegador de verdade faz ao reiniciar.
  • Até cerca de 3,5 MB comprimido. Se o IndexedDB de um site passar disso, o IndexedDB fica de fora; cookies e localStorage continuam sendo salvos.
  • Privado. Um perfil é só seu (o “shop-account” de outro cliente é outro perfil), fica armazenado criptografado e só é entregue ao servidor que roda a sua sessão. Liste os perfis com GET /api/v1/browsers/profiles, apague um com DELETE /api/v1/browsers/profiles/<name> ou use o painel.

IPs de saída: rotativos, sticky e por localização

  • Padrão: cada sessão de navegador recebe o próprio IP residencial de saída e fica com ele durante a sessão inteira.
  • Sticky: passe o mesmo rótulo proxySession (por exemplo, um por conta que você gerencia) para receber de volta o mesmo IP de saída numa sessão posterior. Os rótulos são privados da sua conta. Um IP residencial fica disponível enquanto o peer dele estiver online, normalmente por várias horas; quando ele sai do ar, você recebe outro IP da mesma rede.
  • Localização: country e, opcionalmente, state e city. Quanto mais específico o alvo, menor o pool.
  • Seu próprio proxy: proxy: { server: "http://host:port", username, password }, ou socks5://host:port (nomes resolvidos do nosso lado) ou socks5h://host:port (nomes resolvidos pelo seu proxy). A requisição é validada com rigor: porta explícita, credenciais em username / password (uma URL user:pass@host dá 400; no máximo 255 bytes cada), e country, state, city e proxySession são rejeitados em vez de ignorados, porque descrevem o pool gerenciado. O endereço do próprio proxy precisa ser público. O tráfego é cobrado do mesmo jeito.

Por padrão, fuso horário e idioma acompanham o IP de saída (geoip). Passe timezone / locale para escolhê-los você mesmo, ou geoip: false para desativar.

Fixando uma versão

As sessões rodam a versão atual do Clearcote. Para rodar uma mais antiga, passe version: a versão completa ("152.0.7977.82-r21"), só o rebuild ("r21"), uma versão ou major do Chromium ("152" pega o build mais recente dela) ou "latest". São as mesmas versões que a opção version do SDK baixa, então uma sessão hospedada e uma execução local fixadas na mesma versão usam o mesmo build.

javascript
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 ... " ]
  • Confira os warnings. Uma versão mais antiga não tem o que as posteriores adicionaram. Opções e correções que vieram depois dela podem estar ausentes, ou ser ignoradas em silêncio em vez de rejeitadas, então uma configuração que funciona na versão atual pode simplesmente não fazer nada numa versão fixada.
  • O engine da resposta sempre informa qual versão a sessão roda, fixada ou não.
  • Uma versão que não existe resulta em 400 com o código UNKNOWN_VERSION, e a mensagem lista as versões que você pode escolher.
  • A primeira sessão numa versão que os nossos servidores ainda não usaram pode levar até um minuto a mais para iniciar, enquanto o build é baixado. As sessões seguintes nessa versão iniciam tão rápido quanto qualquer outra.

Página inicial e bloqueio de anúncios

  • url abre uma página na primeira aba antes de você se conectar, então ela já está carregando quando o seu script se conecta.
  • adblock: true recusa requisições para hosts conhecidos de anúncios, de verificação de anúncios e de analytics antes que elas sejam feitas, então elas nunca são cobradas. A lista é conservadora de propósito (gerenciadores de tags, ferramentas de consentimento, SDKs de login e CAPTCHAs ficam de fora), mas alguns sites percebem a falta dos anúncios; deixe desativado onde isso importar.

Visualização ao vivo: assistir, assumir o controle, compartilhar

No painel, clique numa sessão para ver para quais sites foi o tráfego dela e para assistir ao vivo. Clique em Take control para clicar, digitar, rolar, colar e navegar nela você mesmo, por exemplo para fazer login ou passar por uma verificação que o seu script não consegue. O seu script continua conectado o tempo todo, então pause-o enquanto você age. Input humano conta como atividade, então uma sessão que você está controlando não é fechada por inatividade. Share gera um link que qualquer pessoa pode abrir sem conta, só para assistir ou com controle, por 15 minutos a 4 horas e nunca além do fim da sessão.

Pela API:

bash
# 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>/share

Com controle, envie mensagens de texto JSON pelo mesmo WebSocket. As coordenadas são frações (de 0 a 1) do frame que você está vendo; qualquer outra coisa é ignorada.

MensagemO que faz
{"t":"mouse","e":"down","x":0.5,"y":0.3,"b":"left","n":1,"m":0}Pressiona (down), solta (up) ou move; n é o número de cliques e m, os modificadores (Alt 1, Ctrl 2, Meta 4, Shift 8).
{"t":"wheel","x":0.5,"y":0.5,"dx":0,"dy":400}Rola uma quantidade de pixels num ponto.
{"t":"key","e":"down","key":"a","code":"KeyA","kc":65,"text":"a"}Uma tecla sendo pressionada ou solta, do jeito que um teclado envia.
{"t":"text","text":"pasted text"}Insere texto como se tivesse sido digitado (até 5000 caracteres).
{"t":"nav","a":"back"}, forward, reload ou {"t":"nav","a":"go","url":"example.com"}Histórico, recarregar ou abrir um endereço http(s).

GET /api/v1/browsers/<id> inclui traffic: os 20 sites com mais bytes naquela sessão.

Gerenciando sessões

bash
# 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&note=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>

Dê uma note à sessão ao criá-la para encontrá-la de novo na lista e no painel. Para iniciar uma sessão no mesmo servidor de uma anterior (caches aquecidos, a mesma máquina), passe o worker daquela sessão; quando esse servidor estiver cheio, você recebe um 503, e não outro servidor.

Uma sessão também termina quando você fecha o navegador ou desconecta, e quando um dos limites abaixo é atingido. Um pedido de parada encerra na hora uma sessão à qual ninguém se conectou; um navegador em execução é fechado pelo servidor dele dentro de um intervalo de relatório, cerca de 15 segundos. O GET responde com:

json
{
  "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 }
}
statusSignificado
pendingCreated; nobody has connected yet. Counts towards the concurrency limit until it starts or expires.
activeA browser is running and reporting usage.
lostNo usage report for 5 minutes. Billed up to the last report; a late report puts it back to active.
endedClosed: you disconnected, stopped it, or a limit or the balance ended it. endReason says which.
expiredNobody connected within two minutes of creating it. Never billed.

A chamada de listagem, GET /api/v1/browsers, retorna { balanceEur, sessions: [...] } com os mesmos objetos de sessão, dos mais recentes para os mais antigos.

Mantendo o navegador aberto e reconectando

Por padrão, uma sessão termina quando o seu cliente desconecta. Inicie-a com keepAlive: true e o navegador continua rodando, com as abas, os cookies e o IP de saída, para que um script posterior (ou o mesmo, depois de um crash ou de fechar o notebook) continue de onde o anterior parou:

bash
# 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
  • Deixe-o rodando desconectando: browser.close() do Playwright (via connectOverCDP ele só desconecta), browser.disconnect() do Puppeteer ou simplesmente encerrando o seu processo.
  • Encerre-o com DELETE /api/v1/browsers/<id> ou enviando o comando CDP Browser.close: o browser.close() do Puppeteer faz isso e, no Playwright, await (await browser.newBrowserCDPSession()).send("Browser.close"). Até lá, ele continua ocupando uma vaga no seu limite de concorrência.
  • Um cliente por vez: uma reconexão enquanto outro cliente está conectado é recusada com 409, assim como uma reconexão a uma sessão iniciada sem keepAlive.
  • Os limites continuam valendo enquanto ninguém está conectado: idleTimeoutSec (aumente, até 1800, para um navegador ao qual você pretende voltar), timeoutSec, maxGb e o seu saldo. Uma página deixada aberta continua carregando tráfego em segundo plano, que é cobrado como qualquer outro.

Limites

  • 24 navegadores rodando ou iniciando ao mesmo tempo por conta.
  • As sessões duram no máximo 4 horas.
  • Uma sessão sem nenhum comando CDP por 5 minutos é fechada (altere isso com idleTimeoutSec).
  • Toda sessão começa com um perfil de navegador novo, apagado quando a sessão termina, a menos que você use um perfil com nome, que guarda os cookies e o armazenamento dos sites entre sessões.
  • Por segurança, o navegador não pode abrir arquivos locais (file://), fazer upload de arquivos a partir do servidor, acessar redes privadas ou internas nem enviar e-mail pela porta 25.
  • Upload de arquivos funciona pelo Playwright: setInputFiles() envia o arquivo da sua máquina (até 50 MB). O uploadFile() do Puppeteer é recusado. Os downloads ficam no nosso servidor e são apagados junto com a sessão; para ficar com um arquivo, busque-o de dentro da página e retorne o conteúdo.
  • Não é possível carregar extensões do Chrome em navegadores hospedados.
  • O navegador não repassa mensagens do console nem erros da página, então page.on("console") fica em silêncio. Colete o que precisar dentro da página e leia de volta com evaluate.

Erros

StatuscodeSignificado
400—The body is not JSON, or an option is invalid; the message says which.
400UNKNOWN_VERSIONNo release matches version; the message lists the ones you can pick.
401—Missing, malformed or revoked API key.
402INSUFFICIENT_BALANCEBalance below the minimum. Top up in the dashboard.
404NOT_FOUNDNo session with that id on your account.
409NOT_RUNNINGLive view or a reconnect asked for before the browser started or after it ended.
409NOT_KEEPALIVEThis session cannot be reconnected. Start it with keepAlive: true.
409PROFILE_IN_USEAnother session is already saving to that profile. Stop it, or open the profile with persist: false.
429CONCURRENCY_LIMITToo many browsers running or starting at once. Close one first.
429—More than 60 create calls in a minute from one address. Slow down.
503NO_CAPACITYNo free browser slot right now. Retry after a few seconds.
503NO_WORKERThe server running that session is not reachable at the moment.
503NOT_CONFIGUREDHosted browsers are not configured on this server.
503NOT_AVAILABLENotes or profiles are not enabled on this server yet.

Os erros vêm em JSON: { "error": "...", "code": "..." }. Se a própria conexão WebSocket for recusada, crie uma nova sessão: as URLs de conexão são de uso único e expiram após dois minutos. Um upgrade de WebSocket recusado responde com um status HTTP e um error em JSON: 409 quando a URL já foi usada, a sessão foi cancelada, não foi iniciada com keepAlive ou já está conectada; 401 quando a URL expirou.

O que tentar de novo

  • Tente de novo com backoff: 503 NO_CAPACITY e 503 NO_WORKER (espere 1, 2, 4… segundos com um pouco de jitter e desista depois de algumas tentativas), e um 429 sem código (o rate limit por endereço).
  • Tente algumas vezes com backoff: outras respostas 5xx e uma conexão recusada antes de o seu script começar (com uma nova sessão: a URL de conexão antiga já foi gasta).
  • Nunca em loop: 400 (corrija a requisição), 401, 402 (adicione crédito), 429 CONCURRENCY_LIMIT (feche um navegador antes) e 409 PROFILE_IN_USE. Alguém precisa agir; tentar de novo só queima requisições.

Boas práticas

  1. Conecte-se, nunca faça launch. Use connectOverCDP ou puppeteer.connect com a connectUrl; já o chromium.launch() inicia um navegador na sua própria máquina.
  2. Use o que já existe. Pegue browser.contexts()[0] e a primeira página dele em vez de criar um contexto novo: um contexto novo começa sem os cookies e o armazenamento do perfil, e o Playwright dá a ele uma viewport emulada de 1280×720 que não bate com a janela do navegador.
  3. Defina a persona na criação, não pelo script. País, fuso horário e idioma vão na chamada de criação. Sobrescrever o user agent, a viewport ou propriedades do navigator a partir de um script cria exatamente as inconsistências que a detecção procura.
  4. Mantenha os hooks de CDP enxutos. Listeners amplos, interceptação de todas as requisições e init scripts são o fingerprint da própria automação. O Playwright e o Puppeteer originais funcionam como estão: o motor mantém os efeitos colaterais do Runtime.enable longe da própria página, então um driver modificado como o Patchright é opcional, e não necessário.
  5. Uma sessão, muitas páginas. Iniciar um navegador é a parte lenta; navegue dentro dele. Faça login uma vez com um perfil em vez de fazer a cada execução.
  6. Sempre encerre. Feche o navegador num finally e ajuste maxGb e idleTimeoutSec ao trabalho, para que um bug não deixe um navegador rodando às custas do seu saldo.
  7. Olhe antes de escrever código. Quando um site se comportar de forma estranha, acompanhe-o na visualização ao vivo (ou assuma o controle) antes de adicionar waits e workarounds.

Frameworks

Qualquer coisa que se conecte ao Chrome via CDP funciona com a connectUrl. Ela é de uso único, então um framework que se reconecta sozinho precisa de uma nova sessão para cada conexão. O navegador inicia quando você se conecta, geralmente em poucos segundos; não há nenhum status para consultar antes.

javascript
// 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];
python
# 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")
javascript
// Stagehand v4
import { localBrowser, Stagehand } from "@browserbasehq/stagehand";
const stagehand = await Stagehand.create({ browser: await localBrowser.connect({ cdpUrl: connectUrl }) });

Um helper para começar

Criar com as novas tentativas descritas acima, conectar e sempre encerrar, tudo numa só função:

typescript
// 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");
// });

O Playground

O Playground do painel roda um script contra um desses navegadores com a visualização ao vivo, o console e os screenshots ao lado, e a área “Use in your code”, logo abaixo, mostra as mesmas opções de sessão do código acima. O page dele é um pequeno helper que fala diretamente via CDP, e não o Playwright, então um script do Playground é um rascunho para adaptar, não um arquivo para colar:

HelperO que faz
page.goto(url, { timeout? })Navega e espera o evento load.
page.click(sel) · page.type(sel, text) · page.press(key)Input real de mouse e teclado, rolando o elemento para a área visível antes.
page.evaluate(fn, ...args)Executa uma função na página e devolve o resultado em JSON.
page.waitForSelector(sel, { timeout? }) · page.waitForNavigation()Espera um elemento ou o próximo carregamento de página.
page.scroll(px) · page.screenshot({ fullPage? })Rolagem com a roda do mouse; um JPEG que vai para a aba “Screenshots”.
page.title() · page.url() · page.content()O título, o endereço e o HTML do documento.
log(...values) · sleep(ms)Imprime no console (objetos saem formatados); pausa.
cdp(method, params)Um comando CDP bruto para o navegador (Target.*, Browser.*, Storage.*).
page.cdp(method, params)Um comando CDP bruto para a página (Page.*, Runtime.*, DOM.*, Network.*).

Os erros informam a linha do script de onde vieram. Share copia um link que leva o script e as opções de sessão na própria URL, então nada fica armazenado do nosso lado; quem abrir o link roda o script com o próprio saldo.