本文へスキップ

ホスト型ブラウザ

API を 1 回呼び出すだけで当社のサーバー上に Clearcote ブラウザを起動し、Playwright や Puppeteer、その他任意の CDP クライアントから Chrome DevTools Protocol 経由で操作できます。自分で何かをインストールしたり運用したりする必要はありません。トラフィックはデフォルトで住宅用 IP から出ていき、料金はプリペイド残高から GB 単位で支払います。

  • データセンターではなく住宅用 IP。Web サイトから見えるのは、一般消費者向けプロバイダーの実在する家庭用インターネット回線であり、ホスティングやクラウドのアドレスではありません。
  • VPS ではなく実機。ブラウザは共有のクラウド仮想マシンではなく、当社が所有する専用の物理サーバー上で動作します。

無料GitHub を連携すると €5 分の通信量が無料。カード登録は不要です。

1 回限り、GitHub アカウント 1 つにつき 1 回です。GitHub アカウントは作成から 30 日以上経っている必要があります。

€5 を受け取る

料金

  • 1 GB あたり €1.00、住宅用プロキシ込み。デフォルトでは、すべてのセッションが住宅用 IP からインターネットに出ます。これは一般消費者向けインターネットプロバイダーの実在する家庭用回線で、データセンターやホスティングのアドレスではありません。€1.00 で支払うのはこのトラフィックの料金であり、プロキシ代が別途請求されることはありません。
  • トラフィックはブラウザとインターネットの間で計測し、アップロードとダウンロードを合算します(1 GB = 109 バイト)。詳しくは「トラフィックに含まれるもの」を参照してください。
  • 利用時間、セッション数、CDP メッセージには課金されません。
  • プリペイド制です。残高はダッシュボードでチャージします。ブラウザの起動には最低 €0.50 の残高が必要です。各セッションの上限は起動時点の残高でまかなえる額で、実行中のブラウザ全体で共有されます(自分で指定した maxGb のほうが低ければ、そちらが適用されます)。残高がゼロになると、実行中のブラウザは停止されます(使用量の報告はおよそ 15 秒ごとのため、最後の報告で残高がわずかにマイナスになることがあります)。

トラフィックに含まれるもの

ブラウザが Web サイトとの間で送受信するすべてのバイトを、プロキシ事業者と同じく実際の通信経路上で計測します。一般的なページでいえば、内訳は次のとおりです。

  • カウントされるもの:ページ本体と、そのページが読み込むすべてのもの(スクリプト、スタイルシート、画像、フォント、動画、API 呼び出し、広告やトラッカー、WebSocket)に加え、リクエストヘッダー、Cookie、各接続の暗号化(TLS)のオーバーヘッド。待っている間もページは読み込みを続けるため、バックグラウンドのポーリングやアナリティクスもカウントされます。
  • カウントされないもの:コードとブラウザの間の CDP 接続(コマンド、結果、スクリーンショット、PDF、読み出したページ内容)、ダッシュボードのライブビュー、そしてブラウザが実際には取得しないもの(ブロックしたリクエスト、ブラウザ自身のキャッシュから返されるファイル)。

おおよその目安として、軽いテキストページは 1 MB を大きく下回り、一般的なニュースサイトや EC サイトのページは 2〜5 MB、重いシングルページアプリや動画を含むページは 10 MB 以上になります。1 GB あたり €1.00 の場合、3 MB のページ 1,000 件でおよそ 3 GB です。実際の数値は自分のセッションで確認できます。ダッシュボードには各セッションのトラフィックとバイト数上位 20 サイトが表示されます。また、maxGb でセッションに上限をかければ、暴走したページに残高を食いつぶされることはありません。

トラフィックを減らす

ページの容量の大半は、たいていスクリプトにとって不要なものです。削減効果の大きい順に挙げます。

  1. 画像・メディア・フォントをブロックする。これだけでページの半分以上を占めることもよくあります。ブラウザ内で URL パターンによってブロックすれば、リクエストはブラウザの外に出ないため課金されません。しかも、ブラウザのキャッシュはそのまま使えます。
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"] });

