Navigateurs hébergés
Lancez un navigateur Clearcote sur nos serveurs en un seul appel d’API et pilotez-le via le Chrome DevTools Protocol, depuis Playwright, Puppeteer ou n’importe quel client CDP. Rien à installer ni à faire tourner vous-même. Par défaut, le trafic sort par des IP résidentielles, et vous payez au Go sur un solde prépayé.
- Des IP résidentielles, pas de datacenter. Les sites voient une vraie connexion internet domestique, chez un fournisseur d’accès grand public, pas une adresse d’hébergeur ou de cloud.
- Du vrai matériel, pas un VPS. Les navigateurs tournent sur nos propres serveurs physiques dédiés, pas sur des machines virtuelles partagées dans le cloud.
GratuitRecevez €5 de trafic offerts en connectant GitHub. Sans carte.
Offre unique, une seule par compte GitHub. Le compte GitHub doit avoir été créé il y a au moins 30 jours.
Tarifs
- €1.00 par Go, proxy résidentiel inclus. Par défaut, chaque session sort sur internet par une IP résidentielle : une vraie connexion domestique chez un fournisseur d’accès grand public, pas une adresse de datacenter ou d’hébergeur. C’est ce trafic que vous payez €1.00 ; il n’y a pas de facture de proxy séparée.
- Le trafic est mesuré entre le navigateur et internet, envoi et réception cumulés (1 Go = 109 octets). Voir ce qui est compté comme trafic.
- Rien n’est facturé au temps, à la session ni au message CDP.
- Prépayé : rechargez votre solde dans le tableau de bord. Un navigateur a besoin d’au moins €0.50 pour démarrer, et chaque session est plafonnée à ce que le solde peut payer au moment où elle démarre, montant réparti entre les navigateurs en cours d’exécution (votre propre
maxGbs’applique s’il est plus bas). Un navigateur en cours d’exécution est arrêté quand le solde atteint zéro (la consommation est remontée environ toutes les 15 secondes, si bien que le dernier relevé peut le faire passer légèrement sous zéro).
Ce qui est compté comme trafic
Chaque octet que le navigateur envoie à un site web ou en reçoit est compté tel qu’il passe sur le réseau, exactement comme le compte un fournisseur de proxy. Pour une page typique, cela donne :
- Compté : la page elle-même et tout ce qu’elle charge : scripts, feuilles de style, images, polices, vidéos, appels d’API, publicités et trackers, WebSockets, plus les en-têtes de requête, les cookies et le surcoût du chiffrement (TLS) de chaque connexion. Les pages continuent de se charger pendant que vous attendez : le polling en arrière-plan et les outils d’analytics comptent donc aussi.
- Non compté : la connexion CDP entre votre code et le navigateur (commandes, résultats, captures d’écran, PDF, contenu de page que vous récupérez), la vue en direct du tableau de bord, et tout ce que le navigateur ne télécharge jamais (requêtes que vous bloquez, fichiers qu’il sert depuis son propre cache).
À titre indicatif, une page de texte légère pèse bien moins de 1 Mo, une page d’actualité ou de boutique typique 2 à 5 Mo, et une single-page app lourde, ou tout ce qui contient de la vidéo, 10 Mo ou plus. À €1.00 le Go, 1000 pages de 3 Mo chacune représentent environ 3 Go. Votre propre session affiche ses chiffres réels : le tableau de bord indique le trafic de chaque session et ses 20 sites les plus gourmands en octets, et maxGb plafonne une session pour qu’une page qui s’emballe ne puisse pas engloutir votre solde.
Réduire le trafic
L’essentiel du poids d’une page correspond généralement à des éléments dont un script n’a pas besoin. Les plus grosses économies, par ordre d’importance :
- Bloquez images, médias et polices. C’est souvent la moitié d’une page, voire plus. Bloquez-les par motif d’URL dans le navigateur : ces requêtes n’en sortent jamais, elles ne sont donc jamais facturées — et le navigateur conserve son cache :
// 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"] });Évitez page.route() / context.route() et l’interception de requêtes de Puppeteer pour simplement abandonner des requêtes : ces mécanismes désactivent le cache du navigateur, si bien que chaque page retélécharge ses scripts et ses styles, ce qui coûte plus cher que les images économisées. Réservez-les aux cas où vous devez modifier des requêtes. L’option adblock bloque aussi les publicités et les trackers pour vous.
- Bloquez publicités, analytics et trackers. Annulez les requêtes vers les domaines tiers dont vous n’avez pas besoin. Dans le tableau de bord, la liste des sites les plus consommateurs de la session montre lesquels vous coûtent le plus.
- N’attendez pas plus que nécessaire.
waitUntil: "networkidle"attend tout ce que charge la page, publicités comprises. Préférez"domcontentloaded", puis attendez le seul élément dont vous avez réellement besoin. - Fermez le navigateur dès que vous avez terminé. Une page ouverte continue de faire du polling en arrière-plan. Réduisez
idleTimeoutSecpour qu’une session oubliée se ferme d’elle-même. - Réutilisez un même navigateur pour de nombreuses pages. Grâce à son cache, les scripts et les styles partagés entre les pages d’un même site ne sont téléchargés qu’une fois, et non à chaque page. Naviguez dans la même session plutôt que d’en démarrer une nouvelle pour chaque URL.
- Appelez l’API du site quand c’est possible. Une fois que le navigateur a une session fonctionnelle, un
fetch()lancé depuis la page pour récupérer le JSON dont vous avez besoin ne pèse qu’une fraction d’un nouveau chargement de la page. - Plafonnez. Définissez
maxGbsur chaque session pour qu’une page plus lourde que prévu s’arrête au lieu de vider votre solde.
Le blocage fonctionne sur la plupart des sites, mais quelques-uns vérifient que les images ou les polices ont bien été chargées. Si un site se comporte différemment quand le blocage est actif, réautorisez ce type de ressource pour ce site.
Envie de le voir fonctionner d’abord ? Le Playground exécute un script dans un navigateur cloud directement depuis votre tableau de bord, avec la vue en direct, la sortie de la console et les captures d’écran côte à côte.
1. Obtenir une clé d’API
Créez-en une sur la page « API keys ». Elle commence par cc_live_ et est envoyée comme jeton Bearer. Gardez-la secrète : quiconque la détient peut dépenser votre solde.
2. Démarrer un navigateur et s’y connecter
POST /api/v1/browsers renvoie un connectUrl : une URL WebSocket à usage unique pour ce navigateur. Connectez-vous dans les deux minutes ; elle ne peut pas servir deux fois.
// 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()L’appel de création répond 201 avec tout ce dont votre code a besoin pour se connecter et connaître le prix :
{
"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
}Options
Toutes sont facultatives. Envoyez-les dans le corps JSON de l’appel de création.
| Champ | Type | Signification |
|---|---|---|
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. |
Furtivité par défaut et identités
Chaque session part des réglages décrits sur la page Réglages recommandés : lightStealth activé, un seed, et un fuseau horaire et une langue qui suivent l’IP de sortie. Avec lightStealth (le défaut), le seed choisit le profil d’appareil dans un petit ensemble qui fait varier le nombre de cœurs CPU, la mémoire et la densité de pixels ; canvas, WebGL et audio sont ceux de la machine sur laquelle tourne la session. Passez lightStealth: false pour une persona complète propre à chaque seed : les pixels relus depuis canvas et WebGL reçoivent un bruit par site dérivé du seed, et les chaînes GPU, l’écran et les réglages audio (fréquence d’échantillonnage, latence) suivent la persona. Sans identity ni fingerprint, chaque session reçoit un seed aléatoire et une nouvelle IP.
Passez identity: "account-42" pour retrouver le même profil d’appareil lors des sessions suivantes, sur la même IP tant que cette IP résidentielle reste en ligne : c’est ce qu’attend un compte connecté. Les identités sont propres à votre compte : un autre client qui utilise le même libellé obtient son propre seed et sa propre IP. À elle seule, une identité ne conserve pas les cookies : pour cela, utilisez un profil.
Profils : se connecter une seule fois
Un profil conserve sous un nom les cookies, le localStorage et l’IndexedDB d’une session, si bien que la session suivante sous ce nom démarre déjà connectée. Passez profile: { name: "shop-account", persist: true } pour le charger puis le réenregistrer à la fin de la session, ou simplement profile: "shop-account" pour le charger en lecture seule. La première session sous un nouveau nom démarre vide et crée le profil.
// 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" }),
});- Même appareil, même IP. Un profil apporte sa propre identité (
profile:<name>) : le site voit la même empreinte, et la même IP résidentielle tant que celle-ci reste en ligne, comme pour un client qui revient. Passez vous-mêmeidentityoufingerprintpour en décider autrement, et gardez le même pays d’une exécution à l’autre. - Une seule session en écriture à la fois. Seule une session en cours peut enregistrer dans un profil ; une deuxième avec
persist: truereçoit409 PROFILE_IN_USE. Des sessions en lecture seule peuvent tourner en parallèle et voient le dernier état enregistré. - Enregistré à la fin de la session, que vous fermiez le navigateur, que vous vous déconnectiez, que vous l’arrêtiez ou qu’une limite y mette fin. Si le navigateur a planté, l’état enregistré précédent est conservé plutôt que remplacé par un état partiel. Les cookies de session (ceux sans date d’expiration) sont supprimés, comme un vrai navigateur les supprime au redémarrage.
- Jusqu’à environ 3,5 Mo une fois compressé. Si l’IndexedDB d’un site fait dépasser cette taille, l’IndexedDB est laissé de côté ; les cookies et le localStorage sont tout de même enregistrés.
- Privé. Un profil n’appartient qu’à vous (le « shop-account » d’un autre client est un autre profil), il est stocké chiffré et n’est transmis qu’au serveur qui exécute votre session. Listez vos profils avec
GET /api/v1/browsers/profiles, supprimez-en un avecDELETE /api/v1/browsers/profiles/<name>, ou passez par le tableau de bord.
IP de sortie : rotatives, sticky et géociblées
- Par défaut : chaque session de navigateur reçoit sa propre IP de sortie résidentielle et la garde pendant toute la session.
- Sticky : passez le même libellé
proxySession(par exemple un par compte que vous gérez) pour retrouver la même IP de sortie lors d’une session ultérieure. Les libellés sont propres à votre compte. Une IP résidentielle reste disponible tant que son pair est en ligne, généralement plusieurs heures ; quand il se déconnecte, vous obtenez une autre IP du même réseau. - Localisation :
country, puis éventuellementstateetcity. Plus le ciblage est précis, plus le pool est restreint. - Votre propre proxy :
proxy: { server: "http://host:port", username, password }, ousocks5://host:port(noms résolus de notre côté) ousocks5h://host:port(noms résolus par votre proxy). La requête est vérifiée strictement : un port explicite, des identifiants dansusername/password(une URLuser:pass@hostrenvoie une erreur 400 ; 255 octets au maximum chacun), etcountry,state,cityetproxySessionsont rejetés plutôt qu’ignorés, car ils décrivent le pool géré. L’adresse du proxy lui-même doit être publique. Le trafic est facturé de la même façon.
Par défaut, le fuseau horaire et la langue suivent l’IP de sortie (geoip). Passez timezone / locale pour les choisir vous-même, ou geoip: false pour désactiver ce comportement.
Épingler une version
Les sessions exécutent la version actuelle de Clearcote. Pour en exécuter une plus ancienne, passez version : la version complète ("152.0.7977.82-r21"), seulement le rebuild ("r21"), une version ou une version majeure de Chromium ("152" prend le build le plus récent de celle-ci), ou "latest". Ce sont les mêmes versions que télécharge l’option version du SDK : une session hébergée et une exécution locale épinglées sur la même valeur utilisent donc le même 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 ... " ]- Vérifiez
warnings. Une version plus ancienne n’a pas ce que les versions suivantes ont ajouté. Les options et les correctifs arrivés après elle peuvent manquer, ou être ignorés silencieusement au lieu d’être rejetés : un réglage qui fonctionne sur la version actuelle peut donc ne rien faire, sans rien signaler, sur une version épinglée. - Le champ
enginede la réponse indique toujours quelle version exécute la session, épinglée ou non. - Une version qui n’existe pas renvoie une erreur
400avec le codeUNKNOWN_VERSION, et le message liste les versions parmi lesquelles vous pouvez choisir. - La première session sur une version que nos serveurs n’ont pas encore utilisée peut mettre jusqu’à une minute de plus à démarrer, le temps de récupérer le build. Les sessions suivantes sur cette version démarrent aussi vite que les autres.
Page de démarrage et blocage des publicités
urlouvre une page dans le premier onglet avant votre connexion : elle est déjà en cours de chargement quand votre script s’y attache.adblock: truerefuse les requêtes vers les hôtes connus de publicité, de vérification publicitaire et d’analytics avant même qu’elles ne partent : elles ne sont donc jamais facturées. La liste est volontairement prudente (tag managers, outils de consentement, SDK de connexion et CAPTCHA ne sont pas touchés), mais quelques sites remarquent l’absence de publicités ; laissez l’option désactivée là où cela compte.
Vue en direct : regarder, prendre la main, partager
Dans le tableau de bord, cliquez sur une session pour voir vers quels sites est allé son trafic et pour la suivre en direct. Appuyez sur « Take control » pour cliquer, saisir du texte, faire défiler, coller et naviguer vous-même, par exemple pour vous connecter ou franchir une vérification que votre script ne sait pas franchir. Votre script reste connecté pendant tout ce temps : mettez-le en pause pendant que vous intervenez. Les saisies humaines comptent comme de l’activité : une session que vous pilotez n’est donc pas fermée pour inactivité. « Share » crée un lien que n’importe qui peut ouvrir sans compte, en lecture seule ou avec le contrôle, pour une durée de 15 minutes à 4 heures et jamais au-delà de la fin de la session.
Via l’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>/shareAvec le contrôle, envoyez des messages texte JSON sur le même WebSocket. Les coordonnées sont des fractions (de 0 à 1) de l’image que vous regardez ; tout le reste est ignoré.
| Message | Effet |
|---|---|
{"t":"mouse", | Appui (down), relâchement (up) ou move ; n est le nombre de clics, m les modificateurs (Alt 1, Ctrl 2, Meta 4, Shift 8). |
{"t":"wheel", | Défilement d’un nombre de pixels à un point donné. |
{"t":"key", | Une touche enfoncée ou relâchée, telle qu’un clavier l’envoie. |
{"t":"text", | Insère du texte comme s’il avait été tapé (jusqu’à 5000 caractères). |
{"t":"nav",, forward, reload ou {"t":"nav", | Historique, rechargement ou ouverture d’une adresse http(s). |
GET /api/v1/browsers/<id> inclut traffic : les 20 sites qui ont consommé le plus d’octets pendant cette session.
Gérer les sessions
# 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>Donnez une note à une session dès sa création pour la retrouver dans la liste et dans le tableau de bord. Pour démarrer une session sur le même serveur qu’une session précédente (caches déjà chauds, même machine), passez le worker de cette session ; si ce serveur est plein, vous recevez une 503, pas un autre serveur.
Une session se termine aussi quand vous fermez le navigateur ou vous déconnectez, et quand l’une des limites ci-dessous est atteinte. Une demande d’arrêt met fin immédiatement à une session à laquelle personne ne s’est connecté ; un navigateur en cours d’exécution est fermé par son serveur dans l’intervalle de remontée suivant, soit environ 15 secondes. GET répond avec :
{
"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 | Signification |
|---|---|
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. |
L’appel de liste, GET /api/v1/browsers, renvoie { balanceEur, sessions: [...] } avec les mêmes objets de session, du plus récent au plus ancien.
Garder un navigateur ouvert et s’y reconnecter
Par défaut, une session se termine quand votre client se déconnecte. Démarrez-la avec keepAlive: true et le navigateur continue au contraire de tourner, avec ses onglets, ses cookies et son IP de sortie : un script ultérieur (ou le même, après un plantage ou un portable refermé) peut reprendre là où le précédent s’était arrêté :
# 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- Laissez-le tourner en vous déconnectant :
browser.close()de Playwright (viaconnectOverCDP, il ne fait que se déconnecter),browser.disconnect()de Puppeteer, ou tout simplement la fin de votre processus. - Arrêtez-le avec
DELETE /api/v1/browsers/<id>, ou en envoyant la commande CDPBrowser.close: c’est ce que faitbrowser.close()de Puppeteer, et dans Playwright,await (await browser.newBrowserCDPSession()).send("Browser.close"). D’ici là, il occupe toujours une place dans votre limite de concurrence. - Un seul client à la fois : une reconnexion alors qu’un autre client est attaché est refusée avec
409, tout comme une reconnexion à une session démarrée sanskeepAlive. - Les limites s’appliquent même quand personne n’est connecté :
idleTimeoutSec(augmentez-le, jusqu’à 1800, pour un navigateur auquel vous comptez revenir),timeoutSec,maxGbet votre solde. Une page laissée ouverte continue de générer son trafic en arrière-plan, facturé comme n’importe quel autre.
Limites
- 24 navigateurs en cours d’exécution ou de démarrage simultanément par compte.
- Les sessions durent au maximum 4 heures.
- Une session sans aucune commande CDP pendant 5 minutes est fermée (modifiable avec
idleTimeoutSec). - Chaque session démarre avec un profil de navigateur vierge, supprimé à la fin de la session, sauf si vous utilisez un profil nommé, qui conserve les cookies et le stockage des sites d’une session à l’autre.
- Par sécurité, le navigateur ne peut ni ouvrir de fichiers locaux (
file://), ni uploader des fichiers depuis le serveur, ni accéder à des réseaux privés ou internes, ni envoyer d’e-mails sur le port 25. - L’upload de fichiers fonctionne depuis Playwright :
setInputFiles()envoie le fichier depuis votre machine (jusqu’à 50 Mo).uploadFile()de Puppeteer est refusé. Les téléchargements restent sur notre serveur et sont supprimés avec la session ; pour conserver un fichier, récupérez-le depuis la page et renvoyez son contenu. - Les extensions Chrome ne peuvent pas être chargées dans les navigateurs hébergés.
- Le navigateur ne transmet ni les messages de console ni les erreurs de page :
page.on("console")reste donc muet. Collectez ce dont vous avez besoin dans la page et relisez-le avecevaluate.
Erreurs
| Statut | code | Signification |
|---|---|---|
| 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. |
Les erreurs sont renvoyées en JSON : { "error": "...", "code": "..." }. Si la connexion WebSocket elle-même est refusée, créez une nouvelle session : les URL de connexion sont à usage unique et expirent au bout de deux minutes. Un upgrade WebSocket refusé répond avec un statut HTTP et un champ JSON error : 409 si l’URL a déjà été utilisée, si la session a été annulée, n’a pas été démarrée avec keepAlive ou est déjà connectée ; 401 si l’URL a expiré.
Ce qu’il faut réessayer
- Réessayez avec backoff :
503 NO_CAPACITYet503 NO_WORKER(attendez 1, 2, 4… secondes avec un peu de jitter, et abandonnez après quelques tentatives), ainsi qu’une429sans code (la limite de débit par adresse). - Réessayez quelques fois avec backoff : les autres réponses
5xx, et une connexion refusée avant le démarrage de votre script (avec une nouvelle session : l’ancienne URL de connexion est consommée). - Ne bouclez jamais :
400(corrigez la requête),401,402(ajoutez du crédit),429 CONCURRENCY_LIMIT(fermez d’abord un navigateur) et409 PROFILE_IN_USE. Une personne doit intervenir ; réessayer ne fait que gaspiller des requêtes.
Bonnes pratiques
- Connectez-vous, ne lancez jamais. Utilisez
connectOverCDPoupuppeteer.connectavec leconnectUrl;chromium.launch(), lui, démarre un navigateur sur votre propre machine. - Utilisez ce qui existe déjà. Prenez
browser.contexts()[0]et sa première page plutôt qu’un nouveau contexte : un nouveau contexte démarre sans les cookies ni le stockage du profil, et Playwright lui attribue un viewport émulé de 1280×720 qui ne correspond pas à la fenêtre du navigateur. - Définissez la persona à la création, pas depuis le script. Le pays, le fuseau horaire et la langue ont leur place dans l’appel de création. Surcharger le user agent, le viewport ou les propriétés de navigator depuis un script crée précisément les incohérences que recherche la détection.
- Limitez les hooks CDP au strict nécessaire. Les listeners trop larges, l’interception de toutes les requêtes et les scripts d’initialisation constituent l’empreinte propre de l’automatisation. Playwright et Puppeteer d’origine fonctionnent tels quels : le moteur tient les effets de bord de
Runtime.enableà l’écart de la page elle-même, si bien qu’un driver patché comme Patchright est facultatif plutôt que nécessaire. - Une session, de nombreuses pages. Le démarrage du navigateur est l’étape lente ; naviguez à l’intérieur. Connectez-vous une seule fois grâce à un profil plutôt qu’à chaque exécution.
- Arrêtez toujours. Fermez le navigateur dans un
finally, et réglezmaxGbetidleTimeoutSecen fonction de la tâche, pour qu’un bug ne puisse pas laisser un navigateur tourner sur votre solde. - Regardez avant d’ajouter du code. Quand un site se comporte mal, observez-le dans la vue en direct (ou prenez la main) avant d’ajouter des attentes et autres rustines.
Frameworks
Tout ce qui s’attache à Chrome via CDP fonctionne avec le connectUrl. Comme celui-ci est à usage unique, un framework qui se reconnecte de lui-même a besoin d’une nouvelle session pour chaque connexion. Le navigateur démarre au moment où vous vous connectez, généralement en quelques secondes ; il n’y a aucun statut à interroger au préalable.
// 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 pour démarrer
Création avec les nouvelles tentatives décrites plus haut, connexion et arrêt systématique, dans une seule fonction :
// 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");
// });Le Playground
Le Playground du tableau de bord exécute un script sur l’un de ces navigateurs, avec la vue en direct, la console et les captures d’écran à côté, et le panneau « Use in your code » situé en dessous affiche les mêmes options de session que le code ci-dessus. Son objet page est un petit helper qui passe directement par CDP, pas par Playwright : un script du Playground est donc une ébauche à transposer, pas un fichier à coller tel quel :
| Helper | Effet |
|---|---|
page.goto(url, { timeout? }) | Navigue et attend l’événement load. |
page.click(sel) · page.type(sel, text) · page.press(key) | Vraies saisies souris et clavier, après avoir fait défiler l’élément jusqu’à ce qu’il soit visible. |
page.evaluate(fn, ...args) | Exécute une fonction dans la page et en renvoie le résultat JSON. |
page.waitForSelector(sel, { timeout? }) · page.waitForNavigation() | Attend un élément, ou le prochain chargement de page. |
page.scroll(px) · page.screenshot({ fullPage? }) | Défilement à la molette ; un JPEG qui arrive dans l’onglet « Screenshots ». |
page.title() · page.url() · page.content() | Le titre, l’adresse et le HTML du document. |
log(...values) · sleep(ms) | Affiche dans la console (les objets sont mis en forme) ; met en pause. |
cdp(method, params) | Une commande CDP brute envoyée au navigateur (Target.*, Browser.*, Storage.*). |
page.cdp(method, params) | Une commande CDP brute envoyée à la page (Page.*, Runtime.*, DOM.*, Network.*). |
Les erreurs indiquent la ligne du script dont elles proviennent. « Share » copie un lien qui transporte le script et ses options de session dans l’URL : rien n’est donc stocké de notre côté, et quiconque l’ouvre l’exécute sur son propre solde.