リクエストを破棄するだけの目的で page.route() / context.route() や Puppeteer のリクエストインターセプトを使うのは避けてください。これらを使うとブラウザのキャッシュが無効になり、ページごとにスクリプトやスタイルを再ダウンロードするため、節約した画像の分よりも高くつきます。使うのは、リクエストを書き換える必要がある場合だけにしましょう。なお、adblock オプションを使えば、広告やトラッカーのブロックも任せられます。

  1. 広告・アナリティクス・トラッカーをブロックする。不要なサードパーティドメインへのリクエストは中止します。どのドメインに最もコストがかかっているかは、ダッシュボードにあるセッションの上位サイト一覧でわかります。
  2. 必要以上に待たない。waitUntil: "networkidle" は、広告も含めてページが読み込むすべてを待ちます。"domcontentloaded" を使い、そのうえで本当に必要な要素 1 つだけを待つようにしましょう。
  3. 終わったらすぐにブラウザを閉じる。開いたままのページはバックグラウンドでポーリングを続けます。idleTimeoutSec を短くしておけば、閉じ忘れたセッションも自動的に終了します。
  4. 1 つのブラウザで多くのページを回る。キャッシュが効くため、同じサイト内のページ間で共通するスクリプトやスタイルは、ページごとではなく 1 回だけダウンロードされます。URL ごとに新しいセッションを起動するのではなく、同じセッション内でページを移動しましょう。
  5. 可能ならサイトの API を直接呼ぶ。ブラウザがサイトとの有効なセッションを確立したら、必要な JSON をページ内から fetch() で取得しましょう。ページを読み込み直すのに比べて、ごくわずかなデータ量で済みます。
  6. 上限を設ける。すべてのセッションに maxGb を設定しておけば、予想外に重いページがあっても、残高を使い切る前に停止します。

ブロックはほとんどのサイトで問題なく機能しますが、画像やフォントが実際に読み込まれたかを確認するサイトも一部あります。ブロックを有効にするとサイトの挙動が変わる場合は、そのサイトに限って該当する種類を再び許可してください。

まず実際に動かしてみたい場合は、Playground を使ってください。ダッシュボードから直接クラウドブラウザでスクリプトを実行でき、ライブビュー、コンソール出力、スクリーンショットを並べて確認できます。

1. API キーを取得する

ダッシュボードの「API keys」ページで作成します。キーは cc_live_ で始まり、Bearer トークンとして送信します。他人に知られないようにしてください。キーを持っている人なら誰でも、あなたの残高を使えてしまいます。

2. ブラウザを起動して接続する

POST /api/v1/browsers は connectUrl を返します。これはそのブラウザ専用の、1 回限り有効な WebSocket URL です。2 分以内に接続してください。同じ URL は 2 回使えません。

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()

作成リクエストは 201 を返します。レスポンスには、接続と料金の把握に必要な情報がすべて含まれています。

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
}

オプション

すべて省略可能です。作成リクエストの JSON ボディとして送信します。

フィールド型説明
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.

ステルスのデフォルト設定とアイデンティティ

すべてのセッションは「推奨設定」ページの設定から始まります。つまり lightStealth が有効で、シードが割り当てられ、タイムゾーンと言語は出口 IP に合わせられます。lightStealth(デフォルト)では、CPU コア数、メモリ、ピクセル比を変えた少数のデバイスプロファイルの中から、シードが 1 つを選びます。canvas、WebGL、オーディオは、セッションが動作しているマシンのものがそのまま使われます。シードごとに完全なペルソナを使うには lightStealth: false を設定します。この場合、canvas と WebGL の読み取り結果にはシードに基づくサイトごとのノイズが加わり、GPU 文字列、画面、オーディオ設定(サンプルレート、レイテンシ)はペルソナに従います。identity も fingerprint も指定しない場合、セッションごとにランダムなシードと新しい IP が割り当てられます。

identity: "account-42" を渡すと、以降のセッションでも同じデバイスプロファイルで、しかもその住宅用 IP がオンラインである限り同じ IP で戻ってこられます。ログイン済みのアカウントにとっては、これが自然な状態です。アイデンティティはアカウント単位で非公開です。別の顧客が同じラベルを使っても、その顧客には別のシードと IP が割り当てられます。アイデンティティだけでは Cookie は保持されません。Cookie を保持するにはプロファイルを使ってください。

プロファイル:ログインは一度だけ

プロファイルはセッションの Cookie、localStorage、IndexedDB を名前を付けて保存するため、同じ名前で次に起動したセッションはログイン済みの状態から始まります。profile: { name: "shop-account", persist: true } を渡すとプロファイルを読み込み、セッション終了時に書き戻します。profile: "shop-account" だけを渡すと、読み取り専用で読み込みます。新しい名前での最初のセッションは空の状態で始まり、そのプロファイルを作成します。

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" }),
});
  • 同じデバイス、同じ IP。プロファイルは専用のアイデンティティ(profile:<name>)を持つため、サイトからは同じフィンガープリントに見え、その IP がオンラインである限り同じ住宅用 IP からのアクセスになります。再訪した顧客と同じ見え方です。別の設定にしたい場合は identity または fingerprint を自分で渡してください。また、実行ごとに国を変えないようにしてください。
  • 書き込めるのは同時に 1 セッションだけ。プロファイルに保存できる実行中のセッションは 1 つだけで、persist: true を指定した 2 つ目のセッションには 409 PROFILE_IN_USE が返ります。読み取り専用のセッションは並行して実行でき、最後に保存された状態を参照できます。
  • ブラウザを閉じた場合、切断した場合、停止した場合、制限によって終了した場合のいずれでも、セッション終了時に保存されます。ブラウザがクラッシュした場合は、不完全な状態で上書きせず、前回保存された状態を保持します。セッション Cookie(有効期限のないもの)は、実際のブラウザが再起動時に破棄するのと同じく破棄されます。
  • 圧縮後で最大約 3.5 MB です。サイトの IndexedDB のせいでこれを超える場合は IndexedDB を除外しますが、Cookie と localStorage は引き続き保存されます。
  • 非公開。プロファイルはあなた専用で(別の顧客の「shop-account」は別のプロファイルです)、暗号化して保存され、あなたのセッションを実行するサーバーにだけ渡されます。一覧の取得は GET /api/v1/browsers/profiles、削除は DELETE /api/v1/browsers/profiles/<name> で行えるほか、ダッシュボードからも操作できます。

出口 IP:ローテーション、スティッキー、地域指定

  • デフォルト:ブラウザセッションごとに専用の住宅用出口 IP が割り当てられ、セッション中はずっとその IP を使います。
  • スティッキー:同じ proxySession ラベル(たとえば管理するアカウントごとに 1 つ)を渡すと、以降のセッションでも同じ出口 IP を再び使えます。ラベルはあなたのアカウント内でのみ有効です。住宅用 IP はそのピアがオンラインである間(通常は数時間)利用でき、オフラインになると同じネットワークの別の IP が割り当てられます。
  • ロケーション:country を指定し、必要に応じて state と city も指定します。対象を絞り込むほど、IP プールは小さくなります。
  • 独自のプロキシ:proxy: { server: "http://host:port", username, password }、または socks5://host:port(名前解決は当社側)や socks5h://host:port(名前解決はお使いのプロキシ側)を指定します。リクエストは厳密にチェックされます。ポートは明示する必要があり、認証情報は username / password に入れます(user:pass@host 形式の URL は 400 になります。長さはそれぞれ最大 255 バイトです)。また、country、state、city、proxySession はマネージドプールを指定するためのものなので、無視されるのではなく拒否されます。プロキシ自体のアドレスはパブリックなものでなければなりません。トラフィックの課金方法は同じです。

タイムゾーンと言語は、デフォルトで出口 IP に合わせられます(geoip)。自分で指定するには timezone / locale を渡し、無効にするには geoip: false を渡します。

リリースを固定する

セッションでは現行の Clearcote リリースが動作します。古いリリースを使うには version を渡します。指定できるのは、完全なリリース名("152.0.7977.82-r21")、リビルド番号のみ("r21")、Chromium のバージョンまたはメジャーバージョン("152" ならその最新ビルドが選ばれます)、または "latest" です。これらは SDK の version オプションがダウンロードするリリースと同じなので、同じバージョンに固定すれば、ホスト型セッションとローカル実行で同じビルドが使われます。

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 ... " ]
  • warnings を確認する。古いリリースには、それ以降のリリースで追加されたものが含まれていません。後から追加されたオプションや修正は存在しないか、拒否されずに黙って無視されることがあります。そのため、現行リリースで機能する設定が、固定したリリースでは何の効果もないまま終わる可能性があります。
  • 固定しているかどうかにかかわらず、レスポンスの engine には、そのセッションが実行しているリリースが常に示されます。
  • 存在しないリリースを指定すると、コード UNKNOWN_VERSION 付きの 400 が返り、メッセージには選択可能なリリースが列挙されます。
  • 当社のサーバーでまだ使われたことのないリリースで最初のセッションを起動すると、ビルドを取得するため、起動に最大 1 分ほど余計にかかることがあります。それ以降のセッションは、ほかと同じ速さで起動します。

開始ページと広告ブロック

  • url を指定すると、接続前に最初のタブでページが開かれます。そのため、スクリプトが接続した時点ですでに読み込みが始まっています。
  • adblock: true は、よく知られた広告、広告検証、アナリティクスのホストへのリクエストを送信前に拒否するため、それらが課金されることはありません。リストは意図的に控えめにしてあります(タグマネージャー、同意管理ツール、ログイン SDK、CAPTCHA には手を加えません)が、広告が表示されないことに気づくサイトも一部あります。それが問題になるサイトでは無効のままにしてください。

ライブビュー:視聴、操作、共有

ダッシュボードでセッションをクリックすると、そのトラフィックの送信先サイトを確認したり、ライブで視聴したりできます。「Take control」を押すと、自分でクリック、入力、スクロール、貼り付け、ページ移動ができます。たとえば、ログインしたり、スクリプトでは通過できないチェックを通過したりするときに使います。その間もスクリプトは接続されたままなので、操作中はスクリプトを一時停止してください。人による入力もアクティビティとみなされるため、操作中のセッションがアイドル状態として閉じられることはありません。「Share」を使うと、アカウントがなくても誰でも開けるリンクを作成できます。視聴のみか操作付きかを選べ、有効期間は 15 分から 4 時間で、セッションの終了後まで有効になることはありません。

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

操作権限がある場合は、同じ WebSocket で JSON のテキストメッセージを送信します。座標は、表示しているフレームに対する割合(0〜1)で指定します。それ以外は無視されます。

メッセージ動作
{"t":"mouse","e":"down","x":0.5,"y":0.3,"b":"left","n":1,"m":0}押下(down)、解放(up)、または move。n はクリック回数、m は修飾キー(Alt 1、Ctrl 2、Meta 4、Shift 8)です。
{"t":"wheel","x":0.5,"y":0.5,"dx":0,"dy":400}指定した位置で、ピクセル単位でスクロールします。
{"t":"key","e":"down","key":"a","code":"KeyA","kc":65,"text":"a"}キーボードが送るのと同じ形式の、キーの押下または解放です。
{"t":"text","text":"pasted text"}入力したかのようにテキストを挿入します(最大 5000 文字)。
{"t":"nav","a":"back"}、forward、reload、または {"t":"nav","a":"go","url":"example.com"}履歴の移動、再読み込み、または http(s) アドレスを開きます。

GET /api/v1/browsers/<id> のレスポンスには traffic が含まれます。これはそのセッションのバイト数上位 20 サイトです。

セッションの管理

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>

作成時にセッションへ note を付けておくと、一覧やダッシュボードで見つけやすくなります。以前のセッションと同じサーバーで新しいセッションを起動するには(キャッシュが温まった状態の同じマシン)、そのセッションの worker を渡します。そのサーバーが満杯の場合は、別のサーバーが割り当てられるのではなく 503 が返ります。

セッションは、ブラウザを閉じたときや切断したとき、また後述の制限に達したときにも終了します。停止リクエストを送ると、誰も接続していないセッションは即座に終了します。実行中のブラウザは、1 回の報告間隔(約 15 秒)以内にサーバーによって閉じられます。GET のレスポンスは次のとおりです。

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 }
}
status説明
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.

一覧取得の GET /api/v1/browsers は、同じ形式のセッションオブジェクトを新しい順に並べた { balanceEur, sessions: [...] } を返します。

ブラウザを開いたままにして再接続する

デフォルトでは、クライアントが切断するとセッションは終了します。keepAlive: true を指定して起動すると、ブラウザはタブ、Cookie、出口 IP を保ったまま動作し続けます。そのため、後から実行するスクリプト(あるいはクラッシュ後やノート PC を閉じた後の同じスクリプト)で、前回の続きから作業を再開できます。

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
  • 切断すれば実行したままにできます。Playwright の browser.close()(connectOverCDP 経由では切断するだけです)、Puppeteer の browser.disconnect()、あるいは単にプロセスを終了するだけでもかまいません。
  • DELETE /api/v1/browsers/<id> を送るか、CDP コマンド Browser.close を送ると終了します。Puppeteer の browser.close() はこのコマンドを送ります。Playwright では await (await browser.newBrowserCDPSession()).send("Browser.close") を使います。終了するまでは、同時実行数の枠を 1 つ占有し続けます。
  • 接続できるクライアントは同時に 1 つだけです。別のクライアントが接続している間の再接続は 409 で拒否されます。keepAlive なしで起動したセッションへの再接続も同様です。
  • 誰も接続していない間も、制限は適用されます。対象は idleTimeoutSec(後で戻ってくるつもりのブラウザでは、最大 1800 まで引き上げてください)、timeoutSec、maxGb、そして残高です。開いたままのページはバックグラウンドのトラフィックを読み込み続け、その分もほかと同様に課金されます。

制限

  • 1 アカウントあたり、同時に実行中または起動中にできるブラウザは 24 個までです。
  • セッションの最長時間は 4 時間です。
  • CDP コマンドが 5 分間送られないセッションは閉じられます(idleTimeoutSec で変更できます)。
  • 名前付きのプロファイル(セッション間で Cookie やサイトストレージを保持します)を使わない限り、各セッションは新しいブラウザプロファイルで始まり、そのプロファイルはセッション終了時に削除されます。
  • 安全のため、ブラウザはローカルファイル(file://)を開くこと、サーバー上のファイルをアップロードすること、プライベートネットワークや内部ネットワークにアクセスすること、ポート 25 でメールを送信することができません。
  • ファイルのアップロードは Playwright から行えます。setInputFiles() は手元のマシンからファイルを送信します(最大 50 MB)。Puppeteer の uploadFile() は拒否されます。ダウンロードしたファイルは当社のサーバーに残り、セッションとともに削除されます。ファイルを残したい場合は、ページ内から取得してその内容を返してください。
  • ホスト型ブラウザには Chrome 拡張機能を読み込めません。
  • ブラウザはコンソールメッセージやページエラーを転送しないため、page.on("console") には何も届きません。必要な情報はページ内で収集し、evaluate で読み出してください。

エラー

ステータスcode説明
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.

エラーは JSON 形式で返ります({ "error": "...", "code": "..." })。WebSocket 接続そのものが拒否された場合は、新しいセッションを作成してください。接続 URL は 1 回限りで、2 分後に失効します。WebSocket のアップグレードが拒否された場合は、HTTP ステータスと JSON の error が返ります。URL が使用済みの場合、セッションがキャンセルされた場合、keepAlive なしで起動された場合、すでに接続済みの場合は 409、URL の有効期限が切れている場合は 401 です。

リトライすべきもの

  • バックオフ付きでリトライする:503 NO_CAPACITY と 503 NO_WORKER(多少のジッターを加えながら 1、2、4… 秒と待ち、数回で諦めます)、およびコードのない 429(アドレス単位のレート制限)。
  • バックオフ付きで数回だけリトライする:その他の 5xx レスポンスと、スクリプトの開始前に拒否された接続(新しいセッションで行います。古い接続 URL は使用済みです)。
  • ループでリトライしない:400(リクエストを修正します)、401、402(クレジットを追加します)、429 CONCURRENCY_LIMIT(先にブラウザを閉じます)、409 PROFILE_IN_USE。人が対処する必要があり、リトライしてもリクエストを無駄にするだけです。

ベストプラクティス

  1. 起動するのではなく接続する。connectUrl を指定して connectOverCDP または puppeteer.connect を使ってください。chromium.launch() では、手元のマシンでブラウザが起動してしまいます。
  2. 既存のものを使う。新しいコンテキストを作らずに、browser.contexts()[0] とその最初のページを使ってください。新しいコンテキストはプロファイルの Cookie やストレージなしで始まり、しかも Playwright はブラウザウィンドウと一致しない 1280×720 のエミュレートされたビューポートを割り当てます。
  3. ペルソナはスクリプトからではなく作成時に設定する。国、タイムゾーン、言語は作成リクエストで指定します。スクリプトからユーザーエージェント、ビューポート、navigator のプロパティを上書きすると、検知が探しているまさにその不整合を生んでしまいます。
  4. CDP フックは最小限にする。広範なリスナー、全リクエストのインターセプト、初期化スクリプトは、それ自体が自動化のフィンガープリントになります。標準の Playwright と Puppeteer はそのままで動作します。エンジンが Runtime.enable の副作用をページ自体から切り離しているため、Patchright のようなパッチ適用済みドライバーは必須ではなく任意です。
  5. 1 つのセッションで多くのページを。時間がかかるのはブラウザの起動なので、起動したブラウザの中でページを移動してください。ログインは実行のたびに行うのではなく、プロファイルを使って一度だけ行います。
  6. 必ず停止する。finally でブラウザを閉じ、ジョブに合わせて maxGb と idleTimeoutSec を設定してください。そうすれば、バグがあってもブラウザが残高を消費しながら動き続けることはありません。
  7. コードを足す前に様子を見る。サイトの挙動がおかしいときは、待機処理やワークアラウンドを追加する前に、ライブビューで様子を見てください(または操作を引き継いでください)。

フレームワーク

CDP 経由で Chrome に接続するものであれば、何でも connectUrl で動作します。URL は 1 回限りなので、自動で再接続するフレームワークでは接続ごとに新しいセッションが必要です。ブラウザは接続時に起動し、通常は数秒以内に立ち上がります。事前にステータスをポーリングする必要はありません。

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 }) });

たたき台にできるヘルパー

上記のリトライ付きの作成、接続、確実な停止を 1 つの関数にまとめたものです。

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

Playground

ダッシュボードの Playground では、これらのブラウザの 1 つでスクリプトを実行し、その横にライブビュー、コンソール、スクリーンショットを表示できます。その下の「Use in your code」パネルには、上記のコードと同じセッションオプションが表示されます。Playground の page は Playwright ではなく、CDP を直接扱う小さなヘルパーです。そのため、Playground のスクリプトはそのまま貼り付けて使うファイルではなく、書き換えて使うためのたたき台です。

ヘルパー動作
page.goto(url, { timeout? })ページに移動し、load イベントを待ちます。
page.click(sel) · page.type(sel, text) · page.press(key)要素を表示領域までスクロールしてから、実際のマウスとキーボードで入力します。
page.evaluate(fn, ...args)ページ内で関数を実行し、その結果を JSON で受け取ります。
page.waitForSelector(sel, { timeout? }) · page.waitForNavigation()要素の出現、または次のページ読み込みを待ちます。
page.scroll(px) · page.screenshot({ fullPage? })マウスホイールでのスクロール。スクリーンショットは JPEG で「Screenshots」タブに表示されます。
page.title() · page.url() · page.content()ドキュメントのタイトル、アドレス、HTML。
log(...values) · sleep(ms)コンソールへの出力(オブジェクトは整形して表示)と一時停止。
cdp(method, params)ブラウザへの生の CDP コマンド(Target.*、Browser.*、Storage.*)。
page.cdp(method, params)ページへの生の CDP コマンド(Page.*、Runtime.*、DOM.*、Network.*)。

エラーには、発生元のスクリプトの行番号が示されます。「Share」は、スクリプトとそのセッションオプションを URL に含めたリンクをコピーするため、当社側には何も保存されません。リンクを開いた人は、自分の残高でそのスクリプトを実行します。