# Browserless Documentation

> Guides, references, and examples for Browserless

---

# Vercel Agent Browser Integration

URL: https://docs.browserless.io/ai-integrations/agent-browser

# Vercel Agent Browser Integration

[Agent Browser](https://github.com/vercel-labs/agent-browser) is a headless browser automation CLI for AI agents, built with a fast Rust CLI and Node.js fallback. By connecting agent-browser to Browserless, you get cloud-hosted browsers without managing Chrome infrastructure yourself, complete with stealth mode, residential proxies, and enterprise-grade reliability.

- Node.js 18 or later
- agent-browser installed globally or as a project dependency
- Browserless API token (available in your [account dashboard](https://browserless.io/account/))

## Step-by-Step Setup

  
    Go to your <a href="https://browserless.io/account/" target="_blank" rel="noopener noreferrer">Browserless account dashboard</a> and copy your API token.
    Then set your environment variable:

    
      
**.env file:**

```text
BROWSERLESS_API_KEY=your-api-token
```
      
      
**Command line:**

```bash
export BROWSERLESS_API_KEY="your-api-token"
```
      
    
  

  
    
      
**Global (recommended):**

```bash
npm install -g agent-browser
```
      
      
**npx (no install):**

```bash
npx agent-browser open https://example.com
```
      
      
**Project dependency:**

```bash
npm install agent-browser
```
      
    
  

  
    Use the <code>-p browserless</code> flag to route all browser commands through Browserless:

```bash
export BROWSERLESS_API_KEY="your-api-token"
agent-browser -p browserless open https://example.com
```

    Or use environment variables for CI/scripts:

```bash
export AGENT_BROWSER_PROVIDER=browserless
export BROWSERLESS_API_KEY="your-api-token"
agent-browser open https://example.com
```

    When enabled, agent-browser connects to a Browserless cloud session instead of launching a local browser. All commands work identically.
  

  
    Use agent-browser's CLI commands with the Browserless-powered browser:

```bash
# Navigate and take a snapshot
agent-browser -p browserless open https://www.browserless.io
agent-browser snapshot

# Interact using refs from the snapshot
agent-browser click @e2
agent-browser fill @e3 "test@example.com"

# Get text content and screenshots
agent-browser get text @e1
agent-browser screenshot page.png

# Clean up
agent-browser close
```
  

## Optional Configuration

Configure Browserless behavior via environment variables:

| Variable | Description | Default |
|---|---|---|
| `BROWSERLESS_API_URL` | Base API URL (for custom regions or self-hosted) | `https://production-sfo.browserless.io` |
| `BROWSERLESS_BROWSER_TYPE` | Type of browser to use (chromium or chrome) | `chromium` |
| `BROWSERLESS_TTL` | Session TTL in milliseconds | `300000` |
| `BROWSERLESS_STEALTH` | Enable stealth mode (`true` / `false`) | `true` |

### Regional endpoints

Pick the region closest to you for lower latency:

| Region | `BROWSERLESS_API_URL` value |
|---|---|
| US West (San Francisco) | `https://production-sfo.browserless.io` |
| Europe (London) | `https://production-lon.browserless.io` |
| Europe (Amsterdam) | `https://production-ams.browserless.io` |

## Usage with AI Agents

agent-browser is designed for AI coding assistants and autonomous agents. Add it to your agent's instructions:

```text
Use agent-browser to browse the web. Run agent-browser --help for all commands.

Core workflow:
1. agent-browser -p browserless open <url>
2. agent-browser snapshot -i  (get interactive elements with refs @e1, @e2)
3. agent-browser click @e1 / fill @e2 "text"  (interact using refs)
4. Re-snapshot after page changes
```

For richer context in AI coding assistants (Claude Code, Cursor, Codex, etc.):

```bash
npx skills add vercel-labs/agent-browser
```

## Why use Browserless with agent-browser

- **Stealth mode**: bypass bot detection with stealth enabled by default
- **Residential proxies**: route traffic through real residential IPs to avoid blocks
- **Global regions**: choose from US West (San Francisco), Europe (London), or Europe (Amsterdam) endpoints for lower latency
- **No infrastructure**: skip managing Chrome installations, updates, or scaling
- **Parallel sessions**: run multiple browser sessions simultaneously
- **Enterprise reliability**: 99.9% uptime SLA with automatic failover

## Resources

- [Agent Browser Documentation](https://agent-browser.dev)
- [Agent Browser GitHub Repository](https://github.com/vercel-labs/agent-browser)
- [Browserless Connection Options](/overview/connection-urls)
- [Stealth Mode](/baas/bot-detection/stealth)
- [Proxy Configuration](/baas/bot-detection/proxies)

## FAQ & Troubleshooting

<details>
<summary>How do I connect agent-browser to Browserless?</summary>

Pass the `-p browserless` flag (or set `AGENT_BROWSER_PROVIDER=browserless`) along with your `BROWSERLESS_API_KEY` environment variable, as shown in step 3 above. agent-browser then routes all commands through a Browserless cloud session instead of a local browser.

</details>

<details>
<summary>agent-browser is still getting blocked by bot detection</summary>

Stealth mode is enabled by default (`BROWSERLESS_STEALTH=true`). If a site still blocks you, add a [residential proxy](/baas/bot-detection/proxies) to your Browserless connection.

</details>

## Next steps

  <a href="/mcp/browser-agent" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Browser Agent</h3>
      a managed alternative that skips the setup and model wiring
    
  </a>
  <a href="/ai-integrations/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">AI integrations overview</h3>
      all supported AI frameworks and tools
    
  </a>

---

# AgentKit Integration

URL: https://docs.browserless.io/ai-integrations/agentkit

# AgentKit Integration

[AgentKit](https://agentkit.inngest.com/) is a TypeScript framework by Inngest for building fault-tolerant AI agents with tools, state, and multi-agent networks. By connecting AgentKit to Browserless, your agents can browse the web with stealth capabilities, CAPTCHA solving, and residential proxies without managing browser infrastructure.

- Node.js 18 or higher
- Browserless API token (available in your [account dashboard](https://browserless.io/account/))
- An LLM API key (Anthropic, OpenAI, etc.), required for the agent to reason and call tools

## Step-by-Step Setup

  
    Go to your <a href="https://browserless.io/account/" target="_blank" rel="noopener noreferrer">Browserless account dashboard</a> and copy your API token.
    Then set your environment variables:

    
      
**.env file:**

```text
BROWSERLESS_API_KEY=your-browserless-token
ANTHROPIC_API_KEY=your-anthropic-key
```
      
      
**Command line:**

```bash
export BROWSERLESS_API_KEY=your-browserless-token
export ANTHROPIC_API_KEY=your-anthropic-key
```
      
    
  

  
    AgentKit requires <code>inngest</code> as a peer dependency. Install both alongside Playwright and Zod:

    
      
**npm:**

```bash
npm install @inngest/agent-kit inngest playwright-core zod
```
      
      
**yarn:**

```bash
yarn add @inngest/agent-kit inngest playwright-core zod
```
      
      
**pnpm:**

```bash
pnpm add @inngest/agent-kit inngest playwright-core zod
```
      
    
  

  
    Create a file called <code>index.ts</code>. Start by defining a tool that connects to Browserless using Playwright's <code>connectOverCDP</code> method:

```typescript
import { createTool } from "@inngest/agent-kit";
import { z } from "zod";
import { chromium } from "playwright-core";

const scrapeHackerNews = createTool({
  name: "scrape_hackernews",
  description: "Get the top stories from Hacker News",
  parameters: z.object({
    limit: z.number().describe("Number of stories to fetch").default(5),
  }),
  handler: async ({ limit }, { step }) => {
    const scrape = async () => {
      const browser = await chromium.connectOverCDP(
        `wss://production-sfo.browserless.io?token=${process.env.BROWSERLESS_API_KEY}`
      );

      try {
        const page = await browser.newPage();
        await page.goto("https://news.ycombinator.com");

        const stories = await page.evaluate((limit) => {
          const items = document.querySelectorAll(".athing");
          return Array.from(items)
            .slice(0, limit)
            .map((item) => {
              const titleEl = item.querySelector(".titleline > a");
              const subtext = item.nextElementSibling;
              const scoreEl = subtext?.querySelector(".score");
              return {
                title: titleEl?.textContent,
                url: titleEl?.getAttribute("href"),
                points: scoreEl?.textContent || "0 points",
              };
            });
        }, limit);

        return stories;
      } finally {
        await browser.close();
      }
    };

    return await step?.run("scrape-hn", scrape) ?? await scrape();
  },
});
```

    When running inside an Inngest function, <code>step.run()</code> ensures the operation only executes once, even if the agent retries. The <code>?? await scrape()</code> fallback lets the same tool work in standalone mode without Inngest.
  

  
    Add the agent and network definitions to the same file. Pass the tool directly to the agent's <code>tools</code> array:

```typescript
import {
  anthropic,
  createAgent,
  createNetwork,
} from "@inngest/agent-kit";

const newsAgent = createAgent({
  name: "hackernews_agent",
  description: "Fetches and summarizes Hacker News stories",
  system: "You fetch top stories from Hacker News and provide summaries.",
  tools: [scrapeHackerNews],
});

const network = createNetwork({
  name: "news_network",
  agents: [newsAgent],
  maxIter: 3,
  defaultModel: anthropic({
    model: "claude-sonnet-4-20250514",
    defaultParameters: {
      max_tokens: 4096,
    },
  }),
});
```
  

  
    Add the following to the same file to run the network with a prompt:

```typescript
const result = await network.run(
  "Get the top 5 stories from Hacker News and summarize them"
);

console.log(result);
```

    Run the script:

```bash
npx tsx ./index.ts
```

    You should see the agent fetch and summarize the top Hacker News stories.
  

## How It Works

1. **Connection**: Playwright connects to Browserless via WebSocket using `connectOverCDP`
2. **Tool execution**: The agent calls your tool, which runs browser operations through Browserless
3. **Cleanup**: The browser closes in a `finally` block to prevent session leaks

## Advanced Configuration

### Stealth mode

For sites with bot detection, use a [stealth route](/baas/bot-detection/stealth):

```typescript
const browser = await chromium.connectOverCDP(
  `wss://production-sfo.browserless.io/stealth?token=${process.env.BROWSERLESS_API_KEY}`
);
```

### Residential proxies

Route traffic through residential IPs for geo-targeting and rate-limit avoidance:

```typescript
const browser = await chromium.connectOverCDP(
  `wss://production-sfo.browserless.io?token=${process.env.BROWSERLESS_API_KEY}&proxy=residential&proxyCountry=us`
);
```

### Regional endpoints

Pick the region closest to your users or target sites:

| Region | Endpoint |
|---|---|
| US West (San Francisco) | `wss://production-sfo.browserless.io` |
| Europe (London) | `wss://production-lon.browserless.io` |
| Europe (Amsterdam) | `wss://production-ams.browserless.io` |

## Resources

- [AgentKit documentation](https://agentkit.inngest.com/)
- [Browserless connection options](/overview/connection-urls)
- [Stealth routes](/baas/bot-detection/stealth)
- [Proxy configuration](/baas/bot-detection/proxies)

## FAQ & Troubleshooting

<details>
<summary>How does an AgentKit tool connect to Browserless?</summary>

Use Playwright's `connectOverCDP` method inside your tool's `handler`, pointing at your Browserless WebSocket URL with your token, as shown in step 3 above.

</details>

<details>
<summary>My tool's browsing gets blocked by bot detection</summary>

Switch to the [stealth route](/baas/bot-detection/stealth) (`wss://production-sfo.browserless.io/stealth?token=...`) and add a residential proxy, as shown in [Advanced Configuration](#advanced-configuration) above.

</details>

<details>
<summary>Why does the tool call <code>browser.close()</code> in a <code>finally</code> block?</summary>

Browserless sessions consume units for as long as they're open. Closing in `finally` guarantees cleanup even if the tool throws, preventing leaked sessions from running until they time out.

</details>

## Next steps

  <a href="/mcp/browser-agent" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Browser Agent</h3>
      a managed alternative that skips the setup and model wiring
    
  </a>
  <a href="/ai-integrations/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">AI integrations overview</h3>
      all supported AI frameworks and tools
    
  </a>

---

# Anthropic SDK

URL: https://docs.browserless.io/ai-integrations/anthropic

# Anthropic SDK

Anthropic exposes two paths for putting Claude behind a Browserless-powered agent loop:

- **Direct integration** with the [Messages API MCP connector](https://docs.claude.com/en/docs/agents-and-tools/mcp-connector). One `messages.create` call, MCP server as a single `mcp_servers` entry. Lightest possible shape.
- **Managed integration** with [Managed Agents (beta)](https://docs.claude.com/en/docs/agents-and-tools/managed-agents/overview). A persistent `Environment`, a named `Agent`, and `Session` + `events` streams. Better when you need long-lived sessions or server-stored history.

Both routes call the same hosted MCP server, though the Messages API uses `/mcp` while Managed Agents requires the legacy `/sse` endpoint. This page builds the same example using both integration paths: a `GET /hn` endpoint that returns the top 10 Hacker News stories as JSON.

- Browserless API token (available in your [account dashboard](https://browserless.io/account/))
- An [Anthropic API key](https://console.anthropic.com/settings/keys)
- Node.js 20.6 or later (for the built-in `--env-file` flag)

## Setting environment variables

  
**.env file:**

```bash
ANTHROPIC_API_KEY=your-anthropic-key
BROWSERLESS_API_KEY=your-browserless-token
PORT=3000
```
  
  
**Command line:**

```bash
export ANTHROPIC_API_KEY=your-anthropic-key
export BROWSERLESS_API_KEY=your-browserless-token
export PORT=3000
```
  

## Direct integration: Messages API MCP connector

The Messages API accepts hosted MCP servers via the `mcp_servers` parameter (beta). Anthropic's API server makes the MCP calls on Claude's behalf and returns the final answer in one request.

### Install

```bash
npm init -y
npm install @anthropic-ai/sdk express
```

### `server.js`

```js

const PORT = process.env.PORT ?? 3000;
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY || "";
const BROWSERLESS_API_KEY = process.env.BROWSERLESS_API_KEY || "";
const INSTRUCTIONS =
  "You are a news scraper. Use the browserless tools to load the Hacker News front page and return the top 10 stories as a JSON array. Each item must have the shape { rank, title, url, points }. Reply with ONLY the JSON array — no markdown fences, no commentary.";

const app = express();
const client = new Anthropic({ apiKey: ANTHROPIC_API_KEY });

app.get("/hn", async (_req, res) => {
  const message = await client.beta.messages.create({
    model: "claude-opus-4-7",
    max_tokens: 4096,
    system: INSTRUCTIONS,
    mcp_servers: [
      {
        type: "url",
        name: "browserless",
        url: "https://mcp.browserless.io/mcp",
        authorization_token: BROWSERLESS_API_KEY,
      },
    ],
    messages: [
      {
        role: "user",
        content: "Fetch the top 10 stories from https://news.ycombinator.com",
      },
    ],
    betas: ["mcp-client-2025-04-04"],
  });

  const text = message.content
    .filter((block) => block.type === "text")
    .map((block) => block.text)
    .join("");

  res.type("application/json").send(text);
});

app.listen(PORT, () => {
  console.log(`Listening on http://localhost:${PORT}`);
});
```

Add `"type": "module"` to your `package.json` so the `import` statements work.

### Run it

```bash
node --env-file=.env server.js
```

```bash
curl http://localhost:3000/hn
# → [
#     { "rank": 1, "title": "Show HN: …", "url": "https://…", "points": 412 },
#     …
#   ]
```

### How it works

1. Express receives `GET /hn`.
2. The server calls `client.beta.messages.create` with the `mcp_servers` array pointing at `https://mcp.browserless.io/mcp`, authenticated via `authorization_token`.
3. Anthropic's API fetches the MCP tool list, Claude picks the right Browserless tool (typically [`browserless_smartscraper`](/mcp/rest-api-tools#browserless_smartscraper)), and Anthropic executes the call server-side against `https://news.ycombinator.com`.
4. The scraped page comes back to Claude, which extracts the top 10 stories into the JSON shape requested by `system`.
5. The model's final response is returned as content blocks. We concatenate the `text` blocks and pipe them straight to the HTTP response.

### Multi-turn state

The Messages API is stateless — to maintain a conversation, append the assistant turn back into `messages` and send the next user turn:

```js
const turns = [
  { role: "user", content: "Fetch the top 10 stories from https://news.ycombinator.com" },
];
const first = await client.beta.messages.create({ /* …, messages: turns */ });
turns.push({ role: "assistant", content: first.content });
turns.push({ role: "user", content: "Now sort them by points." });
const second = await client.beta.messages.create({ /* …, messages: turns */ });
```

Persist `turns` in your own store (Redis, Postgres) keyed by user or session. Unlike OpenAI's `previous_response_id`, Anthropic does not retain conversation state server-side on this path — you own it.

## Managed integration: Managed Agents

[Managed Agents](https://docs.claude.com/en/docs/agents-and-tools/managed-agents/overview) move the entire agent — system prompt, tools, and MCP server bindings — onto Anthropic's infrastructure. You bootstrap a named `Agent` once, then open `Session`s against it and stream events. Server-side state and a richer event stream are the payoff.

Reach for this when you need long-lived sessions across many turns, server-stored history, or a centrally-versioned agent definition.

### Bootstrap (run once)

This script creates the Environment and the Agent. Save the resulting IDs into `.env`.

```js
// bootstrap-agent.js

const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
const BROWSERLESS_API_KEY = process.env.BROWSERLESS_API_KEY;
const MCP_URL = `https://mcp.browserless.io/sse?token=${BROWSERLESS_API_KEY}`;
const MA_BETA = "managed-agents-2026-04-01";

const INSTRUCTIONS =
  "You are a news scraper. Use the browserless tools to load the Hacker News front page and return the top 10 stories as a JSON array. Each item must have the shape { rank, title, url, points }. Reply with ONLY the JSON array — no markdown fences, no commentary.";

const environment = await client.beta.environments.create({
  name: "browserless-hn-env",
  config: { type: "cloud", networking: { type: "unrestricted" } },
}, { betas: [MA_BETA] });

const agent = await client.beta.agents.create({
  name: "browserless-hn",
  model: "claude-opus-4-7",
  system: INSTRUCTIONS,
  mcp_servers: [{ name: "browserless", type: "url", url: MCP_URL }],
  tools: [
    {
      type: "mcp_toolset",
      mcp_server_name: "browserless",
      default_config: {
        enabled: true,
        permission_policy: { type: "always_allow" },
      },
    },
  ],
}, { betas: [MA_BETA] });

console.log(`MANAGED_AGENTS_ENVIRONMENT_ID=${environment.id}`);
console.log(`MANAGED_AGENTS_AGENT_ID=${agent.id}`);
console.log(`MANAGED_AGENTS_AGENT_VERSION=1`);
```

Run once and paste the output into your `.env`:

```bash
node --env-file=.env bootstrap-agent.js
```

> **The Browserless token is baked into the Agent's MCP URL**
> The agent definition stores the token as part of `mcp_servers[0].url`. To rotate the token, create a new agent version via `client.beta.agents.update(...)` with the new URL. For production deployments where the token must never appear in any stored config, Anthropic's [Vault credentials](https://docs.claude.com/en/docs/agents-and-tools/managed-agents/credentials) are the right tool — but get this recipe working first.

### `server.js`

The per-request handler opens a session against the bootstrapped agent, sends a user-message event, and consumes the event stream until the agent stops.

```js

const PORT = process.env.PORT ?? 3000;
const MA_BETA = "managed-agents-2026-04-01";

const app = express();
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });

app.get("/hn", async (_req, res) => {
  const session = await client.beta.sessions.create({
    agent: {
      type: "agent",
      id: process.env.MANAGED_AGENTS_AGENT_ID,
      version: Number(process.env.MANAGED_AGENTS_AGENT_VERSION),
    },
    environment_id: process.env.MANAGED_AGENTS_ENVIRONMENT_ID,
    betas: [MA_BETA],
  });

  // Open the stream BEFORE sending the message — avoids a race per MA docs.
  const events = await client.beta.sessions.events.stream(session.id, { betas: [MA_BETA] });
  await client.beta.sessions.events.send(session.id, {
    events: [{
      type: "user.message",
      content: [{ type: "text", text: "Fetch the top 10 stories from https://news.ycombinator.com" }],
    }],
    betas: [MA_BETA],
  });

  let finalText = "";
  for await (const event of events) {
    if (event.type === "agent.message") {
      for (const block of event.content) {
        if (block.type === "text") finalText += block.text;
      }
    }
    if (event.type === "session.status_idle") break;
    if (event.type === "session.status_terminated") {
      res.status(500).type("text/plain").send("session terminated");
      return;
    }
  }

  res.type("application/json").send(finalText);
});

app.listen(PORT, () => {
  console.log(`Listening on http://localhost:${PORT}`);
});
```

### What you get over the Messages API path

- **Server-side session state.** Conversation history, tool-call history, and intermediate state live on Anthropic's side. You only hold `session.id`.
- **Versioned agents.** The system prompt, tools, and MCP server bindings are part of a named `Agent` you can roll back or A/B by `version`.
- **Richer event stream.** You see tool calls, results, and partial deltas as discrete events instead of one final blob.

Trade-off: more moving pieces, a bootstrap step, and the MA-specific `/sse` workaround called out below.

## Stealth, CAPTCHAs, and regional endpoints

Both paths route through the same Browserless infrastructure, so [stealth mode](/baas/bot-detection/stealth), [automatic CAPTCHA solving](/baas/bot-detection/captchas), and [residential proxies](/baas/bot-detection/proxies) all apply.

The Messages API `mcp_servers` entry and the Managed Agents `mcp_servers` entry both accept the regional pin via URL query parameter, since neither lets you set arbitrary headers on the MCP request:

```bash
https://mcp.browserless.io/mcp?browserlessUrl=https://production-lon.browserless.io
```

For Managed Agents, swap `/mcp` for `/sse` (see [FAQ & Troubleshooting](#faq--troubleshooting) below). See the [MCP setup page](/mcp/browserless-mcp-server/setup) for the full list of configuration options.

## Resources

- [Anthropic Messages API — MCP connector](https://docs.claude.com/en/docs/agents-and-tools/mcp-connector)
- [Anthropic Managed Agents overview](https://docs.claude.com/en/docs/agents-and-tools/managed-agents/overview)
- [Claude Agent SDK page](/ai-integrations/claude-agent-sdk) – a third option using Playwright over CDP instead of the hosted MCP server
- [Browserless MCP setup](/mcp/browserless-mcp-server/setup)
- [REST API tools](/mcp/rest-api-tools)
- [Browser agent](/mcp/browser-agent)
- [Connection URLs](/overview/connection-urls)

## FAQ & Troubleshooting

<details>
<summary>Messages API: "Unknown parameter: mcp_servers" or 400</summary>

The MCP connector is still in beta. Use `client.beta.messages.create` (not `client.messages.create`) and include `betas: ["mcp-client-2025-04-04"]` on the request. Without that opt-in, the parameter is rejected.

</details>

<details>
<summary>Managed Agents: MCP init fails with "SSE data line is not valid JSON"</summary>

Managed Agents' MCP client does not currently complete the Streamable HTTP handshake at `https://mcp.browserless.io/mcp`. Use the legacy SSE endpoint instead (`https://mcp.browserless.io/sse`). This is the opposite of the Messages API path, which must use `/mcp`. If you copy snippets between the two integrations, double-check the URL.

</details>

<details>
<summary>Empty response text</summary>

Both paths return content blocks rather than a single string field. For the Messages API, filter `message.content` for `block.type === "text"`. For Managed Agents, accumulate text from `agent.message` events (each carries a `content` array of `{ text }` blocks) and stop the loop on `session.status_idle` or `session.status_terminated`.

</details>

<details>
<summary><code>max_tokens</code> truncates the JSON</summary>

`max_tokens` caps the model's final output on the Messages API. If you ask for 10 stories with rich fields and hit a truncated array, raise it. 4096 is usually plenty for this recipe; Managed Agents handles this via the agent definition rather than per-request.

</details>

<details>
<summary><code>mcp_servers</code> "disappear" between calls (Messages API)</summary>

The Messages API does not persist MCP server configuration server-side. Re-send the full `mcp_servers` array on every `messages.create` call, even in a multi-turn conversation. The Managed Agents path stores the binding on the `Agent` resource, so you set it once at bootstrap.

</details>

## Next steps

  <a href="/ai-integrations/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">AI integrations overview</h3>
      all supported AI frameworks and tools
    
  </a>
  <a href="/browserql/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BQL overview</h3>
      use BrowserQL for advanced browser automation
    
  </a>

---

# Browser Use Integration

URL: https://docs.browserless.io/ai-integrations/browser-use/python

# Browser Use Integration

[Browser Use](https://github.com/browser-use/browser-use) is a Python library that allows AI agents to control a browser. By integrating Browserless with Browser Use, you can give your AI applications web browsing capabilities without managing browser infrastructure.

- Python 3.11+
- Browserless API token (available in your [account dashboard](https://browserless.io/account/))
- An LLM provider API key (OpenAI used in this guide; get your key from [OpenAI's API keys page](https://platform.openai.com/api-keys))

### Tooling

- **Virtual environment**: `python -m venv` is the recommended, built-in way to create a virtual environment (no additional installation required).
- **Optional tools**:
  - [uv](https://github.com/astral-sh/uv) - Fast Python package installer and resolver
  - [conda](https://docs.conda.io/en/latest/miniconda.html) - Package and environment manager

## Step-by-Step Setup

  
    You'll need three environment variables:
    <ul>
      <li><code>BROWSERLESS_TOKEN</code> - Your Browserless token (get it from your <a href="https://browserless.io/account/" target="_blank" rel="noreferrer noopener">Browserless account dashboard</a>)</li>
      <li><code>OPENAI_API_KEY</code> - Your OpenAI key (get it from <a href="https://platform.openai.com/api-keys" target="_blank" rel="noreferrer noopener">OpenAI's API keys page</a>)</li>
      <li><code>BROWSERLESS_WS_URL</code> - Browserless WebSocket URL (default: <code>wss://production-sfo.browserless.io</code>)</li>
    </ul>
    Note: While other LLM providers can be used, this guide uses OpenAI for the examples.
    See the <a href="#environment-variables--env-file">Environment Variables + .env file</a> section below for detailed setup instructions.
  

  
    Set up a Python virtual environment to manage your dependencies. The built-in <code>venv</code> module is recommended as it requires no additional installs:
    
          
**venv (recommended):**

            
              
**macOS/Linux:**

```bash
# Verify Python version (should be 3.11+)
python3 --version

# Create virtual environment
python3 -m venv .venv
source .venv/bin/activate
```
              
              
**Windows (CMD):**

```cmd
# Verify Python version (should be 3.11+)
py --version

# Create virtual environment
py -m venv .venv
.venv\Scripts\activate
```
              
            
          
          
**Optional: Using uv:**

            If you have <a href="https://github.com/astral-sh/uv" target="_blank" rel="noreferrer noopener">uv</a> installed, you can use it as an alternative:
```bash
python -m venv .venv
source .venv/bin/activate  # On Windows (CMD): .venv\Scripts\activate
```
            After activation, install dependencies with <code>uv pip install</code> (see step 3).
          
          
**Optional: Using conda:**

            If you have <a href="https://docs.conda.io/en/latest/miniconda.html" target="_blank" rel="noreferrer">conda</a> installed, you can use it as an alternative:
```bash
conda create -n browserless-env python=3.11
conda activate browserless-env
```
          
    
    Note: The default path is <code>venv</code> + <code>pip</code>. <code>uv</code> and <code>conda</code> are optional alternatives.
  

  
    Make sure your virtual environment is activated, then install the required packages:
    
      
**pip (default):**

        
          
**macOS/Linux:**

```bash
# Make sure your virtual environment is activated
source .venv/bin/activate

# Install required packages
pip install "browser-use>=0.12.0" python-dotenv openai
```
              
              
**Windows:**

```cmd
# Make sure your virtual environment is activated
.venv\Scripts\activate

# Install required packages
pip install "browser-use>=0.12.0" python-dotenv openai
```
              
            
          
          
**Optional: Using uv:**

            If you're using <code>uv</code> (see step 2), install with:
```bash
# Make sure your virtual environment is activated
source .venv/bin/activate  # macOS/Linux
# or
.venv\Scripts\activate  # Windows CMD

# Install required packages
uv pip install browser-use python-dotenv openai
```
        Note: The quickstart uses <code>uv pip install</code>, not <code>uv add</code> or <code>uv init</code>.
      
    
  

  
    
    Create a <code>.env</code> file in your project directory with the following variables:
```env
BROWSERLESS_TOKEN=your_browserless_token_here
OPENAI_API_KEY=your_openai_key_here
BROWSERLESS_WS_URL=wss://production-sfo.browserless.io
```
    Note: This guide uses <code>BROWSERLESS_TOKEN</code> to match the repository standard. Other scripts in this repository also use <code>BROWSERLESS_TOKEN</code> for consistency.
    Important: Do not commit <code>.env</code> to version control. Add <code>.env</code> to your <code>.gitignore</code> file.
    If you prefer to set environment variables directly (without a <code>.env</code> file):
    
      
**macOS/Linux:**

```bash
export BROWSERLESS_TOKEN=your_browserless_token_here
export OPENAI_API_KEY=your_openai_key_here
export BROWSERLESS_WS_URL=wss://production-sfo.browserless.io
```
          
          
**Windows (PowerShell):**

```powershell
$env:BROWSERLESS_TOKEN="your_browserless_token_here"
$env:OPENAI_API_KEY="your_openai_key_here"
$env:BROWSERLESS_WS_URL="wss://production-sfo.browserless.io"
```
          
          
**Windows (CMD):**

```cmd
set BROWSERLESS_TOKEN=your_browserless_token_here
set OPENAI_API_KEY=your_openai_key_here
set BROWSERLESS_WS_URL=wss://production-sfo.browserless.io
```
          
    
    Note: The examples in this guide use <code>python-dotenv</code> to automatically load the <code>.env</code> file.
    
  

  
    Create a new file named <code>main.py</code> with the following complete code:

```python
    from browser_use import Agent, BrowserSession
    from browser_use.llm import ChatOpenAI
    from dotenv import load_dotenv
    import os
    import asyncio

    # Load environment variables from .env file
    load_dotenv()

    async def main():
        # Validate required environment variables
        browserless_token = os.getenv('BROWSERLESS_TOKEN')
        openai_key = os.getenv('OPENAI_API_KEY')
        browserless_ws_url = os.getenv('BROWSERLESS_WS_URL', 'wss://production-sfo.browserless.io')

        if not browserless_token:
            raise RuntimeError("BROWSERLESS_TOKEN environment variable is required. Get your token from https://browserless.io/account/")
        if not openai_key:
            raise RuntimeError("OPENAI_API_KEY environment variable is required. Get your key from https://platform.openai.com/api-keys")

        # Create browser session using BROWSERLESS_WS_URL
        browser_session = BrowserSession(
            cdp_url=f"{browserless_ws_url}?token={browserless_token}"
        )

        # Setup LLM
        llm = ChatOpenAI(model="gpt-4o-mini", api_key=openai_key)

        # Create and run agent with a simple task
        agent = Agent(
            task="Go to https://example.com and tell me the main heading on the page",
            llm=llm,
            browser_session=browser_session
        )

        result = await agent.run()
        print(result)

    if __name__ == "__main__":
        asyncio.run(main())
```
  

  
    Make sure your virtual environment is activated, then run your application:
    
      
**macOS/Linux:**

```bash
# Make sure your virtual environment is activated
source .venv/bin/activate

# Run your application
python main.py
```
          
          
**Windows:**

```cmd
# Make sure your virtual environment is activated
.venv\Scripts\activate

# Run your application
python main.py
```
      
    
    You should see output indicating that the browser is initialized and the agent is running.
  

## How It Works

1. **Connection Setup**: Browser Use connects to Browserless using the WebSocket endpoint with your API token
2. **Agent Configuration**: The AI agent is configured with a task and a language model
3. **Automation**: The agent uses the browser to navigate and interact with websites
4. **LLM Integration**: The agent uses an LLM (like GPT-4o) to interpret web content and make decisions

## Complete Example with Cloud Browser

Here's a complete example that demonstrates the modern `BrowserSession` approach with proper environment variable handling:

```python
"""Simple browser-use + Browserless connection example"""

from browser_use import Agent
from browser_use.browser import BrowserSession
from browser_use.llm import ChatOpenAI
from dotenv import load_dotenv

# Load environment variables from .env file
load_dotenv()

async def main():
    # Validate required environment variables
    browserless_token = os.getenv('BROWSERLESS_TOKEN')
    openai_key = os.getenv('OPENAI_API_KEY')
    browserless_ws_url = os.getenv('BROWSERLESS_WS_URL', 'wss://production-sfo.browserless.io')

    if not browserless_token:
        raise RuntimeError("BROWSERLESS_TOKEN environment variable is required. Get your token from https://browserless.io/account/")
    if not openai_key:
        raise RuntimeError("OPENAI_API_KEY environment variable is required. Get your key from https://platform.openai.com/api-keys")

    # Setup LLM
    llm = ChatOpenAI(model="gpt-4o-mini", api_key=openai_key)

    # Setup browser session using BROWSERLESS_WS_URL
    url = f"{browserless_ws_url}?token={browserless_token}"
    browser_session = BrowserSession(cdp_url=url)
    print("🌐 Using cloud browser")

    # Create and run agent
    agent = Agent(
        task="Go to https://example.com and tell me the main heading",
        llm=llm,
        browser_session=browser_session
    )

    result = await agent.run(max_steps=5)
    print(f"✅ Done! Result: {type(result).__name__}")

if __name__ == "__main__":
    asyncio.run(main())
```

## Advanced / Bot-Protected Sites

For sites with bot protection (like eBay), you may need additional configuration:

```python
from browser_use import Agent, BrowserSession
from browser_use.llm import ChatOpenAI
from dotenv import load_dotenv

load_dotenv()

async def main():
    browserless_token = os.getenv('BROWSERLESS_TOKEN')
    openai_key = os.getenv('OPENAI_API_KEY')
    browserless_ws_url = os.getenv('BROWSERLESS_WS_URL', 'wss://production-sfo.browserless.io')

    if not browserless_token:
        raise RuntimeError("BROWSERLESS_TOKEN environment variable is required")
    if not openai_key:
        raise RuntimeError("OPENAI_API_KEY environment variable is required")

    # Use stealth mode and residential proxy for bot-protected sites
    browser_session = BrowserSession(
        cdp_url=f"{browserless_ws_url}/chromium/stealth?token={browserless_token}&proxy=residential"
    )

    llm = ChatOpenAI(model="gpt-4o-mini", api_key=openai_key)

    agent = Agent(
        task="Find me the top cheapest trainer on ebay.co.uk",
        llm=llm,
        browser_session=browser_session
    )

    result = await agent.run()
    print(result)

if __name__ == "__main__":
    asyncio.run(main())
```

## Additional Configuration Options

### Using Different Browserless Regions

You can connect to different Browserless regions for better performance by setting `BROWSERLESS_WS_URL` in your `.env` file:

```env
# US West Coast (default)
BROWSERLESS_WS_URL=wss://production-sfo.browserless.io

# Europe (London)
BROWSERLESS_WS_URL=wss://production-lon.browserless.io
```

### Proxy Support

You can enable a residential proxy for improved website compatibility:

```python
browserless_ws_url = os.getenv('BROWSERLESS_WS_URL', 'wss://production-sfo.browserless.io')
browserless_token = os.getenv('BROWSERLESS_TOKEN')
browser_session = BrowserSession(
    cdp_url=f"{browserless_ws_url}?token={browserless_token}&proxy=residential"
)
```

### Stealth Mode and Proxy Support

Enable stealth mode and residential proxies for better website compatibility. Stealth mode uses the `/chromium/stealth` path:

```python
browserless_ws_url = os.getenv('BROWSERLESS_WS_URL', 'wss://production-sfo.browserless.io')
browserless_token = os.getenv('BROWSERLESS_TOKEN')
browser_session = BrowserSession(
    cdp_url=f"{browserless_ws_url}/chromium/stealth?token={browserless_token}&proxy=residential"
)
```

### Automatic CAPTCHA Solving

> **Requires browser-use 0.12.0+**
> The automatic CAPTCHA pause/resume feature requires **browser-use version 0.12.0 or later**, which includes the CaptchaWatchdog. Earlier versions don't have this. Upgrade with `pip install browser-use>=0.12.0`.

Enable automatic CAPTCHA solving so the agent pauses while CAPTCHAs are solved and resumes when done. This requires `solveCaptchas=true` to enable the captcha engine and `integrations=browseruse` to bridge the events to browser-use's watchdog:

```python
browserless_ws_url = os.getenv('BROWSERLESS_WS_URL', 'wss://production-sfo.browserless.io')
browserless_token = os.getenv('BROWSERLESS_TOKEN')
browser_session = BrowserSession(
    cdp_url=f"{browserless_ws_url}/chromium?token={browserless_token}&solveCaptchas=true&integrations=browseruse"
)
```

When the agent navigates to a page with a CAPTCHA (reCAPTCHA, Cloudflare Turnstile, etc.), Browserless automatically detects and solves it. The agent pauses during solving and resumes once the CAPTCHA is resolved.

### Stealth + Proxy + CAPTCHA Solving

Combine all features for maximum compatibility with bot-protected sites:

```python
browserless_ws_url = os.getenv('BROWSERLESS_WS_URL', 'wss://production-sfo.browserless.io')
browserless_token = os.getenv('BROWSERLESS_TOKEN')

# Stealth mode + residential proxy + auto CAPTCHA solving
browser_session = BrowserSession(
    cdp_url=(
        f"{browserless_ws_url}/chromium/stealth?token={browserless_token}"
        f"&proxy=residential&proxyCountry=US"
        f"&solveCaptchas=true&integrations=browseruse"
    )
)

llm = ChatOpenAI(model="gpt-4o-mini", api_key=os.getenv('OPENAI_API_KEY'))

agent = Agent(
    task="Navigate to a protected site and extract data",
    llm=llm,
    browser_session=browser_session
)

result = await agent.run()
```

**Available query parameters:**

| Parameter | Description | Example |
|-----------|-------------|---------|
| `token` | Your API token (required) | `token=your_token` |
| `solveCaptchas` | Enable automatic CAPTCHA solving | `solveCaptchas=true` |
| `integrations` | Enable browser-use event bridge for CAPTCHA pause/resume | `integrations=browseruse` |
| `proxy` | Enable residential proxy | `proxy=residential` |
| `proxyCountry` | Proxy country (two-letter code) | `proxyCountry=US` |
| `proxyCity` | Proxy city | `proxyCity=New+York` |
| `proxyState` | Proxy state/region | `proxyState=CA` |
| `stealth` | Enable stealth mode (use `/chromium/stealth` path instead) | Path-based |

**Available stealth routes:**

| Route | Description |
|-------|-------------|
| `/chromium` | Standard Chromium (default) |
| `/chromium/stealth` | Chromium with anti-detection |
| `/chrome/stealth` | Chrome with anti-detection |

### Custom Browser Configuration

Configure browser settings using BrowserProfile:

```python
from browser_use.browser import BrowserProfile

browserless_ws_url = os.getenv('BROWSERLESS_WS_URL', 'wss://production-sfo.browserless.io')
browserless_token = os.getenv('BROWSERLESS_TOKEN')

browser_session = BrowserSession(
    cdp_url=f"{browserless_ws_url}?token={browserless_token}",
    browser_profile=BrowserProfile(
        user_agent="Custom User Agent",
        viewport_size={"width": 1920, "height": 1080},
        headless=True,
    )
)
```

## FAQ & Troubleshooting

<details>
<summary>Missing environment variables (<code>BROWSERLESS_TOKEN</code> or <code>OPENAI_API_KEY</code>)</summary>

Ensure your `.env` file contains both variables and is in the same directory as your script. Call `load_dotenv()` at the top of your script before accessing them. If using `uv`, run `uv pip install python-dotenv`.

</details>

<details>
<summary><code>.env</code> file not being loaded</summary>

Install `python-dotenv` (`pip install python-dotenv`) and call `load_dotenv()` before accessing environment variables. Verify the `.env` file is in the same directory as your script.

</details>

<details>
<summary><code>uv add</code> fails or does not work as expected</summary>

The quickstart uses `uv pip install` instead of `uv add`. The `uv add` command requires a project initialized with `uv init`. Use `uv pip install browser-use python-dotenv` after activating your virtual environment.

</details>

<details>
<summary>Windows <code>ExecutionPolicy</code> error when activating the virtual environment</summary>

Run PowerShell as Administrator and execute `Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser`. Alternatively, use Command Prompt (CMD) or activate with `.venv\Scripts\activate.bat`.

</details>

<details>
<summary>Connection timeout or failed to connect</summary>

Verify `BROWSERLESS_WS_URL` is set correctly (default: `wss://production-sfo.browserless.io`). Check that your `BROWSERLESS_TOKEN` is valid and active. Try a different region endpoint if you are experiencing latency, and ensure your network allows WebSocket connections.

</details>

## Next steps

  <a href="/mcp/browser-agent" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Browser Agent</h3>
      a managed alternative that skips the setup and model wiring
    
  </a>
  <a href="/ai-integrations/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">AI integrations overview</h3>
      all supported AI frameworks and tools
    
  </a>

---

# Claude Agent SDK

URL: https://docs.browserless.io/ai-integrations/claude-agent-sdk

# Claude Agent SDK

The [Claude Agent SDK](https://platform.claude.com/docs/en/agent-sdk/overview) lets you build autonomous AI agents powered by Claude. Unlike simple chat completions, agents execute tools autonomously, persist across multiple turns, and use MCP servers to extend capabilities. By connecting to Browserless, your agents can browse bot-protected websites with stealth mode, automatic CAPTCHA solving, and residential proxies. All without local browser infrastructure.

- Browserless API token (available in your [account dashboard](https://browserless.io/account/))
- An [Anthropic API key](https://platform.claude.com/settings/keys)
- Node.js 18 or later (TypeScript)

## Setup

  
    Go to your <a href="https://browserless.io/account/" target="_blank" rel="noopener noreferrer">Browserless account dashboard</a> and copy your API token. You will also need an <a href="https://platform.claude.com/settings/keys" target="_blank" rel="noopener noreferrer">Anthropic API key</a>.
    Set both as environment variables:

    
      
**.env file:**

```text
BROWSERLESS_API_KEY=your-browserless-token
ANTHROPIC_API_KEY=your-anthropic-key
```
      
      
**Command line:**

```bash
export BROWSERLESS_API_KEY=your-browserless-token
export ANTHROPIC_API_KEY=your-anthropic-key
```
      
    
  

  
    Install the Claude Agent SDK alongside Playwright and Zod:

```bash
npm install @anthropic-ai/claude-agent-sdk playwright-core zod
```
  

  
    The key change is connecting Playwright to Browserless via CDP instead of launching a local browser:

```typescript
import { chromium } from "playwright-core";

// Browserless connection with stealth + CAPTCHA solving
const BROWSERLESS_URL = `wss://production-sfo.browserless.io/stealth?token=${process.env.BROWSERLESS_API_KEY}&solveCaptchas=true&timeout=300000`;

// Connect via CDP (instead of chromium.launch())
const browser = await chromium.connectOverCDP(BROWSERLESS_URL);
const context = browser.contexts()[0] || await browser.newContext();
const page = context.pages()[0] || await context.newPage();
```
  

  
    Claude doesn't know Browserless handles CAPTCHAs automatically. Add these rules to your system prompt:

```typescript
systemPrompt: `You have access to a Browserless cloud browser via the execute_playwright MCP tool.
The browser has stealth mode and automatic CAPTCHA solving enabled at the infrastructure level.

IMPORTANT RULES:
1. ALWAYS use execute_playwright for web interactions. Do NOT use WebFetch or other tools.
2. The 'page' and 'browser' objects are ALREADY CONNECTED. Do NOT call connectOverCDP yourself.
3. If you encounter a CAPTCHA or challenge page, wait 15-30 seconds and check again — it will be solved automatically.
4. Return data from your code using: return yourData;
5. ALWAYS use a long timeout on page.goto() to allow time for CAPTCHA solving.
6. Complete ALL your work in a SINGLE execute_playwright call. Do NOT make multiple separate calls.`
```
  

  
    Run your script with a command and pass a prompt as the argument:

```bash
npx tsx your-agent.ts "Go to https://news.ycombinator.com and get the top 5 story titles"
```
  

## Connection URL anatomy

The Browserless WebSocket URL controls which features are active for the session:

```bash
wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN&solveCaptchas=true&timeout=300000&proxy=residential&blockAds=true
```

| Segment | Purpose |
|---------|---------|
| `production-sfo.browserless.io` | Regional endpoint (US West). See [Connection URLs](/overview/connection-urls) for all regions |
| `/stealth` | [Stealth route](/baas/bot-detection/stealth): hides automation signals, spoofs fingerprints |
| `token=` | Your Browserless API token |
| `solveCaptchas=true` | Automatically detects and [solves CAPTCHAs](/baas/bot-detection/captchas) (reCAPTCHA, Cloudflare Turnstile, DataDome, and more) |
| `timeout=300000` | Session timeout in ms (5 minutes). Allows time for CAPTCHA solving |
| `proxy=residential` | Route through [residential IPs](/baas/bot-detection/proxies) for better IP reputation |
| `blockAds=true` | Block ads and trackers for cleaner page loads |

> **Cloud browser without stealth or CAPTCHA solving**
> If you don't need anti-detection or automatic CAPTCHA solving and just want Browserless as managed cloud browser infrastructure, use the standard route and omit `solveCaptchas`:
> 
> ```bash
> wss://production-sfo.browserless.io?token=YOUR_API_TOKEN
> ```
> 
> You still get a fully managed, remote browser, just without the stealth fingerprinting and CAPTCHA solving layers.

## How stealth mode works

The [`/stealth`](/baas/bot-detection/stealth) route automatically applies anti-detection techniques before your agent interacts with any page:

- Hides `navigator.webdriver` and other automation signals
- Spoofs browser fingerprints (canvas, WebGL, fonts)
- Randomizes user-agent strings
- Mimics human-like behavior patterns

No code changes needed. The protections are applied at the browser level, transparently to Playwright and the Claude Agent SDK.

## How automatic CAPTCHA solving works

With `solveCaptchas=true` in your connection URL, Browserless monitors every page for CAPTCHA challenges and solves them in the background:

1. **Detects** CAPTCHAs by monitoring network requests for known patterns (reCAPTCHA, Cloudflare Turnstile, DataDome, and more)
2. **Solves** them programmatically using Browserless's solving engine
3. **Continues** once solved. Your agent sees the unblocked page

Solving typically takes 15 to 60 seconds depending on complexity. The system prompt should instruct your agent to wait and retry if it encounters a challenge page.

> **Note**
> Each successful CAPTCHA solve costs 10 units; attempts that don't clear aren't charged. Using `/stealth` and residential proxies reduces how often CAPTCHAs appear in the first place.

## Example use cases

The integration works well for tasks that would normally trigger bot detection:

Scrape bot-protected review sites:

```text
"Go to https://www.g2.com/products/browserless-io/reviews and get the reviews"
```

Extract product data from e-commerce:

```text
"Navigate to https://www.amazon.com/dp/B09V3KXJPB and get the product title and price"
```

Research news sites:

```text
"Go to https://news.ycombinator.com and get the top 5 story titles"
```

Multi-step workflows:

```text
"Search Google for 'best headless browser 2026', click the first result, and summarize the page"
```

## Resources

- [Claude Agent SDK documentation](https://platform.claude.com/docs/en/agent-sdk/overview)
- [Stealth routes](/baas/bot-detection/stealth)
- [CAPTCHA solving](/baas/bot-detection/captchas)
- [Proxy configuration](/baas/bot-detection/proxies)
- [Connection URLs](/overview/connection-urls)

## FAQ & Troubleshooting

<details>
<summary>Claude tries to solve CAPTCHAs manually</summary>

Add clear instructions to your system prompt that CAPTCHAs solve automatically and to wait 15 to 30 seconds. See the system prompt guidance step in [Setup](#setup) above.

</details>

<details>
<summary>Connection timeout on <code>page.goto()</code></summary>

Use a long timeout: `page.goto(url, { timeout: 120000, waitUntil: 'domcontentloaded' })`. The default 30-second timeout isn't enough when CAPTCHA solving is happening.

</details>

<details>
<summary>"Target page, context or browser has been closed"</summary>

This means the Browserless session expired. Increase the session timeout in the URL (`&timeout=300000` for 5 minutes), instruct Claude to complete all work in a single tool call, or rely on the code's automatic reconnect on the next tool call.

</details>

<details>
<summary>Site still blocks the browser</summary>

Layer your defenses: use the [`/stealth`](/baas/bot-detection/stealth) route for anti-detection, add `proxy=residential` for [IP reputation](/baas/bot-detection/proxies), and add `solveCaptchas=true` for [CAPTCHA challenges](/baas/bot-detection/captchas).

</details>

<details>
<summary>"WebSocket connection failed"</summary>

Check that your `BROWSERLESS_API_KEY` is set correctly, you have available units in your Browserless account, and the regional endpoint is accessible from your network.

</details>

## Next steps

  <a href="/ai-integrations/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">AI integrations overview</h3>
      all supported AI frameworks and tools
    
  </a>
  <a href="/browserql/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BQL overview</h3>
      use BrowserQL for advanced browser automation
    
  </a>

---

# Claude Code + Playwright MCP

URL: https://docs.browserless.io/ai-integrations/claude-code-playwright-mcp

# Claude Code + Playwright MCP

Claude Code connects to a Browserless cloud browser over CDP using Microsoft's official [Playwright MCP](https://github.com/microsoft/playwright-mcp) server, giving your AI agent managed infrastructure with anti-detection, residential proxies, and automatic CAPTCHA solving. No local browser required.

- A Browserless API token (available in your [account dashboard](https://browserless.io/account/))
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) CLI installed (`npm install -g @anthropic-ai/claude-code`)
- Node.js 18 or later (required by the Playwright MCP server)

## Setup

Register the Playwright MCP server and point it at your Browserless endpoint:

```bash
claude mcp add playwright-browserless \
  -- npx -y @playwright/mcp@latest \
  --cdp-endpoint "wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN&solveCaptchas=true"
```

That single command does three things:

1. Registers Microsoft's official `@playwright/mcp` server with Claude Code
2. Routes all browser traffic through Browserless's [`/stealth`](/baas/bot-detection/stealth) endpoint (anti-detection enabled)
3. Turns on [automatic CAPTCHA solving](/baas/bot-detection/captchas) at the infrastructure level

Start Claude Code and verify:

```bash
claude
```

Then try:

```text
Use playwright to navigate to https://www.google.com and take a snapshot
```

> **Tip**
> Say "playwright" explicitly in your first message. Claude Code sometimes defaults to running Playwright through bash commands if you don't mention it by name.

## Connection URL anatomy

The CDP endpoint URL controls which Browserless features are active for the session:

```bash
wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN&solveCaptchas=true
```

| Segment | Purpose |
|---------|---------|
| `production-sfo.browserless.io` | Regional endpoint (SFO). Also available: `production-lon` for EU, `production-ams` for Amsterdam |
| `/stealth` | [Stealth route](/baas/bot-detection/stealth) — hides automation signals, spoofs fingerprints, reduces bot detection |
| `token=` | Your Browserless API token |
| `solveCaptchas=true` | Automatically detects and [solves CAPTCHAs](/baas/bot-detection/captchas) in real time |

### Route options

| Route | Use case |
|-------|----------|
| `/stealth` | Best for most AI agent workflows. Anti-detection enabled by default |
| `/` | Standard route. Use when stealth is not needed |

> **Cloud browser without stealth or CAPTCHA solving**
> If you don't need anti-detection or automatic CAPTCHA solving and just want Browserless as managed cloud browser infrastructure, use the standard route and omit `solveCaptchas`:
> 
> ```bash
> claude mcp add playwright-browserless \
>   -- npx -y @playwright/mcp@latest \
>   --cdp-endpoint "wss://production-sfo.browserless.io?token=YOUR_API_TOKEN"
> ```
> 
> You still get a fully managed, remote browser, just without the stealth fingerprinting and CAPTCHA solving layers.

### Useful query parameters

| Parameter | Example | Description |
|-----------|---------|-------------|
| `solveCaptchas` | `true` | Auto-solve CAPTCHAs without any code changes |
| `timeout` | `300000` | Session timeout in ms (default 30 s, increase for long-running tasks) |
| `proxy` | `residential` | Route traffic through [residential proxies](/baas/bot-detection/proxies) for additional anti-detection |
| `blockAds` | `true` | Block ads and trackers for cleaner page loads |

Combine them as needed:

```bash
claude mcp add playwright-browserless \
  -- npx -y @playwright/mcp@latest \
  --cdp-endpoint "wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN&solveCaptchas=true&timeout=300000&proxy=residential"
```

## How stealth mode works

The [`/stealth`](/baas/bot-detection/stealth) route automatically applies anti-detection techniques before your agent interacts with any page:

- Hides `navigator.webdriver` and other automation signals
- Spoofs browser fingerprints (canvas, WebGL, fonts)
- Randomizes user-agent strings
- Mimics human-like browser behavior patterns

No code or configuration changes needed. The stealth protections are applied at the browser level, transparently to Playwright and Claude Code.

When stealth alone isn't enough (some sites still trigger challenges), `solveCaptchas=true` provides the second layer of defense.

## How automatic CAPTCHA solving works

With `solveCaptchas=true` in your connection URL, Browserless monitors every page for CAPTCHA challenges and solves them in the background. This happens entirely at the infrastructure layer. Playwright and Claude Code don't need to do anything.

The system:

1. **Detects** CAPTCHAs by monitoring network requests for known CAPTCHA service patterns (reCAPTCHA, Cloudflare Turnstile, etc.)
2. **Solves** them programmatically using Browserless's solving engine
3. **Fires CDP events** (`Browserless.captchaFound`, `Browserless.captchaAutoSolved`) for monitoring

Since solving can take a few seconds to a minute, you'll want to give Claude Code guidance on how to handle this. See [System prompt for Claude Code](#system-prompt-for-claude-code) below.

> **Note**
> Each successful CAPTCHA solve costs 10 units; attempts that don't clear aren't charged. Using `/stealth` and residential proxies reduces how often CAPTCHAs appear in the first place.

## System prompt for Claude Code

Claude Code doesn't inherently know that Browserless is handling CAPTCHAs and stealth in the background. Without guidance, it may try to interact with CAPTCHA elements directly or give up when it sees a challenge page.

Add this to your Claude Code system prompt (via `CLAUDE.md` in your project root, or passed via `--system-prompt`):

```markdown
## Browser automation context

You are connected to a Browserless cloud browser via Playwright MCP. The browser
has stealth anti-detection and automatic CAPTCHA solving enabled at the
infrastructure level. Important behavior guidelines:

### CAPTCHA handling
- **Do NOT attempt to click, solve, or interact with CAPTCHA elements yourself.**
  CAPTCHAs (reCAPTCHA, Cloudflare Turnstile, etc.) are solved
  automatically by the browser infrastructure.
- If you detect a CAPTCHA or challenge page, **wait 15-30 seconds**, then take a
  new snapshot to check if the page has updated.
- If the CAPTCHA is still present after 60 seconds, report it to the user rather
  than attempting to interact with it.

### Bot detection and blocking
- The browser is already running in stealth mode with anti-detection protections.
  You do not need to take any special steps to avoid detection.
- If a site blocks you or shows a "bot detected" page, wait 10 seconds and retry
  once. If still blocked, report it to the user.

### Session management
- The browser session has a timeout configured by the connection URL. For
  long-running tasks, be mindful of the session duration.
- Each `browser_navigate` call reuses the same browser context. Cookies and
  session state persist across navigations within the same session.

### Best practices
- Use `browser_snapshot` (accessibility tree) as your primary observation method.
  It's faster and more token-efficient than screenshots.
- Use `browser_take_screenshot` only when you need to verify visual layout or
  see something the accessibility tree doesn't capture.
- When filling forms, use `browser_type` for text inputs and `browser_click`
  for buttons and checkboxes.
```

### Why this matters

Without this prompt, Claude Code will:

- See a CAPTCHA element in the accessibility snapshot and try to click it
- Report "I'm unable to proceed because there's a CAPTCHA" before auto-solve finishes
- Attempt its own "stealth" workarounds (modifying user-agent, etc.) that conflict with Browserless's stealth

With this prompt, Claude Code knows to wait and let the infrastructure handle it.

## Example use cases

### Web research with anti-detection

```text
Navigate to https://www.g2.com/products/browserless/reviews and extract
the 5 most recent reviews. Get the reviewer name, rating, and summary
for each.
```

Claude Code will navigate via the stealth Browserless browser, handle any Cloudflare challenges automatically, and extract the data from the accessibility tree.

### Form submission through CAPTCHA-protected pages

```text
Go to https://example.com/contact, fill in the form with:
- Name: Test User
- Email: test@example.com
- Message: "I'd like to learn more about your enterprise plan"

Then submit the form.
```

If the form has a reCAPTCHA, Browserless solves it transparently before (or during) form submission.

### Multi-step authenticated workflow

```text
1. Navigate to https://app.example.com/login
2. Wait for me to log in manually
3. Once I confirm, navigate to the settings page and take a screenshot
```

Since Browserless maintains a persistent browser context, cookies and auth state carry through the entire session.

## Configuration options

### Per-project setup

Scope the MCP registration to a project so teammates get the same config:

```bash
claude mcp add --scope project playwright-browserless \
  -- npx -y @playwright/mcp@latest \
  --cdp-endpoint "wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN&solveCaptchas=true"
```

This writes to `.mcp.json` in the project directory, which you can commit to version control (with token managed via environment variable).

### Using environment variables for the token

Avoid committing tokens by using an environment variable:

```bash

claude mcp add playwright-browserless \
  -- npx -y @playwright/mcp@latest \
  --cdp-endpoint "wss://production-sfo.browserless.io/stealth?token=${BROWSERLESS_TOKEN}&solveCaptchas=true"
```

### Increasing the session timeout

For tasks that take more than 30 seconds (the default), increase the timeout:

```bash
claude mcp add playwright-browserless \
  -- npx -y @playwright/mcp@latest \
  --cdp-endpoint "wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN&solveCaptchas=true&timeout=300000"
```

`timeout=300000` gives you 5 minutes. Adjust as needed for your workflow.

### Adding residential proxies

For sites with aggressive geo-based blocking or IP reputation checks:

```bash
claude mcp add playwright-browserless \
  -- npx -y @playwright/mcp@latest \
  --cdp-endpoint "wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN&solveCaptchas=true&proxy=residential"
```

See [Proxy configuration](/baas/bot-detection/proxies) for more options including country targeting and sticky sessions.

## Available Playwright MCP tools

Once connected, Claude Code has access to these browser automation tools:

| Tool | Description |
|------|-------------|
| `browser_navigate` | Navigate to a URL |
| `browser_snapshot` | Capture the accessibility tree (primary observation method) |
| `browser_click` | Click an element by reference |
| `browser_type` | Type text into a form field |
| `browser_take_screenshot` | Capture a PNG screenshot |
| `browser_go_back` / `browser_go_forward` | Browser history navigation |
| `browser_wait` | Wait for a specified duration |
| `browser_press_key` | Press a keyboard key |
| `browser_select_option` | Select from a dropdown |
| `browser_hover` | Hover over an element |
| `browser_drag` | Drag and drop |
| `browser_resize` | Resize the browser viewport |
| `browser_tab_*` | Tab management (new, select, close, list) |
| `browser_file_upload` | Upload files to file inputs |
| `browser_pdf_save` | Save page as PDF |

All of these tools execute against your Browserless cloud browser with stealth and CAPTCHA solving active.

## FAQ & Troubleshooting

<details>
<summary>Claude Code tries to solve CAPTCHAs manually</summary>

Add the [system prompt](#system-prompt-for-claude-code) above to your `CLAUDE.md`. Without it, Claude sees CAPTCHA elements in the accessibility tree and tries to interact with them.

</details>

<details>
<summary><code>"Connection timeout"</code> errors</summary>

The Playwright MCP server defaults to a 30-second CDP connection timeout. If Browserless is slow to provision a browser, add `--cdp-timeout 60000`:

```bash
claude mcp add playwright-browserless \
  -- npx -y @playwright/mcp@latest \
  --cdp-endpoint "wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN&solveCaptchas=true" \
  --cdp-timeout 60000
```

</details>

<details>
<summary>Claude Code uses bash Playwright instead of MCP</summary>

Say "use Playwright MCP" explicitly in your first message. Claude Code defaults to writing and running Playwright scripts via bash if it doesn't know MCP is available. After the first interaction, it remembers.

</details>

<details>
<summary>CAPTCHA solving takes too long</summary>

Solving can take up to 60 seconds for complex challenges. If you're hitting timeouts, increase the session timeout (`timeout=300000`), add [residential proxies](/baas/bot-detection/proxies) (`proxy=residential`) to reduce CAPTCHA frequency, and use the `/stealth` route (not `/`) to prevent most CAPTCHAs from appearing.

</details>

<details>
<summary>Site still detects the browser as a bot</summary>

Layer your defenses. Start with the [`/stealth`](/baas/bot-detection/stealth) route (included in [Setup](#setup)), add `proxy=residential` for IP reputation, and if still blocked, try the [Site Unblocking](/baas/bot-detection/unblocking) API for the most aggressive anti-detection.

</details>

## Next steps

  <a href="/ai-integrations/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">AI integrations overview</h3>
      all supported AI frameworks and tools
    
  </a>
  <a href="/browserql/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BQL overview</h3>
      use BrowserQL for advanced browser automation
    
  </a>

---

# Claude Code Plugin

URL: https://docs.browserless.io/ai-integrations/claude-code-plugin

# Claude Code Plugin

The Browserless plugin for [Claude Code](https://docs.anthropic.com/en/docs/claude-code) gives Claude direct access to the Browserless REST APIs. It can scrape webpages, take screenshots, generate PDFs, search the web, map site structures, and run custom browser automation, all from natural language.

- A Browserless API token (available in your [account dashboard](https://browserless.io/account/))
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) installed

## Installation

Install the plugin in Claude Code:

Clone the repo and point Claude Code at the plugin directory:

```bash
git clone https://github.com/browserless/claude-plugin.git
cd claude-plugin
claude --plugin-dir .
```

## Authentication

Run the auth skill inside Claude Code:

```text
/browserless:auth
```

This prompts you for your token and preferred API region (SFO, LON, or a custom URL for private deployments), then saves the credentials to `~/.browserless/.env`.

Alternatively, set the environment variable directly:

```bash
# On Windows:
# set BROWSERLESS_TOKEN=your-token-here

```

### Auth Management

| Command | Description |
|---------|-------------|
| `/browserless:auth` | Interactive setup — set token and region |
| `/browserless:auth status` | Check if authentication is configured |
| `/browserless:auth clear` | Remove saved credentials |
| `/browserless:auth region` | Change API region without re-entering token |

Credentials are stored in `~/.browserless/.env`, but the `BROWSERLESS_TOKEN` environment variable takes precedence if set.

## API Regions

| Region | URL |
|--------|-----|
| SFO (US West, default) | `https://production-sfo.browserless.io` |
| LON (Europe) | `https://production-lon.browserless.io` |
| AMS (Amsterdam) | `https://production-ams.browserless.io` |
| Custom | Any self-hosted or custom Browserless URL |

## Skills

Once authenticated, all skills are available as slash commands:

```text
/browserless:smart-scrape https://example.com
/browserless:screenshot https://example.com
/browserless:pdf https://example.com
/browserless:search what is browserless
/browserless:map https://example.com
/browserless:function click the login button on https://example.com
```

| Skill | Command | Description | Example Prompt |
|-------|---------|-------------|----------------|
| **Smart Scrape** | `/browserless:smart-scrape` | Scrape webpages with cascading strategies (HTTP fetch, proxy, headless browser, captcha solving). Returns markdown, HTML, screenshots, PDFs, or links. | `summarize the main content of https://news.ycombinator.com` |
| **Screenshot** | `/browserless:screenshot` | Capture screenshots of webpages. Supports full-page, element-specific, viewport sizing, image formats (PNG/JPEG/WebP), and proxy/geo-targeting. | `take a screenshot of https://inet-ip.info/ using a French proxy, wait 5 seconds before taking it` |
| **PDF** | `/browserless:pdf` | Generate PDFs from webpages or HTML. Supports paper formats, margins, headers/footers, landscape, background graphics, and tagged/accessible PDFs. | `save https://en.wikipedia.org/wiki/Headless_browser as a landscape A4 PDF` |
| **Search** | `/browserless:search` | Search the web and optionally scrape result pages. Supports web, news, and image sources with time-based filtering and content categories. | `find recent AI news en español from the last week` |
| **Map** | `/browserless:map` | Discover and list all URLs on a website. Crawls sitemaps, pages, and subdomains with relevance-based search filtering. | `save a list of all URLs on https://browserless.io in json format` |
| **Function** | `/browserless:function` | Execute custom Puppeteer JavaScript in a cloud browser. Run arbitrary automation scripts, interact with page elements, fill forms, and return structured data. | `go to https://news.ycombinator.com and return the top 10 story titles as JSON` |

## API Reference

Each skill maps to a Browserless REST API endpoint. Full API documentation is available in the [REST APIs guide](/rest-apis/intro).

| Skill | Endpoint |
|-------|----------|
| Smart Scrape | `POST /smart-scrape` |
| Screenshot | `POST /screenshot` |
| PDF | `POST /pdf` |
| Search | `POST /search` |
| Map | `POST /map` |
| Function | `POST /function` |

## FAQ & Troubleshooting

<details>
<summary>How do I set or change my API token and region?</summary>

Run `/browserless:auth` inside Claude Code for interactive setup, or `/browserless:auth region` to change the region without re-entering your token. Credentials are stored in `~/.browserless/.env`, unless the `BROWSERLESS_TOKEN` environment variable is set, which takes precedence.

</details>

<details>
<summary>Which skill should I use to scrape a page?</summary>

Use `/browserless:smart-scrape` for most scraping tasks. It cascades through HTTP fetch, proxy, headless browser, and CAPTCHA solving strategies automatically, returning markdown, HTML, screenshots, PDFs, or links.

</details>

<details>
<summary>How do I run custom automation beyond the built-in skills?</summary>

Use `/browserless:function` to execute custom Puppeteer JavaScript in a cloud browser, for example clicking elements, filling forms, or returning structured data that the other skills don't cover.

</details>

## Next steps

  <a href="/ai-integrations/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">AI integrations overview</h3>
      all supported AI frameworks and tools
    
  </a>
  <a href="/browserql/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BQL overview</h3>
      use BrowserQL for advanced browser automation
    
  </a>

---

# Anthropic Computer Use Integration

URL: https://docs.browserless.io/ai-integrations/computer-use/anthropic-computer-use

# Anthropic Computer Use Integration

[Anthropic Computer Use](https://docs.anthropic.com/en/docs/agents-and-tools/computer-use) enables Claude to interact with browsers like a human. It takes screenshots, moves the cursor, clicks elements, and types text. By connecting Computer Use to Browserless, you get cloud-hosted browser infrastructure without managing local browsers, enabling scalable AI-powered browser automation with stealth mode, residential proxies, and enterprise-grade reliability.

- Node.js 18+ (for TypeScript) or Python 3.9+ (for Python)
- Browserless API token (available in your [account dashboard](https://browserless.io/account/))
- Anthropic API key (from your [Anthropic Settings](https://platform.claude.com/settings/keys))

## How it works

This integration uses a **screenshot + action loop** with Playwright CDP:

1. **Connect** — Playwright connects to Browserless via CDP WebSocket
2. **Screenshot** — Capture the browser screen as an image
3. **Send to Claude** — Claude analyzes the screenshot using the Computer Use API
4. **Execute action** — Claude requests an action (click, type, scroll) and you execute it via Playwright
5. **Repeat** — Take a new screenshot and continue until the task is complete

Anthropic calls this the "agent loop": Claude responds with a tool use request, and your application responds with the results. You implement the loop that coordinates between Claude and the browser.

## Step-by-Step Setup

  
    Go to your <a href="https://browserless.io/account/" target="_blank" rel="noopener noreferrer">Browserless account dashboard</a> and copy your API token.
    Then set your environment variables:

    
      
**.env file:**

```text
BROWSERLESS_API_KEY=your-browserless-token
ANTHROPIC_API_KEY=your-anthropic-key
```
      
      
**Command line:**

```bash
export BROWSERLESS_API_KEY="your-browserless-token"
export ANTHROPIC_API_KEY="your-anthropic-key"
```
      
    
  

  
    
      
**TypeScript:**

```bash
npm install playwright-core @anthropic-ai/sdk dotenv typescript ts-node @types/node
```
      
      
**Python:**

```bash
pip install playwright anthropic python-dotenv
```
      
    
  

  
    Computer Use requires you to implement the agent loop yourself. Below is a complete working example that navigates to Hacker News and identifies the top post.

    
      When using the Anthropic API directly, you must use the correct combination of model, tool version, and beta flag. Check the <a href="https://docs.anthropic.com/en/docs/about-claude/models">Anthropic Models Documentation</a> and <a href="https://docs.anthropic.com/en/docs/agents-and-tools/computer-use">Computer Use Documentation</a> for the latest values.
      Current working values (as of this writing):
      <ul>
        <li>Model: <code>claude-sonnet-4-6</code></li>
        <li>Beta: <code>computer-use-2025-11-24</code></li>
        <li>Tool type: <code>computer_20251124</code></li>
      </ul>
    

  
**TypeScript:**

```typescript

dotenv.config();

const BROWSERLESS_URL = `wss://production-sfo.browserless.io?token=${process.env.BROWSERLESS_API_KEY}`;
const DISPLAY_WIDTH = 1024;
const DISPLAY_HEIGHT = 768;

async function captureScreenshot(page: Page): Promise<string> {
  const screenshot = await page.screenshot({ type: "png" });
  return screenshot.toString("base64");
}

async function executeComputerAction(
  page: Page,
  action: string,
  params: Record<string, unknown>
): Promise<string> {
  const coordinate = params.coordinate as [number, number] | undefined;

  switch (action) {
    case "screenshot":
      return "Screenshot captured";
    case "left_click":
      if (coordinate) {
        await page.mouse.click(coordinate[0], coordinate[1]);
        return `Clicked at (${coordinate[0]}, ${coordinate[1]})`;
      }
      return "Missing coordinate for left_click";
    case "type":
      await page.keyboard.type(params.text as string);
      return `Typed: "${params.text}"`;
    case "key":
      await page.keyboard.press(params.text as string);
      return `Pressed key: ${params.text}`;
    case "scroll": {
      const direction = params.scroll_direction as string;
      const amount = (params.scroll_amount as number) ?? 3;
      const scrollCoord = coordinate ?? [DISPLAY_WIDTH / 2, DISPLAY_HEIGHT / 2];
      await page.mouse.move(scrollCoord[0], scrollCoord[1]);
      const deltaX = direction === "left" ? -amount * 100 : direction === "right" ? amount * 100 : 0;
      const deltaY = direction === "down" ? amount * 100 : direction === "up" ? -amount * 100 : 0;
      await page.mouse.wheel(deltaX, deltaY);
      return `Scrolled ${direction} by ${amount}`;
    }
    default:
      return `Unknown action: ${action}`;
  }
}

async function runComputerUseLoop(
  client: Anthropic,
  page: Page,
  task: string
): Promise<string> {
  const initialScreenshot = await captureScreenshot(page);

  const messages: Anthropic.Beta.Messages.BetaMessageParam[] = [
    {
      role: "user",
      content: [
        {
          type: "image",
          source: {
            type: "base64",
            media_type: "image/png",
            data: initialScreenshot,
          },
        },
        { type: "text", text: task },
      ],
    },
  ];

  const tools: Anthropic.Beta.Messages.BetaToolUnion[] = [
    {
      type: "computer_20251124",
      name: "computer",
      display_width_px: DISPLAY_WIDTH,
      display_height_px: DISPLAY_HEIGHT,
    },
  ];

  const MAX_ITERATIONS = 10;

  for (let i = 0; i < MAX_ITERATIONS; i++) {
    const response = await client.beta.messages.create({
      model: "claude-sonnet-4-6",
      max_tokens: 4096,
      tools,
      messages,
      betas: ["computer-use-2025-11-24"],
    });

    const assistantContent: Anthropic.Beta.Messages.BetaContentBlock[] = [];
    const toolResults: Anthropic.Beta.Messages.BetaToolResultBlockParam[] = [];

    for (const block of response.content) {
      assistantContent.push(block);

      if (block.type === "tool_use") {
        const input = block.input as Record<string, unknown>;
        const action = input.action as string;

        if (action === "screenshot") {
          const screenshot = await captureScreenshot(page);
          toolResults.push({
            type: "tool_result",
            tool_use_id: block.id,
            content: [
              {
                type: "image",
                source: {
                  type: "base64",
                  media_type: "image/png",
                  data: screenshot,
                },
              },
            ],
          });
        } else {
          const result = await executeComputerAction(page, action, input);
          toolResults.push({
            type: "tool_result",
            tool_use_id: block.id,
            content: result,
          });
        }
      }
    }

    messages.push({ role: "assistant", content: assistantContent });

    if (toolResults.length === 0) {
      return response.content
        .filter(
          (b): b is Anthropic.Beta.Messages.BetaTextBlock => b.type === "text"
        )
        .map((b) => b.text)
        .join("\n");
    }

    messages.push({ role: "user", content: toolResults });
  }

  return "Max iterations reached";
}

async function main() {
  const browser = await chromium.connectOverCDP(BROWSERLESS_URL);
  const page = await browser.newPage();
  await page.setViewportSize({ width: DISPLAY_WIDTH, height: DISPLAY_HEIGHT });
  await page.goto("https://news.ycombinator.com", { waitUntil: "networkidle" });

  const client = new Anthropic();
  const result = await runComputerUseLoop(
    client,
    page,
    "What is the title of the top post on this page? Take a screenshot first to see the page."
  );

  console.log("Result:", result);
  await browser.close();
}

main();
```

  
  
**Python:**

```python

from dotenv import load_dotenv
from playwright.async_api import async_playwright

load_dotenv()

BROWSERLESS_URL = f"wss://production-sfo.browserless.io?token={os.environ['BROWSERLESS_API_KEY']}"
DISPLAY_WIDTH = 1024
DISPLAY_HEIGHT = 768

async def capture_screenshot(page) -> str:
    screenshot = await page.screenshot(type="png")
    return base64.b64encode(screenshot).decode()

async def execute_computer_action(page, action: str, params: dict) -> str:
    if action == "screenshot":
        return "Screenshot captured"
    elif action == "left_click":
        x, y = params["coordinate"]
        await page.mouse.click(x, y)
        return f"Clicked at ({x}, {y})"
    elif action == "type":
        await page.keyboard.type(params["text"])
        return f'Typed: "{params["text"]}"'
    elif action == "key":
        await page.keyboard.press(params["text"])
        return f'Pressed key: {params["text"]}'
    elif action == "scroll":
        direction = params.get("scroll_direction", "down")
        amount = params.get("scroll_amount", 3)
        coord = params.get("coordinate", [DISPLAY_WIDTH // 2, DISPLAY_HEIGHT // 2])
        await page.mouse.move(coord[0], coord[1])
        delta_x = -amount * 100 if direction == "left" else amount * 100 if direction == "right" else 0
        delta_y = amount * 100 if direction == "down" else -amount * 100 if direction == "up" else 0
        await page.mouse.wheel(delta_x, delta_y)
        return f"Scrolled {direction} by {amount}"
    return f"Unknown action: {action}"

async def run_computer_use_loop(client, page, task: str) -> str:
    initial_screenshot = await capture_screenshot(page)

    messages = [
        {
            "role": "user",
            "content": [
                {
                    "type": "image",
                    "source": {
                        "type": "base64",
                        "media_type": "image/png",
                        "data": initial_screenshot,
                    },
                },
                {"type": "text", "text": task},
            ],
        }
    ]

    tools = [
        {
            "type": "computer_20251124",
            "name": "computer",
            "display_width_px": DISPLAY_WIDTH,
            "display_height_px": DISPLAY_HEIGHT,
        }
    ]

    max_iterations = 10

    for _ in range(max_iterations):
        response = await client.beta.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=4096,
            tools=tools,
            messages=messages,
            betas=["computer-use-2025-11-24"],
        )

        assistant_content = []
        tool_results = []

        for block in response.content:
            assistant_content.append(block)

            if block.type == "tool_use":
                action = block.input.get("action")

                if action == "screenshot":
                    screenshot = await capture_screenshot(page)
                    tool_results.append(
                        {
                            "type": "tool_result",
                            "tool_use_id": block.id,
                            "content": [
                                {
                                    "type": "image",
                                    "source": {
                                        "type": "base64",
                                        "media_type": "image/png",
                                        "data": screenshot,
                                    },
                                }
                            ],
                        }
                    )
                else:
                    result = await execute_computer_action(page, action, block.input)
                    tool_results.append(
                        {
                            "type": "tool_result",
                            "tool_use_id": block.id,
                            "content": result,
                        }
                    )

        messages.append({"role": "assistant", "content": assistant_content})

        if not tool_results:
            return "\n".join(b.text for b in response.content if b.type == "text")

        messages.append({"role": "user", "content": tool_results})

    return "Max iterations reached"

async def main():
    async with async_playwright() as p:
        browser = await p.chromium.connect_over_cdp(BROWSERLESS_URL)
        page = await browser.new_page()
        await page.set_viewport_size({"width": DISPLAY_WIDTH, "height": DISPLAY_HEIGHT})
        await page.goto("https://news.ycombinator.com", wait_until="networkidle")

        client = anthropic.AsyncAnthropic()
        result = await run_computer_use_loop(
            client,
            page,
            "What is the title of the top post on this page? Take a screenshot first to see the page.",
        )

        print("Result:", result)
        await browser.close()

asyncio.run(main())
```

  

  

## Available actions

Claude can request these actions through the Computer Use tool:

| Action | Description |
|--------|-------------|
| `screenshot` | Capture current screen state |
| `left_click` | Click at coordinates `[x, y]` |
| `right_click` | Right-click at coordinates |
| `double_click` | Double-click at coordinates |
| `type` | Type a text string |
| `key` | Press a key or combo via `text` param (e.g., `Enter`, `ctrl+s`) |
| `mouse_move` | Move cursor to coordinates |
| `scroll` | Scroll at coordinates with `scroll_direction` and `scroll_amount` |

## Advanced Configuration

### Stealth mode

For sites with bot detection, use the [stealth route](/baas/bot-detection/stealth):

  
**TypeScript:**

```typescript
const browser = await chromium.connectOverCDP(
  `wss://production-sfo.browserless.io/stealth?token=${process.env.BROWSERLESS_API_KEY}`
);
```
  
  
**Python:**

```python
browser = await p.chromium.connect_over_cdp(
    f"wss://production-sfo.browserless.io/stealth?token={os.environ['BROWSERLESS_API_KEY']}"
)
```
  

### Residential proxies

Route traffic through residential IPs for geo-targeting and rate-limit avoidance:

  
**TypeScript:**

```typescript
const browser = await chromium.connectOverCDP(
  `wss://production-sfo.browserless.io?token=${process.env.BROWSERLESS_API_KEY}&proxy=residential&proxyCountry=us`
);
```
  
  
**Python:**

```python
browser = await p.chromium.connect_over_cdp(
    f"wss://production-sfo.browserless.io?token={os.environ['BROWSERLESS_API_KEY']}&proxy=residential&proxyCountry=us"
)
```
  

### Regional endpoints

Pick the region closest to you for lower latency:

| Region | Endpoint |
|--------|----------|
| US West (San Francisco) | `wss://production-sfo.browserless.io` |
| Europe (London) | `wss://production-lon.browserless.io` |
| Europe (Amsterdam) | `wss://production-ams.browserless.io` |

## Why use Browserless with Anthropic Computer Use

- **Stealth mode**: bypass bot detection by adding `/stealth` to the endpoint URL
- **Residential proxies**: route traffic through real residential IPs to avoid blocks
- **Global regions**: choose from US West (San Francisco), Europe (London), or Europe (Amsterdam) endpoints for lower latency
- **No infrastructure**: skip managing Chrome installations, updates, or scaling
- **Parallel sessions**: run multiple browser sessions simultaneously
- **Enterprise reliability**: 99.9% uptime SLA with automatic failover

## Resources

- [Anthropic Computer Use Documentation](https://docs.anthropic.com/en/docs/agents-and-tools/computer-use)
- [Anthropic Models Documentation](https://docs.anthropic.com/en/docs/about-claude/models)
- [Computer Use Reference Implementation](https://github.com/anthropics/anthropic-quickstarts/tree/main/computer-use-demo)
- [Browserless Connection Options](/overview/connection-urls)
- [Stealth Mode](/baas/bot-detection/stealth)
- [Proxy Configuration](/baas/bot-detection/proxies)

## FAQ & Troubleshooting

<details>
<summary>Which model, beta flag, and tool version should I use?</summary>

Anthropic updates these together, and mismatched combinations return an error. Check the [Anthropic Models Documentation](https://docs.anthropic.com/en/docs/about-claude/models) and [Computer Use Documentation](https://docs.anthropic.com/en/docs/agents-and-tools/computer-use) for current values before hardcoding them, since they change as new tool versions ship.

</details>

<details>
<summary>Claude's clicks land in the wrong place</summary>

The `display_width_px` and `display_height_px` values passed to the `computer` tool must match the actual Playwright viewport size (`page.setViewportSize`/`page.set_viewport_size`). A mismatch scales Claude's coordinates against the wrong dimensions, so clicks and scrolls land off-target.

</details>

<details>
<summary>The loop stops with "Max iterations reached"</summary>

`MAX_ITERATIONS` caps how many screenshot-action rounds the loop runs before giving up. Raise the constant for tasks that need more steps, or break the task into smaller prompts so each run completes within the cap.

</details>

<details>
<summary>A site blocks the browser mid-task</summary>

Add the [`/stealth`](/baas/bot-detection/stealth) route and [residential proxies](/baas/bot-detection/proxies) to the CDP connection URL. See [Advanced Configuration](#advanced-configuration) above for both examples.

</details>

## Next steps

  <a href="/ai-integrations/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">AI integrations overview</h3>
      all supported AI frameworks and tools
    
  </a>
  <a href="/browserql/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BQL overview</h3>
      use BrowserQL for advanced browser automation
    
  </a>

---

# Gemini Computer Use Integration

URL: https://docs.browserless.io/ai-integrations/computer-use/gemini-computer-use

# Gemini Computer Use Integration

Google's [Gemini Computer Use](https://ai.google.dev/gemini-api/docs/computer-use) model looks at a screenshot of the page and returns UI actions (click, type, scroll) that Playwright executes in a Browserless cloud browser. This enables form filling, web research, and multi-step automation without managing browser infrastructure yourself, backed by Browserless stealth mode and residential proxies.

  Unlike OpenAI CUA and Anthropic Computer Use, which return absolute pixel coordinates, Gemini returns coordinates on a normalized 0–1000 grid. You must scale them to your actual viewport (<code>pixel_x = x / 1000 * viewport_width</code>) before passing them to Playwright. Feeding Gemini's raw coordinates directly into <code>page.mouse.click()</code> makes every click land in the top-left corner of the page — forms get mis-filled and submit/CAPTCHA elements are missed. This is the single most common mistake when porting an OpenAI/Anthropic loop to Gemini. See <a href="#step-by-step-setup">Denormalize coordinates</a>.

- Browserless API token (available in your [account dashboard](https://browserless.io/account/))
- Gemini API key (get it from [Google AI Studio](https://aistudio.google.com/apikey))
- Python 3.10+ and the [`google-genai`](https://pypi.org/project/google-genai/) SDK (`2.7.0`+)

## How it works

Gemini Computer Use runs an agentic **screenshot + action loop**:

1. **Send a request** — call `generate_content` with the `computer_use` tool, the task prompt, and a screenshot of the current page
2. **Receive an action** — the model responds with a `function_call` (e.g. `click`, `type`) whose `x`/`y` are on a normalized 0–1000 grid, plus an `intent` explaining the step
3. **Execute the action** — denormalize the coordinates to viewport pixels and run the action via Playwright
4. **Return the new state** — capture a fresh screenshot and send it back in a `function_response`, then repeat until the model stops emitting actions

  <code>gemini-3.5-flash</code> is Google's recommended Computer Use model and uses the streamlined action set below (<code>click</code>, <code>type</code>, ...). The legacy <code>gemini-2.5-computer-use-preview-10-2025</code> uses a different set (<code>click_at</code>, <code>type_text_at</code>, ...). Both return normalized 0–1000 coordinates. Check the <a href="https://ai.google.dev/gemini-api/docs/computer-use">Gemini Computer Use docs</a> for the latest model IDs.

## Step-by-Step Setup

In this guide you'll build an example that navigates to Hacker News and reports the title of the top post. We use [stealth mode](/baas/bot-detection/stealth) to avoid bot detection. The example follows Google's official [Computer Use reference implementation](https://github.com/google-gemini/computer-use-preview), adapted to connect to Browserless over CDP.

  
    Grab your Browserless token from your <a href="https://browserless.io/account/" target="_blank" rel="noopener noreferrer">account dashboard</a> and your Gemini key from <a href="https://aistudio.google.com/apikey" target="_blank" rel="noopener noreferrer">Google AI Studio</a>.

    
      
**.env file:**

```text
BROWSERLESS_API_KEY=your-browserless-token
GEMINI_API_KEY=your-gemini-key
```
      
      
**Command line:**

```bash
export BROWSERLESS_API_KEY=your-browserless-token
export GEMINI_API_KEY=your-gemini-key
```
      
    
  

  
```bash
pip install google-genai playwright
playwright install chromium
```

    
      Gemini Computer Use is Python-first during preview; this guide uses the <code>google-genai</code> Python SDK. A Node.js SDK (<a href="https://www.npmjs.com/package/@google/genai"><code>@google/genai</code></a>) exposes the same <code>computerUse</code> tool if you prefer TypeScript — the Browserless connection and coordinate-denormalization steps are identical.
    
  

  
    Connect Playwright to Browserless over CDP using the <a href="/baas/bot-detection/stealth">stealth route</a> (recommended for avoiding bot detection). Set a fixed viewport and reuse those exact dimensions when you denormalize coordinates.

```python
import os
from playwright.sync_api import sync_playwright

SCREEN_WIDTH = 1440
SCREEN_HEIGHT = 900

BROWSERLESS_URL = (
    "wss://production-sfo.browserless.io/chromium/stealth"
    f"?token={os.environ['BROWSERLESS_API_KEY']}"
)

playwright = sync_playwright().start()
browser = playwright.chromium.connect_over_cdp(BROWSERLESS_URL, timeout=60000)
context = browser.new_context(
    viewport={"width": SCREEN_WIDTH, "height": SCREEN_HEIGHT}
)
page = context.new_page()
page.goto("https://news.ycombinator.com", wait_until="load")
```
  

  
    Gemini's <code>x</code>/<code>y</code> are on a 0–1000 grid. Scale them to the viewport you configured above. This is the step that's missing when a ported OpenAI/Anthropic loop "clicks in the wrong place".

```python
def denormalize_x(x: int) -> int:
    return int(x / 1000 * SCREEN_WIDTH)

def denormalize_y(y: int) -> int:
    return int(y / 1000 * SCREEN_HEIGHT)
```
  

  
    Translate each <code>function_call</code> into a Playwright action, denormalizing every coordinate first. This covers the <code>gemini-3.5-flash</code> action set; the legacy <code>gemini-2.5-*</code> names are noted in <a href="#supported-actions">Supported actions</a>.

```python
KEY_MAP = {
    "control": "ControlOrMeta",
    "cmd": "Meta",
    "command": "Meta",
    "enter": "Enter",
    "return": "Enter",
    "tab": "Tab",
    "escape": "Escape",
    "backspace": "Backspace",
    "delete": "Delete",
    "up": "ArrowUp",
    "down": "ArrowDown",
    "left": "ArrowLeft",
    "right": "ArrowRight",
}

def press_keys(keys: list[str]) -> None:
    mapped = [KEY_MAP.get(k.lower(), k) for k in keys]
    for key in mapped[:-1]:
        page.keyboard.down(key)
    page.keyboard.press(mapped[-1])
    for key in reversed(mapped[:-1]):
        page.keyboard.up(key)

def execute_action(call) -> None:
    name = call.name
    args = dict(call.args or {})

    if name in ("open_web_browser", "take_screenshot"):
        pass
    elif name == "wait":
        page.wait_for_timeout(int(args.get("seconds", 1)) * 1000)
    elif name == "navigate":
        page.goto(args["url"], wait_until="load")
    elif name == "go_back":
        page.go_back()
    elif name == "go_forward":
        page.go_forward()
    elif name in ("click", "double_click", "right_click", "middle_click", "move"):
        x, y = denormalize_x(args["x"]), denormalize_y(args["y"])
        if name == "click":
            page.mouse.click(x, y)
        elif name == "double_click":
            page.mouse.dblclick(x, y)
        elif name == "right_click":
            page.mouse.click(x, y, button="right")
        elif name == "middle_click":
            page.mouse.click(x, y, button="middle")
        else:
            page.mouse.move(x, y)
    elif name == "type":
        page.keyboard.type(args["text"])
        if args.get("press_enter"):
            page.keyboard.press("Enter")
    elif name == "scroll":
        x, y = denormalize_x(args["x"]), denormalize_y(args["y"])
        direction = args["direction"]
        raw = args.get("magnitude", args.get("magnitude_in_pixels", 800))
        step = denormalize_y(raw) if direction in ("up", "down") else denormalize_x(raw)
        dx = -step if direction == "left" else step if direction == "right" else 0
        dy = -step if direction == "up" else step if direction == "down" else 0
        page.mouse.move(x, y)
        page.mouse.wheel(dx, dy)
    elif name in ("hotkey", "key_combination"):
        keys = args["keys"]
        press_keys(keys if isinstance(keys, list) else str(keys).split("+"))
    elif name == "press_key":
        press_keys([args["key"]])
    elif name == "drag_and_drop":
        x, y = denormalize_x(args["x"]), denormalize_y(args["y"])
        dx, dy = denormalize_x(args["destination_x"]), denormalize_y(args["destination_y"])
        page.mouse.move(x, y)
        page.mouse.down()
        page.mouse.move(dx, dy)
        page.mouse.up()
    else:
        raise ValueError(f"Unsupported action: {name}")

    page.wait_for_load_state()
```
  

  
    Send the task plus the initial screenshot, execute each returned action, and send a fresh screenshot back in a <code>function_response</code>. The loop ends when the model stops emitting function calls.

    
      This example uses <code>client.models.generate_content</code> with a client-side <code>contents</code> history, matching Google's official <a href="https://github.com/google-gemini/computer-use-preview">Computer Use reference implementation</a>. Google also offers a newer <a href="https://ai.google.dev/gemini-api/docs/computer-use">Interactions API</a> (<code>client.interactions.create</code>) that manages conversation state server-side via <code>previous_interaction_id</code> and is recommended for new development generally; <code>generate_content</code> remains fully supported. The Browserless connection and the 0–1000 coordinate denormalization below are identical either way.
    

```python
from google import genai
from google.genai import types

def capture(page) -> bytes:
    page.wait_for_load_state()
    return page.screenshot(type="png")

client = genai.Client()  # reads GEMINI_API_KEY from the environment

config = types.GenerateContentConfig(
    tools=[
        types.Tool(
            computer_use=types.ComputerUse(
                environment=types.Environment.ENVIRONMENT_BROWSER,
            )
        )
    ],
    thinking_config=types.ThinkingConfig(include_thoughts=True),
)

contents = [
    types.Content(
        role="user",
        parts=[
            types.Part(text="What is the title of the top post on this page?"),
            types.Part.from_bytes(data=capture(page), mime_type="image/png"),
        ],
    )
]

for _ in range(30):  # cap iterations so the loop can't run forever
    response = client.models.generate_content(
        model="gemini-3.5-flash",
        contents=contents,
        config=config,
    )
    candidate = response.candidates[0]
    contents.append(candidate.content)

    parts = candidate.content.parts or []
    function_calls = [part.function_call for part in parts if part.function_call]

    if not function_calls:
        print("Done:", " ".join(part.text for part in parts if part.text))
        break

    function_responses = []
    for call in function_calls:
        # A `safety_decision` of "require_confirmation" means you should
        # confirm before executing — see the Safety section below.
        execute_action(call)
        function_responses.append(
            types.FunctionResponse(
                name=call.name,
                response={"url": page.url},
                parts=[
                    types.FunctionResponsePart(
                        inline_data=types.FunctionResponseBlob(
                            mime_type="image/png", data=capture(page)
                        )
                    )
                ],
            )
        )

    contents.append(
        types.Content(
            role="user",
            parts=[types.Part(function_response=fr) for fr in function_responses],
        )
    )

browser.close()
playwright.stop()
```
  

## Supported actions

Gemini `3.5-flash` (recommended) returns these browser actions. Every coordinate is on the 0–1000 grid and must be denormalized before use.

| Action | Properties | Description |
|---|---|---|
| `click` / `double_click` / `right_click` / `middle_click` | `x`, `y` | Click at coordinates |
| `move` | `x`, `y` | Move the cursor |
| `type` | `text`, `press_enter` | Type text, optionally pressing Enter |
| `press_key` / `key_down` / `key_up` | `key` | Press / hold / release a key |
| `hotkey` | `keys[]` | Press a key combination |
| `scroll` | `x`, `y`, `direction`, `magnitude` | Scroll at a position |
| `navigate` | `url` | Go directly to a URL |
| `go_back` / `go_forward` | – | Browser history navigation |
| `drag_and_drop` | `x`, `y`, `destination_x`, `destination_y` | Drag from one point to another |
| `wait` | `seconds` | Pause for the page to settle |
| `open_web_browser` / `take_screenshot` | – | No-ops in this loop (state is captured each turn) |

  The legacy <code>gemini-2.5-computer-use-preview-10-2025</code> model uses <code>click_at</code>, <code>hover_at</code>, <code>type_text_at</code>, <code>scroll_document</code>, <code>scroll_at</code>, <code>wait_5_seconds</code>, <code>key_combination</code>, <code>search</code>, <code>navigate</code>, <code>go_back</code>, <code>go_forward</code>, and <code>drag_and_drop</code>. The coordinate handling is identical.

## Safety confirmations

For sensitive actions (e.g. clicking a CAPTCHA checkbox or completing a purchase), Gemini may attach a `safety_decision` to the function call with `decision: "require_confirmation"`. In production you should pause and require explicit approval before executing those actions, then acknowledge the decision when returning the `function_response`. See Google's [safety guidance](https://ai.google.dev/gemini-api/docs/computer-use) for details.

## Advanced configuration

### Residential proxies

reCAPTCHA v3 and similar defenses score requests largely on IP reputation. If your automation completes the form but the site rejects the submission, route traffic through residential IPs:

```python
BROWSERLESS_URL = (
    "wss://production-sfo.browserless.io/chromium/stealth"
    f"?token={os.environ['BROWSERLESS_API_KEY']}&proxy=residential&proxyCountry=us"
)
```

See [Proxy configuration](/baas/bot-detection/proxies) for country and session options.

### Without stealth mode

If you don't need anti-detection and just want a managed cloud browser:

```python
BROWSERLESS_URL = (
    f"wss://production-sfo.browserless.io?token={os.environ['BROWSERLESS_API_KEY']}"
)
```

### Regional endpoints

Connect to the closest region for lower latency. See [Connection URLs](/overview/connection-urls) for all available endpoints.

## When to use Computer Use vs. the MCP browser agent

Computer Use drives the browser from **screenshots and pixel coordinates**, so its reliability depends on the model clicking the right spot. For structured, multi-step flows (logins, form filling), the [Browserless MCP server](/mcp/browserless-mcp-server/setup)'s `browserless_agent` tool is often more robust: it drives the browser from **accessibility/DOM snapshots and element references** rather than coordinates, so it doesn't depend on pixel-accurate clicks and works the same across models. If you're seeing coordinate-related flakiness with Computer Use, consider the MCP agent for the structured parts of your workflow.

## Why use Browserless with Gemini Computer Use

- **Stealth mode**: bypass bot detection by adding `/stealth` to the endpoint URL
- **Residential proxies**: route traffic through real residential IPs to improve IP reputation for score-based defenses like reCAPTCHA v3
- **Global regions**: choose US West, London, or Amsterdam endpoints for lower latency
- **No infrastructure**: skip managing Chrome installations, updates, or scaling
- **Parallel sessions**: run multiple browser sessions simultaneously

## Resources

- [Gemini Computer Use Documentation](https://ai.google.dev/gemini-api/docs/computer-use)
- [Gemini Computer Use Reference Implementation](https://github.com/google-gemini/computer-use-preview)
- [Browserless Connection Options](/overview/connection-urls)
- [Stealth Mode](/baas/bot-detection/stealth)
- [Proxy Configuration](/baas/bot-detection/proxies)

## FAQ & Troubleshooting

<details>
<summary>The model's clicks land in the wrong place (usually the top-left)</summary>

You're passing Gemini's raw 0–1000 coordinates straight to Playwright. Denormalize every `x`/`y` to your viewport first (`x / 1000 * viewport_width`), and make sure the width/height you scale against match the viewport you set with `new_context(viewport=...)`. See [Denormalize coordinates](#step-by-step-setup).

</details>

<details>
<summary>The automation fills the form but the site rejects the submission</summary>

This is usually not a coordinate issue — score-based defenses like reCAPTCHA v3 rely heavily on IP reputation. Add [residential proxies](/baas/bot-detection/proxies) to the CDP URL and keep the `/stealth` route. See [Advanced configuration](#advanced-configuration).

</details>

<details>
<summary>The model returns key names Playwright doesn't recognize</summary>

The `KEY_MAP` in the [action handler](#step-by-step-setup) covers common mismatches (`control` → `ControlOrMeta`, `cmd` → `Meta`, etc.). Add new ones as needed — the Playwright [keyboard API docs](https://playwright.dev/docs/api/class-keyboard) list all valid key names.

</details>

<details>
<summary>The loop never stops</summary>

The `range(30)` cap bounds how many screenshot-action rounds run before giving up. Raise it for longer tasks, or break the task into smaller prompts so each run completes within the cap.

</details>

<details>
<summary>A site blocks the browser mid-task</summary>

Add [residential proxies](/baas/bot-detection/proxies) to the CDP connection URL and confirm you're still using the `/stealth` route from [Connect to Browserless](#step-by-step-setup).

</details>

## Next steps

  <a href="/ai-integrations/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">AI integrations overview</h3>
      all supported AI frameworks and tools
    
  </a>
  <a href="/browserql/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BQL overview</h3>
      use BrowserQL for advanced browser automation
    
  </a>

---

# OpenAI CUA Integration

URL: https://docs.browserless.io/ai-integrations/computer-use/openai-cua

# OpenAI CUA Integration

[OpenAI's Computer Use Agent (CUA)](https://developers.openai.com/api/docs/guides/tools-computer-use) analyzes screenshots and returns structured actions (click, type, scroll) that Playwright executes in a Browserless cloud browser. This enables tasks like form filling, web research, and data extraction without managing browser infrastructure yourself.

- Browserless API token (available in your [account dashboard](https://browserless.io/account/))
- OpenAI API key (get it from [OpenAI API Keys](https://platform.openai.com/api-keys))
- Node.js 18+ or Python 3.10+

## How it works

1. **Capture screenshot** — take a screenshot of the current browser state
2. **Send to CUA model** — call the Responses API with a `computer` tool
3. **Execute actions** — parse the `computer_call` response and run actions via Playwright
4. **Loop** — repeat until the task is complete

## Step-by-Step Setup

In this guide you'll build an example that navigates to Bing, searches for "Browserless.io", and returns a summary of what the company does. We use [stealth mode](/baas/bot-detection/stealth) to avoid bot detection.

  
    Grab your Browserless token from your <a href="https://browserless.io/account/" target="_blank" rel="noopener noreferrer">account dashboard</a> and your OpenAI key from <a href="https://platform.openai.com/api-keys" target="_blank" rel="noopener noreferrer">OpenAI API Keys</a>.

    
      
**.env file:**

```text
BROWSERLESS_API_KEY=your-browserless-token
OPENAI_API_KEY=your-openai-key
```
      
      
**Command line:**

```bash
export BROWSERLESS_API_KEY=your-browserless-token
export OPENAI_API_KEY=your-openai-key
```
      
    
  

  
    
      
**TypeScript:**

```bash
npm install openai playwright-core typescript ts-node @types/node
```
      
      
**Python:**

```bash
pip install openai playwright
```
      
    
  

  
    Use Playwright's CDP connection with stealth mode (recommended for avoiding bot detection):

    
      
**TypeScript:**

```typescript
import { chromium, Page } from "playwright-core";
import OpenAI from "openai";

const client = new OpenAI();
const browser = await chromium.connectOverCDP(
  `wss://production-sfo.browserless.io/chromium/stealth?token=${process.env.BROWSERLESS_API_KEY}`,
  { timeout: 60000 }
);
const context = await browser.newContext({ viewport: { width: 1024, height: 768 } });
const page = await context.newPage();
```
      
      
**Python:**

```python
import os
import base64
from openai import OpenAI
from playwright.async_api import async_playwright

client = OpenAI()

p = await async_playwright().start()
browser = await p.chromium.connect_over_cdp(
    f"wss://production-sfo.browserless.io/chromium/stealth?token={os.environ['BROWSERLESS_API_KEY']}",
    timeout=60000
)
context = await browser.new_context(viewport={"width": 1024, "height": 768})
page = await context.new_page()
```

        
          All subsequent Python snippets in this guide run inside the same <code>async</code> function. When you're done, call <code>await browser.close()</code> and <code>await p.stop()</code> to clean up.
        
      
    
  

  
    Navigate to Bing and capture the initial screenshot to send to the model. Over remote WebSocket connections, standard Playwright screenshots can timeout, so we include a CDP fallback:

    
      
**TypeScript:**

```typescript
await page.goto("https://www.bing.com", { waitUntil: "networkidle" });

async function getScreenshot(page: Page): Promise<string> {
  try {
    const buffer = await page.screenshot({ timeout: 10000 });
    return buffer.toString("base64");
  } catch {
    // Fallback: use CDP directly
    const cdp = await page.context().newCDPSession(page);
    const result = await cdp.send("Page.captureScreenshot", { format: "png" });
    await cdp.detach();
    return result.data;
  }
}

const screenshotBase64 = await getScreenshot(page);
```
      
      
**Python:**

```python
await page.goto("https://www.bing.com", wait_until="networkidle")

async def get_screenshot(page) -> str:
    try:
        screenshot_bytes = await page.screenshot(timeout=10000)
        return base64.b64encode(screenshot_bytes).decode("utf-8")
    except Exception:
        # Fallback: use CDP directly
        cdp = await page.context.new_cdp_session(page)
        result = await cdp.send("Page.captureScreenshot", {"format": "png"})
        await cdp.detach()
        return result["data"]

screenshot_base64 = await get_screenshot(page)
```
      
    
  

  
    Define the task and send it along with the screenshot:

    
      
**TypeScript:**

```typescript
const task = "Search for 'Browserless.io' and tell me what the company does";

let response = await client.responses.create({
  model: "computer-use-preview",
  tools: [
    {
      type: "computer_use_preview",
      display_width: 1024,
      display_height: 768,
      environment: "browser",
    },
  ],
  input: [
    {
      role: "user",
      content: [
        { type: "input_text", text: task },
        {
          type: "input_image",
          image_url: `data:image/png;base64,${screenshotBase64}`,
        },
      ],
    },
  ],
  truncation: "auto",
});
```
      
      
**Python:**

```python
task = "Search for 'Browserless.io' and tell me what the company does"

response = client.responses.create(
    model="computer-use-preview",
    tools=[
        {
            "type": "computer_use_preview",
            "display_width": 1024,
            "display_height": 768,
            "environment": "browser",
        }
    ],
    input=[
        {
            "role": "user",
            "content": [
                {"type": "input_text", "text": task},
                {
                    "type": "input_image",
                    "image_url": f"data:image/png;base64,{screenshot_base64}",
                },
            ],
        }
    ],
    truncation="auto",
)
```
      
    
  

  
    The model returns a <code>computer_call</code> item with an action to execute. Run the action, capture a new screenshot, and send it back. Repeat until no more <code>computer_call</code> items appear (task complete).

    
      The model may return key names like <code>CTRL</code> or <code>CMD</code> that Playwright doesn't recognize. The examples below map these to Playwright's expected format (e.g., <code>Control</code>, <code>Meta</code>).
    

    
          
**TypeScript:**

```typescript
// Map model key names to Playwright key names
const keyMap: Record<string, string> = {
  enter: "Enter", return: "Enter",
  ctrl: "Control", cmd: "Meta",
  esc: "Escape", backspace: "Backspace",
  tab: "Tab", space: "Space",
  up: "ArrowUp", down: "ArrowDown",
  left: "ArrowLeft", right: "ArrowRight",
};

while (true) {
  const computerCalls = response.output.filter(
    (item: { type: string }) => item.type === "computer_call"
  );

  if (computerCalls.length === 0) {
    // Task complete — print result
    console.log(response.output_text);
    break;
  }

  const computerCall = computerCalls[0];
  const action = computerCall.action;

  switch (action.type) {
    case "click":
      await page.mouse.click(action.x, action.y);
      break;
    case "double_click":
      await page.mouse.dblclick(action.x, action.y);
      break;
    case "type":
      await page.keyboard.type(action.text);
      break;
    case "keypress": {
      const mappedKeys = action.keys.map(
        (key: string) => keyMap[key.toLowerCase()] || key
      );
      await page.keyboard.press(mappedKeys.join("+"));
      break;
    }
    case "scroll":
      await page.mouse.move(action.x, action.y);
      await page.evaluate(
        `window.scrollBy(${action.scroll_x}, ${action.scroll_y})`
      );
      break;
    case "screenshot":
      // Model wants a fresh screenshot — just continue
      break;
  }

  // Capture new screenshot and send back
  const newScreenshot = await getScreenshot(page);

  response = await client.responses.create({
    model: "computer-use-preview",
    previous_response_id: response.id,
    tools: [
      {
        type: "computer_use_preview",
        display_width: 1024,
        display_height: 768,
        environment: "browser",
      },
    ],
    input: [
      {
        type: "computer_call_output",
        call_id: computerCall.call_id,
        output: {
          type: "input_image",
          image_url: `data:image/png;base64,${newScreenshot}`,
        },
      },
    ],
    truncation: "auto",
  });
}
```
          
          
**Python:**

```python
# Map model key names to Playwright key names
key_map = {
    "enter": "Enter", "return": "Enter",
    "ctrl": "Control", "cmd": "Meta",
    "esc": "Escape", "backspace": "Backspace",
    "tab": "Tab", "space": "Space",
    "up": "ArrowUp", "down": "ArrowDown",
    "left": "ArrowLeft", "right": "ArrowRight",
}

while True:
    computer_calls = [
        item for item in response.output
        if item.type == "computer_call"
    ]

    if not computer_calls:
        # Task complete — print result
        print(response.output_text)
        break

    computer_call = computer_calls[0]
    action = computer_call.action

    if action.type == "click":
        await page.mouse.click(action.x, action.y)
    elif action.type == "double_click":
        await page.mouse.dblclick(action.x, action.y)
    elif action.type == "type":
        await page.keyboard.type(action.text)
    elif action.type == "keypress":
        mapped_keys = [key_map.get(key.lower(), key) for key in action.keys]
        await page.keyboard.press("+".join(mapped_keys))
    elif action.type == "scroll":
        await page.mouse.move(action.x, action.y)
        await page.evaluate(
            f"window.scrollBy({action.scroll_x}, {action.scroll_y})"
        )
    elif action.type == "screenshot":
        pass  # Model wants a fresh screenshot — just continue

    # Capture new screenshot and send back
    screenshot_base64 = await get_screenshot(page)

    response = client.responses.create(
        model="computer-use-preview",
        previous_response_id=response.id,
        tools=[
            {
                "type": "computer_use_preview",
                "display_width": 1024,
                "display_height": 768,
                "environment": "browser",
            }
        ],
        input=[
            {
                "type": "computer_call_output",
                "call_id": computer_call.call_id,
                "output": {
                    "type": "input_image",
                    "image_url": f"data:image/png;base64,{screenshot_base64}",
                },
            }
        ],
        truncation="auto",
    )
```
      
    
  

## Supported actions

| Action | Properties | Description |
|---|---|---|
| `click` | `x`, `y`, `button` | Click at coordinates |
| `double_click` | `x`, `y` | Double-click at coordinates |
| `type` | `text` | Type text |
| `keypress` | `keys[]` | Press keyboard keys |
| `scroll` | `x`, `y`, `scroll_x`, `scroll_y` | Scroll at position |
| `drag` | `start_x`, `start_y`, `end_x`, `end_y` | Drag from start to end |
| `wait` | `ms` | Wait for milliseconds |
| `screenshot` | - | Request new screenshot |

## Advanced configuration

### Without stealth mode

If you don't need anti-detection and just want a managed cloud browser:

  
**TypeScript:**

```typescript
const browser = await chromium.connectOverCDP(
  `wss://production-sfo.browserless.io?token=${process.env.BROWSERLESS_API_KEY}`
);
```
  
  
**Python:**

```python
browser = await p.chromium.connect_over_cdp(
    f"wss://production-sfo.browserless.io?token={os.environ['BROWSERLESS_API_KEY']}"
)
```
  

### Residential proxies

Route traffic through real residential IPs for additional anti-detection:

  
**TypeScript:**

```typescript
const browser = await chromium.connectOverCDP(
  `wss://production-sfo.browserless.io?token=${process.env.BROWSERLESS_API_KEY}&proxy=residential&proxyCountry=us`
);
```
  
  
**Python:**

```python
browser = await p.chromium.connect_over_cdp(
    f"wss://production-sfo.browserless.io?token={os.environ['BROWSERLESS_API_KEY']}&proxy=residential&proxyCountry=us"
)
```
  

### Regional endpoints

Connect to the closest region for lower latency. See [Connection URLs](/overview/connection-urls) for all available endpoints.

## Resources

- [OpenAI Computer Use Guide](https://developers.openai.com/api/docs/guides/tools-computer-use)
- [Browserless Connection Options](/overview/connection-urls)
- [Stealth Mode](/baas/bot-detection/stealth)
- [Proxy Configuration](/baas/bot-detection/proxies)

## FAQ & Troubleshooting

<details>
<summary>The screenshot fallback still times out</summary>

Increase the Playwright connection timeout, or check your network connection to Browserless:

  
**TypeScript:**

```typescript
const browser = await chromium.connectOverCDP(url, { timeout: 120000 });
```
  
  
**Python:**

```python
browser = await p.chromium.connect_over_cdp(url, timeout=120000)
```
  

</details>

<details>
<summary>The model returns key names Playwright doesn't recognize</summary>

The `keyMap` / `key_map` in the [action loop](#step-by-step-setup) covers the most common mismatches (`CTRL` → `Control`, `CMD` → `Meta`, etc.). If you encounter new ones, add them to the map. The Playwright [keyboard API docs](https://playwright.dev/docs/api/class-keyboard) list all valid key names.

</details>

<details>
<summary>A site blocks the browser mid-task</summary>

Add [residential proxies](/baas/bot-detection/proxies) to the CDP connection URL, and confirm you're still using the `/stealth` route from [Connect to Browserless](#step-by-step-setup). See [Advanced configuration](#advanced-configuration) above for both examples.

</details>

## Next steps

  <a href="/ai-integrations/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">AI integrations overview</h3>
      all supported AI frameworks and tools
    
  </a>
  <a href="/browserql/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BQL overview</h3>
      use BrowserQL for advanced browser automation
    
  </a>

---

# Hermes Agent

URL: https://docs.browserless.io/ai-integrations/hermes-agent

# Hermes Agent

[Hermes Agent](https://github.com/NousResearch/hermes-agent) is NousResearch's open-source AI agent. This guide configures Browserless as its browser: managed cloud sessions with stealth, automatic CAPTCHA solving, residential proxies, and cascading scrape strategies, through one MCP server and one API token.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- [Hermes Agent](https://github.com/NousResearch/hermes-agent) installed with MCP support (included in the standard install script)

## Why Browserless as Hermes's browser

Most automation targets use bot detection, login walls, or CAPTCHAs. Browserless handles these by default, so one configuration works on protected sites and permissive ones alike:

- `browserless_smartscraper` runs cascading strategies on every request: direct fetch, then proxy, then headless browser, then CAPTCHA solving, until the content returns.
- `browserless_agent` can preserve cookies and page state through logins, multi-step forms, and pagination. In v1.33.0+, [multi-call sessions require first-call opt-in](/mcp/browser-agent#session-lifetime): set `keepSessionAlive: true`, then echo the returned `sessionId` on later calls.
- The browsers run on Browserless's cloud rather than the machine running Hermes. A single API token covers stealth, residential proxies, and CAPTCHA solving; there are no separate provider accounts to set up.
- The same server exposes nine tools covering scraping, search, crawling, exports, Lighthouse audits, and file downloads.

Hermes's built-in browser tools work for public, permissive pages. We recommend Browserless for automation workloads because the unblocking, proxying, and solving are built into every request instead of configured per site.

## Step 1: Add the server to your config

Hermes configures MCP servers in `~/.hermes/config.yaml`. The Browserless server is remote, so it needs only a URL and your token. Put the token in `~/.hermes/.env` so it stays out of the config file:

```bash
# Add to ~/.hermes/.env
BROWSERLESS_API_TOKEN=YOUR_API_TOKEN_HERE
```

Then reference it from the config. Hermes expands `${VAR}` in server entries from `.env` or the process environment:

```yaml
mcp_servers:
  browserless:
    url: "https://mcp.browserless.io/mcp"
    headers:
      Authorization: "Bearer ${BROWSERLESS_API_TOKEN}"
```

The hosted server speaks the Streamable HTTP transport, which is what Hermes uses for URL-based servers. No local process, Node, or Docker is required.

## Step 2: Verify the tools loaded

Start Hermes from the terminal and ask it what it can do:

```bash
hermes chat
```

```text
Tell me which MCP-backed tools are available right now.
```

The reply should list the Browserless tools. Hermes prefixes MCP tools with the server name, so they appear as `mcp__browserless__browserless_agent`, `mcp__browserless__browserless_smartscraper`, and so on. If you edited the config while Hermes was running, reload it with `/reload-mcp`. See the full tool list in the [MCP server reference](/mcp/browserless-mcp-server/setup#tools).

## Step 3: Try it on a real task

Give Hermes a task its built-in browser tends to get blocked on:

```text
Use the Browserless tools to scrape https://www.browserless.io/pricing
and list each plan with its unit allowance, then tell me which strategy
the scraper used.
```

```text
Use the Browserless agent to open https://docs.browserless.io, navigate
to the BaaS quickstart, and summarize the setup steps.
```

## Optional: Limit which tools load

The server exposes nine tools. If you want a smaller surface, use Hermes's tool filtering to whitelist just the ones you need:

```yaml
mcp_servers:
  browserless:
    url: "https://mcp.browserless.io/mcp"
    headers:
      Authorization: "Bearer ${BROWSERLESS_API_TOKEN}"
    tools:
      include: [browserless_agent, browserless_smartscraper]
```

`browserless_agent` plus `browserless_smartscraper` covers most workflows: one stateful browser for interaction, one stateless scraper for content.

## FAQ & Troubleshooting

<details>
<summary>Can I point Hermes's <code>/browser connect</code> at a Browserless CDP endpoint instead?</summary>

No. `/browser connect` is built for local Chromium-family browsers: its readiness probe requests `/json/version` without preserving the URL's query string, so an authenticated endpoint like `wss://production-sfo.browserless.io?token=...` fails the check. Use the MCP route on this page, which authenticates properly and exposes richer tools than raw CDP.

</details>

<details>
<summary>Hermes doesn't list any Browserless tools</summary>

Run `/reload-mcp` if you changed `config.yaml` during a session, and check the Hermes logs for a connection error. The usual causes are a URL typo (the endpoint must be exactly `https://mcp.browserless.io/mcp`), an expired token, or `BROWSERLESS_API_TOKEN` not being set in `~/.hermes/.env` — Hermes leaves an unset `${VAR}` as its literal placeholder text, so the server receives a header of `Bearer ${BROWSERLESS_API_TOKEN}` and rejects it.

</details>

<details>
<summary>What do MCP sessions cost?</summary>

Each tool call opens a browser session that consumes units based on session time, plus proxy traffic and CAPTCHA solving if used. See [unit consumption](/overview/unit-consumption#mcp-session-cost) for the breakdown.

</details>

## Next steps

  <a href="/mcp/browserless-mcp-server/setup" className="next-step-card-link">
    
      <h3 className="next-step-card-title">MCP server setup</h3>
      authentication options, regions, and the full tool list
    
  </a>
  <a href="/mcp/browser-agent" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Browser Agent</h3>
      the stateful browser session your agent drives across turns
    
  </a>
  <a href="/mcp/rest-api-tools" className="next-step-card-link">
    
      <h3 className="next-step-card-title">REST API tools</h3>
      the eight stateless tools for scraping, search, and exports
    
  </a>

---

# LangChain Integration

URL: https://docs.browserless.io/ai-integrations/langchain

# LangChain Integration

[LangChain](https://www.langchain.com/) is a framework for developing applications powered by language models. By integrating Browserless with LangChain, you can give your AI applications web scraping and content processing capabilities without managing browser infrastructure.

- Python 3.8 or higher
- Browserless API token (available in your [account dashboard](https://browserless.io/account/))

> **Note**
> This guide focuses on `BrowserlessLoader`, which is useful for scraping and document ingestion. If you need full browser control or multi-step automation, start with [BrowserQL](/browserql/start) or one of the [agent-focused guides](/ai-integrations/start#agent-frameworks).

## Step-by-Step Setup

  
    Go to your <a href="https://browserless.io/account/" target="_blank" rel="noopener noreferrer">Browserless account dashboard</a> and copy your API token.
    Then set the <code>BROWSERLESS_API_TOKEN</code> environment variable in your <code>.env</code> file:
    
      
**.env file:**

```text
BROWSERLESS_API_TOKEN=your-token-here
```
      
      
**Command line:**

```bash
export BROWSERLESS_API_TOKEN=your-token-here
```
      
    
  

  
    Set up a Python virtual environment to manage your dependencies:
    
      
**venv:**

```bash
python -m venv .venv
source .venv/bin/activate  # On Windows: .venv\Scripts\activate
```
      
      
**conda:**

```bash
conda create -n langchain-env python=3.8
conda activate langchain-env
```
      
    
  

  
    Install LangChain and other required packages:
    
      
**pip:**

```bash
pip install langchain-community python-dotenv
```
      
      
**Poetry:**

```bash
poetry add langchain-community python-dotenv
```
      
    
  

  
    Create a file named <code>scraper.py</code> with the following complete code:

```python
from dotenv import load_dotenv
import os
from langchain_community.document_loaders import BrowserlessLoader

def main():
    # Load environment variables
    load_dotenv()

    # Initialize the loader with your API token
    loader = BrowserlessLoader(
        api_token=os.getenv("BROWSERLESS_API_TOKEN"),
        urls=["https://example.com"],
        text_content=True  # Get text content instead of raw HTML
    )

    # Load and process the documents
    documents = loader.load()

    # Print the results
    for doc in documents:
        print(f"Source: {doc.metadata.get('source')}")
        print(f"Content: {doc.page_content[:200]}...")

if __name__ == "__main__":
    main()
```
  

  
    Run your application with the following command:

```bash
python scraper.py
```

    You should see output showing the scraped content from the example website.
  

## How It Works

**1. Connection Setup**: BrowserlessLoader connects to Browserless using your API token

**2. Content Loading**: The loader fetches and processes web content

**3. Document Creation**: Content is converted into LangChain Documents

**4. Processing**: Documents can be further processed with LangChain's tools

## Advanced Configuration

### Multiple URLs

Process multiple websites in a single operation:

```python
loader = BrowserlessLoader(
    api_token=api_token,
    urls=[
        "https://example1.com",
        "https://example2.com",
        "https://example3.com"
    ]
)
```

### Raw HTML Mode

Get raw HTML content instead of text:

```python
loader = BrowserlessLoader(
    api_token=api_token,
    urls=["https://example.com"],
    text_content=False
)
```

## Performance Optimization

1. **Batch Processing**
   - Process multiple URLs in batches
   - Implement proper error handling
   - Use async/await for better performance

2. **Resource Management**
   - Monitor memory usage
   - Implement proper cleanup
   - Handle timeouts appropriately

## Security Best Practices

1. **API Token Management**
   - Never commit tokens to version control
   - Use environment variables
   - Rotate tokens regularly

2. **Input Validation**
   - Validate URLs before processing
   - Implement rate limiting
   - Handle sensitive data appropriately

## Common Use Cases

### News Aggregation
```python
def aggregate_news(api_token, news_sites):
    loader = BrowserlessLoader(
        api_token=api_token,
        urls=news_sites,
        text_content=True
    )
    documents = loader.load()

    # Process and analyze the news content
    for doc in documents:
        print(f"Source: {doc.metadata.get('source')}")
        print(f"Content: {doc.page_content[:200]}...")
```

### Content Analysis
```python
from langchain.text_splitter import RecursiveCharacterTextSplitter

def analyze_content(api_token, url):
    # Load content
    loader = BrowserlessLoader(
        api_token=api_token,
        urls=[url],
        text_content=True
    )
    documents = loader.load()

    # Split content into chunks
    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=1000,
        chunk_overlap=200
    )
    chunks = text_splitter.split_documents(documents)

    # Process chunks
    for chunk in chunks:
        print(f"Chunk: {chunk.page_content[:100]}...")
```

For more advanced usage scenarios, please refer to:
- [LangChain Documentation](https://www.langchain.com/docs)
- [Browserless BaaS Documentation](/baas/start)
- [Integrating with other LLM providers](/ai-integrations/start)

## FAQ & Troubleshooting

<details>
<summary>How does <code>BrowserlessLoader</code> authenticate with Browserless?</summary>

Pass your token directly via the `api_token` parameter, or set it as the `BROWSERLESS_API_TOKEN` environment variable and load it with `os.getenv("BROWSERLESS_API_TOKEN")` as shown in the setup steps above.

</details>

<details>
<summary>Should I use <code>BrowserlessLoader</code> or BrowserQL?</summary>

Use `BrowserlessLoader` for straightforward scraping and document ingestion into LangChain. If the target site has aggressive bot detection, or you need multi-step interaction (clicking, filling forms) before extracting content, use [BrowserQL](/browserql/start) instead.

</details>

<details>
<summary>The loaded content is missing text I can see in the browser</summary>

Set `text_content=False` to get raw HTML instead of extracted text, which preserves structure that text extraction can drop. See [Raw HTML Mode](#raw-html-mode) above.

</details>

## Next steps

  <a href="/browserql/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BrowserQL</h3>
      for stealth, multi-step interaction, or advanced browser control
    
  </a>
  <a href="/ai-integrations/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">AI integrations overview</h3>
      all supported AI frameworks and tools
    
  </a>

---

# Browserless templates for Make.com

URL: https://docs.browserless.io/ai-integrations/make

# Using Browserless with Make.com

Make.com (formerly Integromat) is a visual automation platform that connects different services and automates tasks. This guide shows you how to use Browserless with Make.com to automate browser-based tasks.

- A Browserless account with an API token
- A Make.com account (free or paid)

## Getting Started

Create a free account on [Make.com](https://www.make.com) to get started with automation workflows.

    ![Sign up for Make.com](/img/ai/make/make_1_signup.png)

  Once logged in, go to the Scenarios section where you can create and manage your automation workflows.

    ![Navigate to Scenarios](/img/ai/make/make_2_scenarios.png)

  Click on "Create a new scenario" to start building your automation workflow with Browserless.

    ![Create a New Scenario](/img/ai/make/make_3_createScenario.png)

  Search for and add the HTTP module to your scenario. This will allow you to make requests to Browserless APIs.

    ![Add HTTP Module](/img/ai/make/make_4_http.png)

  Choose the "Make a Request" action from the HTTP module options.

    ![Make a Request](/img/ai/make/make_5_request.png)

  Configure your HTTP request with the Browserless endpoint and your API token. See the examples below for specific endpoint configurations.

    ![Configure Browserless](/img/ai/make/make_6_configure_browserless.png)

For the templates below, remember to replace `YOUR_API_TOKEN_HERE` with your actual Browserless API token.

## Available Endpoints

### Screenshot

The `/screenshot` endpoint allows you to capture screenshots of web pages. This is useful for:
- Visual monitoring of websites
- Creating thumbnails
- Documenting web content

[Learn more about the Screenshot API](/rest-apis/screenshot-api)

#### HTTP Module Configuration

1. Add an **HTTP** module and select the **Make a Request** action
2. Set **URL** to `https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE`
3. Set **Method** to `POST`
4. Set **Body type** to `Raw`
5. Set **Content type** to `application/json`
6. Add this JSON to the **Request content**:

```json
{
  "url": "https://www.example.com"
}
```

### PDF Generation

The `/pdf` endpoint generates PDF documents from web pages. This is useful for:
- Creating printable versions of web content
- Archiving web pages
- Generating reports

[Learn more about the PDF API](/rest-apis/pdf-api)

#### HTTP Module Configuration

1. Add an **HTTP** module and select the **Make a Request** action
2. Set **URL** to `https://production-sfo.browserless.io/pdf?token=YOUR_API_TOKEN_HERE`
3. Set **Method** to `POST`
4. Set **Body type** to `Raw`
5. Set **Content type** to `application/json`
6. Add this JSON to the **Request content**:

```json
{
  "url": "https://www.example.com"
}
```

### Content Extraction

The `/content` endpoint extracts the HTML content from web pages. This is useful for:
- Web scraping
- Content analysis
- Data extraction

[Learn more about the Content API](/rest-apis/content)

#### HTTP Module Configuration

1. Add an **HTTP** module and select the **Make a Request** action
2. Set **URL** to `https://production-sfo.browserless.io/content?token=YOUR_API_TOKEN_HERE`
3. Set **Method** to `POST`
4. Set **Body type** to `Raw`
5. Set **Content type** to `application/json`
6. Add this JSON to the **Request content**:

```json
{
  "url": "https://www.example.com"
}
```

### Unblock

The `/unblock` endpoint helps bypass common anti-bot measures. This is useful for:
- Accessing protected content
- Handling CAPTCHAs
- Managing cookies and sessions

[Learn more about the Unblock API](/rest-apis/unblock)

#### HTTP Module Configuration

1. Add an **HTTP** module and select the **Make a Request** action
2. Set **URL** to `https://production-sfo.browserless.io/unblock?token=YOUR_API_TOKEN_HERE`
3. Set **Method** to `POST`
4. Set **Body type** to `Raw`
5. Set **Content type** to `application/json`
6. Add this JSON to the **Request content**:

```json
{
  "url": "https://www.example.com",
  "cookies": true,
  "browserWSEndpoint": true,
  "content": true,
  "screenshot": true
}
```

### Browser Query Language (BQL)

The `/chrome/bql` endpoint allows you to execute complex browser automation tasks using GraphQL. This is useful for:
- Complex form filling
- Multi-step workflows
- Custom browser automation

[Learn more about BQL](/browserql/start)

#### HTTP Module Configuration

1. Add an **HTTP** module and select the **Make a Request** action
2. Set **URL** to `https://production-sfo.browserless.io/chrome/bql?token=YOUR_API_TOKEN_HERE`
3. Set **Method** to `POST`
4. Set **Body type** to `Raw`
5. Set **Content type** to `application/json`
6. Add this JSON to the **Request content**:

```json
{
  "query": "mutation FormExample {\n  goto(url: \"https://www.browserless.io/practice-form\") {\n    status\n  }\n select(selector:\"#Contact-Subject\",value:\"support\"){     time   } \n typeEmail: type(text: \"john@email.com\", selector: \"#Email\") {\n    time\n  }\n  typeMessage: type(\n    selector: \"#Message\"\n    text: \"Hello world!\"\n  ) {\n    time\n  }\n  solve(\n    type: cloudflare\n  ){\n    solved\n  }\n waitForTimeout(time:3000){time}\n screenshot{\n    base64\n  }\n}",
  "variables": {},
  "operationName": "FormExample"
}
```

### Function

The `/function` endpoint allows you to execute custom JavaScript code in a browser context. This is useful for:
- Custom browser automation
- Complex data extraction
- Multi-step workflows

[Learn more about the Function API](/rest-apis/function)

#### HTTP Module Configuration

1. Add an **HTTP** module and select the **Make a Request** action
2. Set **URL** to `https://production-sfo.browserless.io/function?token=YOUR_API_TOKEN_HERE`
3. Set **Method** to `POST`
4. Set **Body type** to `Raw`
5. Set **Content type** to `application/javascript`
6. Add this JavaScript to the **Request content**:

```javascript

  await page.goto("https://example.com/");
  const url = await page.content();
  const buffer = await page.pdf({ format: "A4" });
  const base64PDF = buffer.toString('base64');
  const screenshot = await page.screenshot({ encoding: "base64" });

  return {
    data: {
      url,
      screenshot,
      base64PDF
    },
    type: "application/json",
  };
}
```

## FAQ & Troubleshooting

<details>
<summary>The HTTP module returns 401 Unauthorized</summary>

Check that `token=YOUR_API_TOKEN_HERE` is on the request URL, not just in the body. A missing or expired token is the most common cause.

</details>

<details>
<summary>The screenshot or PDF response looks corrupted in Make</summary>

Make's HTTP module doesn't render binary responses inline. Add `"encoding": "base64"` to the request body (for endpoints that support it) so Browserless returns JSON with a base64 string you can pass to a downstream module instead of a raw binary buffer.

</details>

<details>
<summary>The request times out on slow-loading pages</summary>

Increase the module's timeout setting, and consider adding `waitUntil` or `bestAttempt` parameters to the Browserless endpoint so it doesn't wait indefinitely on pages that never fully settle.

</details>

## Next steps

  <a href="/ai-integrations/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">AI integrations overview</h3>
      all supported AI frameworks and tools
    
  </a>
  <a href="/rest-apis/intro" className="next-step-card-link">
    
      <h3 className="next-step-card-title">REST APIs overview</h3>
      explore every Browserless REST endpoint
    
  </a>

---

# n8n integration templates

URL: https://docs.browserless.io/ai-integrations/n8n

# n8n integration templates

Use n8n's HTTP Request node to call Browserless REST endpoints and BrowserQL. Copy a template below, paste it into your workflow, and replace the token value with your API token.

> **Before you copy**
> Store your API token in n8n Credentials and reference it in the HTTP Request node as the `token` query parameter.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Quickstart: Take a screenshot

Sign in to your n8n instance and create a new workflow.

    ![Create a new workflow](/img/ai/n8n/n8n_2_create.png)

  Add a manual trigger node to control when the workflow runs.

    ![Add manual trigger](/img/ai/n8n/n8n_3_manualtrigger.png)

  Add an HTTP Request node and configure it:

    - Method: POST
    - URL: `https://production-sfo.browserless.io/screenshot?token=YOUR_TOKEN`
    - Body: `{"url": "https://example.com"}`

    ![Add HTTP Request](/img/ai/n8n/n8n_4_httprequest.png)

    ![Configure endpoint](/img/ai/n8n/n8n_5_configureendpoint.png)

  Click "Execute workflow" to capture a screenshot. The response will contain the image buffer.

Copy the templates below to quickly add other Browserless endpoints to your workflows.

## Templates

### Screenshot

Capture a screenshot of any URL. POST `https://production-sfo.browserless.io/screenshot` with `url` in the body.

Use it to: visual monitoring, creating thumbnails, documenting web content.

Learn more about the [Screenshot API](/rest-apis/screenshot-api).

  
{`{
  "nodes": [
    {
      "parameters": {},
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ],
      "id": "73479c89-44a3-4f49-ba59-97f99a23f5b9",
      "name": "When clicking 'Execute workflow'"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://production-sfo.browserless.io/screenshot",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "token",
              "value": "YOUR_API_TOKEN_HERE"
            }
          ]
        },
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "url",
              "value": "https://www.example.com"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        240,
        -80
      ],
      "id": "afd7858f-1db8-4251-b3f9-aaa6a1f8ad95",
      "name": "Screenshot buffer"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://production-sfo.browserless.io/screenshot",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "token",
              "value": "YOUR_API_TOKEN_HERE"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "{\\n  \\\"url\\\": \\\"https://www.example.com\\\",\\n  \\\"options\\\": {\\n    \\\"encoding\\\": \\\"base64\\\"\\n  }\\n}",
        "options": {
          "redirect": {
            "redirect": {}
          }
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        240,
        128
      ],
      "id": "d2ba4e93-62ff-49ed-874a-78c077045b9d",
      "name": "Screenshot base64"
    },
    {
      "parameters": {
        "content": "## Take a Screenshot\\nYou can generate a screenshot as a **Buffer or Base64** Learn more about the [Screenshot API here.](https://docs.browserless.io/rest-apis/screenshot-api)",
        "width": 304
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -368,
        0
      ],
      "typeVersion": 1,
      "id": "82d552de-4927-4c8d-8ec1-8956d939d155",
      "name": "Sticky Note"
    }
  ],
  "connections": {
    "When clicking 'Execute workflow'": {
      "main": [
        [
          {
            "node": "Screenshot buffer",
            "type": "main",
            "index": 0
          },
          {
            "node": "Screenshot base64",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Screenshot buffer": {
      "main": [
        []
      ]
    }
  },
  "pinData": {},
  "meta": {
    "templateCredsSetupCompleted": true,
    "instanceId": "72f423f1e1c447db670aa3dc4919caee6676a7384f1dac4911c50b24b11b1249"
  }
}`}
  
  <button id="copyButton" onClick={() => {
    const textToCopy = document.getElementById('myTextToCopy').innerText;
    const copyIcon = document.getElementById('copyIcon');
    const copyText = document.getElementById('copyText');
    navigator.clipboard.writeText(textToCopy)
      .then(() => {
        copyIcon.innerHTML = '<polyline points="20 6 9 17 4 12"></polyline>';
        copyText.textContent = 'Copied to your clipboard!';
        setTimeout(() => {
          copyIcon.innerHTML = '<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>';
          copyText.textContent = 'Copy Screenshot Template to Clipboard';
        }, 2000);
      })
      .catch(err => {
        console.error('Failed to copy text: ', err);
        alert('Failed to copy text.');
      });
  }} style={{display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '6px', fontFamily: 'inherit', backgroundColor: 'var(--ifm-color-primary)', color: 'white', border: 'none', padding: '15px 30px', fontSize: '0.8rem', fontWeight: '500', cursor: 'pointer', transition: 'background-color 0.3s', borderRadius: '0.25rem'}}>
    <svg id="copyIcon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
      <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
      <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
    </svg>
    Copy Screenshot Template to Clipboard
  </button>

### PDF

Generate a PDF from a URL. POST `https://production-sfo.browserless.io/pdf` with `url` in the body.

Use it to: creating printable versions, archiving web pages, generating reports.

Learn more about the [PDF API](/rest-apis/pdf-api).

  
{`{
  "nodes": [
    {
      "parameters": {},
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        -64,
        -48
      ],
      "id": "f7231783-1ab2-4587-bdaf-5139d2dfad95",
      "name": "When clicking 'Execute workflow'"
    },
    {
      "parameters": {
        "content": "## Generate a PDF\\nYou can generate a PDF Learn more about the [PDF API here.](https://docs.browserless.io/rest-apis/pdf-api)",
        "width": 304
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -432,
        -48
      ],
      "typeVersion": 1,
      "id": "82cceda8-fee2-42f9-abef-a6178b8f9e69",
      "name": "Sticky Note"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://production-sfo.browserless.io/pdf",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "token",
              "value": "YOUR_API_TOKEN_HERE"
            }
          ]
        },
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "url",
              "value": "https://www.example.com"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        144,
        -48
      ],
      "id": "3373552b-36c4-4061-bd38-40398211904d",
      "name": "PDF"
    }
  ],
  "connections": {
    "When clicking 'Execute workflow'": {
      "main": [
        [
          {
            "node": "PDF",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "meta": {
    "templateCredsSetupCompleted": true,
    "instanceId": "72f423f1e1c447db670aa3dc4919caee6676a7384f1dac4911c50b24b11b1249"
  }
}`}
  
  <button id="copyButtonPdf" onClick={() => {
    const textToCopy = document.getElementById('myTextToCopyPdf').innerText;
    const copyIcon = document.getElementById('copyIconPdf');
    const copyText = document.getElementById('copyTextPdf');
    navigator.clipboard.writeText(textToCopy)
      .then(() => {
        copyIcon.innerHTML = '<polyline points="20 6 9 17 4 12"></polyline>';
        copyText.textContent = 'Copied to your clipboard!';
        setTimeout(() => {
          copyIcon.innerHTML = '<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>';
          copyText.textContent = 'Copy PDF Template to Clipboard';
        }, 2000);
      })
      .catch(err => {
        console.error('Failed to copy text: ', err);
        alert('Failed to copy text.');
      });
  }} style={{display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '6px', fontFamily: 'inherit', backgroundColor: 'var(--ifm-color-primary)', color: 'white', border: 'none', padding: '15px 30px', fontSize: '0.8rem', fontWeight: '500', cursor: 'pointer', transition: 'background-color 0.3s', borderRadius: '0.25rem'}}>
    <svg id="copyIconPdf" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
      <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
      <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
    </svg>
    Copy PDF Template to Clipboard
  </button>

### Content

Fetch the page HTML. POST `https://production-sfo.browserless.io/content` with `url` in the body.

Use it to: web scraping, content analysis, data extraction.

Learn more about the [Content API](/rest-apis/content).

  
{`{
  "nodes": [
    {
      "parameters": {},
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        432,
        -144
      ],
      "id": "dc05fa4c-e60d-4082-8a1f-158e28032c38",
      "name": "When clicking 'Execute workflow'"
    },
    {
      "parameters": {
        "content": "## Scrape content\\nFetch the HTML content of a site, read more about the [Content API here.](https://docs.browserless.io/rest-apis/content)",
        "width": 304
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        64,
        -144
      ],
      "typeVersion": 1,
      "id": "4f000aa2-aeb9-4bb9-83f6-5a5f9b0baec4",
      "name": "Sticky Note"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://production-sfo.browserless.io/content",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "token",
              "value": "YOUR_API_TOKEN_HERE"
            }
          ]
        },
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "url",
              "value": "https://www.example.com"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        640,
        -144
      ],
      "id": "a3aa914f-8f37-404d-b7c6-5f5c3493a303",
      "name": "Content"
    }
  ],
  "connections": {
    "When clicking 'Execute workflow'": {
      "main": [
        [
          {
            "node": "Content",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "meta": {
    "templateCredsSetupCompleted": true,
    "instanceId": "72f423f1e1c447db670aa3dc4919caee6676a7384f1dac4911c50b24b11b1249"
  }
}`}
  
  <button id="copyButtonContent" onClick={() => {
    const textToCopy = document.getElementById('myTextToCopyContent').innerText;
    const copyIcon = document.getElementById('copyIconContent');
    const copyText = document.getElementById('copyTextContent');
    navigator.clipboard.writeText(textToCopy)
      .then(() => {
        copyIcon.innerHTML = '<polyline points="20 6 9 17 4 12"></polyline>';
        copyText.textContent = 'Copied to your clipboard!';
        setTimeout(() => {
          copyIcon.innerHTML = '<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2 2v1"></path>';
          copyText.textContent = 'Copy Content Template to Clipboard';
        }, 2000);
      })
      .catch(err => {
        console.error('Failed to copy text: ', err);
        alert('Failed to copy text.');
      });
  }} style={{display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '6px', fontFamily: 'inherit', backgroundColor: 'var(--ifm-color-primary)', color: 'white', border: 'none', padding: '15px 30px', fontSize: '0.8rem', fontWeight: '500', cursor: 'pointer', transition: 'background-color 0.3s', borderRadius: '0.25rem'}}>
    <svg id="copyIconContent" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
      <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
      <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
    </svg>
    Copy Content Template to Clipboard
  </button>

### Unblock

Bypass bot detection and optionally return session details (cookies, browserWSEndpoint, content, screenshot). POST `https://production-sfo.browserless.io/unblock` with `url` and flags.

Use it to: accessing protected content, handling CAPTCHAs, managing cookies and sessions.

Learn more about the [Unblock API](/rest-apis/unblock).

  
{`{
  "nodes": [
    {
      "parameters": {},
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        -64,
        -48
      ],
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "name": "When clicking 'Execute workflow'"
    },
    {
      "parameters": {
        "content": "## Unblock\\nBypass common anti-bot measures, read more about the [Unblock API here.](https://docs.browserless.io/rest-apis/unblock)",
        "width": 304
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -432,
        -48
      ],
      "typeVersion": 1,
      "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
      "name": "Sticky Note"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://production-sfo.browserless.io/unblock",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "token",
              "value": "YOUR_API_TOKEN_HERE"
            }
          ]
        },
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "url",
              "value": "https://www.example.com"
            },
            {
              "name": "cookies",
              "value": "true"
            },
            {
              "name": "browserWSEndpoint",
              "value": "true"
            },
            {
              "name": "content",
              "value": "true"
            },
            {
              "name": "screenshot",
              "value": "true"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        144,
        -48
      ],
      "id": "c3d4e5f6-a789-0123-cdef-123456789012",
      "name": "Unblock"
    }
  ],
  "connections": {
    "When clicking 'Execute workflow'": {
      "main": [
        [
          {
            "node": "Unblock",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "meta": {
    "templateCredsSetupCompleted": true,
    "instanceId": "72f423f1e1c447db670aa3dc4919caee6676a7384f1dac4911c50b24b11b1249"
  }
}`}
  
  <button id="copyButtonUnblock" onClick={() => {
    const textToCopy = document.getElementById('myTextToCopyUnblock').innerText;
    const copyIcon = document.getElementById('copyIconUnblock');
    const copyText = document.getElementById('copyTextUnblock');
    navigator.clipboard.writeText(textToCopy)
      .then(() => {
        copyIcon.innerHTML = '<polyline points="20 6 9 17 4 12"></polyline>';
        copyText.textContent = 'Copied to your clipboard!';
        setTimeout(() => {
          copyIcon.innerHTML = '<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>';
          copyText.textContent = 'Copy Unblock Template to Clipboard';
        }, 2000);
      })
      .catch(err => {
        console.error('Failed to copy text: ', err);
        alert('Failed to copy text.');
      });
  }} style={{display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '6px', fontFamily: 'inherit', backgroundColor: 'var(--ifm-color-primary)', color: 'white', border: 'none', padding: '15px 30px', fontSize: '0.8rem', fontWeight: '500', cursor: 'pointer', transition: 'background-color 0.3s', borderRadius: '0.25rem'}}>
    <svg id="copyIconUnblock" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
      <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
      <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
    </svg>
    Copy Unblock Template to Clipboard
  </button>

### Scrape

Extract structured data with CSS selectors. POST `https://production-sfo.browserless.io/scrape` with `elements` array.

Use it to: extracting specific content, structured data collection, automated data gathering.

Learn more about the [Scrape API](/rest-apis/scrape).

  
{`{
  "nodes": [
    {
      "parameters": {},
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        -64,
        -48
      ],
      "id": "j7k8l9m0-n1o2-3456-jklm-no7890123456",
      "name": "When clicking 'Execute workflow'"
    },
    {
      "parameters": {
        "content": "## Scrape\\nExtract structured data from web pages using CSS selectors, read more about the [Scrape API here.](https://docs.browserless.io/rest-apis/scrape)",
        "width": 304
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -432,
        -48
      ],
      "typeVersion": 1,
      "id": "k8l9m0n1-o2p3-4567-klmn-op8901234567",
      "name": "Sticky Note"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://production-sfo.browserless.io/scrape",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "token",
              "value": "YOUR_API_TOKEN_HERE"
            }
          ]
        },
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "{\\"url\\": \\"https://www.example.com\\", \\"elements\\": [{\\"selector\\": \\"h1\\"}, {\\"selector\\": \\"p\\"}]}"
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        144,
        -48
      ],
      "id": "l9m0n1o2-p3q4-5678-lmno-pq9012345678",
      "name": "Scrape"
    }
  ],
  "connections": {
    "When clicking 'Execute workflow'": {
      "main": [
        [
          {
            "node": "Scrape",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "meta": {
    "templateCredsSetupCompleted": true,
    "instanceId": "72f423f1e1c447db670aa3dc4919caee6676a7384f1dac4911c50b24b11b1249"
  }
}`}
  
  <button id="copyButtonScrape" onClick={() => {
    const textToCopy = document.getElementById('myTextToCopyScrape').innerText;
    const copyIcon = document.getElementById('copyIconScrape');
    const copyText = document.getElementById('copyTextScrape');
    navigator.clipboard.writeText(textToCopy)
      .then(() => {
        copyIcon.innerHTML = '<polyline points="20 6 9 17 4 12"></polyline>';
        copyText.textContent = 'Copied to your clipboard!';
        setTimeout(() => {
          copyIcon.innerHTML = '<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2 2v1"></path>';
          copyText.textContent = 'Copy Scrape Template to Clipboard';
        }, 2000);
      })
      .catch(err => {
        console.error('Failed to copy text: ', err);
        alert('Failed to copy text.');
      });
  }} style={{display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '6px', fontFamily: 'inherit', backgroundColor: 'var(--ifm-color-primary)', color: 'white', border: 'none', padding: '15px 30px', fontSize: '0.8rem', fontWeight: '500', cursor: 'pointer', transition: 'background-color 0.3s', borderRadius: '0.25rem'}}>
    <svg id="copyIconScrape" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
      <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
      <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
    </svg>
    Copy Scrape Template to Clipboard
  </button>

### Browser Query Language (BQL)

Run BrowserQL (GraphQL) to automate multi-step flows. POST `https://production-sfo.browserless.io/chrome/bql` with a GraphQL query.

Use it to: complex form filling, multi-step workflows, custom browser automation.

Learn more about [BrowserQL](/browserql/start).

  
{`{
  "nodes": [
    {
      "parameters": {},
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        -64,
        -48
      ],
      "id": "d1e2f3a4-b5c6-7890-defg-hi1234567890",
      "name": "When clicking 'Execute workflow'"
    },
    {
      "parameters": {
        "content": "## Browser Query Language (BQL)\\nExecute complex browser automation using GraphQL, read more about [BQL here.](https://docs.browserless.io/browserql/start)",
        "width": 304
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -432,
        -48
      ],
      "typeVersion": 1,
      "id": "e2f3a4b5-c6d7-8901-efgh-ij2345678901",
      "name": "Sticky Note"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://production-sfo.browserless.io/chrome/bql",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "token",
              "value": "YOUR_API_TOKEN_HERE"
            }
          ]
        },
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "{\\"query\\": \\"mutation FormExample {\\\\n  goto(url: \\\\\\"https://www.browserless.io/practice-form\\\\\\") {\\\\n    status\\\\n  }\\\\n select(selector:\\\\\\"#Contact-Subject\\\\\\",value:\\\\\\"support\\\\\\"){     time   } \\\\n typeEmail: type(text: \\\\\\"john@email.com\\\\\\", selector: \\\\\\"#Email\\\\\\") {\\\\n    time\\\\n  }\\\\n  typeMessage: type(\\\\n    selector: \\\\\\"#Message\\\\\\"\\\\n    text: \\\\\\"Hello world!\\\\\\"\\\\n  ) {\\\\n    time\\\\n  }\\\\n  solve(\\\\n    type: cloudflare\\\\n  ){\\\\n    solved\\\\n  }\\\\n waitForTimeout(time:3000){time}\\\\n screenshot{\\\\n    base64\\\\n  }\\\\n}\\", \\"variables\\": {}, \\"operationName\\": \\"FormExample\\"}"
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        144,
        -48
      ],
      "id": "f3a4b5c6-d7e8-9012-fghi-jk3456789012",
      "name": "BQL"
    }
  ],
  "connections": {
    "When clicking 'Execute workflow'": {
      "main": [
        [
          {
            "node": "BQL",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "meta": {
    "templateCredsSetupCompleted": true,
    "instanceId": "72f423f1e1c447db670aa3dc4919caee6676a7384f1dac4911c50b24b11b1249"
  }
}`}
  
  <button id="copyButtonBql" onClick={() => {
    const textToCopy = document.getElementById('myTextToCopyBql').innerText;
    const copyIcon = document.getElementById('copyIconBql');
    const copyText = document.getElementById('copyTextBql');
    navigator.clipboard.writeText(textToCopy)
      .then(() => {
        copyIcon.innerHTML = '<polyline points="20 6 9 17 4 12"></polyline>';
        copyText.textContent = 'Copied to your clipboard!';
        setTimeout(() => {
          copyIcon.innerHTML = '<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>';
          copyText.textContent = 'Copy BQL Template to Clipboard';
        }, 2000);
      })
      .catch(err => {
        console.error('Failed to copy text: ', err);
        alert('Failed to copy text.');
      });
  }} style={{display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '6px', fontFamily: 'inherit', backgroundColor: 'var(--ifm-color-primary)', color: 'white', border: 'none', padding: '15px 30px', fontSize: '0.8rem', fontWeight: '500', cursor: 'pointer', transition: 'background-color 0.3s', borderRadius: '0.25rem'}}>
    <svg id="copyIconBql" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
      <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
      <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
    </svg>
    Copy BQL Template to Clipboard
  </button>

### Function

Run custom JavaScript in a browser context. POST `https://production-sfo.browserless.io/function` with JS code.

Use it to: custom browser automation, complex data extraction, multi-step workflows.

Learn more about the [Function API](/rest-apis/function).

  
{`{
  "nodes": [
    {
      "parameters": {},
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        -64,
        -48
      ],
      "id": "g4h5i6j7-k8l9-0123-ghij-kl4567890123",
      "name": "When clicking 'Execute workflow'"
    },
    {
      "parameters": {
        "content": "## Function\\nExecute custom JavaScript code in a browser context, read more about the [Function API here.](https://docs.browserless.io/rest-apis/function)",
        "width": 304
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -432,
        -48
      ],
      "typeVersion": 1,
      "id": "h5i6j7k8-l9m0-1234-hijk-lm5678901234",
      "name": "Sticky Note"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://production-sfo.browserless.io/function",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "token",
              "value": "YOUR_API_TOKEN_HERE"
            }
          ]
        },
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/javascript",
        "body": "export default async function ({ page }) {await page.goto(\\"https://example.com/\\");\\n  const url = await page.content();\\n  const buffer = await page.pdf({ format: \\"A4\\" });\\n  const base64PDF = buffer.toString('base64');\\n  const screenshot = await page.screenshot({ encoding: \\"base64\\" });\\n\\n  return {\\n    data: {\\n      url,\\n      screenshot,\\n      base64PDF\\n    },\\n    type: \\"application/json\\",\\n  };\\n}"
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        144,
        -48
      ],
      "id": "i6j7k8l9-m0n1-2345-ijkl-mn6789012345",
      "name": "Function"
    }
  ],
  "connections": {
    "When clicking 'Execute workflow'": {
      "main": [
        [
          {
            "node": "Function",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "meta": {
    "templateCredsSetupCompleted": true,
    "instanceId": "72f423f1e1c447db670aa3dc4919caee6676a7384f1dac4911c50b24b11b1249"
  }
}`}
  
  <button id="copyButtonFunction" onClick={() => {
    const textToCopy = document.getElementById('myTextToCopyFunction').innerText;
    const copyIcon = document.getElementById('copyIconFunction');
    const copyText = document.getElementById('copyTextFunction');
    navigator.clipboard.writeText(textToCopy)
      .then(() => {
        copyIcon.innerHTML = '<polyline points="20 6 9 17 4 12"></polyline>';
        copyText.textContent = 'Copied to your clipboard!';
        setTimeout(() => {
          copyIcon.innerHTML = '<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>';
          copyText.textContent = 'Copy Function Template to Clipboard';
        }, 2000);
      })
      .catch(err => {
        console.error('Failed to copy text: ', err);
        alert('Failed to copy text.');
      });
  }} style={{display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '6px', fontFamily: 'inherit', backgroundColor: 'var(--ifm-color-primary)', color: 'white', border: 'none', padding: '15px 30px', fontSize: '0.8rem', fontWeight: '500', cursor: 'pointer', transition: 'background-color 0.3s', borderRadius: '0.25rem'}}>
    <svg id="copyIconFunction" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
      <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
      <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
    </svg>
    Copy Function Template to Clipboard
  </button>

### Google Maps Scraper

Scrape Google Maps business listings (name, address, rating, reviews, phone, website) for a list of cities, paginate through results, and write everything to a Google Sheet. Uses stealth BQL to open Google Maps search results, click into each listing, and extract details before advancing to the next page.

Use it to: local lead generation, business directory building, competitor mapping across multiple cities.

Learn more about [BrowserQL here](/browserql/start).

  
{`{
  "nodes": [
    {
      "parameters": {},
      "id": "5e563e67-240b-4fc3-b92b-f393a3c34c03",
      "name": "Start",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        -64,
        -80
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const cityName = $input.item.json.cities;\\nconst maxPages = $input.item.json.maxPages || 6;\\n\\nif (!cityName) {\\n  throw new Error(\\"cityName is required\\");\\n}\\n\\nconst searchQuery = \`dentist near \${cityName}\`;\\n\\nconst googleUrl = \`https://www.google.com/search?q=\${encodeURIComponent(searchQuery)}&sa=X&udm=1\`;\\n\\nreturn {\\n  json: {\\n    cityName,\\n    searchQuery,\\n    maxPages,\\n    googleUrl\\n  }\\n};"
      },
      "id": "dcf9844f-c921-4345-adae-301b1d5d1bd0",
      "name": "Prepare Search",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        736,
        -64
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://production-sfo.browserless.io/stealth/bql",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "blockConsentModals",
              "value": "true"
            },
            {
              "name": "token",
              "value": "YOUR_API_TOKEN_HERE"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ { \\"query\\": \\"mutation GoToSite { viewport(width: 1366, height: 768) { width height time } goto(url: \\\\\\"\\" + $json.googleUrl + \\"\\\\\\", waitUntil: networkIdle) { status } reconnect(timeout: 30000) { browserQLEndpoint } }\\", \\"operationName\\": \\"GoToSite\\" } }}",
        "options": {}
      },
      "id": "dde09a44-ba04-43ed-b735-8e57b0475bda",
      "name": "Open Google",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        960,
        -64
      ]
    },
    {
      "parameters": {
        "jsCode": "const cityName = $(\\"Prepare Search\\").first().json.cityName;\\nconst searchQuery = $(\\"Prepare Search\\").first().json.searchQuery;\\nconst currentPageData = $input.first().json.data.scrapeBusinesses.value;\\n\\nconst results = [];\\n\\ntry {\\n  const businesses = JSON.parse(currentPageData);\\n  businesses.forEach((business, businessIndex) => {\\n    results.push({\\n      cityName,\\n      searchQuery,\\n      page: $input.first().json.currentPage,\\n      index: businessIndex,\\n      title: business.title || \\"\\",\\n      address: business.address || \\"\\",\\n      rating: business.rating || \\"\\",\\n      reviews: business.reviews || \\"\\",\\n      phone: (business.phone || \\"\\").replace(/^\\\\+/, \\"\\"),\\n      website: business.website || \\"\\",\\n      error: business.error || null\\n    });\\n  });\\n} catch (e) {\\n  results.push({\\n    cityName,\\n    searchQuery,\\n    page: $input.first().json.currentPage,\\n    index: 0,\\n    title: \\"\\",\\n    address: \\"\\",\\n    rating: \\"\\",\\n    reviews: \\"\\",\\n    phone: \\"\\",\\n    error: \`Parse error: \${e.message}\`\\n  });\\n}\\n\\nreturn results.map(item => ({ json: item }));"
      },
      "id": "d94b80e5-494d-43b2-9590-bc52cf97dc3b",
      "name": "Parse Places",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1328,
        160
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $json.browserQLEndpoint }}",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "token",
              "value": "YOUR_API_TOKEN_HERE"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ { \\"query\\": \\"mutation ScrapeTwentyPlacesThenClickNext {\\\\n  scrapeBusinesses: evaluate(\\\\n    timeout: 120000\\\\n    content: \\\\\\"\\\\\\"\\\\\\"\\\\n      const sleep = (ms) =>\\\\n        new Promise(resolve => setTimeout(resolve, ms));\\\\n\\\\n      const clean = (value) =>\\\\n        (value || \\\\\\"\\\\\\")\\\\n          .replace(/[\\\\\\\\u200E\\\\\\\\u200F\\\\\\\\u2066-\\\\\\\\u2069]/g, \\\\\\"\\\\\\")\\\\n          .replace(/\\\\\\\\s+/g, \\\\\\" \\\\\\")\\\\n          .trim();\\\\n\\\\n      const cardSelector =\\\\n        '#rso [id^=\\\\\\"pv-\\\\\\"][role=\\\\\\"button\\\\\\"]:has([role=\\\\\\"heading\\\\\\"])';\\\\n\\\\n      const waitForBusiness = async (expectedTitle, timeout = 10000) => {\\\\n        const started = Date.now();\\\\n\\\\n        while (Date.now() - started < timeout) {\\\\n          const currentTitle = clean(\\\\n            document.querySelector(\\\\n              '#local-place-viewer h2.BK5CCe'\\\\n            )?.textContent\\\\n          );\\\\n\\\\n          if (currentTitle === expectedTitle) {\\\\n            return true;\\\\n          }\\\\n\\\\n          await sleep(200);\\\\n        }\\\\n\\\\n        return false;\\\\n      };\\\\n\\\\n      const waitForWebsite = async (panel, timeout = 2500) => {\\\\n        const started = Date.now();\\\\n\\\\n        while (Date.now() - started < timeout) {\\\\n          const websiteLink = [\\\\n            ...panel.querySelectorAll(\\\\\\"a[href]\\\\\\")\\\\n          ].find(element => {\\\\n            const text = clean(element.textContent);\\\\n            const ariaLabel = clean(\\\\n              element.getAttribute(\\\\\\"aria-label\\\\\\")\\\\n            );\\\\n\\\\n            return (\\\\n              text === \\\\\\"Website\\\\\\" ||\\\\n              /^Website:/i.test(ariaLabel) ||\\\\n              element.getAttribute(\\\\\\"data-item-id\\\\\\") === \\\\\\"authority\\\\\\"\\\\n            );\\\\n          });\\\\n\\\\n          if (websiteLink?.href) {\\\\n            return websiteLink.href;\\\\n          }\\\\n\\\\n          await sleep(200);\\\\n        }\\\\n\\\\n        return null;\\\\n      };\\\\n\\\\n      const results = [];\\\\n\\\\n      for (let index = 0; index < 20; index++) {\\\\n        // Re-query every time because Google can rerender the cards.\\\\n        const businesses = [\\\\n          ...document.querySelectorAll(cardSelector)\\\\n        ];\\\\n\\\\n        const business = businesses[index];\\\\n\\\\n        if (!business) {\\\\n          break;\\\\n        }\\\\n\\\\n        const expectedTitle = clean(\\\\n          business.querySelector('[role=\\\\\\"heading\\\\\\"]')\\\\n            ?.textContent\\\\n        );\\\\n\\\\n        try {\\\\n          business.scrollIntoView({\\\\n            block: \\\\\\"center\\\\\\",\\\\n            inline: \\\\\\"nearest\\\\\\"\\\\n          });\\\\n\\\\n          business.click();\\\\n\\\\n          const loaded = await waitForBusiness(expectedTitle);\\\\n\\\\n          if (!loaded) {\\\\n            throw new Error(\\\\n              \`Timed out waiting for \${expectedTitle}\`\\\\n            );\\\\n          }\\\\n\\\\n          // Let the detail panel finish rendering.\\\\n          await sleep(500);\\\\n\\\\n          const panel = document.querySelector(\\\\n            \\\\\\"#local-place-viewer\\\\\\"\\\\n          );\\\\n\\\\n          if (!panel) {\\\\n            throw new Error(\\\\n              \\\\\\"Business detail panel not found\\\\\\"\\\\n            );\\\\n          }\\\\n\\\\n          const reviewBlock = [\\\\n            ...panel.querySelectorAll(\\\\\\".C9waJd\\\\\\")\\\\n          ].find(element =>\\\\n            element.querySelector(\\\\\\".Bye9Fc\\\\\\") &&\\\\n            /reviews?/i.test(clean(element.textContent))\\\\n          );\\\\n\\\\n          const rating =\\\\n            clean(\\\\n              reviewBlock\\\\n                ?.querySelector(\\\\\\".Bye9Fc\\\\\\")\\\\n                ?.childNodes?.[0]\\\\n                ?.textContent\\\\n            ) ||\\\\n            clean(\\\\n              panel.querySelector(\\\\\\".rGaJuf\\\\\\")\\\\n                ?.textContent\\\\n            ) ||\\\\n            null;\\\\n\\\\n          const reviews = [\\\\n            ...(reviewBlock?.querySelectorAll(\\\\\\"span\\\\\\") || [])\\\\n          ]\\\\n            .map(element => clean(element.textContent))\\\\n            .find(text => /reviews?/i.test(text)) ||\\\\n            null;\\\\n\\\\n          const phoneLinks = panel.querySelectorAll(\\\\n            \\\\\\"a[data-phone-number]\\\\\\"\\\\n          );\\\\n\\\\n          const phoneLink =\\\\n            phoneLinks[1] || phoneLinks[0] || null;\\\\n\\\\n          const website = await waitForWebsite(panel);\\\\n\\\\n          results.push({\\\\n            index,\\\\n\\\\n            title:\\\\n              clean(\\\\n                panel.querySelector(\\\\\\"h2.BK5CCe\\\\\\")\\\\n                  ?.textContent\\\\n              ) || expectedTitle,\\\\n\\\\n            address:\\\\n              clean(\\\\n                panel.querySelector(\\\\n                  \\\\\\".C9waJd.y7xX3d span\\\\\\"\\\\n                )?.textContent\\\\n              ) ||\\\\n              clean(\\\\n                panel.querySelector(\\\\n                  \\\\\\".C9waJd.y7xX3d\\\\\\"\\\\n                )?.textContent\\\\n              ) ||\\\\n              null,\\\\n\\\\n            rating,\\\\n\\\\n            reviews,\\\\n\\\\n            phone:\\\\n              clean(\\\\n                phoneLink\\\\n                  ?.querySelector(\\\\\\".C9waJd\\\\\\")\\\\n                  ?.textContent\\\\n              ) ||\\\\n              clean(\\\\n                phoneLink\\\\n                  ?.getAttribute(\\\\\\"aria-label\\\\\\")\\\\n                  ?.replace(/^Call\\\\\\\\s+/i, \\\\\\"\\\\\\")\\\\n              ) ||\\\\n              phoneLink?.getAttribute(\\\\\\"data-phone-number\\\\\\") ||\\\\n              null,\\\\n\\\\n            website\\\\n          });\\\\n        } catch (error) {\\\\n          results.push({\\\\n            index,\\\\n            title: expectedTitle,\\\\n            address: null,\\\\n            rating: null,\\\\n            reviews: null,\\\\n            phone: null,\\\\n            website: null,\\\\n            error: error?.message || String(error)\\\\n          });\\\\n        }\\\\n      }\\\\n\\\\n      return JSON.stringify(results);\\\\n    \\\\\\"\\\\\\"\\\\\\"\\\\n  ) {\\\\n    value\\\\n  }\\\\n\\\\n  nextPage: click(\\\\n    selector: \\\\\\"[aria-label='Next']\\\\\\"\\\\n    visible: true\\\\n    timeout: 10000\\\\n  ) {\\\\n    time\\\\n  }\\\\n\\\\n  reconnect(timeout: 30000) {\\\\n    browserQLEndpoint\\\\n  }\\\\n}\\", \\"operationName\\": \\"ScrapeTwentyPlacesThenClickNext\\" } }}",
        "options": {
          "timeout": 240000
        }
      },
      "id": "8e334cf3-29c0-4e4c-b500-7e1b99ef393b",
      "name": "Scrape Current Page",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        944,
        160
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "currentPageData",
              "name": "currentPageData",
              "value": "={{ $json.data.scrapeBusinesses.value }}",
              "type": "string"
            },
            {
              "id": "browserQLEndpoint",
              "name": "browserQLEndpoint",
              "value": "={{ $json.data.reconnect.browserQLEndpoint }}",
              "type": "string"
            },
            {
              "id": "currentPage",
              "name": "currentPage",
              "value": "={{ $('Current Page').first().json.currentPage }}",
              "type": "number"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "id": "ec284a3b-b02c-40a4-9d61-fe0b8fa0f9c5",
      "name": "Save Page Data",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1152,
        160
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "page-check",
              "leftValue": "={{ $('Save Page Data').item.json.currentPage + 1}}",
              "rightValue": "={{ $('Prepare Search').item.json.maxPages }}",
              "operator": {
                "type": "number",
                "operation": "lt"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "31b8902a-eefd-4f9f-b8ab-f75ce19c2660",
      "name": "Check More Pages",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1728,
        304
      ]
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "value": "1_3SDWehtZum1Lby8vKNILCf01zullfxYZkiUBONFldE",
          "mode": "list",
          "cachedResultName": "Cities",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1_3SDWehtZum1Lby8vKNILCf01zullfxYZkiUBONFldE/edit?usp=drivesdk"
        },
        "sheetName": {
          "__rl": true,
          "value": 731100957,
          "mode": "list",
          "cachedResultName": "MyCities",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1_3SDWehtZum1Lby8vKNILCf01zullfxYZkiUBONFldE/edit#gid=731100957"
        },
        "options": {}
      },
      "id": "d3a2e3f6-536b-418a-850b-e7482ea3addb",
      "name": "Read Cities",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        160,
        -80
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "ZihTkNXDWsDwYrGv",
          "name": "Google Sheets OAuth2 API"
        }
      }
    },
    {
      "parameters": {
        "options": {}
      },
      "id": "1ed2f0e0-2c09-428f-8c94-f4502ba02ede",
      "name": "Loop Cities",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        400,
        -80
      ]
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "1_3SDWehtZum1Lby8vKNILCf01zullfxYZkiUBONFldE",
          "mode": "list",
          "cachedResultName": "Cities",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1_3SDWehtZum1Lby8vKNILCf01zullfxYZkiUBONFldE/edit?usp=drivesdk"
        },
        "sheetName": {
          "__rl": true,
          "value": 1622505339,
          "mode": "list",
          "cachedResultName": "Results",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1_3SDWehtZum1Lby8vKNILCf01zullfxYZkiUBONFldE/edit#gid=1622505339"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Title": "={{ $json.title }}",
            "City": "={{ $json.cityName }}",
            "Page": "={{ $json.page }}",
            "Address": "={{ $json.address }}",
            "Rating": "={{ $json.rating }}",
            "Reviews": "={{ $json.reviews }}",
            "Phone": "={{ $json.phone }}",
            "Website": "={{ $json.website }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "Title",
              "displayName": "Title",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "City",
              "displayName": "City",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Page",
              "displayName": "Page",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Address",
              "displayName": "Address",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Rating",
              "displayName": "Rating",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Phone",
              "displayName": "Phone",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Reviews",
              "displayName": "Reviews",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Website",
              "displayName": "Website",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "id": "7b603c79-3183-4044-b1c4-60adaf004f73",
      "name": "Append to Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        1504,
        160
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "ZihTkNXDWsDwYrGv",
          "name": "Google Sheets OAuth2 API"
        }
      }
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "72bab080-7316-4c50-b778-fcf5320c9d48",
              "name": "currentPage",
              "value": "={{ $('Current Page').first().json.currentPage + 1 }}",
              "type": "number"
            },
            {
              "id": "4aa26113-9d13-43d1-9db4-bd3c8c33e7e8",
              "name": "browserQLEndpoint",
              "value": "={{ $('Scrape Current Page').first().json.data.reconnect.browserQLEndpoint }}",
              "type": "string"
            },
            {
              "id": "bfcb49ed-4548-4a01-b712-a71576218529",
              "name": "maxPages",
              "value": "={{ $('Prepare Search').first().json.maxPages }}",
              "type": "number"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        512,
        288
      ],
      "id": "55e778f0-d9b5-4d5a-a75f-754d26e2354d",
      "name": "Pagination Loop",
      "executeOnce": true
    },
    {
      "parameters": {
        "jsCode": "return [$input.first()];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1840,
        544
      ],
      "id": "b0164653-255a-45af-a9f2-25a7e95a9737",
      "name": "Iterate next City"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "browserQLEndpoint",
              "name": "browserQLEndpoint",
              "value": "={{ $json.data.reconnect.browserQLEndpoint }}",
              "type": "string"
            },
            {
              "id": "currentPage",
              "name": "currentPage",
              "value": 0,
              "type": "number"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "id": "e4d21ed1-7517-430a-9358-5738ba1a9883",
      "name": "Initiate Pagination",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1184,
        -64
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "6713fa86-0f8d-43c2-8416-07be75624501",
              "name": "currentPage",
              "value": "={{ $json.currentPage }}",
              "type": "number"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        736,
        160
      ],
      "id": "bd5cea47-2df8-40b5-a622-f5a39fa3cf9d",
      "name": "Current Page"
    },
    {
      "parameters": {
        "content": "## Connect your Sheet\\n**Link** it to a sheet identical to [this one](https://docs.google.com/spreadsheets/d/1_3SDWehtZum1Lby8vKNILCf01zullfxYZkiUBONFldE/)"
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        96,
        -256
      ],
      "id": "14540ab5-a9a4-4980-b64f-64b6f07b3f1b",
      "name": "Sticky Note"
    },
    {
      "parameters": {
        "content": "## Add your API token here\\n**Signup** for a [free account](https://www.browserless.io/signup/email?plan=free) if you don't have one.",
        "height": 192,
        "width": 176
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        912,
        -272
      ],
      "id": "b42d20a8-38c3-4df0-9f94-7cec9c4361fe",
      "name": "Sticky Note1"
    },
    {
      "parameters": {
        "content": "## Add your API token here\\n**Signup** for a [free account](https://www.browserless.io/signup/email?plan=free) if you don't have one."
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        880,
        304
      ],
      "id": "f0343bb2-5261-472b-bf9c-52e17403866c",
      "name": "Sticky Note2"
    },
    {
      "parameters": {
        "content": "## Modify the search here\\n**Change the search** query and maxPages per run here."
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        624,
        -240
      ],
      "id": "313db649-9116-47f3-9101-787244ae16cc",
      "name": "Sticky Note4"
    },
    {
      "parameters": {
        "content": "## Connect your Sheet\\n**Link** it to a sheet identical to [this one](https://docs.google.com/spreadsheets/d/1_3SDWehtZum1Lby8vKNILCf01zullfxYZkiUBONFldE/)"
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1440,
        -16
      ],
      "id": "cc74ec7d-8d30-40ec-adba-29dca6cd7432",
      "name": "Sticky Note5"
    }
  ],
  "connections": {
    "Start": {
      "main": [
        [
          {
            "node": "Read Cities",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Search": {
      "main": [
        [
          {
            "node": "Open Google",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Open Google": {
      "main": [
        [
          {
            "node": "Initiate Pagination",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Places": {
      "main": [
        [
          {
            "node": "Append to Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scrape Current Page": {
      "main": [
        [
          {
            "node": "Save Page Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save Page Data": {
      "main": [
        [
          {
            "node": "Parse Places",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check More Pages": {
      "main": [
        [
          {
            "node": "Pagination Loop",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Iterate next City",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Cities": {
      "main": [
        [
          {
            "node": "Loop Cities",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Cities": {
      "main": [
        [],
        [
          {
            "node": "Prepare Search",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append to Sheet": {
      "main": [
        [
          {
            "node": "Check More Pages",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Pagination Loop": {
      "main": [
        [
          {
            "node": "Current Page",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Iterate next City": {
      "main": [
        [
          {
            "node": "Loop Cities",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Initiate Pagination": {
      "main": [
        [
          {
            "node": "Current Page",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Current Page": {
      "main": [
        [
          {
            "node": "Scrape Current Page",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "meta": {
    "instanceId": "315bbb31459edf8c9d611746bcf962272828eacc9e3cd0dd4c853454e3e02d15"
  }
}`}
  
  <button id="copyButtonGoogleMapsScraper" onClick={() => {
    const textToCopy = document.getElementById('myTextToCopyGoogleMapsScraper').innerText;
    const copyIcon = document.getElementById('copyIconGoogleMapsScraper');
    const copyText = document.getElementById('copyTextGoogleMapsScraper');
    navigator.clipboard.writeText(textToCopy)
      .then(() => {
        copyIcon.innerHTML = '<polyline points="20 6 9 17 4 12"></polyline>';
        copyText.textContent = 'Copied to your clipboard!';
        setTimeout(() => {
          copyIcon.innerHTML = '<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>';
          copyText.textContent = 'Copy Google Maps Scraper Template to Clipboard';
        }, 2000);
      })
      .catch(err => {
        console.error('Failed to copy text: ', err);
        alert('Failed to copy text.');
      });
  }} style={{display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '6px', fontFamily: 'inherit', backgroundColor: 'var(--ifm-color-primary)', color: 'white', border: 'none', padding: '15px 30px', fontSize: '0.8rem', fontWeight: '500', cursor: 'pointer', transition: 'background-color 0.3s', borderRadius: '0.25rem'}}>
    <svg id="copyIconGoogleMapsScraper" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
      <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
      <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
    </svg>
    Copy Google Maps Scraper Template to Clipboard
  </button>

### Performance

Audit any URL's Lighthouse scores (performance, SEO, accessibility, best practices) for a list of websites, then write the scores plus a plain-English `signal` summary back to a Google Sheet. This picks up where the [Google Maps Scraper](#google-maps-scraper) leaves off: feed it the `Website` column that template produces and turn each lead into an SEO audit you can act on.

Use it to: SEO auditing at scale, lead qualification, and outreach that opens with a site's specific speed or SEO problem.

Learn more about the [Performance API](/rest-apis/performance).

  
{`{
  "nodes": [
    {
      "parameters": {
        "batchSize": 2,
        "options": {}
      },
      "id": "5ee408d9-2a7d-4b35-b409-687551903e54",
      "name": "Loop Over Items",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        -576,
        304
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://production-sfo.browserless.io/performance",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "token",
              "value": "YOUR_API_TOKEN_HERE"
            },
            {
              "name": "timeout",
              "value": "120000"
            }
          ]
        },
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\\"url\\":\\"{{ $json.Website }}\\",\\"config\\":{\\"extends\\":\\"lighthouse:default\\",\\"settings\\":{\\"onlyCategories\\":[\\"performance\\",\\"seo\\",\\"accessibility\\",\\"best-practices\\"]}}}",
        "options": {
          "timeout": 130000
        }
      },
      "id": "d9eb51bd-14ff-4d6a-b407-11271d29e489",
      "name": "Browserless Performance API",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        -352,
        416
      ],
      "retryOnFail": true,
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const lhr = $json.data && $json.data.categories ? $json.data : $json;\\nconst cats = (lhr && lhr.categories) || {};\\nconst audits = (lhr && lhr.audits) || {};\\nconst orig = $('Loop Over Items').item.json;\\nconst to100 = c => (c && typeof c.score==='number' ? Math.round(c.score*100) : '');\\nconst a = id => audits[id] || {};\\nconst s = ms => (typeof ms==='number' ? +(ms/1000).toFixed(2) : '');\\nif (lhr.runtimeError && !(cats.performance && typeof cats.performance.score==='number')) {\\n  return { json: { ...orig, perf_score:'',seo_score:'',accessibility_score:'',best_practices_score:'',lcp_s:'',tbt_ms:'',cls:'',ttfb_ms:'',potential_speedup_s:'',top_opportunities:'',\\n    signal: \`Website failed to load in test: \${lhr.runtimeError.message}\`, audit_status: lhr.runtimeError.code||'runtimeError' } };\\n}\\nconst LABELS={'render-blocking-resources':'remove render-blocking scripts/CSS','uses-responsive-images':'properly size images','offscreen-images':'lazy-load offscreen images','unminified-css':'minify CSS','unminified-javascript':'minify JS','unused-css-rules':'remove unused CSS','unused-javascript':'remove unused JS','uses-optimized-images':'compress images','modern-image-formats':'serve next-gen images (WebP/AVIF)','uses-text-compression':'enable text compression','uses-long-cache-ttl':'set cache lifetimes','server-response-time':'speed up server response','redirects':'avoid landing-page redirects','efficient-animated-content':'replace GIFs with video'};\\nconst opps=Object.entries(audits).map(([id,x])=>({id,ms:x.details&&typeof x.details.overallSavingsMs==='number'?x.details.overallSavingsMs:0})).filter(o=>o.ms>=150).sort((x,y)=>y.ms-x.ms);\\nconst top=opps.slice(0,4).map(o=>\`\${LABELS[o.id]||o.id} (~\${(o.ms/1000).toFixed(1)}s)\`);\\nconst perf=to100(cats.performance),seo=to100(cats.seo),a11y=to100(cats.accessibility);\\nconst lcp=s(a('largest-contentful-paint').numericValue),tbt=a('total-blocking-time').numericValue,cls=a('cumulative-layout-shift').numericValue;\\nconst p=[];\\nif(perf!=='')p.push(\`Page speed \${perf}/100 (\${perf>=90?'good':perf>=50?'needs work':'poor'})\`);\\nif(lcp!==''&&lcp>2.5)p.push(\`main content loads in \${lcp}s (Google flags >2.5s)\`);\\nif(top.length)p.push(\`Quick wins: \${top.slice(0,3).join(', ')}\`);\\nif(seo!==''&&seo<90)p.push(\`SEO \${seo}/100\`);\\nif(a11y!==''&&a11y<90)p.push(\`Accessibility \${a11y}/100 (ADA risk)\`);\\nreturn { json: { ...orig, perf_score:perf,seo_score:seo,accessibility_score:a11y,best_practices_score:to100(cats['best-practices']),\\n  lcp_s:lcp, tbt_ms:typeof tbt==='number'?Math.round(tbt):'', cls:typeof cls==='number'?+cls.toFixed(3):'', ttfb_ms:typeof a('server-response-time').numericValue==='number'?Math.round(a('server-response-time').numericValue):'',\\n  potential_speedup_s:+(opps.reduce((t,o)=>t+o.ms,0)/1000).toFixed(1), top_opportunities:top.join(' | '),\\n  signal:p.join('. ')+(p.length?'.':''), audit_status:'ok' } };"
      },
      "id": "5506a4b0-81eb-4704-9c77-a2f1e4d1d77c",
      "name": "Extract Signals",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -64,
        416
      ]
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "value": "1_3SDWehtZum1Lby8vKNILCf01zullfxYZkiUBONFldE",
          "mode": "list",
          "cachedResultName": "Cities",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1_3SDWehtZum1Lby8vKNILCf01zullfxYZkiUBONFldE/edit?usp=drivesdk"
        },
        "sheetName": {
          "__rl": true,
          "value": 1622505339,
          "mode": "list",
          "cachedResultName": "Results",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1_3SDWehtZum1Lby8vKNILCf01zullfxYZkiUBONFldE/edit#gid=1622505339"
        },
        "options": {}
      },
      "id": "faa214f6-4700-467c-82a9-8694cd235c9b",
      "name": "Read Spreadsheet",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        -768,
        304
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "a3gQrk6QQtm83GgV",
          "name": "Google Sheets account"
        }
      }
    },
    {
      "parameters": {
        "operation": "appendOrUpdate",
        "documentId": {
          "__rl": true,
          "value": "1_3SDWehtZum1Lby8vKNILCf01zullfxYZkiUBONFldE",
          "mode": "list",
          "cachedResultName": "Cities",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1_3SDWehtZum1Lby8vKNILCf01zullfxYZkiUBONFldE/edit?usp=drivesdk"
        },
        "sheetName": {
          "__rl": true,
          "value": 1509236866,
          "mode": "list",
          "cachedResultName": "Performance",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1_3SDWehtZum1Lby8vKNILCf01zullfxYZkiUBONFldE/edit#gid=1509236866"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [],
          "schema": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "id": "20c1aa19-f585-4cfd-8a70-33732022dee1",
      "name": "Write Results to New Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        -352,
        208
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "a3gQrk6QQtm83GgV",
          "name": "Google Sheets account"
        }
      }
    },
    {
      "parameters": {},
      "id": "423fdfe6-7a9b-4bfd-bc30-44d542a5ac33",
      "name": "Manual Trigger",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        -944,
        304
      ]
    },
    {
      "parameters": {
        "content": "## Connect your Google Sheet\\n**Link** it to a sheet identical to [this one](https://docs.google.com/spreadsheets/d/1_3SDWehtZum1Lby8vKNILCf01zullfxYZkiUBONFldE/)",
        "height": 128
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -848,
        144
      ],
      "id": "723c93bd-0a5d-4905-8b15-12cd2f196bc8",
      "name": "Sticky Note"
    },
    {
      "parameters": {
        "content": "## Connect your Google Sheet\\n**Link** it to a sheet identical to [this one](https://docs.google.com/spreadsheets/d/1_3SDWehtZum1Lby8vKNILCf01zullfxYZkiUBONFldE/)",
        "height": 128
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -416,
        64
      ],
      "id": "e7d211a6-d475-48b2-b8c4-788d6c2710df",
      "name": "Sticky Note1"
    },
    {
      "parameters": {
        "content": "## Add your API token here\\n**Signup** for a [free account](https://www.browserless.io/signup/email?plan=free) if you don't have one.",
        "height": 128
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -416,
        624
      ],
      "id": "0cbbda4f-b5b4-40e3-85b8-596aceca7015",
      "name": "Sticky Note2"
    }
  ],
  "connections": {
    "Loop Over Items": {
      "main": [
        [
          {
            "node": "Write Results to New Sheet",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Browserless Performance API",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Browserless Performance API": {
      "main": [
        [
          {
            "node": "Extract Signals",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Extract Signals": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Spreadsheet": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Manual Trigger": {
      "main": [
        [
          {
            "node": "Read Spreadsheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "meta": {
    "templateCredsSetupCompleted": true,
    "instanceId": "374209d58dde3cbf5707e7114aa17a9ec5fc159fd5ccd8c68404e9cad4bc1076"
  }
}`}
  
  <button id="copyButtonPerformance" onClick={() => {
    const textToCopy = document.getElementById('myTextToCopyPerformance').innerText;
    const copyIcon = document.getElementById('copyIconPerformance');
    const copyText = document.getElementById('copyTextPerformance');
    navigator.clipboard.writeText(textToCopy)
      .then(() => {
        copyIcon.innerHTML = '<polyline points="20 6 9 17 4 12"></polyline>';
        copyText.textContent = 'Copied to your clipboard!';
        setTimeout(() => {
          copyIcon.innerHTML = '<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>';
          copyText.textContent = 'Copy Performance Template to Clipboard';
        }, 2000);
      })
      .catch(err => {
        console.error('Failed to copy text: ', err);
        alert('Failed to copy text.');
      });
  }} style={{display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '6px', fontFamily: 'inherit', backgroundColor: 'var(--ifm-color-primary)', color: 'white', border: 'none', padding: '15px 30px', fontSize: '0.8rem', fontWeight: '500', cursor: 'pointer', transition: 'background-color 0.3s', borderRadius: '0.25rem'}}>
    <svg id="copyIconPerformance" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
      <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
      <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
    </svg>
    Copy Performance Template to Clipboard
  </button>

## Best practices for n8n

**Binary vs base64**: For screenshots and PDFs, enable the "Download" option in n8n to handle binary responses, or request `encoding: "base64"` in the body to receive JSON responses.

**Timeouts and retries**: Long-running pages may exceed default timeouts. Configure timeout settings in your HTTP Request node and add retry logic for reliability.

**Security**: Store your API token in n8n Credentials or environment variables. Never hardcode tokens in plain text within your workflow templates.

**Regional endpoints**: Choose a regional endpoint close to your location for lower latency. Available regions include `production-sfo` (US West), `production-lon` (UK), and `production-ams` (Netherlands).

## FAQ & Troubleshooting

<details>
<summary>The HTTP Request node returns 401 Unauthorized</summary>

Check that the `token` query parameter is set on the request, not passed in the body or headers. If you're using n8n Credentials, confirm the credential is attached to the query parameter field, not a header.

</details>

<details>
<summary>The screenshot or PDF response looks corrupted</summary>

n8n's HTTP Request node returns binary data by default. Either enable the "Download" option so n8n handles it as a binary file, or request `encoding: "base64"` in the body so Browserless returns JSON instead. See [Best practices for n8n](#best-practices-for-n8n) above.

</details>

<details>
<summary>The request times out on slow-loading pages</summary>

Increase the timeout in the HTTP Request node's options, and add `waitUntil` or `bestAttempt` parameters to the Browserless endpoint to avoid waiting on pages that never fully settle.

</details>

## Next steps

  <a href="/ai-integrations/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">AI integrations overview</h3>
      all supported AI frameworks and tools
    
  </a>
  <a href="/rest-apis/intro" className="next-step-card-link">
    
      <h3 className="next-step-card-title">REST APIs overview</h3>
      explore every Browserless REST endpoint
    
  </a>

---

# OpenClaw Integration

URL: https://docs.browserless.io/ai-integrations/open-claw

# OpenClaw Integration

[OpenClaw](https://openclaw.ai/) is an open-source personal AI assistant that runs locally and can autonomously browse the web, fill out forms, scrape data, and interact with online services. By connecting OpenClaw to Browserless, you offload browser execution to the cloud – gaining stealth, residential proxies, CAPTCHA solving, and scalable sessions without running a local browser.

- [OpenClaw](https://openclaw.ai/) installed and running (`openclaw onboard` completed)
- Browserless API token (available in your [account dashboard](https://browserless.io/account/))

## Step-by-Step Setup

  
    Go to your <a href="https://browserless.io/account/" target="_blank" rel="noopener noreferrer">Browserless account dashboard</a> and copy your API token. You will add it to your OpenClaw configuration in the next step.

    :::tip
    Store your token in an environment variable instead of hardcoding it in config files:

```bash
export BROWSERLESS_API_TOKEN="your-token-here"
```
    :::
  

  
    OpenClaw uses browser profiles defined in <code>~/.openclaw/openclaw.json</code>. Add a <code>browserless</code> profile that points to a Browserless regional endpoint via its CDP URL.

    Open your config file and add the following:

```json5
// ~/.openclaw/openclaw.json
{
  browser: {
    enabled: true,
    defaultProfile: "browserless",
    remoteCdpTimeoutMs: 2000,
    remoteCdpHandshakeTimeoutMs: 4000,
    profiles: {
      browserless: {
        cdpUrl: "wss://production-sfo.browserless.io?token=YOUR_API_TOKEN",
        color: "#FF4500"
      }
    }
  }
}
```

    Replace <code>YOUR_API_TOKEN</code> with your actual Browserless token, or use an environment variable reference if your setup supports it.

    Choose a regional endpoint

    Pick the region closest to you for lower latency:

    | Region | CDP URL |
    |---|---|
    | US West | `wss://production-sfo.browserless.io?token=YOUR_API_TOKEN` |
    | Europe UK | `wss://production-lon.browserless.io?token=YOUR_API_TOKEN` |
    | Europe Amsterdam | `wss://production-ams.browserless.io?token=YOUR_API_TOKEN` |
  

  
    Confirm that OpenClaw can reach the Browserless endpoint:

```bash
openclaw browser status
```

    You should see the <code>browserless</code> profile listed as reachable. If the profile shows as unreachable, double-check your API token and network connectivity.

    You can also start a quick browser session and take a screenshot to validate:

```bash
openclaw browser start
openclaw browser screenshot
```
  

  
    Once connected, OpenClaw's AI can use Browserless just like a local browser. Ask it to perform tasks in natural language through your preferred chat interface (WhatsApp, Discord, Slack, etc.):

    - "Go to example.com and take a screenshot"
    - "Fill out the contact form on browserless.io/practice-form"
    - "Scrape the pricing table from that page"

    OpenClaw will route all browser actions through Browserless automatically.

    Using the CLI directly

    You can also drive the browser with OpenClaw's CLI commands:

```bash
# Navigate to a page
openclaw browser navigate "https://example.com"

# Take a snapshot (returns element references for interaction)
openclaw browser snapshot --efficient

# Click an element by reference
openclaw browser click @e1

# Take a full-page screenshot
openclaw browser screenshot --full-page
```

    :::note
    Always take a new snapshot after every action – element references (`@e1`, `@e2`, etc.) become stale when the DOM changes.
    :::
  

## Why use Browserless with OpenClaw?

OpenClaw's built-in managed browser works well for local tasks, but cloud and headless deployments (servers, Docker, CI) have no display. Browserless solves this and adds a host of other features:

- **Stealth and anti-bot resilience** – bypass bot detection that trips up standard Chromium
- **CAPTCHA and Cloudflare solving** – automatically handle verification challenges
- **Residential proxies** – rotate IPs to avoid rate limits and geo-restrictions
- **No local browser required** – ideal for headless servers, Docker, and cloud VMs
- **Regional endpoints** – choose US West, UK, or Amsterdam for lower latency

## Keeping a local profile alongside Browserless

You can define multiple profiles and switch between them. This is useful for local development (managed browser) and production (Browserless):

```json5
// ~/.openclaw/openclaw.json
{
  browser: {
    enabled: true,
    defaultProfile: "browserless",
    profiles: {
      local: {
        cdpPort: 18800,
        color: "#0066CC"
      },
      browserless: {
        cdpUrl: "wss://production-sfo.browserless.io?token=YOUR_API_TOKEN",
        color: "#FF4500"
      }
    }
  }
}
```

Switch profiles on any command with the `?profile=<name>` parameter, or change `defaultProfile` in your config.

## Additional resources

- [OpenClaw Documentation](https://docs.openclaw.ai/)
- [OpenClaw Browser Tool Reference](https://docs.openclaw.ai/tools/browser)
- [Browserless Connection URLs](/overview/connection-urls)
- [Browserless REST APIs](/rest-apis/screenshot-api)
- [BrowserQL](/browserql/start)

## FAQ & Troubleshooting

<details>
<summary>How do I connect OpenClaw to Browserless?</summary>

Add a `browserless` profile to `~/.openclaw/openclaw.json` with a `cdpUrl` pointing at your Browserless WebSocket endpoint and token, then set it as `defaultProfile`, as shown in step 2 above.

</details>

<details>
<summary><code>openclaw browser status</code> shows the Browserless profile as unreachable</summary>

Verify your API token is correct and active in the [Browserless dashboard](https://browserless.io/account/). If it still fails, increase `remoteCdpTimeoutMs` and `remoteCdpHandshakeTimeoutMs` in your config, and check that outbound HTTPS traffic on port 443 is allowed by your firewall.

</details>

<details>
<summary>I'm getting 501 errors on snapshot, PDF, or AI snapshot features</summary>

These advanced OpenClaw browser features require Playwright. Install it with `npm install playwright`.

</details>

<details>
<summary>I'm hitting rate limit or plan errors</summary>

Browserless returns HTTP 429 when concurrency or request limits are exceeded. Check your usage in the [Browserless dashboard](https://browserless.io/account/) and consider upgrading your plan.

</details>

## Next steps

  <a href="/mcp/browser-agent" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Browser Agent</h3>
      a managed alternative that skips the setup and model wiring
    
  </a>
  <a href="/ai-integrations/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">AI integrations overview</h3>
      all supported AI frameworks and tools
    
  </a>

---

# OpenAI SDK

URL: https://docs.browserless.io/ai-integrations/openai

# OpenAI SDK

OpenAI exposes two paths for putting GPT behind a Browserless-powered agent loop:

- **Direct integration** with the [Responses API](https://platform.openai.com/docs/api-reference/responses). One `responses.create` call, MCP server as a `tool`. Lightest possible shape.
- **Managed integration** with the [OpenAI Agents SDK](https://openai.github.io/openai-agents-js/). An `Agent` class, a `run()` helper, built-in tracing and handoffs. Better when you outgrow a single prompt.

Both routes call the same hosted MCP server at `https://mcp.browserless.io/mcp`. This page builds the same example using both integration paths: a `GET /hn` endpoint that returns the top 10 Hacker News stories as JSON.

- Browserless API token (available in your [account dashboard](https://browserless.io/account/))
- An [OpenAI API key](https://platform.openai.com/api-keys)
- Node.js 20.6 or later (for the built-in `--env-file` flag)

## Setting environment variables

  
**.env file:**

```bash
OPENAI_API_KEY=your-openai-key
BROWSERLESS_API_KEY=your-browserless-token
PORT=3000
```
  
  
**Command line:**

```bash
export OPENAI_API_KEY=your-openai-key
export BROWSERLESS_API_KEY=your-browserless-token
export PORT=3000
```
  

## Direct integration: Responses API

The Responses API runs the tool-calling loop server-side and accepts the hosted MCP server as a single `tool` entry. No SDK, no state machine — one request, one response.

### Install

```bash
npm init -y
npm install openai express
```

### `server.js`

```js

const PORT = process.env.PORT ?? 3000;
const OPENAI_API_KEY = process.env.OPENAI_API_KEY || "";
const BROWSERLESS_API_KEY = process.env.BROWSERLESS_API_KEY || "";
const INSTRUCTIONS =
  "You are a news scraper. Use the browserless tools to load the Hacker News front page and return the top 10 stories as a JSON array. Each item must have the shape { rank, title, url, points }. Reply with ONLY the JSON array — no markdown fences, no commentary.";

const app = express();
const client = new OpenAI({ apiKey: OPENAI_API_KEY });

app.get("/hn", async (_req, res) => {
  const response = await client.responses.create({
    model: "gpt-5.5",
    instructions: INSTRUCTIONS,
    tools: [
      {
        type: "mcp",
        server_label: "browserless",
        server_url: "https://mcp.browserless.io/mcp",
        headers: {
          Authorization: `Bearer ${BROWSERLESS_API_KEY}`,
        },
        require_approval: "never",
      },
    ],
    input: "Fetch the top 10 stories from https://news.ycombinator.com",
  });

  res.type("application/json").send(response.output_text);
});

app.listen(PORT, () => {
  console.log(`Listening on http://localhost:${PORT}`);
});
```

Add `"type": "module"` to your `package.json` so the `import` statements work.

### Run it

```bash
node --env-file=.env server.js
```

```bash
curl http://localhost:3000/hn
# → [
#     { "rank": 1, "title": "Show HN: …", "url": "https://…", "points": 412 },
#     …
#   ]
```

### How it works

1. Express receives `GET /hn`.
2. The server calls `responses.create` with the `mcp` tool pointing at `https://mcp.browserless.io/mcp`.
3. OpenAI fetches the MCP tool list, the model picks the right Browserless tool (typically [`browserless_smartscraper`](/mcp/rest-api-tools#browserless_smartscraper)), and OpenAI executes the call server-side against `https://news.ycombinator.com`.
4. The scraped page comes back to the model, which extracts the top 10 stories into the JSON shape requested by `instructions`.
5. `response.output_text` is piped straight to the HTTP response — one-shot, no persistence.

### Multi-turn state

For a chat-style endpoint, persist the response ID per session and pass it on the next call:

```js
const response = await client.responses.create({
  model: "gpt-5.5",
  previous_response_id: lastResponseId, // from the previous turn
  tools: [/* same mcp tool block — must be re-sent every call */],
  input: userMessage,
});
lastResponseId = response.id;
```

OpenAI persists the conversation server-side under that ID, so you only store the string.

## Managed integration: Agents SDK

The [OpenAI Agents SDK](https://openai.github.io/openai-agents-js/) wraps the Responses API in an `Agent` abstraction with built-in tracing, handoffs to other agents, and richer multi-turn ergonomics. The MCP tool config moves from a raw `tools[]` entry to the `hostedMcpTool` helper.

Reach for this when you want a longer-running agent process, multi-agent orchestration, or just structured tracing of every tool call.

### Install

```bash
npm install @openai/agents express
```

### `server.js`

```js

const PORT = process.env.PORT ?? 3000;
const OPENAI_API_KEY = process.env.OPENAI_API_KEY || "";
const BROWSERLESS_API_KEY = process.env.BROWSERLESS_API_KEY || "";
const INSTRUCTIONS =
  "You are a news scraper. Use the browserless tools to load the Hacker News front page and return the top 10 stories as a JSON array. Each item must have the shape { rank, title, url, points }. Reply with ONLY the JSON array — no markdown fences, no commentary.";

setDefaultOpenAIKey(OPENAI_API_KEY);
setTracingExportApiKey(OPENAI_API_KEY);

const app = express();

const agent = new Agent({
  name: "HN Scraper",
  model: "gpt-5.5",
  instructions: INSTRUCTIONS,
  tools: [
    hostedMcpTool({
      serverLabel: "browserless",
      serverUrl: "https://mcp.browserless.io/mcp",
      headers: {
        Authorization: `Bearer ${BROWSERLESS_API_KEY}`,
      },
      requireApproval: "never",
    }),
  ],
});

app.get("/hn", async (_req, res) => {
  const result = await run(
    agent,
    "Fetch the top 10 stories from https://news.ycombinator.com",
  );
  res.type("application/json").send(result.finalOutput ?? "[]");
});

app.listen(PORT, () => {
  console.log(`Listening on http://localhost:${PORT}`);
});
```

### Run it

```bash
node --env-file=.env server.js
curl http://localhost:3000/hn
```

### What you get over the raw Responses path

- **Tracing.** Every tool call, retry, and handoff is exported to OpenAI's traces dashboard out of the box.
- **Handoffs.** Define multiple `Agent` instances and let the model route between them (e.g. one for scraping, one for summarization).
- **Session helpers.** Pass `previousResponseId` or accumulate input arrays without managing the wire format yourself.
- **Guardrails.** Built-in input/output validation hooks for production use.

Trade-off: another dependency, and an extra abstraction between you and the underlying Responses API.

## Stealth, CAPTCHAs, and regional endpoints

Both paths route through the same hosted MCP server, so [stealth mode](/baas/bot-detection/stealth), [automatic CAPTCHA solving](/baas/bot-detection/captchas), and [residential proxies](/baas/bot-detection/proxies) all apply.

To pin to a regional endpoint, add the `x-browserless-api-url` header alongside the bearer:

```js
headers: {
  Authorization: `Bearer ${BROWSERLESS_API_KEY}`,
  "x-browserless-api-url": "https://production-lon.browserless.io",
},
```

See the [MCP setup page](/mcp/browserless-mcp-server/setup) for the full list of configuration options.

## Resources

- [OpenAI Responses API — MCP tool guide](https://platform.openai.com/docs/guides/tools-remote-mcp)
- [OpenAI Agents SDK (JS) documentation](https://openai.github.io/openai-agents-js/)
- [Browserless MCP setup](/mcp/browserless-mcp-server/setup)
- [REST API tools](/mcp/rest-api-tools)
- [Browser agent](/mcp/browser-agent)
- [Connection URLs](/overview/connection-urls)

## FAQ & Troubleshooting

<details>
<summary>The request hangs forever</summary>

The default value for `require_approval` (Responses API) / `requireApproval` (Agents SDK) is `"always"`, which makes the API return `mcp_approval_request` items and wait for you to approve each tool call. Set it to `"never"` on the MCP tool config so the loop runs to completion.

</details>

<details>
<summary>"Unknown tool type: mcp"</summary>

The `mcp` tool type requires a current model. Use `gpt-5.5` (or later); older completion-only models reject the entry.

</details>

<details>
<summary>Tools "disappear" between calls (Responses API)</summary>

OpenAI does not persist tool configuration server-side on the raw Responses API. Re-send the full `tools[]` array on every `responses.create` call, even when using `previous_response_id`. The Agents SDK handles this for you because tools are bound to the `Agent` instance.

</details>

<details>
<summary>401 from the MCP server</summary>

The bearer is sent fresh on every call and never stored. Double-check that `BROWSERLESS_API_KEY` is loaded into the process and that the `Authorization: Bearer …` header is on the MCP tool config, not on the outer request.

</details>

<details>
<summary>Use <code>/mcp</code>, not <code>/sse</code></summary>

The Browserless MCP endpoint is `https://mcp.browserless.io/mcp` (Streamable HTTP). The legacy `/sse` path will not work with either the Responses API's MCP client or the Agents SDK's `hostedMcpTool`.

</details>

## Next steps

  <a href="/ai-integrations/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">AI integrations overview</h3>
      all supported AI frameworks and tools
    
  </a>
  <a href="/browserql/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BQL overview</h3>
      use BrowserQL for advanced browser automation
    
  </a>

---

# opencode

URL: https://docs.browserless.io/ai-integrations/opencode

# opencode

[opencode](https://opencode.ai) is an open-source terminal coding agent with no built-in browser. This guide adds one through MCP: managed cloud sessions with stealth, automatic CAPTCHA solving, and residential proxies, configured with one entry in `opencode.json` and one API token.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- [opencode](https://opencode.ai/docs/) installed
- Node.js, only if you use the [Playwright MCP alternative](#alternative-playwright-mcp-over-a-cdp-endpoint)

## Why Browserless as opencode's browser

opencode's built-in tools are `bash`, `edit`, `write`, `read`, `grep`, `glob`, `lsp`, `apply_patch`, `skill`, `todowrite`, `webfetch`, `websearch`, and `question`. None of them drive a browser. `webfetch` retrieves a URL and hands back its content, which is enough for reading documentation, but it can't log in, click through a multi-step flow, wait on a JavaScript-rendered view, or get past bot detection. The [feature request for a built-in browser tool](https://github.com/anomalyco/opencode/issues/27541) was closed as not planned, so there's no first-party path coming.

That changes what "remote browser" means here. In an agent that ships its own browser tool, a hosted provider is a swap; in opencode it's the whole capability. Adding the Browserless MCP server gives opencode fourteen tools that cover the gap:

- `browserless_agent` can preserve cookies and page state through logins, multi-step forms, and pagination. In v1.33.0+, [multi-call sessions require first-call opt-in](/mcp/browser-agent#session-lifetime): set `keepSessionAlive: true`, then echo the returned `sessionId` on later calls.
- `browserless_smartscraper` runs cascading strategies on every request: direct fetch, then proxy, then headless browser, then CAPTCHA solving, until the content returns.
- The browsers run on Browserless's cloud, not the machine running opencode. One API token covers stealth, residential proxies, and CAPTCHA solving, with no separate provider accounts.
- The remaining tools cover search, crawling, site mapping, exports, Lighthouse audits, and account usage. See the [full tool list](/mcp/browserless-mcp-server/setup#tools).

## Step 1: Add the server to `opencode.json`

The Browserless MCP server is remote, so it needs only a URL and your token. Keep the token in your environment rather than the config file:

```bash

```

Then add the server to `opencode.json` in your project root, or to `~/.config/opencode/opencode.json` to make it available everywhere:

```json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "browserless": {
      "type": "remote",
      "url": "https://mcp.browserless.io/mcp",
      "headers": {
        "Authorization": "Bearer {env:BROWSERLESS_API_TOKEN}"
      }
    }
  }
}
```

Use `{env:VAR}`, not `$VAR` or `${VAR}`. opencode does its own substitution on config values and doesn't expand shell syntax inside JSON strings, so a `${...}` placeholder reaches Browserless verbatim as the Bearer value and gets rejected.

The hosted server speaks Streamable HTTP, which is what opencode uses for `"type": "remote"`. There's no local process, Node, or Docker involved.

### Optional: sign in with OAuth instead of a token

opencode enables OAuth on remote MCP servers by default, and the hosted Browserless server supports it. Drop the `headers` block and authenticate interactively:

```bash
opencode mcp auth browserless
```

opencode runs the OAuth flow with PKCE, registers a client dynamically, and stores the credentials outside your project config. Prefer this if `opencode.json` is committed to version control, since nothing token-shaped ends up in the repo.

## Step 2: Verify the tools loaded

Check that opencode connected to the server. `opencode mcp list` prints every configured server with its connection status, so `browserless` should be listed and connected:

```bash
opencode mcp list
```

Then start a session and ask what it can reach:

```text
Which MCP tools do you have available right now?
```

opencode namespaces MCP tools as `<server>_<tool>`, so with the server keyed `browserless` they appear as `browserless_browserless_agent`, `browserless_browserless_smartscraper`, and so on. The doubled prefix is expected: opencode prepends the server key to tool names that already start with `browserless_`. If it bothers you, key the server as something shorter, like `bl`, and the tools read `bl_browserless_agent`.

If the list is empty, restart opencode. Config changes aren't picked up mid-session.

## Step 3: Try it on a real task

Give opencode something `webfetch` can't do, so the difference is visible:

```text
Use the Browserless smartscraper to scrape https://www.browserless.io/pricing
and list each plan with its unit allowance, then tell me which strategy
the scraper needed.
```

```text
Use the Browserless agent to open https://docs.browserless.io, navigate to
the BrowserQL quickstart, and summarize the setup steps.
```

The second prompt is the one worth watching. With `keepSessionAlive: true` on the first call and `sessionId` echoed thereafter, `browserless_agent` holds the session open, so opencode can read the page and decide where to click next without losing state.

### Optional: tell opencode what the infrastructure handles

Browserless solves CAPTCHAs in the background, which can take a few seconds. Without context, an agent sees the challenge in the page and either tries to click it or reports that it's blocked. Add this to `AGENTS.md` in your project root, or `~/.config/opencode/AGENTS.md` for every project:

```md
## Browser automation

Browser tools run on Browserless cloud infrastructure with stealth and
automatic CAPTCHA solving enabled.

- Don't click or try to solve CAPTCHA elements. They're solved for you.
- If you see a CAPTCHA or challenge page, wait 15-30 seconds and look again.
  Report it only if it's still there after 60 seconds.
- Don't add your own anti-detection workarounds. They conflict with stealth.
```

## Alternative: Playwright MCP over a CDP endpoint

If you want Playwright's `browser_*` tool semantics instead, register Microsoft's [Playwright MCP](https://github.com/microsoft/playwright-mcp) as a local server and point it at a Browserless CDP endpoint:

```json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "playwright-browserless": {
      "type": "local",
      "command": [
        "npx",
        "-y",
        "@playwright/mcp@latest",
        "--cdp-endpoint",
        "wss://production-sfo.browserless.io/stealth?token={env:BROWSERLESS_API_TOKEN}&solveCaptchas=true"
      ]
    }
  }
}
```

This runs `@playwright/mcp` on your machine, but the browser it drives is still a Browserless cloud session. The endpoint URL is what turns the features on:

| Segment | Purpose |
|---------|---------|
| `production-sfo.browserless.io` | Regional endpoint. Also `production-lon` and `production-ams` for the EU |
| `/stealth` | [Stealth route](/baas/bot-detection/stealth). Hides automation signals and spoofs fingerprints |
| `token=` | Your Browserless API token |
| `solveCaptchas=true` | [Solves CAPTCHAs](/baas/bot-detection/captchas) in real time, at the infrastructure layer |

Add `&timeout=300000` for tasks that run longer than the 30-second default, and `&proxy=residential` for sites with IP reputation checks. The [Claude Code + Playwright MCP](/ai-integrations/claude-code-playwright-mcp) page covers the full parameter set, the tool list, and the CAPTCHA-handling system prompt. Everything on it applies here; only the registration step differs.

We recommend the hosted MCP server over this route for most work. Playwright MCP gives you primitives (navigate, click, type), while `browserless_agent` and `browserless_smartscraper` handle unblocking and retries per request instead of per script.

## FAQ & Troubleshooting

<details>
<summary>opencode doesn't list any Browserless tools</summary>

Run `opencode mcp list` to see whether the server connected. If it's missing entirely, `opencode.json` failed to parse. Check it with `cat opencode.json | jq .`. If it's listed but not connected, the usual causes are a URL typo (the endpoint must be exactly `https://mcp.browserless.io/mcp`) or a bad token. If the server connected before you edited the config, restart opencode; config changes don't apply to a running session.

</details>

<details>
<summary>The server rejects the token, or returns <code>401</code></summary>

Check the substitution syntax first. opencode expands `{env:BROWSERLESS_API_TOKEN}` but leaves `$BROWSERLESS_API_TOKEN` and `${BROWSERLESS_API_TOKEN}` untouched, so the shell-style forms arrive at the server as literal text. An unset variable is a quieter version of the same failure: `{env:...}` resolves to an empty string, and the server sees `Authorization: Bearer` with nothing after it. Confirm the variable is exported in the shell you launch opencode from with `echo $BROWSERLESS_API_TOKEN`.

</details>

<details>
<summary>Can I point an opencode CDP browser plugin at Browserless instead?</summary>

Not without changes to the plugin. The community CDP plugins take a `browser_url` per tool call, which looks like it should accept a Browserless endpoint, but they're built around a locally launched Chrome. [`opencode-chrome-devtools`](https://github.com/different-ai/opencode-browser) discovers tabs by requesting `/json/list`, which Browserless doesn't expose because it provisions a browser per connection rather than keeping a list of open tabs. It then rebuilds each WebSocket URL from the host and path alone, dropping the `?token=` query string that authenticates the session. Use the MCP route on this page, or [Playwright MCP](#alternative-playwright-mcp-over-a-cdp-endpoint), which passes the full endpoint URL through to the browser.

</details>

<details>
<summary>Isn't <code>opencode web</code> the browser feature?</summary>

No. `opencode web` starts a headless opencode server with a web interface, so you drive opencode *from* a browser. It doesn't give opencode a browser to drive. The two get conflated in search results for "opencode browser", so it's worth being explicit about which one you're after.

</details>

<details>
<summary>Long-running tasks time out</summary>

Browserless sessions default to 30 seconds. Add `timeout` to the connection URL on the Playwright MCP route (`&timeout=300000` for five minutes). On the hosted MCP server, break the work into several `browserless_agent` calls rather than one long one. Set `keepSessionAlive: true` on the first call and echo `sessionId` on later calls to preserve page state; see [session lifetime](/mcp/browser-agent#session-lifetime).

</details>

<details>
<summary>What do MCP sessions cost?</summary>

Each tool call opens a browser session that consumes units based on session time, plus proxy traffic and CAPTCHA solving if used. See [unit consumption](/overview/unit-consumption#mcp-session-cost) for the breakdown.

</details>

## Next steps

  <a href="/mcp/browserless-mcp-server/setup" className="next-step-card-link">
    
      <h3 className="next-step-card-title">MCP server setup</h3>
      authentication options, regions, and the full tool list
    
  </a>
  <a href="/mcp/browser-agent" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Browser Agent</h3>
      the stateful browser session your agent drives across turns
    
  </a>
  <a href="/ai-integrations/claude-code-playwright-mcp" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Playwright MCP + Browserless</h3>
      the full CDP endpoint reference for the alternative route
    
  </a>

---

# Stagehand Integration

URL: https://docs.browserless.io/ai-integrations/stagehand

# Stagehand Integration

[Stagehand](https://github.com/browserbase/stagehand) is an open-source AI browser automation framework that lets you control web browsers using natural language and code. By connecting Stagehand to Browserless, you get reliable cloud-hosted browsers without managing Chrome infrastructure yourself, complete with stealth mode, residential proxies, and enterprise-grade reliability.

- Node.js 18 or later
- Browserless API token (available in your [account dashboard](https://browserless.io/account/))
- An LLM API key (Anthropic, OpenAI, etc.)

## Step-by-Step Setup

  
    Go to your <a href="https://browserless.io/account/" target="_blank" rel="noopener noreferrer">Browserless account dashboard</a> and copy your API token.
    Then set your environment variables:
    
      
**.env file:**

```text
BROWSERLESS_API_KEY=your-browserless-token
ANTHROPIC_API_KEY=your-anthropic-key
```
      
      
**Command line:**

```bash
export BROWSERLESS_API_KEY=your-browserless-token
export ANTHROPIC_API_KEY=your-anthropic-key
```
      
    
  

  
    
      
**npm:**

```bash
npm install @browserbasehq/stagehand zod
```
      
      
**pnpm:**

```bash
pnpm add @browserbasehq/stagehand zod
```
      
    
  

  
    Pass the Browserless WebSocket URL directly to Stagehand's <code>cdpUrl</code> config:

```javascript
import { Stagehand } from "@browserbasehq/stagehand";

const stagehand = new Stagehand({
  env: "LOCAL",
  localBrowserLaunchOptions: {
    cdpUrl: `wss://production-sfo.browserless.io?token=${process.env.BROWSERLESS_API_KEY}`,
  },
  model: "anthropic/claude-sonnet-4-5",
});

await stagehand.init();
```
  

  
    Use Stagehand's page methods with the Browserless-powered browser:

```javascript
import { Stagehand } from "@browserbasehq/stagehand";
import { z } from "zod";

const stagehand = new Stagehand({
  env: "LOCAL",
  localBrowserLaunchOptions: {
    cdpUrl: `wss://production-sfo.browserless.io?token=${process.env.BROWSERLESS_API_KEY}`,
  },
  model: "anthropic/claude-sonnet-4-5",
});

await stagehand.init();

const page = stagehand.context.pages()[0];

// Navigate to Browserless
await page.goto("https://www.browserless.io");

// act() - Click on Documentation
await stagehand.act("click on the Docs link in the navigation");

// extract() - Get documentation sections
const docs = await stagehand.extract(
  "extract the main documentation categories or sections",
  z.object({
    sections: z.array(z.object({
      title: z.string(),
      description: z.string().optional(),
    })),
  })
);
console.log("Documentation sections:", docs.sections);

// Clean up when done
await stagehand.close();
```
  

## Why use Browserless with Stagehand

- **Stealth mode**: bypass bot detection by adding `/stealth` to the endpoint URL
- **Residential proxies**: route traffic through real residential IPs to avoid blocks
- **Global regions**: choose from US West (San Francisco), Europe (London), or Europe (Amsterdam) endpoints for lower latency
- **No infrastructure**: skip managing Chrome installations, updates, or scaling
- **Parallel sessions**: run multiple browser sessions simultaneously
- **Enterprise reliability**: 99.9% uptime SLA with automatic failover

## Resources

- [Stagehand Documentation](https://docs.stagehand.dev)
- [Browserless Connection Options](/overview/connection-urls)
- [Stealth Mode](/baas/bot-detection/stealth)
- [Proxy Configuration](/baas/bot-detection/proxies)
- [Stagehand GitHub Repository](https://github.com/browserbase/stagehand)

## FAQ & Troubleshooting

<details>
<summary>How do I connect Stagehand to Browserless?</summary>

Set `localBrowserLaunchOptions.cdpUrl` to your Browserless WebSocket URL (`wss://production-sfo.browserless.io?token=YOUR_TOKEN`) when creating the `Stagehand` instance, as shown in step 3 above.

</details>

<details>
<summary>Which LLM models can I use with Stagehand?</summary>

Stagehand accepts any model string in `provider/model` format, such as `anthropic/claude-sonnet-4-5` or `openai/gpt-4o`. Set the matching API key as an environment variable (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, etc.) for whichever provider you use.

</details>

<details>
<summary>Stagehand's <code>act()</code> or <code>extract()</code> can't find the right element</summary>

Use the `/stealth` endpoint instead of the default one in your `cdpUrl` if the site is blocking automated browsers. See [stealth mode](/baas/bot-detection/stealth) for configuration details. If the page loads dynamic content, make sure you `await` navigation and any async rendering before calling `act()` or `extract()`.

</details>

## Next steps

  <a href="/mcp/browser-agent" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Browser Agent</h3>
      a managed alternative that skips the setup and model wiring
    
  </a>
  <a href="/ai-integrations/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">AI integrations overview</h3>
      all supported AI frameworks and tools
    
  </a>

---

# AI & Agent Integrations Overview

URL: https://docs.browserless.io/ai-integrations/start

# AI & Agent Integrations

Browserless gives your AI a managed, stealth-enabled browser to plan, act, and observe the web. Pick the integration that fits your stack.

## Agentic Browsing

### MCP (Model Context Protocol)

Connect MCP-compatible clients directly to Browserless over the Model Context Protocol (MCP).

- [MCP Overview](/mcp/overview) – Compare the two Browserless MCP servers and pick the right one
- [Browserless MCP Server](/mcp/browserless-mcp-server/setup) – Full browser automation: scrape, screenshot, PDF, crawl, and run custom code from any MCP client
- [Browserless Docs MCP](/mcp/browserless-docs-mcp) – Query Browserless documentation directly from your IDE or AI tool

### Agent Frameworks

Use Browserless as the browser your agent controls.

- [Browser Use](/ai-integrations/browser-use/python) – Agentic browsing in Python
- [Stagehand](/ai-integrations/stagehand) – Natural language and code browser automation
- [LangChain](/ai-integrations/langchain) – Browserless loaders inside chains and agents
- [AgentKit](/ai-integrations/agentkit) – Fault-tolerant AI agents with Browserless browser tools via Inngest
- [Agent Browser](/ai-integrations/agent-browser) – Headless browser automation CLI for AI agents
- [OpenClaw](/ai-integrations/open-claw) – Local AI assistant with cloud-powered browser execution

### AI SDKs & APIs

Build agents and tool-calling flows with Browserless as a browser tool.

- [Vercel AI SDK](/ai-integrations/vercel-ai-sdk) – Browser tools for Next.js AI handlers
- [Claude Agent SDK](/ai-integrations/claude-agent-sdk) – Autonomous Claude agents with stealth browsing and CAPTCHA solving
- [OpenAI SDK](/ai-integrations/openai) – Browserless tools via the OpenAI Responses API MCP connector
- [Anthropic SDK](/ai-integrations/anthropic) – Browserless tools via the Anthropic Messages API MCP connector

### AI Tools & Interfaces

Extend AI tools and IDEs with a cloud browser.

- [Claude Code Plugin](/ai-integrations/claude-code-plugin) – Browserless MCP server installed as a Claude Code plugin
- [Hermes Agent](/ai-integrations/hermes-agent) – Browserless as the browser for NousResearch's open-source agent via MCP
- [opencode](/ai-integrations/opencode) – A browser for the terminal coding agent that ships without one
- [Claude Code + Playwright MCP](/ai-integrations/claude-code-playwright-mcp) – Control a stealth browser from Claude Code via Playwright MCP
- [Anthropic Computer Use](/ai-integrations/computer-use/anthropic-computer-use) – Claude's screenshot-and-action loop backed by Browserless cloud browsers
- [OpenAI Computer Use](/ai-integrations/computer-use/openai-cua) – OpenAI CUA screenshot-driven automation on Browserless
- [Gemini Computer Use](/ai-integrations/computer-use/gemini-computer-use) – Google Gemini's screenshot-and-action loop on Browserless, with 0–1000 coordinate denormalization

## Workflow & Automation

Call Browserless as a reliable API step inside no-code and low-code tools.

- [n8n](/ai-integrations/n8n) – Drag-and-drop workflows with Browserless REST and BQL
- [Make.com](/ai-integrations/make) – No-code automation calling Browserless
- [Zapier](/ai-integrations/zapier) – Trigger Browserless actions from Zapier apps

## FAQ & Troubleshooting

<details>
<summary>How do I connect my agent to Browserless?</summary>

It depends on the integration type. CDP-based agent frameworks (like Browser Use or Stagehand) connect using a Browserless WebSocket URL (`wss://production-sfo.browserless.io/chromium?token=YOUR_TOKEN`) as the browser endpoint. MCP clients connect to a Browserless MCP server URL with your token instead, and AI SDKs and no-code tools call Browserless's REST or function APIs as a tool. See the section above that matches your tool for the exact connection details.

</details>

<details>
<summary>My agent is getting blocked by bot detection</summary>

Use the [Browserless Agent](/mcp/browser-agent) instead of a plain Puppeteer/Playwright connection – it runs on Browserless's managed, stealth-enabled browser infrastructure with residential proxies built in, so most bot detection is handled without extra configuration.

</details>

## Next steps

  <a href="/ai-integrations/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">AI integrations overview</h3>
      all supported AI frameworks and tools
    
  </a>
  <a href="/browserql/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BQL overview</h3>
      use BrowserQL for advanced browser automation
    
  </a>

---

# Vercel AI SDK Integration

URL: https://docs.browserless.io/ai-integrations/vercel-ai-sdk

# Vercel AI SDK Integration

This guide demonstrates a starter AI-powered browser automation application using Browserless, the Vercel AI SDK, and Next.js. The example shows how an agent can control a browser through natural language instructions for tasks like web scraping, form filling, and content extraction.

- Node.js 18 or higher
- Vercel account
- Browserless API token (available in your [account dashboard](https://browserless.io/account/))
- OpenAI API key (or other supported LLM provider)

## Introduction

This integration combines three technologies:
- **Browserless**: A [headless browser service](/overview/intro) that provides browser automation capabilities
- **Vercel AI SDK**: A toolkit for building AI-powered applications
- **Next.js**: A React framework for building web applications

Together, these technologies enable you to create applications where AI can understand and execute browser automation tasks through natural language.

## Step 1: Project Setup

### Create a Next.js Project

```bash
npx create-next-app@latest browserless-ai
cd browserless-ai
```

### Install Required Packages

```bash
npm install @vercel/ai @browserless/ai puppeteer-core openai zod prettier
```

### Configure Environment Variables

Create a `.env.local` file in your project root:

```env
BROWSERLESS_API_KEY=your_browserless_api_key
OPENAI_API_KEY=your_openai_api_key
```

## Step 2: Core Components

### Browser Service

Create `src/lib/browser.ts`:

```typescript

// Schema for browser settings
const BrowserSettingsSchema = z.object({
  viewport: z.object({
    width: z.number().default(1920),
    height: z.number().default(1080),
  }),
  userAgent: z.string().optional(),
  timeout: z.number().default(30000),
});

  private static instance: BrowserlessService;
  private browser: puppeteer.Browser | null = null;
  private settings: BrowserSettings;

  private constructor(settings: Partial = {}) {
    this.settings = BrowserSettingsSchema.parse(settings);
  }

  static getInstance(settings?: Partial): BrowserlessService {
    if (!BrowserlessService.instance) {
      BrowserlessService.instance = new BrowserlessService(settings);
    }
    return BrowserlessService.instance;
  }

  async getBrowser(): Promise<puppeteer.Browser> {
    if (!this.browser) {
      this.browser = await puppeteer.connect({
        browserWSEndpoint: `wss://production-sfo.browserless.io?token=${process.env.BROWSERLESS_API_KEY}`,
        defaultViewport: this.settings.viewport,
      });
    }
    return this.browser;
  }

  async close() {
    if (this.browser) {
      await this.browser.close();
      this.browser = null;
    }
  }
}
```

### AI Route Handler

Create `src/app/api/chat/route.ts`:

```typescript

// Schema for chat messages
const MessageSchema = z.object({
  role: z.enum(['user', 'assistant', 'system']),
  content: z.string(),
});

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

  try {
    const { messages } = await req.json();

    // Validate messages
    const validatedMessages = z.array(MessageSchema).parse(messages);

    const browserService = BrowserlessService.getInstance();
    const browser = await browserService.getBrowser();
    const page = await browser.newPage();

    // Process the message and perform browser actions
    const response = await openai.chat.completions.create({
      model: process.env.OPENAI_MODEL ?? 'gpt-4.1-mini',
      stream: true,
      messages: [
        {
          role: 'system',
          content: `You are a browser automation assistant. You can:
          1. Navigate to URLs
          2. Extract information from pages
          3. Fill out forms
          4. Take screenshots
          5. Click elements
          6. Type text
          Always respond with clear instructions for the browser.`
        },
        ...validatedMessages,
      ],
    });

    const stream = OpenAIStream(response, {
      async onCompletion(completion) {
        try {
          // Process the AI's response and perform browser actions
          if (completion.includes('navigate to')) {
            const url = completion.match(/navigate to (https?:\/\/[^\s]+)/)?.[1];
            if (url) {
              await page.goto(url, { waitUntil: 'networkidle0' });
            }
          }

          if (completion.includes('click')) {
            const selector = completion.match(/click "([^"]+)"/)?.[1];
            if (selector) {
              await page.click(selector);
            }
          }

          if (completion.includes('type')) {
            const [selector, text] = completion.match(/type "([^"]+)" into "([^"]+)"/)?.slice(1) || [];
            if (selector && text) {
              await page.type(selector, text);
            }
          }

          // Add more action handlers as needed
        } catch (error) {
          console.error('Error executing browser action:', error);
        }
      },
    });

    return new StreamingTextResponse(stream);
  } catch (error) {
    console.error('Error processing request:', error);
    return new Response(
      JSON.stringify({ error: 'Failed to process request' }),
      { status: 500 }
    );
  }
}
```

### Chat Interface

Create `src/app/page.tsx`:

```typescript
'use client';

  const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat();
  const [isProcessing, setIsProcessing] = useState(false);

  return (
    
      
        {messages.map((message) => (
          
            {message.content}
          
        ))}
      

      <form onSubmit={handleSubmit} className="flex gap-2">
        <input
          className="flex-1 p-2 border border-gray-300 rounded-lg"
          value={input}
          placeholder="Ask the AI to perform browser actions..."
          onChange={handleInputChange}
          disabled={isLoading || isProcessing}
        />
        <button
          type="submit"
          className="px-4 py-2 bg-blue-500 text-white rounded-lg disabled:opacity-50"
          disabled={isLoading || isProcessing}
        >
          Send
        </button>
      </form>
    
  );
}
```

## Step 3: Advanced Features

### Session Management

Create `src/lib/session.ts`:

```typescript

  private static sessions: Map<string, {
    page: puppeteer.Page;
    lastActive: number;
  }> = new Map();
  private static readonly SESSION_TIMEOUT = 30 * 60 * 1000; // 30 minutes

  static async getSession(sessionId: string): Promise<puppeteer.Page> {
    const session = this.sessions.get(sessionId);

    if (session && Date.now() - session.lastActive < this.SESSION_TIMEOUT) {
      session.lastActive = Date.now();
      return session.page;
    }

    const browser = await BrowserlessService.getInstance().getBrowser();
    const page = await browser.newPage();

    this.sessions.set(sessionId, {
      page,
      lastActive: Date.now(),
    });

    return page;
  }

  static async cleanup() {
    const now = Date.now();
    for (const [sessionId, session] of this.sessions.entries()) {
      if (now - session.lastActive > this.SESSION_TIMEOUT) {
        await session.page.close();
        this.sessions.delete(sessionId);
      }
    }
  }
}
```

### Error Handling Middleware

Create `src/middleware.ts`:

```typescript

  try {
    // Add rate limiting
    const ip = request.headers.get('x-forwarded-for') || 'unknown';
    // Implement your rate limiting logic here

    // Validate request body
    if (request.method === 'POST') {
      const body = await request.json();
      // Add your validation logic here
    }

    return NextResponse.next();
  } catch (error) {
    console.error('Middleware error:', error);
    return new NextResponse(
      JSON.stringify({ error: 'Internal server error' }),
      { status: 500 }
    );
  }
}
```

## Step 4: Testing

### Local Testing

1. Start the development server:
```bash
npm run dev
```

2. Test the API endpoints:
```bash
curl -X POST http://localhost:3000/api/chat \
  -H "Content-Type: application/json" \
  -d '{"messages": [{"role": "user", "content": "Navigate to example.com"}]}'
```

### Production Testing

1. Deploy to Vercel:
```bash
vercel
```

2. Test the deployed endpoints:
```bash
curl -X POST https://your-app.vercel.app/api/chat \
  -H "Content-Type: application/json" \
  -d '{"messages": [{"role": "user", "content": "Navigate to example.com"}]}'
```

## Step 5: Deployment

### Vercel Deployment

1. Push your code to GitHub
2. Import the project in Vercel
3. Add your environment variables:
   - `BROWSERLESS_API_KEY`
   - `OPENAI_API_KEY`
4. Deploy!

### Environment Configuration

Configure your Vercel project settings:

```json
{
  "buildCommand": "next build",
  "outputDirectory": ".next",
  "framework": "nextjs",
  "installCommand": "npm install",
  "regions": ["sfo1"]
}
```

## Best Practices

1. **Resource Management**
   - Always close browser pages when done
   - Implement session timeouts
   - Clean up unused resources

2. **Error Handling**
   - Implement comprehensive error handling
   - Log errors appropriately
   - Provide meaningful error messages

3. **Security**
   - Validate all inputs
   - Implement rate limiting
   - Use environment variables for sensitive data

4. **Performance**
   - Use appropriate timeouts
   - Implement caching where possible
   - Reduce unnecessary browser operations

5. **Monitoring**
   - Set up error tracking
   - Monitor API usage
   - Track performance metrics

## Additional Resources

- [Browserless REST APIs](/rest-apis/intro)
- [BaaS Connection URL Patterns](/baas/connection-url-patterns)
- [Vercel AI SDK Documentation](https://sdk.vercel.ai/docs)
- [Next.js Documentation](https://nextjs.org/docs)
- [OpenAI API Documentation](https://platform.openai.com/docs/api-reference)

## FAQ & Troubleshooting

<details>
<summary>I'm getting connection errors</summary>

Check that your Browserless API token is correct and active, verify network connectivity, and check that your firewall allows outbound WebSocket connections.

</details>

<details>
<summary>My requests are timing out</summary>

Increase your timeout values, reduce unnecessary browser operations per session, and implement retry logic for transient failures.

</details>

<details>
<summary>I'm hitting rate limit errors</summary>

Implement proper rate limiting on your side, monitor your API usage in the [Browserless dashboard](https://browserless.io/account/), and consider upgrading your plan if you consistently hit limits.

</details>

<details>
<summary>How do I debug requests to Browserless?</summary>

Enable verbose logging with `process.env.DEBUG = 'browserless:*'`, use the Browserless dashboard to monitor active sessions, and log request details (`url`, `method`, `headers`) before sending them.

</details>

## Next steps

  <a href="/ai-integrations/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">AI integrations overview</h3>
      all supported AI frameworks and tools
    
  </a>
  <a href="/browserql/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BQL overview</h3>
      use BrowserQL for advanced browser automation
    
  </a>

---

# Browserless templates for Zapier

URL: https://docs.browserless.io/ai-integrations/zapier

# Using Browserless with Zapier

Use Zapier to orchestrate Browserless through our [REST APIs](/rest-apis/intro). This will allow you to trigger real Chrome sessions to take screenshots, generate PDFs, fetch HTML, run BQL flows, or execute custom functions, then pass results to apps like Google Drive, Slack, or Gmail.
There are two ways to use Browserless with Zapier:

- **Browserless Zapier Integration**: our custom app with built‑in actions like Screenshot, PDF, Content, Stats, Scrape, and BQL. No paid Zapier account required.
- **Webhooks by Zapier**: call Browserless REST/BQL endpoints directly with Webhooks by Zapier which requires a paid Zapier account.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Browserless Zapier Integration

Connect your Browserless account and use the prebuilt actions, then fill in the required fields.

### Connect Browserless

1. In Zapier, create a Zap and add the Browserless app as an action.
2. Click “Connect a new account”.
3. Enter:
   - Account Name: any label to recognize your account
   - API token: your Browserless API token from the dashboard
4. Zapier will verify the token by calling the Browserless version endpoint.

### Built-in REST APIs

- Screenshot: capture a screenshot from a URL.
- PDF: generate a PDF from a URL.
- Content: fetch rendered HTML/content from a URL.
- Scrape: scrape data from a URL.
- Stats: retrieve account stats.
- BQL: run multi‑step BrowserQL flows.

Each action exposes “Quick Options” (URL, `waitUntil`, flags) and an “Advanced - Raw JSON” input that overrides quick options if you define the full JSON body.

![Browserless Zapier Integration](/img/fetchHtmlZapier.png)

## Webhooks by Zapier

Use Webhooks by Zapier to run our REST APIs as you would with any HTTP Request and implement our REST APIs directly.

> **Warning**
> Webhooks by Zapier requires a paid Zapier account.

For the templates below, remember to replace `YOUR_API_TOKEN_HERE` with your actual Browserless API token.

### Example workflow: Daily Visual Check

**Trigger** — Schedule by Zapier: run every hour

**Action** — Webhooks by Zapier (POST):
- **URL**: `https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE&headless=false&stealth=true&--window-size=1440,1000&--lang=en-US`
- **Payload (JSON)**:

```json
{
  "url": "https://www.ebay.com/sch/i.html?_nkw=nintendo+switch",
  "options": {
    "fullPage": true,
    "type": "png",
    "viewport": { "width": 1440, "height": 1000 }
  }
}
```
**Optional** — Gmail (Send Email): attach the file taken from Browserless.

![Zapier Webhook Configuration](/img/webHookZapier.png)

### Implementing REST APIs

You can implement all our REST APIs using Webhooks by Zapier as documented below.
Zapier has an AI Beta "Copilot", you can copy paste the below information and it'll configure the webhook for you.

#### Screenshot

**Use Webhooks by Zapier → POST** to `/screenshot` for visual monitoring, thumbnails, and documentation images.

**Zapier Webhook POST Configuration**

1. Add **Webhooks by Zapier** to your Zap
2. Choose **POST** as the event
3. Set **URL** to `https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE`
4. Set **Payload Type** to `JSON`
5. Add these data fields:
   - `url`: `https://www.example.com`

[Learn more about the Screenshot API](/rest-apis/screenshot-api)

#### PDF Generation

**Use Webhooks by Zapier → POST** to `/pdf` for printable reports and archival documents.

**Zapier Webhook POST Configuration**

1. Add **Webhooks by Zapier** to your Zap
2. Choose **POST** as the event
3. Set **URL** to `https://production-sfo.browserless.io/pdf?token=YOUR_API_TOKEN_HERE`
4. Set **Payload Type** to `JSON`
5. Add these data fields:
   - `url`: `https://www.example.com`

[Learn more about the PDF API](/rest-apis/pdf-api)

#### Content Extraction

**Use Webhooks by Zapier → POST** to `/content` for rendered HTML analysis and web scraping.

**Zapier Webhook POST Configuration**

1. Add **Webhooks by Zapier** to your Zap
2. Choose **POST** as the event
3. Set **URL** to `https://production-sfo.browserless.io/content?token=YOUR_API_TOKEN_HERE`
4. Set **Payload Type** to `JSON`
5. Add these data fields:
   - `url`: `https://www.example.com`

[Learn more about the Content API](/rest-apis/content)

#### Unblock

**Use Webhooks by Zapier → POST** to `/unblock` for bypassing anti-bot measures and accessing protected content.

**Zapier Webhook POST Configuration**

1. Add **Webhooks by Zapier** to your Zap
2. Choose **POST** as the event
3. Set **URL** to `https://production-sfo.browserless.io/unblock?token=YOUR_API_TOKEN_HERE`
4. Set **Payload Type** to `JSON`
5. Add these data fields:
   - `url`: `https://www.example.com`
   - `cookies`: `true`
   - `browserWSEndpoint`: `true`
   - `content`: `true`
   - `screenshot`: `true`

[Learn more about the Unblock API](/rest-apis/unblock)

#### Browser Query Language (BQL)

**Use Webhooks by Zapier → POST** to `/chrome/bql` for multi-step BrowserQL automation workflows.

**Zapier Webhook POST Configuration**

1. Add **Webhooks by Zapier** to your Zap
2. Choose **POST** as the event
3. Set **URL** to `https://production-sfo.browserless.io/chrome/bql?token=YOUR_API_TOKEN_HERE`
4. Set **Payload Type** to `JSON`
5. Add these data fields:
   - `query`: `mutation FormExample {\n  goto(url: \"https://www.browserless.io/practice-form\") {\n    status\n  }\n select(selector:\"#Contact-Subject\",value:\"support\"){     time   } \n typeEmail: type(text: \"john@email.com\", selector: \"#Email\") {\n    time\n  }\n  typeMessage: type(\n    selector: \"#Message\"\n    text: \"Hello world!\"\n  ) {\n    time\n  }\n  solve(\n    type: cloudflare\n  ){\n    solved\n  }\n waitForTimeout(time:3000){time}\n screenshot{\n    base64\n  }\n}`
   - `variables`: `{}`
   - `operationName`: `FormExample`

[Learn more about BQL](/browserql/start)

#### Function

**Use Webhooks by Zapier → POST** to `/function` for custom JavaScript execution in a real browser context.

**Zapier Webhook POST Configuration**

1. Add **Webhooks by Zapier** to your Zap
2. Choose **POST** as the event
3. Set **URL** to `https://production-sfo.browserless.io/function?token=YOUR_API_TOKEN_HERE`
4. Set **Payload Type** to `Raw`
5. Set **Content Type** to `application/javascript`
6. Add this JavaScript to the **Data** field:

```javascript

  await page.goto("https://example.com/");
  const url = await page.content();
  const buffer = await page.pdf({ format: "A4" });
  const base64PDF = buffer.toString('base64');
  const screenshot = await page.screenshot({ encoding: "base64" });

  return {
    data: {
      url,
      screenshot,
      base64PDF
    },
    type: "application/json",
  };
}
```

[Learn more about the Function API](/rest-apis/function)

## FAQ & Troubleshooting

<details>
<summary>Do I need a paid Zapier account?</summary>

No, not for the [Browserless Zapier Integration](#browserless-zapier-integration) app with its built-in actions (Screenshot, PDF, Content, Stats, Scrape, BQL). A paid plan is only required for [Webhooks by Zapier](#webhooks-by-zapier), which calls the REST/BQL endpoints directly.

</details>

<details>
<summary>The webhook returns 401 Unauthorized</summary>

Check that `token=YOUR_API_TOKEN_HERE` is on the request URL. If you're using the Browserless Zapier Integration app instead, re-connect your account and re-enter the token; Zapier verifies it against the version endpoint on save.

</details>

<details>
<summary>The screenshot or PDF response isn't usable in later Zap steps</summary>

Binary buffers don't pass cleanly between Zap steps. Add `"encoding": "base64"` to the request body (as shown in the Screenshot API) so Browserless returns a base64 string you can attach directly to an email or file-upload step.

</details>

## Next steps

  <a href="/ai-integrations/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">AI integrations overview</h3>
      all supported AI frameworks and tools
    
  </a>
  <a href="/rest-apis/intro" className="next-step-card-link">
    
      <h3 className="next-step-card-title">REST APIs overview</h3>
      explore every Browserless REST endpoint
    
  </a>

---

# API Reference Overview

URL: https://docs.browserless.io/api-reference

# API Reference Overview

Full specifications for every Browserless endpoint. Use these references to look up exact parameters, types, and response shapes.

## FAQ & Troubleshooting

<details>
<summary>How do I authenticate API requests?</summary>

Pass your API token as a `?token=` query parameter in the request URL. Some endpoints also accept it in the `Authorization` header.

</details>

<details>
<summary>Where can I test API endpoints?</summary>

Use the [API playground](/rest-apis/api-playground) to test endpoints interactively with your token.

</details>

## Next steps

  <a href="/rest-apis/intro" className="next-step-card-link">
    
      <h3 className="next-step-card-title">REST API endpoints</h3>
      all available REST API routes
    
  </a>
  <a href="/rest-apis/api-playground" className="next-step-card-link">
    
      <h3 className="next-step-card-title">API playground</h3>
      test endpoints interactively
    
  </a>

---

# CDP Extensions

URL: https://docs.browserless.io/api-reference/cdp-extensions

# CDP Extensions

Browserless-specific Chrome DevTools Protocol methods you call via `cdp.send()` on a page's CDP session in Puppeteer or Playwright. They extend standard CDP with live URLs, captcha solving, session reconnect, recording, file transfer, and page identification. Several also emit CDP events (listen with `cdp.on()`).

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- An existing WebSocket connection to Browserless via Puppeteer or Playwright
- Familiarity with creating a CDP session (`page.createCDPSession()` in Puppeteer, `page.context().newCDPSession(page)` in Playwright)

Captcha solving, reconnect, and recording are each gated on their own feature flag / query param and return an `error` in the result when unavailable.

## Usage

```javascript
const browser = await puppeteer.connect({ browserWSEndpoint: '...' });
const [page] = await browser.pages();
const cdp = await page.createCDPSession();

// Command: get a live URL for debugging
const { liveURL } = await cdp.send('Browserless.liveURL', { quality: 70 });

// Event: react to an auto-detected captcha
cdp.on('Browserless.captchaFound', ({ type, status }) => {});
```

## Commands

### Browserless.liveURL

Mints a temporary URL that opens a live, interactive view of the current page in any browser. For debugging, QA, or human-in-the-loop workflows. The Live URL `timeout` sets the URL's validity and post-disconnect keep-alive window. The browser and viewer still end at the existing [session deadline](/baas/best-practices#session-duration-limits); minting a Live URL never resets or extends that deadline. If `timeout` is longer than the time the session has left, the URL is minted for the remaining time instead, and a session with no time left returns `error: "The browser session has expired."`. Each Live URL accepts up to five concurrent viewers (a sixth connection receives HTTP `429`). It binds to the page whose CDP session sends it. For one link per tab, see [Multi-Tab Workflows](/baas/advanced-configurations/hybrid-automation-configurations#multi-tab-workflows).

Call via CDP: `cdp.send('Browserless.liveURL', { quality: 70 })`

Returns: `{ error, liveURLId, liveURL, timeout }` (`error` is `null` on success; `timeout` is the lifetime actually applied, in ms).

| Parameter | Type | Default | Description |
|---|---|---|---|
| `quality` | `number` | `70` | Stream image quality (1-100). |
| `type` | `string` | `"jpeg"` | Stream frame image format. |
| `timeout` | `number` | `30000` | How long (ms) the minted URL stays valid, capped to the session's remaining time. |
| `interactable` | `boolean` | `true` | Allow the viewer to click/type into the page. |
| `resizable` | `boolean` | `true` | Allow the viewer to resize the viewport. |
| `showBrowserInterface` | `boolean` | `false` | Show browser chrome around the page. |
| `compressed` | `boolean` | `true` | Compress the stream. |
| `emulateComponents` | `boolean` | `true` | Emulate UI components in the stream. |

### Browserless.closeLiveURL

Closes a live URL opened with `liveURL`, freeing server resources. Called automatically when the session ends.

Call via CDP: `cdp.send('Browserless.closeLiveURL', { liveURLId })`

Returns: `{ error, liveURLId }`.

| Parameter | Type | Description |
|---|---|---|
| `liveURLId` | `string` | Required. The `liveURLId` returned by `liveURL`. |

### Browserless.solveCaptcha

Detects a CAPTCHA on the current page and attempts to solve it via a third-party solving service (reCAPTCHA v2/v3, Cloudflare Turnstile, and more). Requires captcha solving to be available for the session.

Call via CDP: `cdp.send('Browserless.solveCaptcha')`

Returns: `{ ok, captchaFound, solveAttempted, solved, token?, message?, error? }`.

### Browserless.reconnect

Returns a WebSocket URL for reconnecting to this browser session after disconnecting. The browser stays alive server-side for `timeout` ms. Requires reconnect to be enabled; on hosted Cloud plans a `timeout` above the plan's [maximum session duration](/baas/session-management/standard-sessions#reconnection-timeout-limits) returns an `error` instead of being silently clamped. The browser still terminates at its original session deadline even if the TTL has time left.

Call via CDP: `cdp.send('Browserless.reconnect', { timeout: 60000 })`

Returns: `{ auth, error, browserWSEndpoint }` (`browserWSEndpoint` is `null` on error).

| Parameter | Type | Description |
|---|---|---|
| `timeout` | `number` | How long (ms) to keep the browser alive after disconnect. Defaults to the deployment's configured connection timeout. |
| `auth` | `string` | Optional auth token echoed back and required on the reconnect handshake. |

### Browserless.pageId

Returns Browserless's stable identifier for the current page target, useful for routing and correlating a page across reconnects.

Call via CDP: `cdp.send('Browserless.pageId')`

Returns: `{ pageId: '...' }`.

### Browserless.startRecording

Starts server-side video recording of the page. Requires connecting with `?record=true`. Refused after a credential has been filled on the session (the recording could capture the secret).

Call via CDP: `cdp.send('Browserless.startRecording', { width, height })`

Returns: `{ recording, error? }`.

| Parameter | Type | Description |
|---|---|---|
| `width` | `number` | Optional recording width. |
| `height` | `number` | Optional recording height. |

### Browserless.stopRecording

Stops the recording started with `startRecording` and returns the video. Requires `?record=true`.

Call via CDP: `cdp.send('Browserless.stopRecording', { encoding: 'base64' })`

Returns the encoded recording, or `{ error }`.

| Parameter | Type | Description |
|---|---|---|
| `encoding` | `string` | `"binary"` or `"base64"`. |

### Browserless.stopSessionRecording

Stops rrweb session-replay capture, then processes and uploads the events. Requires connecting with `?replay=true`.

Call via CDP: `cdp.send('Browserless.stopSessionRecording')`

Returns: `{ error }` (`null` on success).

### Browserless.uploadFile

Uploads one or more files into a file `<input>` on the page, matched by selector. Total decoded size is capped per request.

Call via CDP: `cdp.send('Browserless.uploadFile', { selector, files })`

Returns: `{ ok, error? }`.

| Parameter | Type | Description |
|---|---|---|
| `selector` | `string` | Required. CSS selector for the file input. |
| `files` | `array` | Required. Objects of `{ name, content, mimeType? }` where `content` is base64 and `mimeType` is inferred from `name` if omitted. |

### Browserless.setDownloadEnabled

Toggles streaming of completed downloads back to the client as `Browserless.fileDownloaded` events. Off by default, so download bytes never flow unless you opt in.

Call via CDP: `cdp.send('Browserless.setDownloadEnabled', { enabled: true })`

Returns: `{ ok, enabled }`.

| Parameter | Type | Description |
|---|---|---|
| `enabled` | `boolean` | Whether to emit `fileDownloaded` events. |

## Events

Subscribe with `cdp.on('', handler)`.

### Browserless.heartbeat

Periodic keep-alive emitted on each attached session at the server's heartbeat interval. No params.

### Browserless.captchaFound

Emitted when a CAPTCHA is auto-detected on a page.

Params: `{ type, status }` — `status` is `"solving"` when auto-solve is on, otherwise `"found"`.

### Browserless.captchaAutoSolved

Emitted after an auto-solve attempt completes (only when auto-solving is enabled).

Params: the solve result `{ found, solved, token, time, error? }`.

### Browserless.fileDownloaded

Emitted when a download completes, if enabled via `setDownloadEnabled`. Mirrored to every attached session.

Params: `{ filename, mimeType, size, data }` (`data` base64), or `{ filename, mimeType, size, error: 'FileTooLarge', maxBytes }` when the file exceeds the transfer limit.

### Browserless.liveComplete

Emitted at most once per mint created with `Browserless.liveURL`, including links nobody opens. Params: `{ liveURLId: string, reason: LiveURLCompleteReason }`.

| Reason | Meaning |
| --- | --- |
| `timeout` | The link's lifetime expired. |
| `closed` | The link was explicitly closed, replaced by a new mint on the same page, or lost its anchor page without a replacement. |
| `viewerDisconnected` | The last viewer disconnected and stayed gone for two seconds, including credential-protection teardown. |
| `userDone` | Reserved for a future viewer “Done” action; not currently produced. |
| `userFailed` | Reserved for a future viewer “Couldn't finish” action; not currently produced. |

Every reason consumes that mint's one completion. After an ordinary `viewerDisconnected`, the URL remains reopenable until expiry while the browser session is available, but later visits or expiry produce no further completion. Reconnecting within the two-second grace period, such as during a viewer reload, cancels the pending disconnect completion. **Credential-protection teardown is an exception:** after a stored credential is filled, new viewers are blocked for that session, so reopening cannot cancel or re-arm completion. Completion does not prove the human task succeeded; verify the page state before continuing.

Delivery requires a CDP session attached to the page; earlier events are not replayed. Browser shutdown records closure internally but does not send this event. `Browserless.reconnect` IDs and links minted through BQL or HTTP do not produce this CDP event. Existing zero-argument listeners continue to work unchanged.

When re-minting on the same page, **register the new listener after the mint response, then share the URL**. Re-minting reuses the ID and emits `closed` for the superseded mint; a listener armed before the response can catch that old completion. See [handling completion reasons](/baas/monitor-sessions/hybrid-automation#handle-completion-reasons).

### Browserless.liveViewerConnected

Emitted each time a viewer WebSocket attaches to a live URL, including additional viewers and reconnects. Both `/live/*` and `/chromium/live/*` viewer routes emit this event.

Params: `{ liveURLId: string }`, identifying the live URL returned by `Browserless.liveURL`. The event is delivered to attached CDP sessions for that URL's page, not other pages. It signals attachment, not that the viewer has finished interacting or received a frame.

Register the listener before sharing the URL; earlier attachments are not replayed:

```js
const cdp = await page.createCDPSession();
cdp.on('Browserless.liveViewerConnected', ({ liveURLId }) => {
  console.log('Viewer connected:', liveURLId);
});
const { liveURL } = await cdp.send('Browserless.liveURL');
// Share liveURL with the viewer.
```

## FAQ & Troubleshooting

<details>
<summary>Why does <code>liveURL</code> or <code>reconnect</code> return an <code>error</code> instead of a URL?</summary>

The commands have different requirements. For `liveURL`, an error means invalid authentication, a runtime failure, or an invalid timeout — a `timeout` that isn't a whole number of milliseconds above 0, or that exceeds your plan's [maximum session duration](/baas/best-practices#session-duration-limits), is rejected when the URL is minted. `reconnect` has its own reconnect ceiling, and recording requires `?record=true` and a plan that includes recording. When a command is unavailable, it returns `error` instead of the expected field (`liveURL`, `browserWSEndpoint`, and so on).

</details>

<details>
<summary>Why isn't my <code>cdp.on()</code> handler firing?</summary>

CDP extension events are scoped to the session that triggered them, so `cdp.on()` has to be attached to the same `CDPSession` you called `cdp.send()` on, before the triggering action happens. A handler attached after the event already fired, or attached to a different page's CDP session, never receives it.

</details>

<details>
<summary>Why does <code>Browserless.startRecording</code> fail after I've already filled in a login form?</summary>

Recording is refused once a credential has been filled on the session, since the recording could capture the secret. Start recording before any authentication step, or use a separate session for the recorded portion of the flow.

</details>

## Next steps

  <a href="/baas/monitor-sessions/screen-recording" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Screen recording</h3>
      Full guide to session recording and playback
    
  </a>
  <a href="/baas/bot-detection/captchas" className="next-step-card-link">
    
      <h3 className="next-step-card-title">CAPTCHA solving</h3>
      Configuring and troubleshooting auto-solve
    
  </a>
  <a href="/api-reference" className="next-step-card-link">
    
      <h3 className="next-step-card-title">API Reference overview</h3>
      BrowserQL schema, OpenAPI spec, and the CLI
    
  </a>

---

# Advanced Hybrid Automation Configurations

URL: https://docs.browserless.io/baas/advanced-configurations/hybrid-automation-configurations

This page is the comprehensive reference for all `Browserless.liveURL` options and advanced patterns. It covers bandwidth tuning, viewport control, recording integration, CAPTCHA fallback handling, multi-stage workflows, and error recovery.

If you are new to hybrid automation, start with the [Hybrid Automation](/baas/monitor-sessions/hybrid-automation) guide.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Puppeteer or Playwright installed locally

> **Session limits**
> The `timeout` values in this guide have to fit within your plan's [maximum session duration](/baas/best-practices#session-duration-limits), and a LiveURL created partway through a session can't extend the browser's existing deadline.

## Bandwidth and Quality Optimization

Fine-tune live sessions for different network conditions and device capabilities.

  
**Puppeteer:**

```js
const { liveURL } = await cdp.send('Browserless.liveURL', {
  quality: 30,        // Optimized for mobile/slow connections
  type: 'jpeg',       // Better compression than PNG
  timeout: 300000,    // 5 minutes for complex workflows
});
```
  
  
**Playwright:**

```js
const { liveURL } = await cdpSession.send('Browserless.liveURL', {
  quality: 30,        // Optimized for mobile/slow connections
  type: 'jpeg',       // Better compression than PNG
  timeout: 300000,    // 5 minutes for complex workflows
});
```
  

## Advanced Viewport Control

Control viewport behavior and browser UI visibility for live sessions.

By default, the live URL viewport resizes to match the end user's screen. Set `resizable: false` to lock the browser at its current viewport dimensions. The live URL client preserves the aspect ratio.

  
**Puppeteer:**

```js
const { liveURL } = await cdp.send('Browserless.liveURL', {
  resizable: false,      // Maintain fixed viewport
  interactable: true,    // Allow user interaction
  showBrowserInterface: false, // Hide browser tabs and UI
});
```
  
  
**Playwright:**

```js
const { liveURL } = await cdpSession.send('Browserless.liveURL', {
  resizable: false,      // Maintain fixed viewport
  interactable: true,    // Allow user interaction
  showBrowserInterface: false, // Hide browser tabs and UI
});
```
  

## Multi-Tab Workflows

`Browserless.liveURL` binds to the page whose CDP session sends it, so one CDP session per tab gives you one independent link per tab.

Open both tabs before sharing either link. These recipes keep each tab's `liveURLId` so you can close its link with `Browserless.closeLiveURL` later.

Open both links and keep the original tabs open until both viewers finish. These minimal examples wait for each tab's `Browserless.liveComplete` event. An unopened link or a detached tab listener can leave that wait pending, even after the browser disconnects. In production, add an application timeout and disconnect handling. The five-minute browser session timeout doesn't settle the application promises.

  
**Puppeteer:**

```js
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint: 'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&timeout=300000',
});
try {
  const tab1 = await browser.newPage();
  await tab1.goto('https://example.com');
  const tab2 = await browser.newPage();
  await tab2.goto('https://browserless.io');

  const liveViews = await Promise.all([tab1, tab2].map(async (page) => {
    const cdp = await page.createCDPSession();
    const complete = new Promise((resolve) => cdp.once('Browserless.liveComplete', resolve));
    const { error, liveURL, liveURLId } = await cdp.send('Browserless.liveURL', {
      timeout: 300000,
    });
    if (error) throw new Error(error);
    return { cdp, liveURL, liveURLId, complete };
  }));

  liveViews.forEach(({ liveURL }, index) => console.log(`Tab ${index + 1}:`, liveURL));

  await Promise.all(liveViews.map(({ complete }) => complete));
} finally {
  await browser.close();
}
```
  
  
**Playwright:**

```js
import { chromium } from 'playwright-core';

const browser = await chromium.connectOverCDP(
  'wss://production-sfo.browserless.io/chromium/stealth?token=YOUR_API_TOKEN_HERE&timeout=300000'
);
try {
  const [context] = browser.contexts();
  const tab1 = await context.newPage();
  await tab1.goto('https://example.com');
  const tab2 = await context.newPage();
  await tab2.goto('https://browserless.io');

  const liveViews = await Promise.all([tab1, tab2].map(async (page) => {
    const cdp = await context.newCDPSession(page);
    const complete = new Promise((resolve) => cdp.once('Browserless.liveComplete', resolve));
    const { error, liveURL, liveURLId } = await cdp.send('Browserless.liveURL', {
      timeout: 300000,
    });
    if (error) throw new Error(error);
    return { cdp, liveURL, liveURLId, complete };
  }));

  liveViews.forEach(({ liveURL }, index) => console.log(`Tab ${index + 1}:`, liveURL));

  await Promise.all(liveViews.map(({ complete }) => complete));
} finally {
  await browser.close();
}
```
  

**How per-tab links behave**

- Minting again for the **same tab** returns the **same link** (same `liveURLId`) with the new options and timeout, and disconnects anyone currently watching it. Minting for a **different tab** returns a separate link, and the two coexist.
- With `showBrowserInterface` off, a link switches to **any new tab** opened after the viewer connected, anywhere in the browser, including other browser contexts. Every open link switches, not only the one whose tab opened a popup. Open all the tabs you want to stream **before sharing** the links. A mode that pins a link to its tab is planned.
- Closing a tab **doesn't end** its link. The link moves to another tab it can see, and ends only when none are left. Call `Browserless.closeLiveURL` with that tab's `liveURLId` before closing the tab if you don't want this.
- Filling a stored credential closes all links for the browser. See [Viewer behavior and limits](/baas/monitor-sessions/embedding-live-url#viewer-behavior-and-limits).

If you'd rather hand out a single link that shows every tab, set `showBrowserInterface: true`. The viewer shows live tab titles and favicons, lets users switch and close tabs, provides back and forward controls, and adds a draggable page scrollbar. This option injects UI overlay code into the page, which may increase CAPTCHA challenge likelihood on some sites.

  
**Puppeteer:**

```js
const { liveURL } = await cdp.send('Browserless.liveURL', {
  showBrowserInterface: true, // Show all tabs and browser UI
  quality: 50,
  timeout: 300000, // 5 minutes for complex workflows
});
```
  
  
**Playwright:**

```js
const { liveURL } = await cdpSession.send('Browserless.liveURL', {
  showBrowserInterface: true, // Show all tabs and browser UI
  quality: 50,
  timeout: 300000, // 5 minutes for complex workflows
});
```
  

## Mobile Viewers

LiveURL links work when the end user opens them on a phone or tablet, but support is limited to single-touch interaction:

- **Touch input**: A single-finger tap maps to a mouse click, and a single-finger drag maps to mouse scroll. Multi-touch gestures (including pinch-to-zoom and two-finger pan) are not forwarded to the remote browser.
- **Virtual keyboard**: When the LiveURL is opened on a device identified as mobile by its user agent, Browserless automatically injects an on-screen keyboard into the remote page so end users can type.
- **Remote viewport**: With the default `resizable: true`, the remote browser's viewport is set to the end user's device width and height, which can cause target sites to render their mobile layout. Use `resizable: false` to keep the viewport you set in your automation script; the stream will be letterboxed to fit the device while preserving aspect ratio.
- **Local zoom**: The LiveURL page does not disable the browser's native pinch-zoom, so end users can zoom the LiveURL page itself, but this only scales the local canvas — it does not zoom the remote page.
- **Bandwidth**: Lower `quality` (for example `quality: 30`) and keep `type: 'jpeg'` to reduce data usage on cellular connections.

## Clipboard Support

The viewer supports copy and paste in both directions. Ctrl-based shortcuts work on Windows and Linux, and Command-based shortcuts work on macOS. When you [embed the Live URL](/baas/monitor-sessions/embedding-live-url), add `allow="clipboard-read; clipboard-write"` to the iframe so the browser can grant clipboard access.

Depending on the end user's browser and operating system configuration, clipboard access may require explicit user permission or interaction before it can be used within the LiveURL session.

## Read-Only Monitoring Sessions

Create view-only sessions for compliance monitoring or training by setting `interactable: false`. The viewer sees the browser but cannot click, type, or interact. Browserless enforces this on the server by dropping viewer input before it reaches the remote browser; client-side CSS isn't a security boundary. See [View-only embedding](/baas/monitor-sessions/embedding-live-url#view-only-embedding).

  
**Puppeteer:**

```js
const { liveURL } = await cdp.send('Browserless.liveURL', {
  interactable: false, // View-only mode
  quality: 80,         // Higher quality for monitoring
  timeout: 1800000,    // 30 minutes for extended monitoring
});

// Share with compliance team or supervisors
console.log('Monitoring URL (read-only):', liveURL);
```
  
  
**Playwright:**

```js
const { liveURL } = await cdpSession.send('Browserless.liveURL', {
  interactable: false, // View-only mode
  quality: 80,         // Higher quality for monitoring
  timeout: 1800000,    // 30 minutes for extended monitoring
});

// Share with compliance team or supervisors
console.log('Monitoring URL (read-only):', liveURL);
```
  

## Session Recording Integration

Combine screen recording with live sessions to capture an audit trail. Set `record=true` as a query parameter when connecting to the browser endpoint. See [Screen Recording](/baas/monitor-sessions/screen-recording) for full details.

  
**Puppeteer:**

```js
// Start recording before creating live session
await cdp.send('Browserless.startRecording');

const { liveURL } = await cdp.send('Browserless.liveURL', {
  timeout: 300000
});

// Wait for session completion
await new Promise(resolve => cdp.on('Browserless.liveComplete', resolve));

// Save recording (see recording docs for details)
const recording = await cdp.send('Browserless.stopRecording');
fs.writeFileSync('audit-trail.webm', Buffer.from(recording.value, 'binary'));
```
  
  
**Playwright:**

```js
// Start recording before creating live session
await cdpSession.send('Browserless.startRecording');

const { liveURL } = await cdpSession.send('Browserless.liveURL', {
  timeout: 300000
});

// Wait for session completion
await new Promise(resolve => cdpSession.on('Browserless.liveComplete', resolve));

// Save recording (see recording docs for details)
const recording = await cdpSession.send('Browserless.stopRecording');
fs.writeFileSync('audit-trail.webm', Buffer.from(recording.value, 'binary'));
```
  

## CAPTCHA Handling with Hybrid Fallback

Try automated CAPTCHA solving first, then fall back to a live URL for human intervention if the solver fails. Listen for the `Browserless.captchaFound` event to trigger this flow. For full CAPTCHA solver documentation, see [CAPTCHA Solving](/baas/bot-detection/captchas).

  
**Puppeteer:**

```js
// Set up CAPTCHA detection before navigation
cdp.on('Browserless.captchaFound', async () => {
  console.log('CAPTCHA detected, attempting automatic solve');

  const { solved } = await cdp.send('Browserless.solveCaptcha');

  if (!solved) {
    // Fall back to human intervention
    const { liveURL } = await cdp.send('Browserless.liveURL', {
      timeout: 120000 // 2 minutes for CAPTCHA solving
    });
    console.log('Manual CAPTCHA solving needed:', liveURL);
  }
});
```
  
  
**Playwright:**

```js
// Set up CAPTCHA detection before navigation
cdpSession.on('Browserless.captchaFound', async () => {
  console.log('CAPTCHA detected, attempting automatic solve');

  const { solved } = await cdpSession.send('Browserless.solveCaptcha');

  if (!solved) {
    // Fall back to human intervention
    const { liveURL } = await cdpSession.send('Browserless.liveURL', {
      timeout: 120000 // 2 minutes for CAPTCHA solving
    });
    console.log('Manual CAPTCHA solving needed:', liveURL);
  }
});
```
  

## Multi-Stage Workflows

Chain multiple hybrid sessions for complex business processes. Each stage alternates between automated steps and human handoffs.

`Browserless.liveComplete` means the link completed, not that the person finished the task: timeout, cancellation, or replacement can also trigger it. Verify each stage's expected page state before advancing. Viewer-driven completion waits about two seconds after the last viewer disconnects, allowing a quick reload without completing the link.

  
**Puppeteer:**

```js
const multiStageWorkflow = async () => {
  const browser = await puppeteer.connect({
    browserWSEndpoint: 'wss://production-sfo.browserless.io?token=YOUR_TOKEN'
  });

  try {
    const page = await browser.newPage();
    const cdp = await page.createCDPSession();

    await page.goto('https://practice.expandtesting.com/inputs');
    await page.waitForSelector('input[name="input-number"]');

    // Stage 1: User fills the number input via liveURL
    let { liveURL } = await cdp.send('Browserless.liveURL', {
      timeout: 180000 // 3 minutes for number entry
    });
    console.log('Stage 1 - Enter a number:', liveURL);
    await new Promise(resolve => cdp.on('Browserless.liveComplete', resolve));
    const numberEntered = await page.$eval(
      'input[name="input-number"]',
      input => input.value !== '' && input.checkValidity()
    );
    if (!numberEntered) {
      throw new Error('Stage 1 ended without a valid number');
    }

    // Stage 2: Automated filling of text and password fields
    await page.type('input[name="input-text"]', 'Automated text entry');
    await page.type('input[name="input-password"]', 'SecurePass123');
    console.log('Stage 2 - Automated text and password fields completed');

    // Stage 3: User fills the date input via liveURL
    ({ liveURL } = await cdp.send('Browserless.liveURL', {
      timeout: 300000 // 5 minutes for date selection
    }));
    console.log('Stage 3 - Select a date:', liveURL);
    await new Promise(resolve => cdp.on('Browserless.liveComplete', resolve));
    const dateEntered = await page.$eval(
      'input[name="input-date"]',
      input => input.value !== '' && input.checkValidity()
    );
    if (!dateEntered) {
      throw new Error('Stage 3 ended without a valid date');
    }

    console.log('Multi-stage workflow completed successfully');

  } finally {
    await browser.close();
  }
};
```
  
  
**Playwright:**

```js
const multiStageWorkflow = async () => {
  const browser = await playwright.chromium.connectOverCDP(
    'wss://production-sfo.browserless.io?token=YOUR_TOKEN'
  );

  try {
    const context = browser.contexts()[0];
    const page = await context.newPage();
    const cdpSession = await context.newCDPSession(page);

    await page.goto('https://practice.expandtesting.com/inputs');
    await page.waitForSelector('input[name="input-number"]');

    // Stage 1: User fills the number input via liveURL
    let { liveURL } = await cdpSession.send('Browserless.liveURL', {
      timeout: 180000 // 3 minutes for number entry
    });
    console.log('Stage 1 - Enter a number:', liveURL);
    await new Promise(resolve => cdpSession.on('Browserless.liveComplete', resolve));
    const numberEntered = await page.$eval(
      'input[name="input-number"]',
      input => input.value !== '' && input.checkValidity()
    );
    if (!numberEntered) {
      throw new Error('Stage 1 ended without a valid number');
    }

    // Stage 2: Automated filling of text and password fields
    await page.fill('input[name="input-text"]', 'Automated text entry');
    await page.fill('input[name="input-password"]', 'SecurePass123');
    console.log('Stage 2 - Automated text and password fields completed');

    // Stage 3: User fills the date input via liveURL
    ({ liveURL } = await cdpSession.send('Browserless.liveURL', {
      timeout: 300000 // 5 minutes for date selection
    }));
    console.log('Stage 3 - Select a date:', liveURL);
    await new Promise(resolve => cdpSession.on('Browserless.liveComplete', resolve));
    const dateEntered = await page.$eval(
      'input[name="input-date"]',
      input => input.value !== '' && input.checkValidity()
    );
    if (!dateEntered) {
      throw new Error('Stage 3 ended without a valid date');
    }

    console.log('Multi-stage workflow completed successfully');

  } finally {
    await browser.close();
  }
};
```
  

## Error Recovery and Resilience

Implement retry logic for production hybrid automation. This helper retries on transient failures but exits immediately on permanent errors like invalid tokens or rate limits.

```js

const createResilientLiveSession = async (cdp, options = {}) => {
  const { timeout = 300000, quality = 50, maxRetries = 2 } = options;

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const { liveURL, liveURLId } = await cdp.send('Browserless.liveURL', {
        timeout,
        quality,
        type: 'jpeg'
      });

      console.log(`Live session created on attempt ${attempt}`);
      return { liveURL, liveURLId };

    } catch (error) {
      console.log(`Live session attempt ${attempt} failed:`, error.message);

      // Don't retry on permanent errors
      if (error.message.includes('Invalid token') ||
          error.message.includes('Rate limit exceeded')) {
        throw error;
      }

      if (attempt === maxRetries) {
        throw new Error(`Failed to create live session after ${maxRetries} attempts: ${error.message}`);
      }

      // Brief wait before retry
      await new Promise(resolve => setTimeout(resolve, 1000));
    }
  }
};

(async () => {
  let browser = null;

  try {
    const BROWSERLESS_TOKEN = 'YOUR_API_TOKEN_HERE';

    browser = await puppeteer.connect({
      browserWSEndpoint: `wss://production-sfo.browserless.io?token=${BROWSERLESS_TOKEN}`,
    });

    const page = await browser.newPage();
    await page.goto('https://practicetestautomation.com/practice-test-login/');

    const cdp = await page.createCDPSession();

    const { liveURL, liveURLId } = await createResilientLiveSession(cdp, {
      timeout: 300000,
      quality: 50,
      maxRetries: 3
    });

    console.log('Live URL created:', liveURL);
    console.log('Live URL ID:', liveURLId);

    await new Promise((resolve) => {
      cdp.on('Browserless.liveComplete', resolve);
    });

    console.log('Live session completed');

  } catch (error) {
    console.error('Error:', error);
  } finally {
    if (browser) {
      await browser.close();
    }
  }
})();
```

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>403 Forbidden</code> error?</summary>

Your API token is missing or expired. Pass it as a `?token=` query parameter in the WebSocket or HTTP URL. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My script works locally but fails on Browserless</summary>

Local browser settings may differ from the Browserless environment. Use `launch` parameters to match your local setup (viewport, user agent, timezone). See [launch parameters](/baas/launch-options) for the full list.

</details>

## Next steps

  <a href="/baas/monitor-sessions/screen-recording" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Screen Recording</h3>
      Capture browser sessions as WebM video files with CDP-based start/stop controls
    
  </a>

  <a href="/baas/bot-detection/stealth" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Stealth Mode</h3>
      Reduce bot detection signals when combining hybrid automation with protected sites
    
  </a>

  <a href="/baas/bot-detection/proxies" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Proxy Configuration</h3>
      Configure residential and third-party proxy services for geographic targeting
    
  </a>

---

# Playwright Configurations

URL: https://docs.browserless.io/baas/advanced-configurations/playwright-customizations

This page covers advanced Playwright configurations for users already familiar with basic Playwright connections to Browserless. If you haven't set up your basic connection yet, start with our [Quickstart guide](/baas/quick-start).

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Playwright installed locally

Here are some advanced configurations you can explore:

### Advanced Connection Methods

#### Understanding Connection Types
The basic guide covers connectOverCDP as the recommended method. For detailed comparison of connection methods and when to use each, see our [Browser Versions documentation](/baas/versions#playwrights-versioning).

#### Multi-Browser Support
For Firefox and WebKit automation, you must use the connect method since CDP is Chrome-only. Complete examples and browser-specific configurations are available in our [Browser Versions guide](/baas/versions#playwrights-versioning).

### Performance Optimization

#### Package Optimization
Switch to playwright-core for production deployments to reduce bundle size:

```bash
npm install playwright-core
```

#### Error Handling and Retry Logic
Implement error handling and retry logic for production environments:

```js
async function withRetry(operation, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await operation();
    } catch (error) {
      if (attempt === maxRetries) throw error;

      console.log(`Attempt ${attempt} failed, retrying...`);
      await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
    }
  }
}
```

### Advanced Session Management

#### Timeout Configuration
For custom session timeouts and launch parameters, see our comprehensive [Launch parameters documentation](/baas/launch-options).

#### Browser Context Configuration
Advanced context options for stealth, geolocation, and viewport settings:

```js
const context = await browser.newContext({
  extraHTTPHeaders: {
    'User-Agent': 'Custom User Agent String'
  },
  viewport: { width: 1920, height: 1080 },
  deviceScaleFactor: 1,
  geolocation: { longitude: -122.4194, latitude: 37.7749 },
  permissions: ['geolocation']
});
```

> **Note**
> Advanced browser context options (stealth, geolocation, viewport, etc.) are only available when using `connectOverCDP`. These options are not supported with the standard `connect` method.

### Integration with Browserless Features

#### Stealth Mode Integration
For comprehensive bot detection bypass, see our [Stealth documentation](/baas/bot-detection/stealth). Basic stealth integration:

```js
const TOKEN = "YOUR_API_TOKEN_HERE";

const browser = await chromium.connectOverCDP(
  `wss://production-sfo.browserless.io/chromium/stealth?token=${TOKEN}`
);
```

#### Proxy Configuration
For built-in residential proxies and advanced proxy setups, see our [Proxies documentation](/baas/bot-detection/proxies). Context-level proxy configuration for third-party providers:

```js
const context = await browser.newContext({
  proxy: {
    server: "http://proxy-provider.com:8080",
    username: "username",
    password: "password"
  }
});
```

#### PuppeteerSharp Proxy Considerations

When using PuppeteerSharp with the `--proxy-server` flag in the WebSocket connection URL, calling `CreateBrowserContextAsync()` creates a new incognito context that does not inherit the proxy settings from the initial browser connection. As a result, requests will default to the Browserless IP instead of routing through your configured proxy.

To ensure the page correctly uses the proxy configured in the WebSocket connection URL, avoid creating a separate browser context. Instead of:

```csharp
var browserContext = await browser.CreateBrowserContextAsync();
var page = await browserContext.NewPageAsync();
```

Use this approach:

```csharp
var page = await browser.NewPageAsync();
```

This ensures the page inherits the proxy configuration from the WebSocket connection URL.

### Version Compatibility
Ensure your Playwright version matches Browserless supported versions. Check the [versions compatibility page](/baas/versions) for current supported versions.

## connect vs connectOverCDP

Playwright offers two ways to connect to Browserless. Each method uses a different protocol and unlocks a different feature set.

| Method | Playwright API | Browserless Path | Protocol |
|---|---|---|---|
| Playwright native | `playwright.chromium.connect({ wsEndpoint })` | `/chromium/playwright`, `/firefox/playwright`, `/webkit/playwright` | Playwright's own browser server protocol |
| CDP mode | `playwright.chromium.connectOverCDP(url)` | `/chromium`, `/chrome`, `/` | Chrome DevTools Protocol |

### Playwright Native (connect)

Connect with `playwright.chromium.connect()` and a `/playwright` path. This mode uses Playwright's native protocol.

```javascript

const browser = await chromium.connect(
  "wss://production-sfo.browserless.io/chromium/playwright?token=YOUR_API_TOKEN_HERE"
);
const page = await browser.newPage();
await page.goto("https://example.com");
await browser.close();
```

### CDP Mode (connectOverCDP)

Connect with `playwright.chromium.connectOverCDP()` and a CDP path. This mode bridges Playwright over the Chrome DevTools Protocol.

```javascript

const browser = await chromium.connectOverCDP(
  "wss://production-sfo.browserless.io/chromium?token=YOUR_API_TOKEN_HERE"
);
const context = browser.contexts()[0];
const page = await context.newPage();
await page.goto("https://example.com");
await browser.close();
```

### Feature Comparison

| Capability | Playwright Native (`connect`) | CDP Mode (`connectOverCDP`) |
|---|---|---|
| Browser extensions | Not supported | Supported (loaded in default context) |
| BQL interoperability | Not supported | Supported |
| Captcha solving | Not supported | Supported |
| `page.route()` network interception | Supported | Not supported (use CDP `Fetch` domain) |
| `APIRequestContext` | Supported | Not supported |
| Firefox / WebKit | Supported | Not supported (Chromium only) |
| Proxy via `browser.newContext()` | Supported | Not supported in default context |
| Proxy via query parameter | Supported | Supported |
| Multiple independent contexts | Supported | Limited (one default context from browser) |
| Connection overhead | Lower | Higher |

> **Default context**
> When you call `connectOverCDP`, the browser already has one context open. This is the **default context**. Access it with `browser.contexts()[0]`. Extensions, cookies, and launch-level settings live in this context. Calling `browser.newContext()` creates a separate context that does not inherit extensions or launch-level proxy configuration. Use the default context when you need access to those features.

### When to Use Each

**Use connect (Playwright native) when you need:**

- `page.route()` for request interception, mocking, or blocking
- `APIRequestContext` for HTTP requests that share browser state
- Firefox or WebKit browser engines
- Lower connection overhead for high-throughput workloads
- Multiple independent browser contexts with separate proxy configs

**Use connectOverCDP when you need:**

- Browser extensions loaded in the session
- BQL queries against the running browser
- Built-in captcha solving
- Interop with Puppeteer or other CDP-based tools in the same session

Default to CDP mode (`connectOverCDP`) for most automation tasks. It supports extensions, BQL, and captcha solving. Switch to Playwright native (`connect`) when you need `page.route()`, `APIRequestContext`, or non-Chromium browsers.

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>403 Forbidden</code> error?</summary>

Your API token is missing or expired. Pass it as a `?token=` query parameter in the WebSocket or HTTP URL. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My script works locally but fails on Browserless</summary>

Local browser settings may differ from the Browserless environment. Use `launch` parameters to match your local setup (viewport, user agent, timezone). See [launch parameters](/baas/launch-options) for the full list.

</details>

## Next steps

  <a href="/baas/session-management/standard-sessions" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Session Management</h3>
      Learn how to create and manage persistent browser sessions for complex workflows
    
  </a>

  <a href="/baas/bot-detection/stealth" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Stealth Mode</h3>
      Implement advanced bot detection bypass techniques for more reliable automation
    
  </a>

  <a href="/baas/bot-detection/proxies" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Proxy Configuration</h3>
      Set up residential and third-party proxies for enhanced privacy and geo-targeting
    
  </a>

---

# Puppeteer Configurations

URL: https://docs.browserless.io/baas/advanced-configurations/puppeteer-customizations

This page covers advanced Puppeteer configurations for users who have already established basic connections to Browserless. If you haven't set up a basic Puppeteer connection yet, start with our [Quickstart](/baas/quick-start) guide.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Puppeteer installed locally

Here are some advanced configurations you can explore:

### Advanced Monitoring and Debugging

#### CDP Performance Monitoring
Access Chrome DevTools Protocol to collect real-time browser performance metrics through direct CDP session communication:

```js
const client = await page.target().createCDPSession();
await client.send('Performance.enable');

// Get performance metrics
const metrics = await client.send('Performance.getMetrics');
console.log('Performance metrics:', metrics);
```

#### Network Request Monitoring
Implement comprehensive network activity tracking through Puppeteer's event-driven request/response lifecycle monitoring:

```js
page.on('request', request => {
  console.log('→', request.method(), request.url());
});

page.on('response', response => {
  // Check if response indicates a non-success status (outside 200-299)
  if (!response.ok()) {
    console.log('✗', response.status(), response.url());
  }
});

page.on('requestfailed', request => {
  // Log network-level failures: timeouts, DNS errors, connection refused
  console.log('✗ Failed:', request.url(), request.failure().errorText);
});
```

### Performance Optimization

#### Package Optimization
Switch to puppeteer-core for production deployments to reduce bundle size:

```bash
npm install puppeteer-core
```

For comprehensive performance optimization techniques including network round-trip reduction and navigation settings, see our [Best Practices guide](/baas/best-practices#reduce-network-round-trips).

### Advanced Launch Configuration
For complex Chrome flags and launch parameters using the JSON `launch` parameter approach, see the [Launch parameters documentation](/baas/launch-options#the-launch-object).

### Proxy Configuration and Cost Optimization
For comprehensive proxy configuration including residential proxies, geographic targeting, and bandwidth cost optimization, see [Proxies Documentation](/baas/bot-detection/proxies).

### Regional Optimization
For regional endpoint optimization and performance considerations, see [Best Practices](/baas/best-practices#avoid-network-latency).

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>403 Forbidden</code> error?</summary>

Your API token is missing or expired. Pass it as a `?token=` query parameter in the WebSocket or HTTP URL. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My script works locally but fails on Browserless</summary>

Local browser settings may differ from the Browserless environment. Use `launch` parameters to match your local setup (viewport, user agent, timezone). See [launch parameters](/baas/launch-options) for the full list.

</details>

## Next steps

  <a href="/baas/session-management/standard-sessions" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Session management</h3>
      persistent sessions and advanced session handling
    
  </a>
  <a href="/baas/bot-detection/proxies" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Proxy configuration</h3>
      residential proxies and geographic targeting
    
  </a>
  <a href="/baas/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      full parameter reference for Chrome flags and configurations
    
  </a>

---

# Best Practices

URL: https://docs.browserless.io/baas/best-practices

This page covers essential practices for creating reliable Puppeteer and Playwright scripts with Browserless. Follow these patterns to avoid common pitfalls, improve performance, and keep your automation code maintainable.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Puppeteer or Playwright installed locally

## Concurrency limits

Always close your browser sessions properly to avoid hitting concurrency limits:

  
**Puppeteer:**

```js
import puppeteer from "puppeteer-core";

const browser = await puppeteer.connect({
  browserWSEndpoint: `wss://production-sfo.browserless.io/?token=YOUR_API_TOKEN_HERE`,
});

try {
  const page = await browser.newPage();
  await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });

  // Your automation logic here

} catch (error) {
  console.error('Automation failed:', error.message);
} finally {
  // Always close the browser, even on errors
  if (browser.isConnected()) {
    await browser.close();
  }
}
```
  
  
**Playwright:**

```js
import playwright from "playwright";

const browser = await playwright.chromium.connectOverCDP(
  `wss://production-sfo.browserless.io/?token=YOUR_API_TOKEN_HERE`
);

try {
  const page = await browser.newPage();
  await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });

  // Your automation logic here

} catch (error) {
  console.error('Automation failed:', error.message);
} finally {
  // Always close the browser, even on errors
  if (browser.isConnected()) {
    await browser.close();
  }
}
```
  

### Concurrency limits by plan

| Plan | Concurrency (Monthly) | Concurrency (Yearly) |
|------|----------------------|---------------------|
| Free | 2 | 2 |
| Prototyping | 5 | 10 |
| Starter | 30 | 40 |
| Scale | 80 | 100 |
| Enterprise | Custom | Custom |

## Timeouts

### Session timeouts

Control the maximum duration of a browser session using the `timeout` parameter in your connection URL. The value is in milliseconds.

  
**Puppeteer:**

```js
import puppeteer from "puppeteer-core";

const browser = await puppeteer.connect({
  browserWSEndpoint: `wss://production-sfo.browserless.io/?token=YOUR_API_TOKEN_HERE&timeout=300000`,
  // 300000ms = 5 minutes
});
```
  
  
**Playwright:**

```python
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.connect_over_cdp(
        "wss://production-sfo.browserless.io/?token=YOUR_API_TOKEN_HERE&timeout=300000"
        # 300000ms = 5 minutes
    )
```
  

### Session duration limits

Maximum session duration depends on your subscription plan:

To increase your limit, see the [pricing page](https://www.browserless.io/pricing).

## Avoid network latency

Call the nearest regional endpoint to reduce network latency and improve performance. Using a geographically closer endpoint significantly reduces connection and data transfer time. For available regional endpoints and load balancing options, see [Load Balancers](/baas/infrastructure/load-balancers#cloud-subscriptions-load-balancing).

## Reduce Network Round-trips

Every `await` call (e.g. `page.click()`, `page.textContent()`) is a separate WebSocket round-trip between your client and the remote browser. Five sequential `await` calls means five round-trips, each adding network latency. Wrapping the same operations in a single `page.evaluate()` executes them all inside the browser in one round-trip, returning the results together. The difference scales linearly with the number of operations.

  
**Puppeteer:**

```js
// DON'T DO - Multiple round-trips
const button = await page.$('.buy-now');
const buttonText = await button.getProperty('innerText');
const isVisible = await button.isIntersectingViewport();
await button.click();

// DO - Single round-trip
const result = await page.evaluate(() => {
  const button = document.querySelector('.buy-now');
  const buttonText = button.innerText;
  const isVisible = button.offsetParent !== null;
  button.click();

  return { buttonText, isVisible, clicked: true };
});
```
  
  
**Playwright:**

```js
// DON'T DO - Multiple round-trips
const button = await page.locator('.buy-now');
const buttonText = await button.textContent();
const isVisible = await button.isVisible();
await button.click();

// DO - Single round-trip
const result = await page.evaluate(() => {
  const button = document.querySelector('.buy-now');
  const buttonText = button.textContent;
  const isVisible = button.offsetParent !== null;
  button.click();

  return { buttonText, isVisible, clicked: true };
});
```
  

## Wait for the page to be ready

When is your page fully ready? It depends on the site. Some pages are usable after the initial DOM load, while others need async data or scripts to finish executing. Here are the signals you can wait for:

- **Load events** (`commit`, `load`, `domcontentloaded`, `networkidle`)
- **Specific selectors** to appear or become visible
- **Network requests** to complete
- **Custom events** fired by the page

### Wait Until the Page has Loaded

The `waitUntil` parameter in `page.goto()` controls when navigation is considered complete. Choose based on what your target content requires:

| Option | Availability | Fires when | Best for |
|--------|-------------|-----------|----------|
| `commit` | Playwright only | Response headers are parsed and session history is updated, before the document starts loading | Fastest possible signal; useful when you only need to confirm navigation started |
| `domcontentloaded` | Puppeteer, Playwright | Document content is loaded and parsed | Fast pages, static content, SPAs with client-side rendering |
| `load` | Puppeteer, Playwright | Page executes scripts and loads resources like stylesheets and images | Pages where visual completeness matters |
| `networkidle2` | Puppeteer only | No more than 2 network connections for at least 500ms | SPAs, pages with async data fetching |
| `networkidle` / `networkidle0` | Playwright (`networkidle`) / Puppeteer (`networkidle0`) | Zero network connections for at least 500ms | Strictest check, slowest option |

Start with `domcontentloaded` and only move to `networkidle2`/`networkidle` if your target content requires it. Using `networkidle0`/`networkidle` on busy pages can cause unnecessary timeouts. Use `commit` (Playwright only) when you need the earliest possible signal that navigation occurred.

For a deeper dive, see our [waitUntil blog post](https://www.browserless.io/blog/waituntil-option-for-puppeteer-and-playwright).

  
**Puppeteer:**

```js
import puppeteer from "puppeteer-core";

const browser = await puppeteer.connect({
  browserWSEndpoint: `wss://production-sfo.browserless.io/?token=YOUR_API_TOKEN_HERE`,
});

const page = await browser.newPage();

// Fast navigation - use when you only need DOM content
await page.goto('https://example.com', {
  waitUntil: 'domcontentloaded',
  timeout: 30000
});

// Use networkidle2 only when you need all resources loaded
await page.goto('https://example.com', {
  waitUntil: 'networkidle2',
  timeout: 60000
});
```
  
  
**Playwright:**

```python
from playwright.async_api import async_playwright

async with async_playwright() as p:
    browser = await p.chromium.connect_over_cdp(
        "wss://production-sfo.browserless.io/?token=YOUR_API_TOKEN_HERE"
    )

    page = await browser.new_page()

    # Fast navigation - use when you only need DOM content
    await page.goto('https://example.com',
                    wait_until='domcontentloaded',
                    timeout=30000)

    # Use networkidle only when you need all resources loaded
    await page.goto('https://example.com',
                    wait_until='networkidle',
                    timeout=60000)
```
  

### Wait for Specific Selectors

Waiting for specific selectors is one of the most reliable ways to ensure your target content is ready. This approach works well for dynamic content that loads asynchronously.

  
**Puppeteer:**

```js
// Wait for a specific element to appear
await page.waitForSelector('.dynamic-content');

// Wait for element to be visible
await page.waitForSelector('.loading-spinner', { hidden: true });

// Wait for multiple elements
await page.waitForSelector('.product-list .product-item');
```
  
  
**Playwright:**

```python
# Wait for a specific element to appear
await page.wait_for_selector('.dynamic-content')

# Wait for element to be visible
await page.wait_for_selector('.loading-spinner', state='hidden')

# Wait for multiple elements
await page.wait_for_selector('.product-list .product-item')
```
  

### Wait for Network Requests

Sometimes you need to wait for specific network requests to complete before proceeding. This is useful when your target data comes from API calls.

  
**Puppeteer:**

```js
// Wait for a specific request to complete
await page.waitForResponse(response =>
  response.url().includes('/api/data') && response.status() === 200
);

// Wait for all requests to finish
await page.waitForLoadState('networkidle');

// Wait for multiple requests
await Promise.all([
  page.waitForResponse(response => response.url().includes('/api/users')),
  page.waitForResponse(response => response.url().includes('/api/posts'))
]);
```
  
  
**Playwright:**

```python
# Wait for a specific request to complete
async with page.expect_response(lambda response: '/api/data' in response.url and response.status == 200):
    pass

# Wait for all requests to finish
await page.wait_for_load_state('networkidle')

# Wait for multiple requests
await asyncio.gather(
    page.wait_for_response(lambda response: '/api/users' in response.url),
    page.wait_for_response(lambda response: '/api/posts' in response.url)
)
```
  

### Wait for Custom Events

Many modern web applications fire custom events when specific actions complete. You can listen for these events to know exactly when your target functionality is ready.

  
**Puppeteer:**

```js
// Wait for a custom event
await page.evaluate(() => {
  return new Promise((resolve) => {
    document.addEventListener('dataLoaded', resolve, { once: true });
  });
});

// Wait for multiple custom events
await Promise.all([
  page.evaluate(() => new Promise(resolve =>
    document.addEventListener('userDataReady', resolve, { once: true })
  )),
  page.evaluate(() => new Promise(resolve =>
    document.addEventListener('contentReady', resolve, { once: true })
  ))
]);
```
  
  
**Playwright:**

```python
# Wait for a custom event
await page.evaluate("""
  () => new Promise((resolve) => {
    document.addEventListener('dataLoaded', resolve, { once: true });
  })
""")

# Wait for multiple custom events
await asyncio.gather(
    page.evaluate("""
      () => new Promise(resolve =>
        document.addEventListener('userDataReady', resolve, { once: true })
      )
    """),
    page.evaluate("""
      () => new Promise(resolve =>
        document.addEventListener('contentReady', resolve, { once: true })
      )
    """)
)
```
  

## HTTP error codes

| Code | Cause | How to resolve |
|------|-------|----------------|
| **400** Bad Request | Malformed JSON, invalid fields, negative or out-of-range timeout, or colliding request arguments | Validate your payload structure and check for conflicting parameters |
| **401** Unauthorized | Missing or invalid API token, or endpoint not supported by your plan | Check that your token is correct and that your plan supports the endpoint |
| **403** Forbidden | Wrong regional endpoint or insufficient permissions. The deprecated `chrome.browserless.io` endpoint returns 403. Use `production-sfo.browserless.io` instead. | Verify you're using the correct regional endpoint for your account |
| **404** Not Found | Endpoint does not exist | Check your endpoint URL |
| **408** Request Timeout | Timeout set too low, waiting for a selector or event that never appears, or session exceeded plan limit | Increase timeout, verify your selectors exist, check session duration limits above |
| **429** Too Many Requests | Exceeding your plan's concurrency limit, usually from unclosed sessions accumulating | Close browser sessions in `finally` blocks; upgrade your plan if needed |

Failed requests appear as "Rejected" sessions in your [account dashboard](https://browserless.io/account/). To check the HTTP status code of a site navigated to by a REST API, look for the `x-response-code` and `x-response-status` response headers.

## Getting Help

If you continue to experience issues after implementing these best practices:

1. Check your [account dashboard](https://browserless.io/account/) for usage metrics
2. Contact [Browserless support](https://www.browserless.io/contact) for assistance

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>403 Forbidden</code> error?</summary>

Your API token is missing or expired. Pass it as a `?token=` query parameter in the WebSocket or HTTP URL. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My script works locally but fails on Browserless</summary>

Local browser settings may differ from the Browserless environment. Use `launch` parameters to match your local setup (viewport, user agent, timezone). See [launch parameters](/baas/launch-options) for the full list.

</details>

## Next steps

  <a href="/baas/versions" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Versions</h3>
      supported browser versions
    
  </a>
  <a href="/baas/infrastructure/load-balancers" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Load balancers</h3>
      regional endpoints and load balancing for lower latency
    
  </a>
  <a href="/baas/troubleshooting/whitelisting-ips" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Whitelisting IPs</h3>
      allowlist Browserless outbound IPs if your infrastructure restricts by IP
    
  </a>

---

# CAPTCHA Solving

URL: https://docs.browserless.io/baas/bot-detection/captchas

Browserless detects and solves CAPTCHAs programmatically using Chrome DevTools Protocol events and commands. Use [stealth routes](/baas/bot-detection/stealth) to prevent CAPTCHAs from appearing in the first place. This page covers what to do when they still appear.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Puppeteer or Playwright installed locally

## How it works

The CAPTCHA system operates through Chrome DevTools Protocol (CDP) events and commands:

- **Automatic CAPTCHA solving**: Add `solveCaptchas=true` to your connection URL to monitor and solve CAPTCHAs in real-time without manual CDP commands
- **Detection**: The `Browserless.captchaFound` event fires automatically when a CAPTCHA is detected
- **Solving**: The `Browserless.solveCaptcha` command programmatically solves detected CAPTCHAs

## Automatic CAPTCHA Solving

Add `solveCaptchas=true` to your connection URL. Browserless then monitors the entire session for CAPTCHAs and solves them as they appear, including on later navigations, popups, and async-loaded forms. Listeners you attach once at the start of the session keep working across every page, so there's no need to re-attach after navigation.

You only need to wait for `Browserless.captchaAutoSolved` at points where your next action depends on a solved CAPTCHA, typically before submitting a form. The solved token is automatically injected into the page DOM, so you don't need to read or pass it yourself.

The example below starts on the 2captcha homepage (no CAPTCHA there), then clicks through to a reCAPTCHA v2 demo page. The CAPTCHA only appears after the second navigation, but because the listener was attached at the start of the session, it catches the auto-solve event without any re-attachment.

  
**Puppeteer:**

```js
import puppeteer from "puppeteer-core";

const TOKEN = "YOUR_API_TOKEN_HERE";

const browser = await puppeteer.connect({
  browserWSEndpoint: `wss://production-sfo.browserless.io/stealth?token=${TOKEN}&proxy=residential&proxyCountry=us&solveCaptchas=true&timeout=300000`,
});

const page = await browser.newPage();
const cdp = await page.createCDPSession();

// Attach BEFORE navigation — the listener survives the whole session.
const captchaSolved = new Promise((resolve) => {
  cdp.on("Browserless.captchaAutoSolved", resolve);
});

// No CAPTCHA on the homepage.
await page.goto("https://2captcha.com/", { waitUntil: "networkidle0" });

// Clicking through navigates to the reCAPTCHA v2 demo — a CAPTCHA appears here.
await page.click(
  "#order-captchas > table > tbody > tr:nth-child(2) span > a:nth-child(2)"
);

// Wait for autosolve to finish before submitting.
const { solved, token, time } = await captchaSolved;
console.log({ solved, token, time });

await page.click("._actionsItem_151cx_41");
await browser.close();
```
  
  
**Playwright:**

    
      
**Javascript:**

```js
import playwright from "playwright-core";

const TOKEN = "YOUR_API_TOKEN_HERE";

const browser = await playwright.chromium.connectOverCDP(
  `wss://production-sfo.browserless.io/stealth?token=${TOKEN}&proxy=residential&proxyCountry=us&solveCaptchas=true&timeout=300000`
);

// Reuse the existing context and page so Browserless CDP events are visible.
const context = browser.contexts()[0];
const page = context.pages()[0];
const cdp = await page.context().newCDPSession(page);

// Attach BEFORE navigation — the listener survives the whole session.
const captchaSolved = new Promise((resolve) => {
  cdp.on("Browserless.captchaAutoSolved", resolve);
});

await page.goto("https://2captcha.com/", { waitUntil: "networkidle" });

await page.click(
  "#order-captchas > table > tbody > tr:nth-child(2) span > a:nth-child(2)"
);

const result = await captchaSolved;
console.log(result);

await page.click("._actionsItem_151cx_41");
await browser.close();
```
      
      
**Python:**

```python
import asyncio
from playwright.async_api import async_playwright

TOKEN = "YOUR_API_TOKEN_HERE"

async def main():
    async with async_playwright() as p:
        browser = await p.chromium.connect_over_cdp(
            f"wss://production-sfo.browserless.io/stealth?token={TOKEN}&proxy=residential&proxyCountry=us&solveCaptchas=true&timeout=300000"
        )

        # Reuse the existing context and page so Browserless CDP events are visible.
        context = browser.contexts[0]
        page = context.pages[0]
        cdp = await page.context.new_cdp_session(page)

        # Attach BEFORE navigation — the listener survives the whole session.
        captcha_solved = asyncio.get_event_loop().create_future()
        cdp.on(
            "Browserless.captchaAutoSolved",
            lambda event: captcha_solved.set_result(event) if not captcha_solved.done() else None,
        )

        await page.goto("https://2captcha.com/", wait_until="networkidle")

        await page.click(
            "#order-captchas > table > tbody > tr:nth-child(2) span > a:nth-child(2)"
        )

        result = await captcha_solved
        print(result)

        await page.click("._actionsItem_151cx_41")
        await browser.close()

asyncio.run(main())
```
      
      
**Java:**

```java
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.*;

import java.util.concurrent.CompletableFuture;

public class AutoCaptchaSolving {
    public static void main(String[] args) throws Exception {
        String TOKEN = "YOUR_API_TOKEN_HERE";
        String endpoint = "wss://production-sfo.browserless.io/stealth?token=" + TOKEN
            + "&proxy=residential&proxyCountry=us&solveCaptchas=true&timeout=300000";

        try (Playwright playwright = Playwright.create()) {
            Browser browser = playwright.chromium().connectOverCDP(endpoint);

            // Reuse the existing context and page so Browserless CDP events are visible.
            BrowserContext context = browser.contexts().get(0);
            Page page = context.pages().get(0);
            CDPSession cdp = page.context().newCDPSession(page);

            // Attach BEFORE navigation — the listener survives the whole session.
            CompletableFuture captchaSolved = new CompletableFuture<>();
            cdp.on("Browserless.captchaAutoSolved", event -> captchaSolved.complete(event));

            page.navigate("https://2captcha.com/", new Page.NavigateOptions()
                .setWaitUntil(LoadState.NETWORKIDLE));

            page.click("#order-captchas > table > tbody > tr:nth-child(2) span > a:nth-child(2)");

            System.out.println(captchaSolved.get());

            page.click("._actionsItem_151cx_41");
            browser.close();
        }
    }
}
```
      
      
**C#:**

```csharp
using System;
using System.Threading.Tasks;
using Microsoft.Playwright;

class Program
{
    static async Task Main(string[] args)
    {
        var TOKEN = "YOUR_API_TOKEN_HERE";
        var endpoint = $"wss://production-sfo.browserless.io/stealth?token={TOKEN}&proxy=residential&proxyCountry=us&solveCaptchas=true&timeout=300000";

        var playwright = await Playwright.CreateAsync();
        var browser = await playwright.Chromium.ConnectOverCDPAsync(endpoint);

        // Use the first context and page so Browserless CDP events are visible.
        var context = browser.Contexts[0];
        var page = context.Pages[0];
        var cdp = await page.Context.NewCDPSessionAsync(page);

        // Attach BEFORE navigation — the listener survives the whole session.
        var captchaSolved = new TaskCompletionSource<object>();
        cdp.On("Browserless.captchaAutoSolved", e => captchaSolved.TrySetResult(e));

        await page.GotoAsync("https://2captcha.com/", new PageGotoOptions
        {
            WaitUntil = WaitUntilState.NetworkIdle
        });

        await page.ClickAsync("#order-captchas > table > tbody > tr:nth-child(2) span > a:nth-child(2)");

        Console.WriteLine(await captchaSolved.Task);

        await page.ClickAsync("._actionsItem_151cx_41");
        await browser.CloseAsync();
    }
}
```
      
    
  

### Across multiple navigations

Because `solveCaptchas=true` runs for the lifetime of the connection, a single `cdp.on("Browserless.captchaAutoSolved", ...)` listener handles every CAPTCHA encountered on every subsequent page in the session. Use `cdp.on` (not `cdp.once`) when you expect more than one CAPTCHA. There's no need to re-attach after navigations, popups, or reloads. Re-create the awaitable promise each time you need to gate the next action on a fresh solve.

> **Pages without a CAPTCHA**
> If you `await` the autosolve event on a page that turns out to have no CAPTCHA, the promise will never resolve. Wrap the wait in a timeout (for example, `Promise.race` against a `setTimeout`) so your script can continue when nothing fires.

## CAPTCHA Detection

The `Browserless.captchaFound` CDP event fires when Browserless detects a CAPTCHA on the page. Set up a listener before navigating so you don't miss the event. `captchaFound` can fire during `page.goto()` (often before it resolves), so attaching afterwards will sometimes drop the event.

The event payload includes a `status` field that distinguishes the two modes:

- With `solveCaptchas=true` on the connection URL, `captchaFound` fires with `{ status: "solving" }` — Browserless is already handling it and a `captchaAutoSolved` event will follow.
- In manual mode (without `solveCaptchas=true`), it fires with `{ status: "found" }` — your code is expected to call `Browserless.solveCaptcha` to trigger solving.

### Passive Detection with Event Listeners

Set up automatic CAPTCHA detection using CDP event listeners. The system monitors network traffic patterns and automatically emits events when CAPTCHA services are detected:

  
**Puppeteer:**

```javascript
const cdp = await page.createCDPSession();
await new Promise((resolve) =>
  cdp.on("Browserless.captchaFound", () => {
    console.log("Found a captcha!");
    return resolve();
  })
);
```

  
  
**Playwright:**

    
      
**Javascript:**

```javascript
const cdp = await page.context().newCDPSession(page);

await new Promise((resolve) =>
  cdp.on("Browserless.captchaFound", () => {
    console.log("Found a captcha!");
    return resolve();
  })
);
```

      
      
**Python:**

```python
cdp = await page.context.new_cdp_session(page)

async def handle_captcha_found(event):
    print("Found a captcha!")
    return

cdp.on("Browserless.captchaFound", handle_captcha_found)

# Wait for the event to resolve
await asyncio.Future()
```

      
      
**Java:**

```java
CDPSession cdp = page.context().newCDPSession(page);

cdp.addListener("Browserless.captchaFound", event -> {
    System.out.println("Found a captcha!");
    // You can add additional handling logic here
});
```

      
      
**C#:**

```csharp
var cdp = await page.Context.NewCDPSessionAsync(page);

cdp.On("Browserless.captchaFound", e =>
{
    Console.WriteLine("Found a captcha!");
    // Additional logic can be added here
});
```

      
    
  

### Detection Mechanism

The system automatically detects CAPTCHAs by monitoring network requests and matching against known CAPTCHA service patterns. When a CAPTCHA is detected, the `Browserless.captchaFound` event is emitted to all active CDP sessions for that page.

> **Playwright and CDP**
> Playwright uses its own browser protocols by default. To use Browserless CDP events, connect over CDP and reuse the existing context and page instead of creating new ones. See the full example below.

Here are complete scripts demonstrating CAPTCHA detection and solving in context:

  
**Puppeteer:**

```js
import puppeteer from "puppeteer-core";

const waitForCaptcha = (cdpSession) => {
  return new Promise((resolve) =>
    cdpSession.on("Browserless.captchaFound", resolve)
  );
};

const browserWSEndpoint =
  "wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE&timeout=300000";

try {
  const browser = await puppeteer.connect({ browserWSEndpoint });

  const page = await browser.newPage();
  const cdp = await page.createCDPSession();

  // Attach BEFORE navigation — captchaFound can fire during page.goto.
  const captchaFound = waitForCaptcha(cdp);

  await page.goto("https://www.google.com/recaptcha/api2/demo", {
    waitUntil: "networkidle0",
  });

  await captchaFound;
  console.log("Captcha found!");

  const { solved, error } = await cdp.send("Browserless.solveCaptcha");
  console.log({ solved, error });

  // Continue...
  await page.click("#recaptcha-demo-submit");
  await browser.close();
} catch (e) {
  console.error("There was a big error :(", e);
  process.exit(1);
}
```
  
  
**Playwright:**

    
      
**Javascript:**

```js
import playwright from "playwright-core";

const waitForCaptcha = (cdpSession) => {
  return new Promise((resolve) =>
    cdpSession.on("Browserless.captchaFound", resolve)
  );
};
const pwEndpoint = `wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE`;

try {
  const browser = await playwright.chromium.connectOverCDP(pwEndpoint);
  // 👇 Reuse the existing context and page
  const context = browser.contexts()[0];
  const page = context.pages()[0];
  const cdp = await page.context().newCDPSession(page);

  // Attach BEFORE navigation — captchaFound can fire during page.goto.
  const captchaFound = waitForCaptcha(cdp);

  await page.goto("https://www.google.com/recaptcha/api2/demo", {
    waitUntil: "networkidle0",
  });

  await captchaFound;
  console.log("Captcha found!");

  const { solved, error } = await cdp.send("Browserless.solveCaptcha");

  console.log({ solved, error });

  // Continue...
  await page.click("#recaptcha-demo-submit");
  await browser.close();
} catch (e) {
  console.error("There was a big error :(", e);
  process.exit(1);
}
```
      
      
**Python:**

```python
import asyncio
from playwright.async_api import async_playwright

async def main():
    pw_endpoint = "wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE"
    async with async_playwright() as p:
        try:
            # Connect to the browser
            browser = await p.chromium.connect_over_cdp(pw_endpoint)

            # Use the first context and page
            context = browser.contexts[0]
            page = context.pages[0]
            cdp = await page.context.new_cdp_session(page)

            # Attach BEFORE navigation — captchaFound can fire during page.goto.
            captcha_found = asyncio.get_event_loop().create_future()
            cdp.on(
                "Browserless.captchaFound",
                lambda event: captcha_found.set_result(event) if not captcha_found.done() else None,
            )

            # Navigate to the captcha demo page
            await page.goto("https://www.google.com/recaptcha/api2/demo", wait_until="networkidle")

            await captcha_found
            print("Captcha found!")

            # Solve the captcha
            result = await cdp.send("Browserless.solveCaptcha")
            solved, error = result.get("solved"), result.get("error")
            print({"solved": solved, "error": error})

            # Continue after solving captcha
            await page.click("#recaptcha-demo-submit")
            await browser.close()
        except Exception as e:
            print("There was a big error :(", e)

asyncio.run(main())

```
      
      
**Java:**

```java
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.*;

import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class PlaywrightCaptchaExample {
    public static void main(String[] args) {
        String pwEndpoint = "wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE";

        try (Playwright playwright = Playwright.create()) {
            Browser browser = playwright.chromium().connectOverCDP(pwEndpoint);

            // Reuse the existing context and page
            BrowserContext context = browser.contexts().get(0);
            Page page = context.pages().get(0);
            CDPSession cdp = page.context().newCDPSession(page);

            // Attach BEFORE navigation — captchaFound can fire during page.navigate.
            CompletableFuture captchaFound = new CompletableFuture<>();
            cdp.on("Browserless.captchaFound", event -> {
                System.out.println("Captcha found!");
                captchaFound.complete(null);
            });

            // Navigate to the captcha demo page
            page.navigate("https://www.google.com/recaptcha/api2/demo", new Page.NavigateOptions()
                .setWaitUntil(LoadState.NETWORKIDLE));

            captchaFound.get(); // Wait for the event

            // Solve the captcha
            Map result = cdp.send("Browserless.solveCaptcha");
            System.out.println("Result: " + result);

            // Continue after solving captcha
            page.click("#recaptcha-demo-submit");
            browser.close();
        } catch (ExecutionException | InterruptedException e) {
            System.err.println("There was a big error :(" + e.getMessage());
            e.printStackTrace();
        }
    }
}
```
      
      
**C#:**

```csharp
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Playwright;

class Program
{
    static async Task Main(string[] args)
    {
        var pwEndpoint = "wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE";

        try
        {
            var playwright = await Playwright.CreateAsync();
            var browser = await playwright.Chromium.ConnectOverCDPAsync(pwEndpoint);

            // Use the first context and page
            var context = browser.Contexts[0];
            var page = context.Pages[0];
            var cdp = await page.Context.NewCDPSessionAsync(page);

            // Attach BEFORE navigation — captchaFound can fire during page.GotoAsync.
            var captchaFound = new TaskCompletionSource<bool>();
            cdp.On("Browserless.captchaFound", _ =>
            {
                Console.WriteLine("Captcha found!");
                captchaFound.TrySetResult(true);
            });

            // Navigate to the captcha demo page
            await page.GotoAsync("https://www.google.com/recaptcha/api2/demo", new PageGotoOptions
            {
                WaitUntil = WaitUntilState.NetworkIdle
            });

            await captchaFound.Task;

            // Solve the captcha
            var result = await cdp.SendAsync>("Browserless.solveCaptcha");
            Console.WriteLine($"Solved: {result["solved"]}, Error: {result["error"]}");

            // Continue after solving captcha
            await page.ClickAsync("#recaptcha-demo-submit");
            await browser.CloseAsync();
        }
        catch (Exception e)
        {
            Console.WriteLine("There was a big error :(");
            Console.WriteLine(e.Message);
        }
    }
}
```
      
    
  

## CAPTCHA Solving

Use this approach when you need programmatic control over when CAPTCHAs are solved. Set up a `Browserless.captchaFound` event listener, then call `Browserless.solveCaptcha` to trigger solving on demand.

### Programmatic Solving

Once a CAPTCHA is detected, use the `Browserless.solveCaptcha` command to solve it:

  
**Puppeteer:**

```js
const cdp = await page.createCDPSession();
const { solved, error } = await cdp.send("Browserless.solveCaptcha");
console.log({
  solved,
  error,
});
```
  
  
**Playwright:**

    
      
**Javascript:**

```js
const cdp = await page.context().newCDPSession(page);

const { solved, error } = await cdp.send("Browserless.solveCaptcha");
console.log({
  solved,
  error,
});
```
      
      
**Python:**

```python
cdp = await page.context.new_cdp_session(page)

result = await cdp.send("Browserless.solveCaptcha")
solved = result.get("solved")
error = result.get("error")

print({
    "solved": solved,
    "error": error,
})
```
      
      
**Java:**

```java
CDPSession cdp = page.context().newCDPSession(page);

Map result = cdp.send("Browserless.solveCaptcha");
Boolean solved = (Boolean) result.get("solved");
String error = (String) result.get("error");

System.out.println(Map.of(
    "solved", solved,
    "error", error
));
```
      
      
**C#:**

```csharp
var cdp = await page.Context.NewCDPSessionAsync(page);

var result = await cdp.SendAsync>("Browserless.solveCaptcha");
var solved = result.ContainsKey("solved") ? result["solved"] : null;
var error = result.ContainsKey("error") ? result["error"] : null;

Console.WriteLine(new {
    solved,
    error
});
```
      
    
  

> **Playwright and CDP**
> Playwright uses its own browser protocols by default. Connect over CDP and reuse the existing context and page instead of creating new ones.

Here are complete scripts demonstrating CAPTCHA detection and solving:

  
**Puppeteer:**

```js
import puppeteer from "puppeteer-core";

const waitForCaptcha = (cdpSession) => {
  return new Promise((resolve) =>
    cdpSession.on("Browserless.captchaFound", resolve)
  );
};

const browserWSEndpoint =
  "wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE&timeout=300000";

try {
  const browser = await puppeteer.connect({ browserWSEndpoint });

  const page = await browser.newPage();
  const cdp = await page.createCDPSession();

  // Attach BEFORE navigation — captchaFound can fire during page.goto.
  const captchaFound = waitForCaptcha(cdp);

  await page.goto("https://www.google.com/recaptcha/api2/demo", {
    waitUntil: "networkidle0",
  });

  await captchaFound;
  console.log("Captcha found!");

  const { solved, error } = await cdp.send("Browserless.solveCaptcha");
  console.log({ solved, error });

  // Continue...
  await page.click("#recaptcha-demo-submit");
  await browser.close();
} catch (e) {
  console.error("There was a big error :(", e);
  process.exit(1);
}
```
  
  
**Playwright:**

    
      
**Javascript:**

```js
import playwright from "playwright-core";

const waitForCaptcha = (cdpSession) => {
  return new Promise((resolve) =>
    cdpSession.on("Browserless.captchaFound", resolve)
  );
};
const pwEndpoint = `wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE`;

try {
  const browser = await playwright.chromium.connectOverCDP(pwEndpoint);
  // 👇 Reuse the existing context and page
  const context = browser.contexts()[0];
  const page = context.pages()[0];
  const cdp = await page.context().newCDPSession(page);

  // Attach BEFORE navigation — captchaFound can fire during page.goto.
  const captchaFound = waitForCaptcha(cdp);

  await page.goto("https://www.google.com/recaptcha/api2/demo", {
    waitUntil: "networkidle0",
  });

  await captchaFound;
  console.log("Captcha found!");

  const { solved, error } = await cdp.send("Browserless.solveCaptcha");

  console.log({ solved, error });

  // Continue...
  await page.click("#recaptcha-demo-submit");
  await browser.close();
} catch (e) {
  console.error("There was a big error :(", e);
  process.exit(1);
}
```
  
  
**Python:**

```python
import asyncio
from playwright.async_api import async_playwright

async def main():
    pw_endpoint = "wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE"
    async with async_playwright() as p:
        try:
            # Connect to the browser
            browser = await p.chromium.connect_over_cdp(pw_endpoint)

            # Use the first context and page
            context = browser.contexts[0]
            page = context.pages[0]
            cdp = await page.context.new_cdp_session(page)

            # Attach BEFORE navigation — captchaFound can fire during page.goto.
            captcha_found = asyncio.get_event_loop().create_future()
            cdp.on(
                "Browserless.captchaFound",
                lambda event: captcha_found.set_result(event) if not captcha_found.done() else None,
            )

            # Navigate to the captcha demo page
            await page.goto("https://www.google.com/recaptcha/api2/demo", wait_until="networkidle")

            await captcha_found
            print("Captcha found!")

            # Solve the captcha
            result = await cdp.send("Browserless.solveCaptcha")
            solved, error = result.get("solved"), result.get("error")
            print({"solved": solved, "error": error})

            # Continue after solving captcha
            await page.click("#recaptcha-demo-submit")
            await browser.close()
        except Exception as e:
            print("There was a big error :(", e)

asyncio.run(main())

```
  
  
**Java:**

```java
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.*;

import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class PlaywrightCaptchaExample {
    public static void main(String[] args) {
        String pwEndpoint = "wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE";

        try (Playwright playwright = Playwright.create()) {
            Browser browser = playwright.chromium().connectOverCDP(pwEndpoint);

            // Reuse the existing context and page
            BrowserContext context = browser.contexts().get(0);
            Page page = context.pages().get(0);
            CDPSession cdp = page.context().newCDPSession(page);

            // Attach BEFORE navigation — captchaFound can fire during page.navigate.
            CompletableFuture captchaFound = new CompletableFuture<>();
            cdp.on("Browserless.captchaFound", event -> {
                System.out.println("Captcha found!");
                captchaFound.complete(null);
            });

            // Navigate to the captcha demo page
            page.navigate("https://www.google.com/recaptcha/api2/demo", new Page.NavigateOptions()
                .setWaitUntil(LoadState.NETWORKIDLE));

            captchaFound.get(); // Wait for the event

            // Solve the captcha
            Map result = cdp.send("Browserless.solveCaptcha");
            System.out.println("Result: " + result);

            // Continue after solving captcha
            page.click("#recaptcha-demo-submit");
            browser.close();
        } catch (ExecutionException | InterruptedException e) {
            System.err.println("There was a big error :(" + e.getMessage());
            e.printStackTrace();
        }
    }
}
```
  
  
**C#:**

```csharp
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Playwright;

class Program
{
    static async Task Main(string[] args)
    {
        var pwEndpoint = "wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE";

        try
        {
            var playwright = await Playwright.CreateAsync();
            var browser = await playwright.Chromium.ConnectOverCDPAsync(pwEndpoint);

            // Use the first context and page
            var context = browser.Contexts[0];
            var page = context.Pages[0];
            var cdp = await page.Context.NewCDPSessionAsync(page);

            // Attach BEFORE navigation — captchaFound can fire during page.GotoAsync.
            var captchaFound = new TaskCompletionSource<bool>();
            cdp.On("Browserless.captchaFound", _ =>
            {
                Console.WriteLine("Captcha found!");
                captchaFound.TrySetResult(true);
            });

            // Navigate to the captcha demo page
            await page.GotoAsync("https://www.google.com/recaptcha/api2/demo", new PageGotoOptions
            {
                WaitUntil = WaitUntilState.NetworkIdle
            });

            await captchaFound.Task;

            // Solve the captcha
            var result = await cdp.SendAsync>("Browserless.solveCaptcha");
            Console.WriteLine($"Solved: {result["solved"]}, Error: {result["error"]}");

            // Continue after solving captcha
            await page.ClickAsync("#recaptcha-demo-submit");
            await browser.CloseAsync();
        }
        catch (Exception e)
        {
            Console.WriteLine("There was a big error :(");
            Console.WriteLine(e.Message);
        }
    }
}
```
      
    
  

### Response Fields

The `solveCaptcha` response includes information about the solving attempt:

- **found**: Whether a CAPTCHA was detected on the page
- **solved**: Whether the CAPTCHA was successfully solved
- **time**: Time taken to solve the CAPTCHA, in milliseconds
- **token**: The solved CAPTCHA token, if available
- **error**: Any errors during execution

## Integration with Live Sessions

CAPTCHA solving integrates with Browserless live URL sessions for [hybrid automation workflows](/baas/monitor-sessions/hybrid-automation). This lets a human step in when automated solving fails, providing a fallback for complex CAPTCHAs.

## Performance Considerations

- CAPTCHA solving can take several seconds to minutes. Adjust timeouts accordingly.
- Each successful CAPTCHA solve costs 10 units; attempts that don't clear aren't charged (see your [account dashboard](https://account.browserless.io) for unit details)
- Use stealth features and residential proxies to reduce CAPTCHA frequency

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>403 Forbidden</code> error?</summary>

Your API token is missing or expired. Pass it as a `?token=` query parameter in the WebSocket or HTTP URL. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My script works locally but fails on Browserless</summary>

Local browser settings may differ from the Browserless environment. Use `launch` parameters to match your local setup (viewport, user agent, timezone). See [launch parameters](/baas/launch-options) for the full list.

</details>

## Next steps

  <a href="/baas/bot-detection/stealth" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Stealth Routes</h3>
      Prevent CAPTCHAs from appearing in the first place with advanced fingerprint mitigations
    
  </a>

  <a href="/baas/bot-detection/unblocking" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Site Unblocking</h3>
      Bypass bot detection entirely before CAPTCHAs appear
    
  </a>

  <a href="/baas/monitor-sessions/hybrid-automation" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Hybrid Automation</h3>
      Combine automated CAPTCHA solving with human intervention for complex scenarios
    
  </a>

---

# OS emulation

URL: https://docs.browserless.io/baas/bot-detection/os-emulation

Browserless sessions run on Linux, so they expose a Linux fingerprint by default. Bot detection systems cross-check signals like the User-Agent, `navigator.platform`, UA Client Hints, GPU renderer, and media devices, and flag sessions where these don't line up. The `emulationOs` query parameter switches the session to a coherent operating system identity — set it to `windows` or `macos` to move off the default Linux fingerprint.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

`emulationOs` is a URL query parameter you add to the endpoint. It works on stealth routes (`/stealth`, `/chromium/stealth`, `/chrome/stealth`), on the unblock and scrape REST APIs (`/unblock`, `/chromium/unblock`, `/chrome/unblock`, `/smart-scrape`), on BQL routes, and on the Agent route (`/chromium/agent`), on any [regional endpoint](/baas/connection-url-patterns#regional-endpoints). It is not a header, a launch option, or a mutation. Anywhere else, including plain `/chromium` or `/chrome` routes, it has no effect: the parameter is ignored and the session runs with its default identity.

## Accepted values

| Value | Emulated identity |
|-------|-------------------|
| `windows` | Windows / Chrome on Win64 |
| `macos` | macOS / Chrome on Intel Mac |
| `linux` | Native Linux identity (the default when omitted) |
| `android` | Mobile Chrome on Android — see [Android (mobile) emulation](#android-mobile-emulation) |

Desktop `windows`, `macos`, and `linux` and mobile `android` identities are supported. iPhone, iOS, and Safari are not. Values are exact-match and lowercase: any other value, including a typo or wrong casing like `macOS`, is silently ignored. The session behaves as if `emulationOs` wasn't set, so double-check the spelling, because there's no error to catch.

## What gets spoofed

When `emulationOs` is set, the session presents one internally consistent set of signals for the target OS:

- **User-Agent** (e.g. `Windows NT 10.0; Win64; x64`)
- **UA Client Hints** — `Sec-CH-UA-Platform`, `Sec-CH-UA-Platform-Version`, `getHighEntropyValues()`
- **`navigator.platform`** — `Win32`, `MacIntel`, or `Linux x86_64`
- **GPU / WebGL renderer** and **CPU core count** (`navigator.hardwareConcurrency`) for the platform
- **Media devices, speech-synthesis voices, and AudioContext latency** for the OS

These signals stay consistent for the session's lifetime, including across navigations and new windows.

## What it doesn't change

`emulationOs` only sets OS-derived browser signals. What stays untouched depends on the identity.

### Desktop identities

For `windows`, `macos`, and `linux`, `emulationOs` does **not** change:

- **Timezone, locale, and `Accept-Language`** — these follow your [proxy](/baas/bot-detection/proxies)'s location, not the emulated OS.
- **Screen and viewport size** — set these on the client (`defaultViewport` / `setViewport`).
- **WebRTC handling** — applied uniformly to every stealth session.

### Android emulation

`emulationOs=android` does set screen and viewport size: it applies a high-DPI mobile viewport (see [Android (mobile) emulation](#android-mobile-emulation)). Everything else stays the same as desktop. It does **not** change:

- **Timezone, locale, and `Accept-Language`** — these follow your proxy's location, not the emulated OS.
- **WebRTC handling** — applied uniformly to every stealth session.

An emulated identity served with a mismatched timezone or locale is itself a detection signal, so pair `emulationOs` with a region-matched proxy for a fully coherent profile.

## Usage with stealth routes

Stealth routes (`/stealth`, `/chromium/stealth`, `/chrome/stealth`) already run the full stealth stack. Adding `emulationOs` shifts the whole identity to the target OS:

  
**Puppeteer:**

```js
import puppeteer from "puppeteer-core";

const TOKEN = "YOUR_API_TOKEN_HERE";

const browser = await puppeteer.connect({
  browserWSEndpoint:
    `wss://production-sfo.browserless.io/stealth?token=${TOKEN}&emulationOs=windows`,
});

const page = await browser.newPage();
const platform = await page.evaluate(() => navigator.platform);
console.log(platform); // "Win32"

await browser.close();
```
  
  
**Playwright:**

    
      
**Javascript:**

```js
import { chromium } from "playwright-core";

const TOKEN = "YOUR_API_TOKEN_HERE";

const browser = await chromium.connectOverCDP(
  `wss://production-sfo.browserless.io/stealth?token=${TOKEN}&emulationOs=windows`
);

const context = browser.contexts()[0] || (await browser.newContext());
const page = await context.newPage();
const platform = await page.evaluate(() => navigator.platform);
console.log(platform); // "Win32"

await browser.close();
```
      
      
**Python:**

```python
import asyncio
from playwright.async_api import async_playwright

TOKEN = "YOUR_API_TOKEN_HERE"

async def main():
    async with async_playwright() as p:
        browser = await p.chromium.connect_over_cdp(
            f"wss://production-sfo.browserless.io/stealth?token={TOKEN}&emulationOs=windows"
        )

        context = browser.contexts[0] if browser.contexts else await browser.new_context()
        page = await context.new_page()
        platform = await page.evaluate("navigator.platform")
        print(platform)  # "Win32"

        await browser.close()

asyncio.run(main())
```
      
    
  

With `emulationOs=windows` or `macos`, the session's User-Agent is authoritative: any `--user-agent` you pass in launch options is overridden to keep the identity coherent. To control the User-Agent yourself, omit `emulationOs` or use BrowserQL's `userAgent()` mutation.

## Usage on BQL routes

BQL routes (`/chromium/bql`, `/chrome/bql`, `/stealth/bql`) accept `emulationOs` the same way, and all of them run stealth-hardened. See [OS emulation in BrowserQL](/browserql/bot-detection/os-emulation) for BQL-specific usage.

## Android (mobile) emulation

Set `emulationOs=android` to make the session present as mobile Chrome on Android instead of a desktop OS. Add `emulatedDevice=<slug>` to pick a specific phone, or omit it and Browserless picks a real device for you and keeps it consistent for the whole session. Android emulation runs on the Chromium and Brave stealth engines. iPhone, iOS, and Safari are not supported.

### Enabling Android emulation

`emulationOs` and `emulatedDevice` are connection query-string parameters you add to the stealth connection URL, the same way you add `emulationOs=windows` for desktop. Add them to a `/stealth` connection:

```bash
wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE&emulationOs=android&emulatedDevice=pixel-8-pro
```

Omit `emulatedDevice` to let Browserless pick a device for you:

```bash
wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE&emulationOs=android
```

Connect Puppeteer or Playwright to that endpoint and the session runs as normal, presenting the mobile persona. The same parameters also work on BQL routes — see [OS emulation in BrowserQL](/browserql/bot-detection/os-emulation#android-mobile-emulation).

### Choosing a device

`emulatedDevice` takes one of the supported Android slugs — for example `pixel-8-pro` or `galaxy-s25-ultra`. See [Supported mobile devices](/baas/bot-detection/supported-mobile-devices) for the full list. Omitting `emulatedDevice` is fine: Browserless picks a real device automatically and keeps it stable across reconnects within the session.

### What gets emulated

`emulationOs=android` shifts the whole identity to a coherent mobile persona:

- **Mobile User-Agent and Client Hints** — the browser reports as Chrome on Android, and the specific model is exposed through UA Client Hints.
- **Phone screen size and pixel ratio** — a high-DPI mobile viewport.
- **Touch input** — coarse-pointer and no-hover behavior.
- **Portrait orientation.**
- **Motion sensors** — accelerometer, gyroscope, and orientation.
- **Mobile GPU / WebGL renderer identity**, Android system fonts, mobile media devices and voices, and Android-appropriate battery and network signals.

### Rules and errors

- `emulatedDevice` requires `emulationOs=android`. Sending `emulatedDevice` on a stealth session without it returns an **HTTP 400** with the message: `emulatedDevice requires emulationOs=android on a stealth request (a /stealth/*, BQL, or /chromium/agent route).`
- Device slugs are **case-sensitive** and matched exactly. Use them as listed (`pixel-8`, not `Pixel-8` or `pixel8`).
- An unrecognized `emulatedDevice` value is **not** an error. Browserless falls back to an automatically chosen device.
- `emulationOs` accepts only the exact lowercase values `windows`, `macos`, `linux`, and `android`. Any other value is silently ignored. A typo shows up as a default-identity fingerprint, not as an error.

### Limitations

- Android only — no iPhone, iOS, or Safari.
- All emulated devices are modern flagship phones in portrait orientation.

## FAQ & Troubleshooting

### I set `emulationOs` but the site still detects me. What else can I do?

OS emulation is one layer of the stealth stack. For heavily protected sites, pair it with a region-matched [residential proxy](/baas/bot-detection/proxies) — which also aligns timezone and locale — and make sure you're on a stealth or BQL route rather than a plain `/chromium` connection. Some sites also fingerprint TLS and HTTP/2 traits that are independent of the OS identity.

### Does `emulationOs` work on plain (non-stealth) WebSocket routes?

No. `emulationOs` only takes effect on stealth routes, the unblock and scrape REST APIs (`/unblock`, `/chromium/unblock`, `/chrome/unblock`, `/smart-scrape`), BQL routes, and the Agent route (`/chromium/agent`). On a plain route like `/chromium` or `/chrome` it is ignored rather than rejected, so the session silently keeps its default identity. Use a stealth route (`/stealth`), `/unblock`, `/smart-scrape`, a BQL route, or `/chromium/agent`.

### Can I emulate an iPhone or iOS?

No. Android is the only mobile persona. There is no iPhone, iOS, or Safari emulation. For mobile testing, use `emulationOs=android` with an optional [device slug](/baas/bot-detection/supported-mobile-devices).

### Can I change `emulationOs` mid-session?

No. The emulated OS identity is fixed when the session is created and reused for the connection's lifetime, including across reconnects — you don't need to re-append `emulationOs` to a reconnect URL. (The User-Agent itself can still be replaced mid-session with BrowserQL's `userAgent()` mutation, but that doesn't change the emulated platform or GPU.)

## Next Steps

  <a href="/baas/bot-detection/stealth" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Stealth Routes</h3>
      Full fingerprint hardening with managed browser configurations.
    
  </a>

  <a href="/baas/bot-detection/user-agent-masking" className="next-step-card-link">
    
      <h3 className="next-step-card-title">User Agent Masking</h3>
      Control your browser's User-Agent string for target site compatibility.
    
  </a>

  <a href="/baas/bot-detection/proxies" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Proxies</h3>
      Add residential proxies for better IP reputation alongside OS emulation.
    
  </a>

---

# Bot Detection Overview

URL: https://docs.browserless.io/baas/bot-detection/overview

Browserless provides multiple layers of bot detection bypass: stealth routes for fingerprint-level evasion, an Unblock API for sites that detect automation libraries, and CAPTCHA solving for challenges that still appear.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Puppeteer or Playwright installed locally

## Our Bot Detection Philosophy

IP reputation and browser fingerprinting are the two primary factors that determine whether automated sessions are detected. Combining both is the most effective way to bypass advanced detection systems. Our approach:

- **Stealth-first automation**: Prevent detection rather than react to it, using human-like fingerprints with our stealth routes.
- **IP reputation**: Residential proxies with high reputation make your sessions appear as regular users and are often more effective than fingerprint masking alone.
- **Layered defense**: Combine multiple techniques for maximum effectiveness. A high-reputation IP with a human-like fingerprint yields the best results.

## Feature Overview

### Stealth Routes

Start here. Stealth routes apply advanced fingerprint mitigations and entropy injection at the connection level, making automated browsers indistinguishable from real users. Changing your connection URL is all that's required:

- `/stealth` (Recommended): Managed stealth environment with advanced fingerprinting
- `/chromium/stealth`: Chromium-optimized fingerprint mitigations
- `/chrome/stealth`: Chrome-native anti-detection protections

See [Stealth Routes](/baas/bot-detection/stealth) for a detailed comparison and usage examples.

### Site Unblocking

If stealth and proxies still can't bypass a site's defenses, the [Unblock API](/baas/bot-detection/unblocking) takes a minimalistic approach that works directly with the browser's native interfaces, leaving no library traces. You can also use [BrowserQL](/browserql/start) for the same result. Both return an unblocked browser session that you connect Puppeteer or Playwright to for continued automation.

### CAPTCHA Solving

When a site is unblocked but still presents CAPTCHAs, [CAPTCHA solving](/baas/bot-detection/captchas) can automatically resolve them:

- Automatic solving: add `solveCaptchas=true` to your connection URL. Browserless monitors and solves CAPTCHAs automatically without you needing to call `solveCaptcha` manually. Try this first.
- Detection via the `Browserless.captchaFound` CDP event for more granular control
- Programmatic solving with `Browserless.solveCaptcha` when you need to trigger solving manually
- Works with Puppeteer, Playwright, and other automation tools
- Human-in-the-loop fallback via [live URL sessions](/baas/monitor-sessions/hybrid-automation)

### User Agent Masking

Some sites check the user agent string for signs of automation or outdated browser versions. [User Agent Masking](/baas/bot-detection/user-agent-masking) lets you set a custom user agent to match real browser traffic.

### Built-in Proxies

If stealth alone isn't enough, add a built-in proxy to your stealth route. Browserless offers two networks. Residential routes through real residential IPs, improving IP reputation so sessions appear to originate from real users. Datacenter is cheaper and faster but more easily detected, so use it when you mainly need low-cost IP rotation, such as working around rate limits on sites without strict IP reputation checks.

See [Proxies](/baas/bot-detection/proxies) for setup details.

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>403 Forbidden</code> error?</summary>

Your API token is missing or expired. Pass it as a `?token=` query parameter in the WebSocket or HTTP URL. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My script works locally but fails on Browserless</summary>

Local browser settings may differ from the Browserless environment. Use `launch` parameters to match your local setup (viewport, user agent, timezone). See [launch parameters](/baas/launch-options) for the full list.

</details>

## Next steps

  <a href="/baas/bot-detection/stealth" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Stealth Routes</h3>
      Advanced stealth techniques for Puppeteer and Playwright integrations
    
  </a>

  <a href="/baas/bot-detection/unblocking" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Site Unblocking</h3>
      Minimalistic approach that bypasses detection without leaving automation traces
    
  </a>

  <a href="/baas/bot-detection/captchas" className="next-step-card-link">
    
      <h3 className="next-step-card-title">CAPTCHA Solving</h3>
      Automatically detect and solve CAPTCHAs across multiple automation libraries
    
  </a>

  <a href="/baas/bot-detection/user-agent-masking" className="next-step-card-link">
    
      <h3 className="next-step-card-title">User Agent Masking</h3>
      Set a custom user agent to match real browser traffic
    
  </a>

---

# Proxies

URL: https://docs.browserless.io/baas/bot-detection/proxies

# Proxies

This page covers routing browser traffic through Browserless's [built-in residential and datacenter proxies](#built-in-proxies). It also covers connecting [your own third-party proxy](#third-party-proxies) with Puppeteer or Playwright.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Puppeteer or Playwright installed locally

## Built-in Proxies

Browserless offers two built-in proxy networks for all users. There's nothing to sign up for, buy, or configure externally: your API token already includes access, and one query parameter on your existing connection URL turns it on:

```bash
wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us
```

| Network | Description | Cost |
|---|---|---|
| `residential` (default) | Routes through real residential IP addresses. Harder to detect. | **6 units/MB** |
| `datacenter` | Routes through datacenter IPs. Cheaper but more easily detected. | **2 units/MB** |

Both networks support country and city targeting and sticky sessions, though the datacenter pool covers fewer countries. If a country has no datacenter coverage, requests won't exit from the requested location. Choose `datacenter` when you mainly need low-cost IP rotation, such as working around rate limits on sites without strict IP reputation checks.

To use either proxy network, adjust your code or API calls to let Browserless route the request for you. For both library connect and REST API calls, the process is the same!

> **Bot Detection**
> For strict bot detectors where browsers and a proxy aren't enough to get past, we recommend using [**BrowserQL**](/browserql/start).

### Puppeteer

The following uses our built-in residential proxy, targeting a node in the US:

```js

const TOKEN = "YOUR_API_TOKEN_HERE";

// Simply add proxy=residential and (optionally) a country
const browserWSEndpoint = `wss://production-sfo.browserless.io?token=${TOKEN}&proxy=residential&proxyCountry=us`;
const url = "https://ip-api.com/";
let browser;

try {
  browser = await puppeteer.connect({ browserWSEndpoint });
  const page = await browser.newPage();
  await page.setViewport({ width: 1920, height: 1080 });
  await page.goto(url);

  const $ipInfo = await page.$("section");
  await $ipInfo.screenshot({ path: "ip.png" });
} catch (e) {
  console.log("Error during script:", e.message);
} finally {
  browser && browser.close();
}
```

> **Sticky Sessions**
> On plain REST endpoints (`/content`, `/pdf`, `/screenshot`, `/export`, `/download`, `/function`) and plain Puppeteer/Playwright WebSocket connections, each request goes through a random node in the proxy pool by default. This may not be desirable and can cause other issues. To keep your session "sticky" (use the same IP node for the whole session), add a `proxySticky=true` parameter:
> 
> ```js
> "https://production-sfo.browserless.io/content?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us&proxySticky=true";
> ```
> 
> Stealth routes (`/stealth`, `/chromium/stealth`, `/chrome/stealth`), all BrowserQL endpoints, `/unblock`, `/scrape`, and any request with `?stealth=true` are **sticky by default** — they pin a single exit IP for the whole session. Anti-bot systems like Akamai and DataDome bind their sensor and cookies to the egress IP, so a mid-session IP rotation can re-trigger the block. To opt out and rotate on those routes, pass `proxySticky=false`.

> **City-Level Proxying**
> You can also target a specific city within a country using the `proxyCity` parameter:
> 
> ```js
> "http://production-sfo.browserless.io/content?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us&proxyCity=chicago";
> ```
> 
> City-level proxying requires a **Scale plan** (500k+ units). Plans under 500k units will receive a `401` error.
> 
> To get a list of available cities, use the following endpoints:
> 
> - **All supported cities**: `https://production-sfo.browserless.io/proxy/cities?token=YOUR_TOKEN`
> - **Cities for a specific country**: `https://production-sfo.browserless.io/proxy/cities?country=US&token=YOUR_TOKEN`

> **Locale Matching**
> On the stealth endpoints (`/stealth`, `/chrome/stealth`, `/chromium/stealth`), you can add the `proxyLocaleMatch` parameter to automatically set the browser's language to match the proxy's geographic location. This ensures websites render content, currency, and formatting in the local language. Recommended when using `proxyCountry`.
> 
> ```js
> "wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=de&proxyLocaleMatch=1";
> ```
> 
> For example, using `proxyCountry=br` with `proxyLocaleMatch=1` will set the browser language to Portuguese (pt-BR), causing websites like Google or Amazon to display in Portuguese.
> 
> Without this parameter, the browser defaults to English (en-US).

### Playwright

Our proxy service is also available for Playwright browsers. You only need to set the same parameters as for Puppeteer.

  
**Javascript:**

```js
import playwright from "playwright-core";

const TOKEN = "YOUR_API_TOKEN_HERE"

// Simply add proxy=residential and (optionally) a country
const pwEndpoint = `wss://production-sfo.browserless.io/chrome/playwright?token=${TOKEN}&proxy=residential&proxyCountry=us`;
const browser = await playwright.chromium.connect(pwEndpoint);

const context = await browser.newContext();
const page = await context.newPage();

await page.goto("https://ip-api.com/");
const $ipInfo = await page.$("section");
await $ipInfo.screenshot({ path: "ip-pw.png" });
await browser.close();
```
  
  
**Python:**

```python
from playwright.sync_api import sync_playwright

TOKEN = "YOUR_API_TOKEN_HERE"
pw_endpoint = f"wss://production-sfo.browserless.io/chrome/playwright?token={TOKEN}&proxy=residential&proxyCountry=us"

with sync_playwright() as p:
    # Connect to the Browserless endpoint
    browser = p.chromium.connect(pw_endpoint)
    context = browser.new_context()
    page = context.new_page()

    # Navigate to the URL and take a screenshot
    page.goto("https://ip-api.com/")
    page.screenshot(path="chrome.png")

    # Close the browser connection
    browser.close()

print("Screenshot saved as chrome.png")
```
  
  
**Java:**

```java
package org.example;

import com.microsoft.playwright.*;

import java.nio.file.Path;

public class Main {
    public static void main(String[] args) {
        String TOKEN = "YOUR_API_TOKEN_HERE";
        String PW_ENDPOINT = "wss://production-sfo.browserless.io/chrome/playwright?token="
                + TOKEN + "&proxy=residential&proxyCountry=us";

        try (Playwright playwright = Playwright.create()) {
            Browser browser = playwright.chromium().connect(PW_ENDPOINT);
            System.out.println("Connected to remote Chrome browser");

            BrowserContext context = browser.newContext();
            Page page = context.newPage();

            page.navigate("https://ip-api.com/");
            Locator ipInfo = page.locator("section.bg-gray");
            ipInfo.screenshot(new Locator.ScreenshotOptions().setPath(
                    Path.of("ip-pw.png")
            ));
            System.out.println("Screenshot saved as ip-pw.png");

            browser.close();
        }
    }
}
```
  
  
**C#:**

```csharp
using System;
using System.Threading.Tasks;
using Microsoft.Playwright;

class Program {
    static async Task Main(string[] args) {
        string TOKEN = "YOUR_API_TOKEN_HERE";
        string PW_ENDPOINT = $"wss://production-sfo.browserless.io/chrome/playwright?token={TOKEN}&proxy=residential&proxyCountry=us";

        var playwright = await Playwright.CreateAsync();
        var browser = await playwright.Chromium.ConnectAsync(PW_ENDPOINT);
        Console.WriteLine("Connected to remote Chrome browser");

        var context = await browser.NewContextAsync();
        var page = await context.NewPageAsync();

        await page.GotoAsync("https://ip-api.com/");
        var ipInfo = await page.QuerySelectorAsync("section.bg-gray");
        await ipInfo.ScreenshotAsync(new LocatorScreenshotOptions { Path = "ip-pw.png" });
        Console.WriteLine("Screenshot saved as ip-pw.png");

        await browser.CloseAsync();
    }
}
```
  

> **Sticky Sessions**
> On plain REST endpoints (`/content`, `/pdf`, `/screenshot`, `/export`, `/download`, `/function`) and plain Puppeteer/Playwright WebSocket connections, each request goes through a random node in the proxy pool by default. This may not be desirable and can cause other issues. To keep your session "sticky" (use the same IP node for the whole session), add a `proxySticky=true` parameter:
> 
> ```js
> "https://production-sfo.browserless.io/content?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us&proxySticky=true";
> ```
> 
> Stealth routes (`/stealth`, `/chromium/stealth`, `/chrome/stealth`), all BrowserQL endpoints, `/unblock`, `/scrape`, and any request with `?stealth=true` are **sticky by default** — they pin a single exit IP for the whole session. Anti-bot systems like Akamai and DataDome bind their sensor and cookies to the egress IP, so a mid-session IP rotation can re-trigger the block. To opt out and rotate on those routes, pass `proxySticky=false`.

## Third Party Proxies

Both Browserless, and Chrome itself, support the usage of external proxies. If you want to use an external, or 3rd party proxy, there are two approaches:

1. **Using `externalProxyServer` query parameter** (recommended) - A simple query parameter that includes credentials directly in the URL.
2. **Using `--proxy-server` Chrome flag** - The traditional approach using Chrome's command-line flag with separate authentication.

### Using externalProxyServer (Recommended)

The simplest way to use an external proxy is with the `externalProxyServer` query parameter. This approach allows you to specify the proxy URL with credentials included, eliminating the need for separate authentication handling.

**Format:** `http(s)://[username:password@]host:port`

  
**Puppeteer:**

```js
import puppeteer from "puppeteer-core";

const TOKEN = "YOUR_API_TOKEN_HERE";
const proxyUrl = encodeURIComponent("http://username:password@proxy.example.com:8080");
const browserWSEndpoint = `wss://production-sfo.browserless.io?token=${TOKEN}&externalProxyServer=${proxyUrl}`;

const browser = await puppeteer.connect({ browserWSEndpoint });
const page = await browser.newPage();
await page.goto("https://ip-api.com/");
console.log(await page.content());
await browser.close();
```
  
  
**Playwright:**

```js
import playwright from "playwright-core";

const TOKEN = "YOUR_API_TOKEN_HERE";
const proxyUrl = encodeURIComponent("http://username:password@proxy.example.com:8080");
const pwEndpoint = `wss://production-sfo.browserless.io/chrome/playwright?token=${TOKEN}&externalProxyServer=${proxyUrl}`;

const browser = await playwright.chromium.connect(pwEndpoint);
const context = await browser.newContext();
const page = await context.newPage();
await page.goto("https://ip-api.com/");
console.log(await page.content());
await browser.close();
```
  

> **Tip**
> Remember to URL-encode the proxy URL when including it as a query parameter, especially if it contains special characters in the username or password.

### Using --proxy-server Chrome Flag

Alternatively, you can use Chrome's `--proxy-server` flag. This approach requires separate authentication handling if your proxy requires credentials.

#### Specifying the proxy

Regardless of whether or not you're using our REST API's or the puppeteer integration, you'll need to specify _where_ the proxy is. Chrome has a command-line flag to do this, and we support this in Browserless via the following query-string parameter:

```bash
?--proxy-server=https://YOUR-PROXY-SERVER-DOMAIN:PORT
```

You can set this parameter in our [live debugger](https://chrome.browserless.io/debugger) by clicking on the gear icon on the left panel and modifying the Browser URL field.

If you're using a proxy that doesn't require a password (maybe just an IP address filter), then that's it! You're free to now use this proxy going forward! Otherwise read on.

#### Using username and password

##### Method 1: page.authenticate (Puppeteer)

Most proxies will require some means of authentication. There's generally two ways you can do this in Puppeteer, and also in Browserless. The first more common method is the `page.authenticate`:

```js
await page.authenticate({
  username: 'joel',
  password: 'browserless-rocks',
});
```

Doing this will apply these parameters to your network requests going forward.

In our REST API's you can specify these fields with the following in your POST JSON body. These parameters work for the `pdf`, `content` and `screenshot` APIs:

```json
{
  "authenticate": {
    "username": "joel",
    "password": "browserless-rocks"
  }
}
```

##### Method 2: page.setExtraHTTPHeaders (Puppeteer)

The other mechanism is to use HTTP headers to send in extra authorization information. Puppeteer makes this pretty easy by allowing us to send in new HTTP headers via `page.setExtraHTTPHeaders`:

> NOTE: This is deprecated in most libraries now, so it's worth keeping in mind that using authentication methods in each library is now the standard.

```js
// Remember to base64 encode your username:password!
await page.setExtraHTTPHeaders({
  'Proxy-Authorization': 'Basic username:password',
  // OR
  Authorization: 'Basic username:password',
});
```

Refer to your libraries documentation on what the name of the headers is, and how to properly encode it.

We also allow this in our REST APIs as well, via the `setExtraHTTPHeaders` property:

```json
{
  "setExtraHTTPHeaders": {
    "Proxy-Authorization": "Basic username:password",
    // OR
    "Authorization": "Basic username:password"
  }
}
```

This will allow your REST APIs to utilize the prior provided proxy!

#### Using Proxies with Playwright

Playwright handles proxies differently than Puppeteer. Instead of using `page.authenticate()` or `setExtraHTTPHeaders()`, Playwright allows you to specify proxy settings directly at the context level, which means all pages created from that context will use the specified proxy.

##### Method: browser.newContext() with proxy option (Playwright)

When using Playwright with Browserless, you can set up a proxy by providing proxy configuration to the `newContext()` method:

```js

const browser = await playwright.chromium.connectOverCDP(
  "wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE"
);
const context = await browser.newContext({
  proxy: {
    server: "http://domain:port",
    username: "username",
    password: "password",
  },
});
const page = await context.newPage();

await page.goto("https://icanhazip.com/");
console.log(await page.content());

await browser.close();
```

## FAQ & Troubleshooting

<details>
<summary>Do I need to buy or set up a proxy separately?</summary>

No. The residential and datacenter networks are part of Browserless: add `proxy=residential` (or `proxy=datacenter`) to your connection URL or REST request and traffic routes through them, metered per MB on your existing token. You only bring your own proxy if you specifically want to, via `externalProxyServer` or the `--proxy-server` launch flag, and doing so requires a paid plan. On the free plan these are rejected with a `401` error stating `Only paid cloud-unit plans can utilize a third-party proxy`.

</details>

<details>
<summary>Can I use a SOCKS5 proxy?</summary>

Not with the managed proxy network or `externalProxyServer`, which are HTTP(S) only. For your own SOCKS5 proxy, pass it as a Chrome launch flag instead: `--proxy-server=socks5://host:port` via the `launch` parameter. Chrome doesn't support username/password authentication for SOCKS proxies, so use a SOCKS proxy that allowlists your IP, or an HTTP(S) proxy when you need credentials.

</details>

<details>
<summary>Why am I getting a <code>403 Forbidden</code> error?</summary>

Your API token is missing or expired. Pass it as a `?token=` query parameter in the WebSocket or HTTP URL. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My script works locally but fails on Browserless</summary>

Local browser settings may differ from the Browserless environment. Use `launch` parameters to match your local setup (viewport, user agent, timezone). See [launch parameters](/baas/launch-options) for the full list.

</details>

<details>
<summary>My proxy isn't working or I'm getting connection errors</summary>

Built-in proxies are available on every plan, including free, but they consume units per MB of traffic. Check that your account has units remaining. If using an external proxy, check that the `externalProxyServer` URL (or `--proxy-server` flag plus authentication credentials) is correct and the proxy is reachable.

</details>

<details>
<summary>How are proxy units calculated?</summary>

Built-in proxy usage is billed per MB of traffic routed through the proxy: 6 units/MB for residential and 2 units/MB for datacenter. External proxies don't consume proxy units. Check your [dashboard](https://browserless.io/account/) for current usage.

</details>

## Next steps

  <a href="/baas/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BaaS Quickstart</h3>
      get started with Puppeteer or Playwright on Browserless
    
  </a>
  <a href="/baas/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure browser behavior and environment
    
  </a>

---

# Stealth Routes

URL: https://docs.browserless.io/baas/bot-detection/stealth

Stealth routes use path-based semantics and the Chrome DevTools Protocol to provide stronger anti-detection than solutions like `puppeteer-stealth`. They are the recommended first step for BaaS users: changing your connection URL is all that's required to get started.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Puppeteer or Playwright installed locally

## Available stealth routes

Browserless offers three stealth routes, each tuned for a different balance of browser fidelity and anti-detection strength:

- **Stealth Route (Recommended)** - `/stealth`
  Our recommended managed stealth environment with advanced anti-detection and realistic fingerprinting. This route applies comprehensive browser fingerprint mitigations and entropy injection for maximum effectiveness against bot detection systems.

- **Chromium Stealth** - `/chromium/stealth`
  Optimized for our platform, this route applies a comprehensive set of fingerprinting mitigations and entropy injection tailored to Chromium. It modifies browser APIs with subtle, realistic values to closely emulate a real browser and reduce detectability. Chromium's flexibility allows this route to be better tailored at stealthiness compared to `/chrome/stealth`.

- **Chrome Stealth** - `/chrome/stealth`
  Designed for the standard Chrome experience, this route delivers anti-detection protections adjusted to preserve Chrome's native behaviors, while also providing enhanced resistance to bot-detection systems.

| Route | Browser | Best For | Notes |
|---|---|---|---|
| `/stealth` | Chromium/Chrome | Most use cases | Recommended. Managed environment with comprehensive fingerprint mitigations and entropy injection. |
| `/chromium/stealth` | Chromium | Custom configurations | Chromium's flexibility allows more tailored stealthiness than `/chrome/stealth`. |
| `/chrome/stealth` | Chrome | Chrome-native behavior | Preserves Chrome's native behaviors while adding anti-detection protections. |

> **Note**
> Stealth routes modify browser behavior in ways that can produce unexpected results for some automation tasks. They are not the default for this reason. Use them when you are dealing with bot detection issues, not as a general-purpose browser configuration.

## Connecting to a stealth route

  
**Puppeteer:**

```js
// Stealth (Recommended):
await puppeteer.connect({
  browserWSEndpoint:
    "wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE",
});

// Chromium:
await puppeteer.connect({
  browserWSEndpoint:
    "wss://production-sfo.browserless.io/chromium/stealth?token=YOUR_API_TOKEN_HERE",
});

// Chrome:
await puppeteer.connect({
  browserWSEndpoint:
    "wss://production-sfo.browserless.io/chrome/stealth?token=YOUR_API_TOKEN_HERE",
});
```
  
  
**Playwright:**

    
      
**Javascript:**

```js
// Stealth (Recommended)
const stealthBrowser = await playwright.chromium.connectOverCDP(
  "wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE"
);

// Chromium
const chromiumBrowser = await playwright.chromium.connectOverCDP(
  "wss://production-sfo.browserless.io/chromium/stealth?token=YOUR_API_TOKEN_HERE"
);

// Chrome
const chromeBrowser = await playwright.chromium.connectOverCDP(
  "wss://production-sfo.browserless.io/chrome/stealth?token=YOUR_API_TOKEN_HERE"
);
```
      
      
**Python:**

```python
import asyncio
from playwright.async_api import async_playwright

STEALTH_URL = "wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE"
CHROMIUM_URL = "wss://production-sfo.browserless.io/chromium/stealth?token=YOUR_API_TOKEN_HERE"
CHROME_URL = "wss://production-sfo.browserless.io/chrome/stealth?token=YOUR_API_TOKEN_HERE"

async def main():
    async with async_playwright() as p:
        # Stealth (Recommended)
        stealth_browser = await p.chromium.connect_over_cdp(STEALTH_URL)

        # Chromium
        chromium_browser = await p.chromium.connect_over_cdp(CHROMIUM_URL)

        # Chrome
        chrome_browser = await p.chromium.connect_over_cdp(CHROME_URL)

asyncio.run(main())

```
      
      
**Java:**

```java
import com.microsoft.playwright.*;

public class PlaywrightConnectExample {
    public static void main(String[] args) {
        String STEALTH_URL = "wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE";
        String CHROMIUM_URL = "wss://production-sfo.browserless.io/chromium/stealth?token=YOUR_API_TOKEN_HERE";
        String CHROME_URL = "wss://production-sfo.browserless.io/chrome/stealth?token=YOUR_API_TOKEN_HERE";

        try (Playwright playwright = Playwright.create()) {
            // Stealth (Recommended)
            Browser stealthBrowser = playwright.chromium().connectOverCDP(STEALTH_URL);

            // Chromium
            Browser chromiumBrowser = playwright.chromium().connectOverCDP(CHROMIUM_URL);

            // Chrome
            Browser chromeBrowser = playwright.chromium().connectOverCDP(CHROME_URL);
        }
    }
}

```
      
      
**C#:**

```csharp
using System;
using System.Threading.Tasks;
using Microsoft.Playwright;

class Program
{
    static async Task Main(string[] args)
    {
        string stealthUrl = "wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE";
        string chromiumUrl = "wss://production-sfo.browserless.io/chromium/stealth?token=YOUR_API_TOKEN_HERE";
        string chromeUrl = "wss://production-sfo.browserless.io/chrome/stealth?token=YOUR_API_TOKEN_HERE";

        var playwright = await Playwright.CreateAsync();

        // Stealth (Recommended)
        var stealthBrowser = await playwright.Chromium.ConnectOverCDPAsync(stealthUrl);

        // Chromium
        var chromiumBrowser = await playwright.Chromium.ConnectOverCDPAsync(chromiumUrl);

        // Chrome
        var chromeBrowser = await playwright.Chromium.ConnectOverCDPAsync(chromeUrl);
    }
}
```
      
    
  

## Customizing Stealth Flags

Stealth routes apply a comprehensive set of Chrome flags by default to optimize anti-detection behavior. However, you can selectively remove specific flags if needed for your use case. This is particularly useful when:

- You have sufficient shared memory configured in your container and want to remove `--disable-dev-shm-usage` for better performance
- You need to customize specific browser behaviors while maintaining other stealth protections
- You're troubleshooting compatibility issues with certain websites

### Removing Stealth Flags

Use the `ignoreDefaultArgs` parameter in your launch options to remove specific stealth flags. This parameter accepts an array of flag names to exclude from the default stealth configuration.

**Example: Remove `--disable-dev-shm-usage` flag**

  
**Puppeteer:**

```js
import puppeteer from "puppeteer-core";

const launchOptions = {
  ignoreDefaultArgs: ["--disable-dev-shm-usage"]
};

const queryParams = new URLSearchParams({
  token: 'YOUR_API_TOKEN_HERE',
  launch: JSON.stringify(launchOptions)
});

const browser = await puppeteer.connect({
  browserWSEndpoint: `wss://production-sfo.browserless.io/chromium/stealth?${queryParams.toString()}`,
});
```
  
  
**Playwright:**

```js
import playwright from "playwright";

const launchOptions = {
  ignoreDefaultArgs: ["--disable-dev-shm-usage"]
};

const queryParams = new URLSearchParams({
  token: 'YOUR_API_TOKEN_HERE',
  launch: JSON.stringify(launchOptions)
});

const browser = await playwright.chromium.connectOverCDP(
  `wss://production-sfo.browserless.io/chromium/stealth?${queryParams.toString()}`
);
```
  

**Example: Remove multiple flags**

```js
const launchOptions = {
  ignoreDefaultArgs: [
    "--disable-dev-shm-usage",
    "--no-sandbox",
    "--disable-infobars"
  ]
};
```

> **Note**
> - All stealth flags are included by default for optimal anti-detection behavior
> - Only flags explicitly listed in `ignoreDefaultArgs` will be removed
> - Other stealth protections remain active even when specific flags are removed
> - This feature is available on all stealth routes: `/stealth`, `/chromium/stealth`, and `/chrome/stealth`

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>403 Forbidden</code> error?</summary>

Your API token is missing or expired. Pass it as a `?token=` query parameter in the WebSocket or HTTP URL. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My script works locally but fails on Browserless</summary>

Local browser settings may differ from the Browserless environment. Use `launch` parameters to match your local setup (viewport, user agent, timezone). See [launch parameters](/baas/launch-options) for the full list.

</details>

## Next steps

  <a href="/baas/bot-detection/unblocking" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Site Unblocking</h3>
      Use the Unblock API when stealth routes aren't enough to bypass bot detection
    
  </a>

  <a href="/baas/bot-detection/captchas" className="next-step-card-link">
    
      <h3 className="next-step-card-title">CAPTCHA Solving</h3>
      Automatically detect and solve CAPTCHAs across multiple automation libraries
    
  </a>

  <a href="/baas/bot-detection/proxies" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Proxies</h3>
      Add residential proxies to your stealth route for better IP reputation
    
  </a>

---

# Supported mobile devices

URL: https://docs.browserless.io/baas/bot-detection/supported-mobile-devices

These are the Android devices you can select with the `emulatedDevice` query parameter when running a stealth or BQL session with [`emulationOs=android`](/baas/bot-detection/os-emulation#android-mobile-emulation). Each slug maps to a real flagship phone, and picking one makes the session report that device's screen size, pixel ratio, and hardware identity coherently.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Familiarity with [OS emulation](/baas/bot-detection/os-emulation) and the `emulationOs=android` persona

Pass the slug exactly as listed — values are case-sensitive and matched exactly (`pixel-8`, not `Pixel-8` or `pixel8`). Omit `emulatedDevice` entirely and Browserless picks a device for you and keeps it stable for the session. An unrecognized slug is not an error; it falls back to an automatically chosen device.

```bash
wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE&emulationOs=android&emulatedDevice=pixel-8-pro
```

## Devices

| Slug | Device | Screen (CSS px) | Pixel ratio |
|---|---|---|---|
| `pixel-8` | Google Pixel 8 | 412 × 915 | 2.625 |
| `pixel-8-pro` | Google Pixel 8 Pro | 448 × 998 | 3 |
| `galaxy-s23` | Samsung Galaxy S23 | 360 × 780 | 3 |
| `galaxy-s23-ultra` | Samsung Galaxy S23 Ultra | 384 × 824 | 3.75 |
| `galaxy-s24` | Samsung Galaxy S24 | 360 × 780 | 3 |
| `galaxy-s24-plus` | Samsung Galaxy S24+ | 384 × 832 | 3.75 |
| `galaxy-s24-ultra` | Samsung Galaxy S24 Ultra | 384 × 832 | 3.75 |
| `galaxy-s25` | Samsung Galaxy S25 | 360 × 780 | 3 |
| `galaxy-s25-plus` | Samsung Galaxy S25+ | 412 × 891 | 3.5 |
| `galaxy-s25-ultra` | Samsung Galaxy S25 Ultra | 412 × 891 | 3.5 |
| `galaxy-s26` | Samsung Galaxy S26 | 360 × 780 | 3 |
| `galaxy-s26-plus` | Samsung Galaxy S26+ | 412 × 891 | 3.5 |
| `galaxy-s26-ultra` | Samsung Galaxy S26 Ultra | 412 × 891 | 3.5 |
| `pixel-9` | Google Pixel 9 | 412 × 915 | 2.625 |
| `pixel-9-pro` | Google Pixel 9 Pro | 427 × 952 | 3 |
| `pixel-9-pro-xl` | Google Pixel 9 Pro XL | 448 × 997 | 3 |

Android emulation uses Chrome's frozen Android User-Agent, so the exact model isn't in the User-Agent string. The device is conveyed through UA Client Hints instead. Every device reports portrait orientation and five touch points.

## FAQ & Troubleshooting

<details>
<summary>My <code>emulatedDevice</code> value returned a <code>400</code> error</summary>

`emulatedDevice` only works alongside `emulationOs=android` on a stealth route, a BQL route, or the Agent route (`/chromium/agent`). Sending it on its own returns `emulatedDevice requires emulationOs=android on a stealth request (a /stealth/*, BQL, or /chromium/agent route).` Add `emulationOs=android` to the URL.

</details>

<details>
<summary>I passed a device slug but the session reports a different phone</summary>

Slugs are case-sensitive and matched exactly. A typo, wrong casing, or a slug not in the table above is silently ignored, and Browserless picks a device automatically. Copy the slug verbatim from the table.

</details>

<details>
<summary>Can I emulate an iPhone or another non-listed device?</summary>

No. Only the Android devices listed above are supported — there is no iPhone, iOS, or Safari emulation, and custom device profiles aren't accepted.

</details>

## Next steps

  <a href="/baas/bot-detection/os-emulation" className="next-step-card-link">
    
      <h3 className="next-step-card-title">OS emulation</h3>
      Enable Android and desktop personas with the emulationOs parameter
    
  </a>
  <a href="/browserql/bot-detection/os-emulation" className="next-step-card-link">
    
      <h3 className="next-step-card-title">OS emulation in BrowserQL</h3>
      Use emulationOs and emulatedDevice on any BQL route
    
  </a>
  <a href="/baas/bot-detection/proxies" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Proxies</h3>
      Match a region-appropriate proxy to your emulated device
    
  </a>

---

# Site Unblocking

URL: https://docs.browserless.io/baas/bot-detection/unblocking

Site unblocking uses the [Unblock API](/rest-apis/unblock) or [BrowserQL](/browserql/start) to bypass bot detection by working directly with the browser's native remote interfaces, launching browsers the way end-users would, and automatically correcting common blockers. Use it when [stealth routes](/baas/bot-detection/stealth) alone can't bypass a site's defenses.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Puppeteer or Playwright installed locally

## How The Unblock API Works

The Unblock API accepts a POST request to `/chromium/unblock` with a target URL. Browserless navigates to that URL in a real browser, handles bot detection challenges automatically, and returns the results you requested. You control exactly what you get back:

- `content`: The rendered HTML of the page
- `cookies`: All cookies set during the session
- `screenshot`: A full-page JPEG screenshot of the page (quality 50)
- `browserWSEndpoint`: A WebSocket endpoint for connecting Puppeteer or Playwright to the live unblocked browser session

Set any field to `false` to skip it. Browserless optimizes execution to only produce the fields you request. If you include `browserWSEndpoint`, the response returns a live browser session that stays open for the duration set by `ttl`. Connect your automation library to that endpoint to continue automating from the unblocked state.

> **API Reference**
> For detailed usage examples, request parameters, and response formats, see the [**/unblock API**](/rest-apis/unblock) reference page.

> **Sticky Proxy by Default**
> When you use a Browserless proxy, the Unblock API and all BrowserQL endpoints pin a **single proxy exit IP for the whole session** by default. Anti-bot systems like Akamai and DataDome bind their sensor and cookies to the egress IP, so rotating mid-session can re-trigger the block. The `proxySticky` parameter shown in the examples below is therefore optional — to rotate IPs instead, pass `proxySticky=false`.

  
**Puppeteer:**

```js
import puppeteer from 'puppeteer-core';

// The underlying site you wish automate
const url = 'https://turo.com/';

// Your API token retrievable from the dashboard.
const token = 'YOUR_API_TOKEN_HERE';

// Set a threshold, in milliseconds, to unblock
const timeout = 5 * 60 * 1000;

// Proxy network: 'residential' (6 units/MB) or 'datacenter' (2 units/MB).
const proxy = 'residential';

// Where you want to proxy from (GB === Great Britain), or remove for none.
const proxyCountry = 'gb';

// Sticky proxy (one exit IP for the whole session) is the default for /unblock.
// Pass proxySticky=false to rotate IPs instead; shown here explicitly.
const proxySticky = true;

const queryParams = new URLSearchParams({
  timeout,
  proxy,
  proxyCountry,
  proxySticky,
  token,
}).toString();

const unblockURL =
  `https://production-sfo.browserless.io/chromium/unblock?${queryParams}`;

const options = {
  method: 'POST',
  headers: {
    'content-type': 'application/json'
  },
  body: JSON.stringify({
    url: url,
    browserWSEndpoint: true,
    cookies: true,
    content: true,
    screenshot: true,
    ttl: 5000,
  }),
};

try {
  console.log(`Unblocking ${url}`);

  const response = await fetch(unblockURL, options);

  if (!response.ok) {
    throw new Error(`Got non-ok response:\n` + (await response.text()));
  }

  const { browserWSEndpoint } = await response.json();

  console.log(`Got OK response! Connecting puppeteer to "${browserWSEndpoint}"...`);
  const browser = await puppeteer.connect({
    browserWSEndpoint: `${browserWSEndpoint}?${queryParams}`
  });
  // Find the page by inspecting the URL and matching it
  const pages = await browser.pages();
  const page = pages.find((p) => p.url().includes(url));
  page.on('response', (res) => {
    if (!res.ok) {
      console.log(`${res.status()}: ${res.url()}`);
    }
  });
  console.log('Reloading page with networkidle0...');
  await page.reload({
    waitUntil: 'networkidle0',
    timeout,
  });
  console.log('Taking page screenshot...');
  await page.screenshot({
    path: 'temp.png',
    fullPage: true,
  });
  console.log('Done!');
  await browser.close();
} catch (error) {
  console.error(error);
}
```
  
  
**Playwright:**

    
      
**Javascript:**

```js
import playwright from 'playwright-core';

// The underlying site you wish automate
const url = 'https://turo.com/';

// Your API token retrievable from the dashboard.
const token = 'YOUR_API_TOKEN_HERE';

// Set a threshold, in milliseconds, to unblock
const timeout = 5 * 60 * 1000;

// Proxy network: 'residential' (6 units/MB) or 'datacenter' (2 units/MB).
const proxy = 'residential';

// Where you want to proxy from (GB === Great Britain), or remove for none.
const proxyCountry = 'gb';

// Sticky proxy (one exit IP for the whole session) is the default for /unblock.
// Pass proxySticky=false to rotate IPs instead; shown here explicitly.
const proxySticky = true;

const queryParams = new URLSearchParams({
  timeout,
  proxy,
  proxyCountry,
  proxySticky,
  token,
}).toString();

const unblockURL =
  `https://production-sfo.browserless.io/chromium/unblock?${queryParams}`;

const options = {
  method: 'POST',
  headers: {
    'content-type': 'application/json'
  },
  body: JSON.stringify({
    url: url,
    browserWSEndpoint: true,
    cookies: true,
    content: true,
    screenshot: true,
    ttl: 5000,
  }),
};

try {
  console.log(`Unblocking ${url}`);

  const response = await fetch(unblockURL, options);

  if (!response.ok) {
    throw new Error(`Got non-ok response:\n` + (await response.text()));
  }

  const { browserWSEndpoint } = await response.json();

  console.log(`Got OK response! Connecting Playwright to "${browserWSEndpoint}"...`);
  const browser = await playwright.chromium.connectOverCDP({
    wsEndpoint: `${browserWSEndpoint}?${queryParams}`,
  });

  // Find the page by inspecting the URL and matching it
  const contexts = browser.contexts();
  const pages = contexts.flatMap((context) => context.pages());
  const page = pages.find((p) => p.url().includes(url));

  if (!page) {
    throw new Error(`Page with URL "${url}" not found.`);
  }

  page.on('response', (res) => {
    if (!res.ok()) {
      console.log(`${res.status()}: ${res.url()}`);
    }
  });

  console.log('Reloading page with networkidle...');
  await page.reload({
    waitUntil: 'networkidle',
    timeout,
  });

  console.log('Taking page screenshot...');
  await page.screenshot({
    path: 'temp.png',
    fullPage: true,
  });

  console.log('Done!');
  await browser.close();
} catch (error) {
  console.error(error);
}
```
      
      
**Python:**

```python
import asyncio
from playwright.async_api import async_playwright
import requests

# The underlying site you wish automate
url = "https://turo.com/"

# Your API token retrievable from the dashboard.
token = "YOUR_API_TOKEN_HERE"

# Set a threshold, in milliseconds, to unblock
timeout = 5 * 60 * 1000

# Proxy network: 'residential' (6 units/MB) or 'datacenter' (2 units/MB).
proxy = "residential"

# Where you want to proxy from (GB === Great Britain), or remove for none.
proxy_country = "gb"

# Sticky proxy (one exit IP for the whole session) is the default for /unblock.
# Pass proxySticky=false to rotate IPs instead; shown here explicitly.
proxy_sticky = True

# Build query parameters
query_params = {
    "timeout": timeout,
    "proxy": proxy,
    "proxyCountry": proxy_country,
    "proxySticky": proxy_sticky,
    "token": token,
}

unblock_url = "https://production-sfo.browserless.io/chromium/unblock"
options = {
    "url": url,
    "browserWSEndpoint": True,
    "cookies": True,
    "content": True,
    "screenshot": True,
    "ttl": 5000,
}

try:
    print(f"Unblocking {url}...")

    # Make the POST request
    response = requests.post(unblock_url, json=options, params=query_params)

    if response.status_code != 200:
        raise Exception(f"Non-OK response:\n{response.text}")

    response_data = response.json()
    browser_ws_endpoint = response_data.get("browserWSEndpoint")

    print(f'Got OK response! Connecting Playwright to "{browser_ws_endpoint}"...')

    async def main():
        async with async_playwright() as p:
            browser = await p.chromium.connect_over_cdp(browser_ws_endpoint)

            # Find the page by inspecting the URL and matching it
            contexts = browser.contexts
            pages = [page for context in contexts for page in context.pages]
            page = next((p for p in pages if url in p.url), None)

            if not page:
                raise Exception(f"Page with URL '{url}' not found.")

            page.on(
                "response",
                lambda res: print(f"{res.status}: {res.url}") if not res.ok else None,
            )

            print("Reloading page with networkidle...")
            await page.reload(wait_until="networkidle", timeout=timeout)

            print("Taking page screenshot...")
            await page.screenshot(path="temp.png", full_page=True)

            print("Done!")
            await browser.close()

    asyncio.run(main())

except Exception as e:
    print(e)

```
      
      
**Java:**

```java
import com.microsoft.playwright.*;
import java.net.http.*;
import java.net.URI;
import java.net.http.HttpClient;
import java.util.Map;
import com.google.gson.Gson;

public class PlaywrightExample {
    private static final String TOKEN = "YOUR_BROWSERLESS_API_TOKEN";
    private static final String URL = "https://turo.com/";
    private static final int TIMEOUT = 5 * 60 * 1000;
    private static final String PROXY = "residential";
    private static final String PROXY_COUNTRY = "gb";
    // Sticky proxy (one exit IP for the whole session) is the default for /unblock.
    // Pass proxySticky=false to rotate IPs instead; shown here explicitly.
    private static final boolean PROXY_STICKY = true;

    public static void main(String[] args) throws Exception {
        String unblockURL = "https://production-sfo.browserless.io/chromium/unblock";
        HttpClient client = HttpClient.newHttpClient();

        // Prepare query params
        String queryParams = String.format(
            "?timeout=%d&proxy=%s&proxyCountry=%s&proxySticky=%b&token=%s",
            TIMEOUT, PROXY, PROXY_COUNTRY, PROXY_STICKY, TOKEN
        );

        // Request payload
        Map payload = Map.of(
            "url", URL,
            "browserWSEndpoint", true,
            "cookies", true,
            "content", true,
            "screenshot", true,
            "ttl", 5000
        );

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(unblockURL + queryParams))
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(new Gson().toJson(payload)))
            .build();

        System.out.println("Unblocking " + URL);
        HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());

        if (response.statusCode() != 200) {
            throw new Exception("Error: " + response.body());
        }

        String browserWSEndpoint = new Gson().fromJson(response.body(), Map.class).get("browserWSEndpoint").toString();
        System.out.println("Connecting to Playwright...");

        try (Playwright playwright = Playwright.create()) {
            Browser browser = playwright.chromium().connectOverCDP(browserWSEndpoint);

            Page page = browser.contexts().get(0).pages().get(0);
            System.out.println("Reloading page...");
            page.reload(new Page.ReloadOptions().setWaitUntil(Page.LoadState.NETWORKIDLE).setTimeout(TIMEOUT));
            System.out.println("Taking screenshot...");
            page.screenshot(new Page.ScreenshotOptions().setPath("screenshot.png"));
        }

        System.out.println("Done!");
    }
}

```
      
      
**C#:**

```csharp
using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Playwright;

class Program
{
    static async Task Main(string[] args)
    {
        const string TOKEN = "YOUR_BROWSERLESS_API_TOKEN";
        const string URL = "https://turo.com/";
        const int TIMEOUT = 5 * 60 * 1000;
        const string PROXY = "residential";
        const string PROXY_COUNTRY = "gb";
        // Sticky proxy (one exit IP for the whole session) is the default for /unblock.
        // Pass proxySticky=false to rotate IPs instead; shown here explicitly.
        const bool PROXY_STICKY = true;

        var unblockUrl = "https://production-sfo.browserless.io/chromium/unblock";
        using var httpClient = new HttpClient();

        var queryParams = $"?timeout={TIMEOUT}&proxy={PROXY}&proxyCountry={PROXY_COUNTRY}&proxySticky={PROXY_STICKY}&token={TOKEN}";
        var payload = new
        {
            url = URL,
            browserWSEndpoint = true,
            cookies = true,
            content = true,
            screenshot = true,
            ttl = 5000
        };

        var requestContent = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
        var response = await httpClient.PostAsync(unblockUrl + queryParams, requestContent);

        if (!response.IsSuccessStatusCode)
        {
            throw new Exception($"Error: {await response.Content.ReadAsStringAsync()}");
        }

        var responseData = JsonSerializer.Deserialize>(await response.Content.ReadAsStringAsync());
        var browserWSEndpoint = responseData["browserWSEndpoint"].ToString();

        Console.WriteLine("Connecting to Playwright...");
        using var playwright = await Playwright.CreateAsync();
        var browser = await playwright.Chromium.ConnectOverCDPAsync(browserWSEndpoint);

        var context = browser.Contexts[0];
        var page = context.Pages[0];

        Console.WriteLine("Reloading page...");
        await page.ReloadAsync(new PageReloadOptions { WaitUntil = WaitUntilState.NetworkIdle, Timeout = TIMEOUT });

        Console.WriteLine("Taking screenshot...");
        await page.ScreenshotAsync(new PageScreenshotOptions { Path = "screenshot.png", FullPage = true });

        Console.WriteLine("Done!");
    }
}

```
      
    
  

## Unblocking with BrowserQL

You can also use our query-language, [**BrowserQL**](/browserql/start), which is designed to bypass sophisticated bot detection mechanisms effectively. This API allows you to specify a target URL and return data you care about: the HTML content, a `.png` screenshot or an unblocked browser session to use with Playwright or Puppeteer.

> **API Reference**
> For the full `reconnect` mutation reference, see [Puppeteer & Playwright reconnection](https://docs.browserless.io/browserql/session-management/puppeteer-playwright).

  
**Puppeteer:**

```js
import puppeteer from "puppeteer-core";

const TOKEN = 'YOUR_API_TOKEN_HERE';
const url = "https://www.browserless.io/"

const unblockWithBrowserQL = async (url) => {
  const opts = {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      "query": "mutation Reconnect($url: String!) { goto(url: $url, waitUntil: networkIdle) { status } reconnect(timeout: 30000) { browserWSEndpoint } }",
      "variables": { url }
    }),
  };

  const response = await fetch(
    `https://production-sfo.browserless.io/stealth/bql?token=${TOKEN}`,
    opts,
  );

  return await response.json();
};

// Reconnect
const { data } = await unblockWithBrowserQL(url);
const browser = await puppeteer.connect({
  browserWSEndpoint: data.reconnect.browserWSEndpoint + `?token=${TOKEN}`,
});
const pages = await browser.pages();

const page = pages.find((p) => p.url() === url);
await page.screenshot({ path: `screenshot-${Date.now()}.png` });
await browser.close();
```
  
  
**Playwright:**

    
      
**Javascript:**

```js
import { chromium } from "playwright-core";

const TOKEN = 'YOUR_API_TOKEN_HERE';
const url = "https://www.browserless.io/";

const unblockWithBrowserQL = async (url) => {
  const opts = {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      "query": "mutation Reconnect($url: String!) { goto(url: $url, waitUntil: networkIdle) { status } reconnect(timeout: 30000) { browserWSEndpoint } }",
      "variables": { url }
    }),
  };

  const response = await fetch(
    `https://production-sfo.browserless.io/stealth/bql?token=${TOKEN}`,
    opts,
  );

  return await response.json();
};

const { data } = await unblockWithBrowserQL(url);

const browser = await chromium.connectOverCDP(data.reconnect.browserWSEndpoint + `?token=${TOKEN}`);
const pages = browser.contexts()[0].pages();

const page = pages.find((p) => p.url() === url);
await page.screenshot({ path: `screenshot-${Date.now()}.png` });
await browser.close();

```
      
      
**Python:**

```python
import asyncio
import requests
from playwright.async_api import async_playwright

TOKEN = "YOUR_API_TOKEN_HERE"
url = "https://www.browserless.io/"

def unblockWithBrowserQL(url):
    opts = {
        "query": """mutation Reconnect($url: String!) {
            goto(url: $url, waitUntil: networkIdle) { status }
            reconnect(timeout: 30000) { browserWSEndpoint }
        }""",
        "variables": {"url": url},
    }

    response = requests.post(
        f"https://production-sfo.browserless.io/stealth/bql?token={TOKEN}",
        json=opts,
        headers={"Content-Type": "application/json"},
    )

    response.raise_for_status()
    return response.json()

async def main():
    data = unblockWithBrowserQL(url)["data"]

    async with async_playwright() as p:
        browser = await p.chromium.connect_over_cdp(
            data["reconnect"]["browserWSEndpoint"] + f"?token={TOKEN}"
        )
        context = browser.contexts[0]
        page = next((p for p in context.pages if p.url == url), None)
        await page.screenshot(path="screenshot.png")
        await browser.close()

asyncio.run(main())

```
      
      
**Java:**

```java
import com.microsoft.playwright.*;
import java.net.http.*;
import java.net.URI;
import java.net.http.HttpClient;
import java.util.Map;
import com.google.gson.Gson;

public class PlaywrightExample {
    private static final String TOKEN = "YOUR_API_TOKEN_HERE";
    private static final String URL = "https://www.browserless.io/";

    public static void main(String[] args) throws Exception {
        HttpClient client = HttpClient.newHttpClient();

        Map payload = Map.of(
            "query", """
                mutation Reconnect($url: String!) {
                    goto(url: $url, waitUntil: networkIdle) { status }
                    reconnect(timeout: 30000) { browserWSEndpoint }
                }
            """,
            "variables", Map.of("url", URL)
        );

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://production-sfo.browserless.io/stealth/bql?token=" + TOKEN))
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(new Gson().toJson(payload)))
            .build();

        System.out.println("Unblocking " + URL);
        HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());

        if (response.statusCode() != 200) {
            throw new Exception("Error: " + response.body());
        }

        String browserWSEndpoint = new Gson().fromJson(response.body(), Map.class)
            .get("data").get("reconnect").get("browserWSEndpoint").toString();

        System.out.println("Connecting to Playwright...");
        try (Playwright playwright = Playwright.create()) {
            Browser browser = playwright.chromium().connectOverCDP(browserWSEndpoint);
            Page page = browser.contexts().get(0).pages().stream()
                .filter(p -> p.url().equals(URL))
                .findFirst()
                .orElseThrow(() -> new Exception("Page not found"));

            page.screenshot(new Page.ScreenshotOptions().setPath("screenshot.png"));
        }

        System.out.println("Done!");
    }
}

```
      
      
**C#:**

```csharp
using System;
using System.Net.Http;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Microsoft.Playwright;

class Program
{
    private const string TOKEN = "YOUR_API_TOKEN_HERE";
    private const string URL = "https://www.browserless.io/";

    static async Task Main(string[] args)
    {
        var httpClient = new HttpClient();
        var payload = new
        {
            query = @"
                mutation Reconnect($url: String!) {
                    goto(url: $url, waitUntil: networkIdle) { status }
                    reconnect(timeout: 30000) { browserWSEndpoint }
                }",
            variables = new { url = URL }
        };

        var requestContent = new StringContent(JsonSerializer.Serialize(payload), System.Text.Encoding.UTF8, "application/json");
        var response = await httpClient.PostAsync($"https://production-sfo.browserless.io/stealth/bql?token={TOKEN}", requestContent);

        if (!response.IsSuccessStatusCode)
        {
            Console.WriteLine($"Error: {await response.Content.ReadAsStringAsync()}");
            return;
        }

        var jsonResponse = JsonSerializer.Deserialize(await response.Content.ReadAsStringAsync());
        var browserWSEndpoint = jsonResponse.GetProperty("data").GetProperty("reconnect").GetProperty("browserWSEndpoint").GetString();

        var playwright = await Playwright.CreateAsync();
        var browser = await playwright.Chromium.ConnectOverCDPAsync(browserWSEndpoint + $"?token={TOKEN}");
        var context = browser.Contexts[0];
        var page = context.Pages[0];

        Console.WriteLine("Taking screenshot...");
        await page.ScreenshotAsync(new PageScreenshotOptions { Path = "screenshot.png" });

        await browser.CloseAsync();
        Console.WriteLine("Done!");
    }
}
```
      
    
  

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>403 Forbidden</code> error?</summary>

Your API token is missing or expired. Pass it as a `?token=` query parameter in the WebSocket or HTTP URL. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My script works locally but fails on Browserless</summary>

Local browser settings may differ from the Browserless environment. Use `launch` parameters to match your local setup (viewport, user agent, timezone). See [launch parameters](/baas/launch-options) for the full list.

</details>

## Next steps

  <a href="/baas/bot-detection/stealth" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Stealth Routes</h3>
      Start here: advanced fingerprint mitigations with a simple connection URL change
    
  </a>

  <a href="/baas/bot-detection/captchas" className="next-step-card-link">
    
      <h3 className="next-step-card-title">CAPTCHA Solving</h3>
      Automatically detect and solve CAPTCHAs that appear after unblocking
    
  </a>

  <a href="/rest-apis/unblock" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Unblock API Reference</h3>
      Full parameter and response documentation for the /unblock endpoint
    
  </a>

---

# User Agent Masking

URL: https://docs.browserless.io/baas/bot-detection/user-agent-masking

The User-Agent header identifies your browser's engine, operating system, and version to websites. Sites use it to adjust rendering, load web fonts, and run basic bot detection checks. In headless automation, the default user agent often signals that you're running a bot.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Puppeteer or Playwright installed locally

## Indirect vs. direct user agent changes

Directly overriding the user agent string in code is increasingly unreliable. Sites cross-check it against other signals: TLS fingerprint, canvas fingerprint, WebGL renderer, and JavaScript APIs. A UA string claiming to be Chrome on Windows while exposing a Linux canvas fingerprint is a common detection pattern.

A more reliable approach is to choose the right browser binary. Each binary ships with a user agent that matches its actual capabilities, so there's no mismatch for fingerprinting tools to catch:

| Binary | User Agent style | Notes |
|--------|-----------------|-------|
| Chromium | `Chrome/...` | Default; open-source, no proprietary codecs |
| Chrome | `Chrome/...` | Proprietary codecs, closer to real-browser fingerprint |
| Firefox | `Firefox/...` | Gecko engine; Playwright only |
| WebKit | `AppleWebKit/...` | Safari engine; Playwright only |

For the full browser support matrix and when to use each binary, see [Supported Browsers](/baas/infrastructure/supported-browsers).

### Disable headless mode

Adding `&headless=false` to your connection URL also changes the user agent. By default, Browserless runs in headless mode, which exposes `HeadlessChrome` in the UA string. Running with `headless=false` removes that signal.

**Default (headless):**
```text
Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/120.0.0.0 Safari/537.36
```

**With `headless=false`:**
```text
Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36
```

Connect with headless disabled:
```bash
wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&headless=false
```

For fingerprint-level bot detection bypass, use [Stealth routes](/baas/bot-detection/stealth) or [BrowserQL](/browserql/start).

## Set the user agent in code

Use this when you need a specific UA string, for example to test mobile rendering or match a target site's expected browser. Keep in mind that an overridden UA can still be detected through fingerprinting if other browser signals don't match.

  
**Puppeteer:**

```js
import puppeteer from "puppeteer-core";

const browser = await puppeteer.connect({
  browserWSEndpoint: `wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE`,
});
const page = await browser.newPage();
await page.setUserAgent(
  "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
);
```
  
  
**Playwright:**

    
      
**Javascript:**

```js
import { chromium } from "playwright-core";

const browser = await chromium.connectOverCDP(
  `wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE`
);

const context = browser.contexts()[0] || (await browser.newContext());
const page = await context.newPage();

await context.setExtraHTTPHeaders({
  'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36'
});
```
      
      
**Python:**

```python
import asyncio
from playwright.async_api import async_playwright

async def main():
    async with async_playwright() as p:
        browser = await p.chromium.connect_over_cdp(
            "wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE"
        )

        context = browser.contexts[0] if browser.contexts else await browser.new_context()
        page = await context.new_page()

        await context.set_extra_http_headers({
            "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
        })

asyncio.run(main())
```
      
      
**Java:**

```java
import com.microsoft.playwright.*;

public class PlaywrightExample {
    public static void main(String[] args) {
        try (Playwright playwright = Playwright.create()) {
            Browser browser = playwright.chromium().connectOverCDP(
                "wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE"
            );

            BrowserContext context = browser.contexts().isEmpty()
                ? browser.newContext()
                : browser.contexts().get(0);

            context.setExtraHTTPHeaders(java.util.Map.of(
                "user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
            ));
        }
    }
}
```
      
      
**C#:**

```csharp
using System;
using System.Threading.Tasks;
using Microsoft.Playwright;

class Program
{
    static async Task Main(string[] args)
    {
        var playwright = await Playwright.CreateAsync();
        var browser = await playwright.Chromium.ConnectOverCDPAsync(
            "wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE"
        );

        var context = browser.Contexts.Count > 0
            ? browser.Contexts[0]
            : await browser.NewContextAsync();

        await context.SetExtraHTTPHeadersAsync(new Dictionary<string, string>
        {
            ["user-agent"] = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
        });
    }
}
```
      
    
  

## Common user agent strings

**Chrome Desktop:**
```text
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
```

**Safari Desktop:**
```text
Mozilla/5.0 (Macintosh; Intel Mac OS X 14_3_1) AppleWebKit/605.1.15
```

**Mobile Chrome:**
```text
Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15
```

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>403 Forbidden</code> error?</summary>

Your API token is missing or expired. Pass it as a `?token=` query parameter in the WebSocket or HTTP URL. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My script works locally but fails on Browserless</summary>

Local browser settings may differ from the Browserless environment. Use `launch` parameters to match your local setup (viewport, user agent, timezone). See [launch parameters](/baas/launch-options) for the full list.

</details>

## Next steps

  <a href="/baas/bot-detection/stealth" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Stealth Routes</h3>
      Use stealth browser routes for fingerprint-level bot detection bypass
    
  </a>

  <a href="/baas/bot-detection/captchas" className="next-step-card-link">
    
      <h3 className="next-step-card-title">CAPTCHA Solutions</h3>
      Automatically solve CAPTCHAs and other challenge-response systems
    
  </a>

  <a href="/baas/infrastructure/supported-browsers" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Supported Browsers</h3>
      Compare browser binaries and choose the right one for your use case
    
  </a>

---

# Connection URL Patterns

URL: https://docs.browserless.io/baas/connection-url-patterns

# Connection URL Patterns

Every browser connection to Browserless (Puppeteer, Playwright, or BrowserQL) uses a WebSocket URL; the [REST APIs](/rest-apis/intro) use an HTTPS URL to the same regional hosts. Either way there's no account-specific URL to look up: you build the URL yourself from a regional endpoint, a path, and your API token, and any region works with any account. This page covers all valid WebSocket paths and explains when to use each one.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Connection URL Builder

## Complete URL Anatomy

A full connection URL combines a regional endpoint, a path, your API token, and any launch options:

```bash
wss://production-sfo.browserless.io/chromium/playwright?token=TOKEN&blockAds=true
```

| Component | Example value | Description |
|-----------|---------------|-------------|
| Protocol | `wss://` | WebSocket Secure, required for all connections |
| Regional endpoint | `production-sfo.browserless.io` | Choose the [region](#regional-endpoints) closest to your target |
| Path | `/chromium/playwright` | Browser and protocol mode |
| Auth | `token=TOKEN` | Your API token |
| Launch options | `blockAds=true` | Query parameters that configure the browser |

See [Launch parameters](/baas/launch-options) for the full list of query parameters.

## Connection Paths

All paths accept WebSocket connections. Paths are case-sensitive.

### CDP

CDP is the default connection mode. Use it with Puppeteer, Playwright in CDP mode, or any library that speaks chrome-devtools-protocol. Connect to `/chromium` or `/chrome`; both work with the same client code. Choose CDP for the widest library compatibility.

| Path | Browser | Notes |
|------|---------|-------|
| `/` | Chromium | Default; equivalent to `/chromium` |
| `/chromium` | Chromium | Open-source Chromium build |
| `/chrome` | Chrome | Licensed Google Chrome binary |

### Playwright Native

Playwright Native uses Playwright's own browser server protocol instead of CDP. Use it when you need Playwright-specific APIs: `page.route()` for network interception, `APIRequestContext`, or multi-browser support with Firefox and WebKit. Connect using `playwright.connect({ wsEndpoint })` with a `/chromium/playwright`, `/firefox/playwright`, or `/webkit/playwright` path.

| Path | Browser | Notes |
|------|---------|-------|
| `/chromium/playwright` | Chromium | Playwright native protocol on Chromium |
| `/chrome/playwright` | Chrome | Playwright native protocol on Chrome |
| `/firefox/playwright` | Firefox | Playwright native protocol on Firefox |
| `/webkit/playwright` | WebKit | Playwright native protocol on WebKit/Safari |
| `/edge/playwright` | Edge | Playwright native protocol on Microsoft Edge |

For a feature comparison of `connect` vs `connectOverCDP`, see [connect vs connectOverCDP](/baas/advanced-configurations/playwright-customizations#connect-vs-connectovercdp).

### Stealth

Stealth routes layer anti-detection hardening on top of the browser. Use stealth when the target site has bot detection. `/stealth` provides the strongest baseline with a purpose-hardened browser. `/chromium/stealth` and `/chrome/stealth` add anti-detection to your chosen browser binary. See [Stealth Routes](/baas/bot-detection/stealth) for guidance on choosing the right variant.

| Path | Browser | Notes |
|------|---------|-------|
| `/stealth` | Stealth browser | Purpose-hardened binary; highest evasion baseline |
| `/chromium/stealth` | Chromium | Chromium with anti-detection layer |
| `/chrome/stealth` | Chrome | Chrome with anti-detection layer |

Any stealth route also accepts the [`emulationOs`](/baas/bot-detection/os-emulation) query parameter (`windows`, `macos`, or `linux`) to present a coherent OS fingerprint instead of the default Linux identity.

> **Chrome vs Chromium**
> `/chrome` and `/chromium` paths use different browser binaries but share the same feature set: CDP proxy, session replay, captcha solving, and proxy support all work on both. Chrome uses the licensed Google Chrome binary, which includes proprietary codec and DRM support. Chromium uses the open-source build, which does not. For most automation tasks, either works identically.

## Regional Endpoints

Browserless runs in multiple regions. Use the endpoint closest to your target sites or your own infrastructure to reduce latency. All paths and protocol modes work identically across regions.

| Region | Endpoint |
|--------|----------|
| US West (San Francisco) | `production-sfo.browserless.io` |
| Europe (London) | `production-lon.browserless.io` |
| Europe (Amsterdam) | `production-ams.browserless.io` |

## FAQ & Troubleshooting

<details>
<summary>Where do I find my WebSocket URL, endpoint, or region?</summary>

There's nothing to find: the URL isn't account-specific, so it doesn't appear in the dashboard. Build it yourself by picking any [regional endpoint](#regional-endpoints) and appending a path and your API token:

```bash
wss://production-sfo.browserless.io/chromium/playwright?token=YOUR_API_TOKEN_HERE
```

Your account has no assigned region; pick whichever is closest to you or your target sites. The only account-specific value is the token, which lives in your [account dashboard](https://browserless.io/account/). If a tool asks for a "base URL" or "host" without the `wss://` protocol, use `production-sfo.browserless.io` (or your chosen region).

</details>

<details>
<summary>Why am I getting <code>401 Unauthorized: Invalid API key</code>?</summary>

The token in your URL isn't valid: it's missing, mistyped, truncated, regenerated since you copied it, or from a different account. Copy the current token from your [account dashboard](https://browserless.io/account/) and pass it as `?token=` in the URL. If your client strips query parameters (some frameworks do), check the final URL it actually dials.

</details>

<details>
<summary>My tool or template uses <code>wss://chrome.browserless.io</code> and won't connect</summary>

That's the legacy v1 domain, and many older tutorials, n8n templates, and code generators still emit it. Replace it with a current regional endpoint, keeping your token:

```bash
# Old (v1, no longer works for v2 accounts)
wss://chrome.browserless.io?token=YOUR_API_TOKEN_HERE

# New
wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE
```

See the [migration guide](/baas/migrate) for the other v1-to-v2 changes.

</details>

<details>
<summary>Is there an Asia-Pacific region?</summary>

Not currently. The regions are San Francisco, London, and Amsterdam; measure latency from your own infrastructure against all three and pick the fastest. If what you actually need is requests exiting from a specific country (say, India), that's a proxy setting rather than a region: use the [residential proxy](/baas/bot-detection/proxies) with `proxyCountry`.

</details>

<details>
<summary>Why am I getting a <code>403 Forbidden</code> error?</summary>

Your API token is missing or expired. Pass it as a `?token=` query parameter in the WebSocket or HTTP URL. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My script works locally but fails on Browserless</summary>

Local browser settings may differ from the Browserless environment. Use `launch` parameters to match your local setup (viewport, user agent, timezone). See [launch parameters](/baas/launch-options) for the full list.

</details>

## Next steps

  <a href="/baas/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch Options</h3>
      Customize browser behavior with query parameters
    
  </a>

  <a href="/baas/session-management" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Session Management</h3>
      Reuse and persist browser sessions across connections
    
  </a>

  <a href="/baas/bot-detection/stealth" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Stealth Routes</h3>
      Choose the right stealth configuration for bot detection evasion
    
  </a>

---

# 1Password

URL: https://docs.browserless.io/baas/features/1password

# 1Password

A 1Password service account lets a browser session fill secrets (usernames, passwords, API keys) straight from your vault **without the value ever reaching your automation code**. It can also fetch and fill an [emailed one-time code](#filling-an-emailed-one-time-code) for sites that gate login on email 2FA. Browserless resolves the reference server-side, types it into the target field, and then locks the session down so the filled secret can't be read back or captured.

**1Password** is the first supported provider. You connect a 1Password [service account](https://developer.1password.com/docs/service-accounts/) once, then reference items by their `op://Vault/Item/field` path from inside a session.

This is useful when:

- You need to log in with real credentials but don't want them in your code, environment variables, or logs.
- You rotate secrets in 1Password and want every session to use the current value automatically.
- You need an auditable record of every credential use, without the secret itself ever being recorded.

> **1Password vs Authenticated Profiles**
> [Authenticated Profiles](/baas/features/authenticated-profiles) replay a *previously captured* logged-in state. 1Password fills live credentials to log in *fresh* each time. Use a profile to skip the login; use 1Password when you must actually authenticate with current secrets. The two can be combined.

- A Browserless API token (available in your [account dashboard](https://browserless.io/account/))
- A 1Password [service account](https://developer.1password.com/docs/service-accounts/) with read access to the vault/items you want to use
- A CDP client like Puppeteer or Playwright installed

## How it works

A service account stores its 1Password token (encrypted at rest) and an optional domain allow-list, scoped to your API token. Inside a session you reference a secret by its `op://` path and Browserless does the rest:

1. **Resolve** — Browserless reads the referenced item from 1Password server-side using the stored service-account token.
2. **Fill** — the resolved value is typed into the element you point at. The secret is **never** returned to your code, and never appears in CDP traffic, logs, or error messages.
3. **Lock down** — once any secret has been filled, the session refuses to expose the page (see [Security model](#security-model)), so the typed value can't be screenshotted or read back.
4. **Audit** — the resolve is recorded against the service account: which `op://` reference, on what origin, success or failure — but never the value.

> **Secrets stay server-side**
> Your automation only ever sends a *reference* (`op://Vault/Item/field`) and a target selector. The plaintext credential is resolved and typed inside Browserless; it never crosses back to the client.

## Adding a service account

> **What these are called**
> In the dashboard and this guide these are **1Password service accounts** (**Profiles → 1Password Service Accounts**). The REST API names the same object with `integrationId` and the `/integrations/onepassword` path.

  
**Dashboard:**

The quickest way is the **Profiles** page in your Browserless dashboard, under **1Password Service Accounts**:

In 1Password, create a [service account](https://developer.1password.com/docs/service-accounts/manage-service-accounts/) and grant it **read** access to only the vault(s) holding the items you'll reference. Scope it as narrowly as possible — Browserless can resolve anything this service account can read. Copy the generated `ops_…` token.
Go to **Profiles** in the dashboard sidebar. Under **1Password Service Accounts**, choose **New Service Account**. Pick the **Provider** (1Password), then paste the **service-account token** (`ops_…`) and a **Label** to recognise it later. The token is encrypted at rest and is never shown again.
By default the service account may fill on **any** site. To restrict it, turn on **domain limiting** and add the allowed origins (for example `https://app.example.com`; an entry also matches its subdomains). Either way, secrets are only ever typed into public `https://` origins — `http://` and private/internal addresses are always refused.

  
  
**API:**

You can also add a service account over the REST API. The service-account token is encrypted at rest and is **never** returned by this or any other endpoint.

```bash
curl -X POST "https://production-sfo.browserless.io/integrations/onepassword?token=YOUR_API_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "label": "acme-prod",
    "serviceAccountToken": "ops_REPLACE_WITH_YOUR_SERVICE_ACCOUNT_TOKEN",
    "allowedDomains": ["https://app.example.com"]
  }'
```

The response is the service-account record — note its `id`, you'll pass it when connecting:

```json
{
  "id": "op_int_0a1b2c3d4e5f6a7b8c9d0e1f",
  "label": "acme-prod",
  "kind": "service_account",
  "allowedDomains": ["https://app.example.com"],
  "connectUrl": null,
  "expiresAt": null,
  "lastResolvedAt": null,
  "createdAt": "2026-06-18T12:00:00.000Z",
  "updatedAt": "2026-06-18T12:00:00.000Z"
}
```

- **`id`** — the service account's identifier, passed as `?integrationId=` when connecting
- **`allowedDomains`** — optional; normalised to origins. When set, fills are limited to these origins and their subdomains. Omit it (or pass `[]`) to allow any public `https://` origin — the default
- The response carries **no token material** — the service-account token can't be read back once stored

## Filling a secret in a session

Connect with `?integrationId=<id>`, navigate to the target page, then call the `Browserless.loadSecret` CDP method with the `op://` reference and the field to fill.

> **CDP client required**
> `Browserless.loadSecret` is a CDP method, so use a CDP-capable client (Puppeteer, Playwright, or raw CDP).

  
**Puppeteer:**

```js

const TOKEN = 'YOUR_API_TOKEN_HERE';
const INTEGRATION_ID = 'op_int_0a1b2c3d4e5f6a7b8c9d0e1f';

const browser = await puppeteer.connect({
  browserWSEndpoint: `wss://production-sfo.browserless.io/chromium?token=${TOKEN}&integrationId=${INTEGRATION_ID}`,
});

const page = await browser.newPage();
await page.goto('https://app.example.com/login');

const cdp = await page.createCDPSession();

// Resolve op://Vault/Item/field from 1Password and type it into the field.
await cdp.send('Browserless.loadSecret', {
  ref: 'op://Acme/login/username',
  targetSelector: '#email',
});
await cdp.send('Browserless.loadSecret', {
  ref: 'op://Acme/login/password',
  targetSelector: '#password',
});

await page.click('button[type="submit"]');
await page.waitForNavigation();

await browser.close();
```

  
  
**Playwright:**

```js

const TOKEN = 'YOUR_API_TOKEN_HERE';
const INTEGRATION_ID = 'op_int_0a1b2c3d4e5f6a7b8c9d0e1f';

const browser = await chromium.connectOverCDP(
  `wss://production-sfo.browserless.io/chromium?token=${TOKEN}&integrationId=${INTEGRATION_ID}`,
);
const page = browser.contexts()[0].pages()[0] ?? (await browser.contexts()[0].newPage());
await page.goto('https://app.example.com/login');

const cdp = await page.context().newCDPSession(page);
await cdp.send('Browserless.loadSecret', {
  ref: 'op://Acme/login/username',
  targetSelector: '#email',
});
await cdp.send('Browserless.loadSecret', {
  ref: 'op://Acme/login/password',
  targetSelector: '#password',
});

await page.click('button[type="submit"]');
await browser.close();
```

  
  
**BrowserQL:**

`loadSecret` is also a BrowserQL mutation. Pass `integrationId` on the endpoint, then resolve and fill within the same query — alias one call per field. The BQL mutation uses `selector` (the CDP method uses `targetSelector`).

```bash
curl -X POST "https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE&integrationId=op_int_0a1b2c3d4e5f6a7b8c9d0e1f" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "mutation Login { goto(url: \"https://app.example.com/login\") { status } username: loadSecret(ref: \"op://Acme/login/username\", selector: \"#email\") { ok error } password: loadSecret(ref: \"op://Acme/login/password\", selector: \"#password\") { ok error } }"
  }'
```

Each `loadSecret` field returns `{ ok, error }`; the resolved value is never in the response. `ref` may be an `op://Vault/Item/field` reference; `selector` is optional (the focused element is used if omitted).

  

`loadSecret` returns `{ ok: true }` on success, or `{ ok: false, error: '<code>' }` on failure (it never throws). See [Failure modes](#failure-modes) for each error code and how to handle it.

:::success The credential is filled
The field now holds the secret value, but your code never saw it. Submit the form as usual.
:::

## Filling an emailed one-time code

Some sites send a one-time code (OTP) by email as a second factor. `loadSecret` can fetch that code from the inbox and type it in for you. It's read over IMAP **inside Browserless**, so like every other secret it's never returned to your automation, logged, or captured.

Instead of an `op://` reference, pass a `mail://` one that points at the inbox:

```text
mail://<vault>/<item>/otp
```

`<vault>`/`<item>` name a **1Password Login item that stores the inbox's own IMAP credentials**. That's the mailbox the codes are delivered to, not the site you're signing in to. Browserless reads that item with your connected service account, signs in to the mailbox over IMAP, waits for the code to arrive, extracts it, and fills your selector.

> **Use a dedicated inbox**
> Point automation at a mailbox reserved for it, not a personal one. The resolver reads the newest message on each call, so a busy human inbox invites false matches. Any IMAP provider that supports implicit TLS on port 993 works, and Gmail is supported and tested.

### Store the inbox as a 1Password Login item

In a vault your [service account](#adding-a-service-account) can read, add a **Login** item whose fields are the inbox's IMAP credentials:

| Field | Value |
|---|---|
| `username` | The inbox address, e.g. `otp-bot@example.com` |
| `password` | An IMAP **app password** for that inbox, not your normal login password |
| `host` *(optional)* | IMAP host for a non-Gmail provider. Defaults to `imap.gmail.com` |
| `port` *(optional)* | IMAP port. Defaults to `993`, implicit TLS |

For Gmail, leave `host`/`port` off and create an [app password](https://support.google.com/accounts/answer/185833) with IMAP access.

### Fill the code

Trigger the site's "email me a code" step, then call `loadSecret` with the `mail://` reference and the code field. As with any secret you only ever see `{ ok, error }`, never the code itself.

  
**Puppeteer:**

```js
const cdp = await page.createCDPSession();

// Submit the login form first so the site emails a code, then fetch it.
await cdp.send('Browserless.loadSecret', {
  ref: 'mail://Acme/login-inbox/otp?from=no-reply@example.com',
  targetSelector: '#otp',
});
await page.click('button[type="submit"]');
```

  
  
**BrowserQL:**

```graphql
mutation Verify {
  # Trigger the code first by clicking the site's "email me a code" button.
  otp: loadSecret(ref: "mail://Acme/login-inbox/otp", selector: "#otp") {
    ok
    error
  }
}
```

  

Tune the lookup with query parameters on the reference:

| Option | Meaning |
|---|---|
| `?wait=<ms>` | How long to wait for a fresh code. Default `45000`, clamped to `5000`–`120000` |
| `?from=<substring>` | Only accept a message whose sender contains this substring |
| `?pattern=<regex>` | Custom extraction regex. Its first capture group, or the whole match, is the code. Without it a 4–8 digit numeric code is detected automatically |

Combine them like any query string, for example `mail://Acme/login-inbox/otp?from=no-reply@example.com&wait=60000`. Because `from` and `pattern` ride in the query string, percent-encode any special characters in their values. An unencoded `&` cuts the value short and a `+` is read as a space, so a `\d+` regex must be written `\d%2B`.

> **Only codes that arrive after you call it**
> `loadSecret` marks the inbox the moment it's called and accepts only a message that arrives **after** that. A stale code already sitting in the inbox is never reused, so trigger the "send code" step first, then call `loadSecret` promptly. It polls until the new code lands or `wait` runs out.

> **Give the session longer than the wait window**
> The poll runs inside the session, so connect with a `?timeout=` **larger than** `wait`. Otherwise the session limiter can close the socket mid-poll. If no fresh code arrives in time, `loadSecret` returns `error: "OtpTimeout"`. See [Failure modes](#failure-modes).

Filling an emailed code is a secret fill like any other, so the [session lockdown](#security-model) applies the moment it succeeds. [Autologin](/baas/features/autologin) can perform this step for you: select an **Email OTP inbox** per site or apply one inbox to all selected sites. Candidates are Login items with username/password fields, including items without a website. The agent uses a separate `emailOtp` credential after triggering delivery; TOTP remains a distinct factor. Gmail is tested; other IMAP providers are best-effort. SMS and push approvals still require manual completion.

## Security model

Once a secret has been filled in a session, Browserless treats the page as sensitive and **disables every channel that could read the value back**:

| Channel | Behaviour after a fill |
|---|---|
| Screenshots / PDFs / screencasts | Blocked (`CaptureBlocked`) |
| Live URLs (and their live-view input-event stream) | New connections refused; an already-streaming live view is torn down |
| Page-content reads (BQL `evaluate`, `html`, `text`, `querySelector`, `cookies`) | Blocked (`CaptureBlocked`) |
| Screen recording (`Browserless.startRecording`) | An in-progress recording is **aborted and discarded** on the first fill |
| Session replay (rrweb — DOM mutations) | Keeps recording, but the filled values are **scrubbed** from the replay before it's stored |

So the typed secret can't be screenshotted, captured on video, streamed to a live viewer, or read back — and any filled value is redacted from the rrweb session replay, logs, and CDP traces before they're persisted (raw and in common encodings).

> **Capture before you fill**
> If you need a screenshot, PDF, or page content, take it **before** the first `Browserless.loadSecret` call. After a fill these are rejected for the rest of the session.

> **Recommended: log in once, then reuse a profile**
> The lockdown is **per session**, so the cleanest way to get both secure login *and* unrestricted screenshots/PDFs is to combine 1Password with [Authenticated Profiles](/baas/features/authenticated-profiles): in **one** session fill your credentials, sign in, then capture the signed-in state with `Browserless.saveProfile`. Every later session starts already logged-in via `?profile=<name>` — no secret is filled there, so captures and page reads work normally.
> 
> Don't want to script the login yourself? [**Autologin**](/baas/features/autologin) runs this entire one-time step for you — point it at a service account and it signs in to your allowed domains and saves the profile automatically.
> 
> ```js
> // One-time: fill credentials from 1Password, sign in, save the auth state.
> const cdp = await page.createCDPSession();
> await cdp.send('Browserless.loadSecret', { ref: 'op://Acme/login/username', targetSelector: '#email' });
> await cdp.send('Browserless.loadSecret', { ref: 'op://Acme/login/password', targetSelector: '#password' });
> await page.click('button[type="submit"]');
> await page.waitForNavigation();
> await cdp.send('Browserless.saveProfile', { name: 'acme-prod' });
> 
> // Everywhere after: reuse the profile — no fill, no lockdown, screenshots work.
> // wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&profile=acme-prod
> ```
> 
> This keeps secrets out of all but the initial login, and the service account stays the single source of truth when credentials rotate — just re-run the one-time step to refresh the profile.

Other guarantees:

- **Origin safety** — a secret is only ever typed into a public `https://` origin; `http://` and private/internal addresses are always refused. A service account can optionally set an allow-list to restrict fills to specific origins (and their subdomains); with no list — the default — any public https origin is allowed.
- **No leakage of the value** — the plaintext is never returned to the client and is scrubbed from logs and error messages.
- **Encrypted at rest** — the service-account token is encrypted and never returned by any API.
- **Audit log** — every resolve is recorded with the `op://` reference, target origin, and outcome — but never the secret value.

## Failure modes

When `loadSecret` returns `{ ok: false, error }`, `error` is one of these codes. Resolve/service-account errors:

| Code | Meaning | What to do |
|---|---|---|
| `InvalidToken` | 1Password rejected the service-account token. | Check the token; replace it if it is invalid or revoked, then update the service account. |
| `BadCredentialReference` | The alias or reference is unknown, malformed, ambiguous, or uses unsupported field options. | Correct the alias or `op://` reference; use unique vault/item/field identifiers and valid field options. |
| `VaultNotFound` | No vault visible to the service account matches the reference. | Check the vault name or ID and the service account's access; retrying the same reference won't fix it. |
| `ItemNotFound` | No item matches the reference in the vault. | Check the item name or ID and its location in the vault. |
| `FieldNotFound` | The referenced field or section was not found in the item. | Check the field and section identifiers. |
| `VaultUnreachable` | A network, timeout, or rate-limit failure prevented the 1Password request. | Retry with backoff; if it persists, check 1Password's status. |
| `UnknownResolveError` | The lookup failed without a recognized cause. | Do not assume the failure is retryable; check the integration and reference, and contact support if it persists. |
| `CredentialNotResolved` | Catch-all for a credential that could not be resolved, such as missing session credential context or an empty reference. | Check the session's service-account configuration and supply a reference. |
| `DomainNotAllowed` | The page's origin isn't a permitted fill target — it isn't `https://`, it resolves to a private/internal address, or the service account has an allow-list the origin isn't on. | Use a public https origin; if the service account sets an allow-list, add the origin to it. |
| `CredentialIntegrationExpired` | The Browserless integration has expired or is no longer available, for example after removal during a session. | Reconnect using an available, unexpired integration. A token rejected by 1Password is reported separately as `InvalidToken`. |
| `OtpTimeout` | No fresh emailed code (a `mail://` reference) arrived within the `wait` window. | Trigger the site's "send code" step right before calling `loadSecret`; raise `?wait=` and the session `?timeout=`. Check any `from`/`pattern` filter and that codes actually reach the inbox. |

Fill-target errors (the credential resolved, but it couldn't be typed in):

| Code | Meaning | What to do |
|---|---|---|
| `SelectorNotFound` | The `selector` (`targetSelector` over CDP) matched no element. | Check the selector and that the field has rendered. |
| `TargetNotFillable` | The target isn't a fillable `<input>` / `<textarea>`. | Point at a fillable element. |
| `NoFocusedElement` | No selector was given and nothing is focused. | Pass a selector, or focus an input first. |
| `AuditWriteFailed` | The fill was refused because its audit record couldn't be written (fail-closed). | Retry; if it persists, contact support. |

## Managing service accounts

Your 1Password service accounts are managed through the same REST surface, scoped to your API token.

### List service accounts

```bash
curl "https://production-sfo.browserless.io/integrations/onepassword?token=YOUR_API_TOKEN_HERE"
```

Returns an array of service-account records (no token material). Paginate with `limit` and `offset`.

### Get one service account

```bash
curl "https://production-sfo.browserless.io/integrations/onepassword/op_int_0a1b2c3d4e5f6a7b8c9d0e1f?token=YOUR_API_TOKEN_HERE"
```

### View the audit log

```bash
curl "https://production-sfo.browserless.io/integrations/onepassword/op_int_0a1b2c3d4e5f6a7b8c9d0e1f/audit?token=YOUR_API_TOKEN_HERE"
```

Returns one entry per credential resolve — the `op://` reference, target origin and selector, and the outcome. Secret values are never stored; the dashboard shows the same log for each service account.

### Delete a service account

```bash
curl -X DELETE "https://production-sfo.browserless.io/integrations/onepassword/op_int_0a1b2c3d4e5f6a7b8c9d0e1f?token=YOUR_API_TOKEN_HERE"
```

Deletion is permanent; the encrypted service-account token is removed.

## FAQ & Troubleshooting

<details>
<summary>Can my automation read the secret value?</summary>

No. You send a reference (`op://Vault/Item/field`) and a target selector; Browserless resolves and types the value inside the session. It is never returned to the client, and after the first fill the page-read and capture channels are disabled so it can't be extracted.

</details>

<details>
<summary>Why is my screenshot/<code>evaluate</code> returning <code>CaptureBlocked</code>?</summary>

Because a secret has already been filled in that session. Capture and page-content reads are disabled after the first `Browserless.loadSecret` to prevent the value from leaking. Run any captures **before** filling, or in a separate session.

</details>

<details>
<summary>Why did <code>loadSecret</code> return <code>ok: false</code>?</summary>

Common causes: the `targetSelector` didn't match an element on the page, the page isn't a permitted origin (not `https://`, a private address, or not on the service account's allow-list when one is set), or the `op://` reference doesn't resolve (wrong vault/item/field, or the service account lacks access).

</details>

<details>
<summary>Can it fill an emailed two-factor code?</summary>

Yes. Reference the inbox with a `mail://Vault/Item/otp` reference instead of an `op://` one, where the item is a Login holding the inbox's IMAP credentials. Browserless waits for the newest code to arrive, extracts it, and fills your selector. The code is never returned to your automation. See [Filling an emailed one-time code](#filling-an-emailed-one-time-code).

</details>

<details>
<summary>How is this different from Authenticated Profiles?</summary>

A [profile](/baas/features/authenticated-profiles) replays a previously captured logged-in state (cookies/storage). 1Password logs in fresh by filling live credentials. Use a profile to skip login entirely; use 1Password when you must authenticate with current secrets — for example when the session needs to perform a real sign-in. They can be used together.

</details>

<details>
<summary>Why am I getting a <code>403 Forbidden</code> error?</summary>

Your API token is missing or expired. Pass it as a `?token=` query parameter in the WebSocket or HTTP URL. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My script works locally but fails on Browserless</summary>

Local browser settings may differ from the Browserless environment. Use `launch` parameters to match your local setup (viewport, user agent, timezone). See [launch parameters](/baas/launch-options) for the full list.

</details>

## Further reading

- [Autologin](/baas/features/autologin) – let Browserless sign in with your 1Password credentials and save a reusable profile, no CDP scripting
- [Authenticated Profiles](/baas/features/authenticated-profiles) – reuse a captured logged-in state instead of re-authenticating
- [1Password service accounts](https://developer.1password.com/docs/service-accounts/) – create and scope the token this feature uses
- [1Password secret references](https://developer.1password.com/docs/cli/secret-references/) – the `op://Vault/Item/field` syntax
- [Connection URL Patterns](/baas/connection-url-patterns) – full reference for connect-time query parameters

## Next steps

  <a href="/baas/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BaaS Quickstart</h3>
      get started with Puppeteer or Playwright on Browserless
    
  </a>
  <a href="/baas/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure browser behavior and environment
    
  </a>

---

# Authenticated Profiles

URL: https://docs.browserless.io/baas/features/authenticated-profiles

# Authenticated Profiles

Authenticated profiles let you log in to a website **once** and reuse that signed-in state across many parallel browser sessions. Once the profile is captured, any session that passes parameter `?profile=<profile-name>` automatically restores the cookies, `localStorage`, and `IndexedDB` entries without requiring you to log in again.

This is useful when:

- Your scraper or agent needs to act as a logged-in user, but you don't want to re-authenticate on every run.
- You want to share an authenticated state across a fleet of workers without distributing credentials.
- You're running multi-step flows (paginated dashboards, account-only endpoints) where logging in each time is the slowest part.

- A Browserless API token (available in your [account dashboard](https://browserless.io/account/))
- A CDP client like Puppeteer or Playwright installed

## How it works

A profile captures three pieces of the browser state from a live session and stores them under a name scoped to your API token:

- **Cookies** — including HttpOnly, Secure, and SameSite attributes
- **`localStorage`** — for every origin the session touched
- **`IndexedDB`** — databases, object stores, and entries

> **Why is `sessionStorage` not stored?**
> `sessionStorage` is excluded because its values only exist for the duration of a single browser tab. Restoring them in a new session would inject stale data that breaks OAuth redirects, CSRF tokens, and other short-lived flows.

> **Sessions are isolated**
> Each time you pass `?profile=<profile-name>`, Browserless restores the saved profile state before your code runs. Any changes during the session stays local to that session and won't affect the original profile.

## Creating a profile

  
**Dashboard:**

Open [Profiles](https://www.browserless.io/account/profiles) in the Browserless dashboard and click **New Profile** under **Authenticated Profiles**.
Select **Sign in via remote browser**. Enter a unique profile **Name** and, optionally, the **URL** where you want the browser to open. Under **Advanced**, you can also configure a proxy for the login session.

Click **Continue** to start the profile session.
Click **Open auth flow in new tab**, then sign in through the remote browser. Complete any MFA, CAPTCHA, or magic-link steps before saving.

Click **Save profile** in the remote browser toolbar. You can also return to the setup dialog and click **Done**. The saved profile appears under **Authenticated Profiles** and is ready to use with `?profile=<profile-name>`.

  
  
**API / CDP:**

> **CDP client required**
> Creation needs a CDP-capable client (Puppeteer, Playwright, or raw CDP) because `Browserless.saveProfile` is a CDP method, not a BrowserQL mutation. Once a profile exists, BrowserQL can [use it](#using-a-profile) freely via `?profile=<profile-name>`.

Call `POST /profile` with a name for the profile:

```bash
curl -X POST "https://production-sfo.browserless.io/profile?token=YOUR_API_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{ "name": "acme-prod" }'
```

The response contains the WebSocket URL under the `connect` key:

```json
{
  "id": "<temporary-session-id>",
  "name": "acme-prod",
  "connect": "wss://production-sfo.browserless.io/session/connect/<id>?token=...",
  "stop": "https://production-sfo.browserless.io/session/<id>?token=..."
}
```

- **`id`** — Unique identifier for the temporary creation session
- **`name`** — The profile name
- **`connect`** — WebSocket URL, this will be passed as `browserWSEndpoint` in Puppeteer or Playwright
- **`stop`** — call `DELETE` to this URL to terminate the creation session early

Use the `connect` URL returned from the previous step to attach Puppeteer to the running browser, then navigate to your site and complete the login:

```js {3}

const browser = await puppeteer.connect({ browserWSEndpoint: session.connect });

const page = await browser.newPage();
await page.goto('https://app.example.com/login');
await page.type('#email', 'me@example.com');
await page.type('#password', process.env.PASSWORD);
await page.click('button[type="submit"]');
await page.waitForNavigation();
```

> **Dealing with CAPTCHAs or MFA?**
> If the site uses CAPTCHAs, MFA, or magic links, use a [live session link](/baas/monitor-sessions/hybrid-automation) to open the browser and complete the login manually instead.

Once the browser holds the authenticated state, send the `Browserless.saveProfile` CDP command. Browserless captures the cookies, `localStorage`, and `IndexedDB` from the running browser and persists them under the profile name.

```js
const cdp = await page.createCDPSession();
const result = await cdp.send('Browserless.saveProfile', { name: 'acme-prod' });

console.log(result);
// { ok: true, profileId: '<id>', name: 'acme-prod', cookieCount: 12, originCount: 1 }

await browser.close();
```

The `name` you pass to `Browserless.saveProfile` is what gets stored. That's what you'll reference as `?profile=<profile-name>` in future sessions. The `name` in the `POST /profile` request is just a label for the session and doesn't have to match.

> **Profile exceeds the limit?**
> If the profile is too large to save, the call returns `{ ok: false, error: ... }` and nothing is stored. See [Captured state is too large](#captured-state-too-large) for how to fix it.

:::success Profile is saved!
Your authenticated profile is now stored. Pass `?profile=<profile-name>` on any session to start already logged in with no credentials required.
:::

<details>
<summary>Complete end-to-end example</summary>

The full flow in a single script. This code creates a session, opens a live session link for manual login, saves the authenticated state, then reconnects with the saved profile.

**Save a profile (with human-assisted login):**

```js

const TOKEN = 'YOUR_API_TOKEN_HERE';
const ORIGIN = 'https://production-sfo.browserless.io';

// 1. Launch a creation session.
const session = await fetch(`${ORIGIN}/profile?token=${TOKEN}&timeout=120000`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'acme-prod' }),
}).then((r) => r.json());

// 2. Connect and navigate to the login page.
const browser = await puppeteer.connect({ browserWSEndpoint: session.connect });
const page = await browser.newPage();
await page.goto('https://app.example.com/login');

// 3. Open a live session link so a human can complete the login (2FA, CAPTCHA, etc.).
// Keep this shorter than the overall session so there is time to verify and save.
const cdp = await page.createCDPSession();
const { liveURL } = await cdp.send('Browserless.liveURL', { timeout: 60000 });
console.log('Complete the login here:', liveURL);

// 4. Wait for the live URL to complete, including timeout or cancellation.
await new Promise((r) => cdp.on('Browserless.liveComplete', r));

// 5. Verify the login landed on the expected page.
await page.waitForFunction(
  () => window.location.href.includes('dashboard'),
  { timeout: 10_000 }
);

// 6. Save the authenticated state.
const result = await cdp.send('Browserless.saveProfile', { name: 'acme-prod' });
console.log(result);
// { ok: true, profileId: '<id>', name: 'acme-prod', cookieCount: 12, originCount: 1 }

await browser.close();
```

> **Note for Playwright users**
> Puppeteer does not have a `page.waitForURL()` method. That API is Playwright-only. Use `page.waitForFunction` instead to wait for URL changes:
> ```js
> await page.waitForFunction(
>   () => window.location.href.includes('example.com/dashboard'),
>   { timeout: 60_000 }
> );
> ```

**Reuse the saved profile:**

```js

const TOKEN = 'YOUR_API_TOKEN_HERE';
const browser = await puppeteer.connect({
  browserWSEndpoint: `wss://production-sfo.browserless.io?token=${TOKEN}&profile=acme-prod`,
});
const page = await browser.newPage();
await page.goto('https://app.example.com/dashboard'); // already logged in

await browser.close();
```

</details>

  

## Using a profile

Once a profile exists, pass `?profile=<profile-name>` on any browser-launching request. The examples below show how to connect to Browserless with a saved profile across different clients and request types:

  
**Puppeteer:**

```js

const TOKEN = 'YOUR_API_TOKEN_HERE';
const browserWSEndpoint = `wss://production-sfo.browserless.io?token=${TOKEN}&profile=acme-prod`;

const browser = await puppeteer.connect({ browserWSEndpoint });
const page = await browser.newPage();
await page.goto('https://app.example.com/dashboard'); // already logged in
```

  
  
**Playwright:**

```js

const TOKEN = 'YOUR_API_TOKEN_HERE';
const browserWSEndpoint = `wss://production-sfo.browserless.io/chromium/playwright?token=${TOKEN}&profile=acme-prod`;

const browser = await chromium.connect(browserWSEndpoint);
const page = await browser.newPage();
await page.goto('https://app.example.com/dashboard');
```

  
  
**Python:**

```python
from playwright.sync_api import sync_playwright

TOKEN = "YOUR_API_TOKEN_HERE"
WS_ENDPOINT = (
    f"wss://production-sfo.browserless.io/chromium/playwright?token={TOKEN}&profile=acme-prod"
)

with sync_playwright() as playwright:
    browser = playwright.chromium.connect_over_cdp(WS_ENDPOINT)
    context = browser.contexts[0]
    page = context.pages[0]
    page.goto("https://app.example.com/dashboard")
    browser.close()
```

  
  
**BrowserQL:**

```bash
curl -X POST "https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE&profile=acme-prod" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "mutation { goto(url: \"https://app.example.com/dashboard\") { status } screenshot { base64 } }"
  }'
```

  
  
**REST APIs:**

`?profile=` works on the REST endpoints that launch a fresh browser per request: `/screenshot`, `/pdf`, `/unblock`, and `/chromium/export` (plus their `/chrome/*` variants). The captured state is loaded into the browser before the page is rendered, so you can take a screenshot or PDF of an authenticated page without scripting the login.

```bash
curl -X POST "https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE&profile=acme-prod" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://app.example.com/dashboard" }' \
  --output dashboard.png
```

See [Endpoint coverage](#endpoint-coverage) for the full list of supported endpoints and the handful of exceptions.

  
  
**Persisted Session:**

A profile can also be attached to a [persisted session](/baas/session-management/persisting-state). Pass `profile` in the `POST /session` body, and the captured state is loaded the first time the session's browser starts.

```js

const TOKEN = 'YOUR_API_TOKEN_HERE';
const ORIGIN = 'https://production-sfo.browserless.io';

// 1. Create a persisted session pre-loaded with a saved profile.
const session = await fetch(`${ORIGIN}/session?token=${TOKEN}`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    ttl: 1_800_000,        // session lifetime in ms (30 minutes here)
    profile: 'acme-prod',  // saved auth state, loaded on first browser launch
  }),
}).then((r) => r.json());

// 2. Connect to the session — the browser is already authenticated.
const browser = await puppeteer.connect({ browserWSEndpoint: session.connect });
const page = await browser.newPage();
await page.goto('https://app.example.com/dashboard');

// 3. Disconnect when done — the browser stays alive for the remaining TTL.
await browser.disconnect();

// 4. Later, reconnect with the same `session.connect` URL — same browser,
//    same cookies/storage you accumulated since the profile was first loaded.
```

The profile is loaded once when the session's browser first starts. After that, any state changes (new cookies set, localStorage writes) accumulate on the live browser and are *not* written back to the saved profile.

  

> **Tip**
> Profile names are scoped to your token. Different tokens can't see or use each other's profiles.

## Managing profiles

Profiles are managed through the following REST endpoints. All endpoints that return data use the same profile metadata object:

```json
{
  "id": "string",
  "name": "string",
  "cookieCount": "number",
  "originCount": "number",
  "lastUsedAt": "string | null",
  "createdAt": "string",
  "updatedAt": "string"
}
```

### List profiles

```bash
curl "https://production-sfo.browserless.io/profiles?token=YOUR_API_TOKEN_HERE"
```

Returns an array of profile metadata objects. Paginate via `limit` (default 100, capped at 1000) and `offset` (default 0).

### Get one profile

```bash
curl "https://production-sfo.browserless.io/profile/acme-prod?token=YOUR_API_TOKEN_HERE"
```

Returns a single profile metadata object.

### Rename

```bash
curl -X PUT "https://production-sfo.browserless.io/profile/acme-prod?token=YOUR_API_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{ "name": "acme-staging" }'
```

Returns the updated profile metadata object with the new name.

### Delete

```bash
curl -X DELETE "https://production-sfo.browserless.io/profile/acme-prod?token=YOUR_API_TOKEN_HERE"
```

Returns a `204 No Content` on success. Deletion is permanent and both the profile and its captured state are removed.

### Remove saved logins

Remove one or more saved login sites from a profile without deleting the profile itself. Every unselected site and the profile stay in place. Reach for this when a single site's stored session goes bad (a forced logout, a rotated password, a cookie the site now rejects) and you want to re-capture just that site instead of rebuilding the whole profile.

From the dashboard, open [Profiles](https://www.browserless.io/account/profiles), expand the profile's saved logins, tick the sites to remove, click **Remove**, and confirm.

Or call the endpoint with the exact site domains as shown in the profile's saved logins:

```bash
curl -X DELETE "https://production-sfo.browserless.io/profile?token=YOUR_API_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{ "name": "acme-prod", "domains": ["example.com", "auth.example.com"] }'
```

Each domain removes that site's cookies along with the `localStorage` and `IndexedDB` scoped to the matching origin. Matching is exact and never cascades to subdomains, so removing `example.com` leaves `app.example.com` in place. This mirrors how browsers scope storage per origin, and it keeps you from wiping a subdomain login you still need. Returns the updated profile metadata object, with `cookieCount` and `originCount` reflecting what remains. Browserless returns `400` for an empty or malformed `domains` array and `404` if the profile doesn't exist for the token.

### Recover saved logins with autologin

If a saved login expires, choose **Recover with autologin** from the profile's actions menu. Select a connected 1Password integration and the sites to recover. The integration must have Login items matching sites already captured in the profile; recovery doesn't add new sites.

For sites that email a login code, select an **Email OTP inbox** before starting. Inbox choices from profile creation aren't saved for later recovery runs, so choose them again. Recovery keeps the profile's name and ID and replaces only successfully recovered sites' state, preserving failed and unselected sites.

While you stay on the Profiles page, you can close the dialog and reopen it with **View recovery progress** to see the same task. See [recovering an existing profile](/baas/features/autologin#recovering-an-existing-profile) for the dashboard steps, API request, and emailed-code troubleshooting.

### Manage profile state via JSON

If you already have authentication state from another system, you can create or refresh a profile directly from JSON. These endpoints do not launch a browser; Browserless stores the supplied `cookies`, `localStorage`, and `IndexedDB` state directly.

The `state` payload must use the Browserless profile format:

- `cookies` is an array of browser cookies. Each cookie needs `name`, `value`, and `domain`; Browserless fills defaults for optional fields such as `path`, `expires`, `httpOnly`, `secure`, and `session`.
- `origins` is an array of origin-scoped storage objects. Each `origin` must be an `http` or `https` origin, and `localStorage` must be an object whose keys and values are strings.
- `indexedDBs` is optional. When present, each database needs `name`, `version`, and `objectStores`; each object store needs `name`, `keyPath`, `autoIncrement`, `entries`, and `indexes`.

> **Adapting Playwright storage state**
> Playwright's `storageState()` returns `origins[].localStorage` as an array of `{ name, value }` pairs. Convert it to an object before uploading:
> 
> ```js
> const toBrowserlessState = (storageState) => ({
>   cookies: storageState.cookies,
>   origins: storageState.origins.map(({ origin, localStorage }) => ({
>     origin,
>     localStorage: Object.fromEntries(
>       (localStorage ?? []).map(({ name, value }) => [name, value]),
>     ),
>   })),
> });
> ```
> 
> If you include IndexedDB data from your own capture pipeline, send it as `indexedDBs` in the Browserless format shown above.

#### Upload a JSON profile

Use `POST /profile/upload` to create a new profile from a pre-captured `state` payload. The `name` must be unique for your API token. Uploading the same name twice returns an error; use [`POST /profile/refresh`](#refresh-a-json-profile) when you want to replace an existing profile's state.

```bash
curl -X POST "https://production-sfo.browserless.io/profile/upload?token=YOUR_API_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "acme-prod",
    "state": {
      "cookies": [
        {
          "name": "sid",
          "value": "abc123",
          "domain": ".example.com",
          "path": "/",
          "expires": -1,
          "httpOnly": true,
          "secure": true,
          "session": true,
          "sameSite": "Lax"
        }
      ],
      "origins": [
        {
          "origin": "https://app.example.com",
          "localStorage": {
            "authToken": "..."
          },
          "indexedDBs": [
            {
              "name": "auth-db",
              "version": 1,
              "objectStores": [
                {
                  "name": "tokens",
                  "keyPath": null,
                  "autoIncrement": false,
                  "entries": [
                    { "key": "refresh", "value": { "token": "..." } }
                  ],
                  "indexes": []
                }
              ]
            }
          ]
        }
      ]
    }
  }'
```

A successful upload returns the stored profile metadata plus a `diagnostics` object that reports anything Browserless dropped or truncated before saving:

```json
{
  "id": "profile_abc123",
  "name": "acme-prod",
  "cookieCount": 1,
  "originCount": 1,
  "lastUsedAt": null,
  "createdAt": "2026-05-22T12:00:00.000Z",
  "updatedAt": "2026-05-22T12:00:00.000Z",
  "diagnostics": {
    "skippedMalformedCookies": 0,
    "skippedPrivateCookies": 0,
    "skippedMalformedOrigins": 0,
    "skippedPrivateOrigins": 0,
    "truncatedOrigins": 0,
    "skippedMalformedIdbDatabases": 0,
    "truncatedIdbDatabases": 0,
    "skippedMalformedIdbStores": 0,
    "truncatedIdbEntries": 0
  }
}
```

#### Refresh a JSON profile

Use `POST /profile/refresh` when an existing profile's cookies or storage have expired and you have a replacement `state` payload. The body is the same shape as `/profile/upload`, but the profile must already exist for the requesting token.

```bash
curl -X POST "https://production-sfo.browserless.io/profile/refresh?token=YOUR_API_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "acme-prod",
    "state": {
      "cookies": [
        {
          "name": "sid",
          "value": "new-value",
          "domain": ".example.com",
          "path": "/",
          "expires": -1,
          "httpOnly": true,
          "secure": true,
          "session": true
        }
      ],
      "origins": [
        {
          "origin": "https://app.example.com",
          "localStorage": {
            "authToken": "new-token"
          }
        }
      ]
    }
  }'
```

Refresh overwrites the stored state in place and returns the same response shape as upload, including diagnostics. The profile keeps its `id` and `name`, while `cookieCount`, `originCount`, and `updatedAt` reflect the new state.

If the profile does not exist for the token, Browserless returns `404`. Existing browser sessions that already loaded the old profile are not changed; new sessions that pass `?profile=acme-prod` use the refreshed state.

#### Download a profile's state

Use `GET /profile/<profile-name>/download` to export a profile's captured state as JSON. This is the read side of upload and refresh. It returns the same `cookies` and `origins` payload those endpoints accept, so you can back up a profile, inspect what it captured, or copy it to another token.

Profile names can't contain whitespace, `/`, `?`, or `#`, so most names drop into the path unchanged. URL-encode the segment when a name contains other URL-significant characters such as `%`. Encode the exact names `.` and `..` as `%2E` and `%2E%2E` too, since a raw `.` or `..` in the path gets collapsed by URL normalization before the request reaches Browserless.

```bash
curl "https://production-sfo.browserless.io/profile/acme-prod/download?token=YOUR_API_TOKEN_HERE" \
  --output acme-prod.json
```

Browserless serves the response as a file attachment (`Content-Disposition: attachment; filename="acme-prod.json"`) with `Cache-Control: no-store`, so the captured state is never held in a proxy cache on the way back. The body is the raw state object:

```json
{
  "cookies": [
    {
      "name": "sid",
      "value": "abc123",
      "domain": ".example.com",
      "path": "/",
      "expires": -1,
      "httpOnly": true,
      "secure": true,
      "session": true,
      "sameSite": "Lax"
    }
  ],
  "origins": [
    {
      "origin": "https://app.example.com",
      "localStorage": { "authToken": "..." }
    }
  ]
}
```

Origins that captured IndexedDB data also include an `indexedDBs` array, in the same format `/profile/upload` accepts. If no profile with that name exists for your token, the endpoint returns `404`.

The downloaded JSON is exactly the `state` object that [`/profile/upload`](#upload-a-json-profile) and `/profile/refresh` expect. Wrap it in `{ "name": "...", "state": <downloaded-json> }` to restore the profile under a new name or move it to another token:

```bash
# Download the current state, then re-upload it under a new name.
curl "https://production-sfo.browserless.io/profile/acme-prod/download?token=YOUR_API_TOKEN_HERE" \
  --output state.json

# Wrap the state in the upload envelope and post it back as a new profile.
jq '{ name: "acme-backup", state: . }' state.json | \
  curl -X POST "https://production-sfo.browserless.io/profile/upload?token=YOUR_API_TOKEN_HERE" \
    -H "Content-Type: application/json" \
    --data @-
```

The exported cookies keep the extra fields Chromium reports, such as `sameSite`, `size`, and `sourceScheme`. Upload ignores the fields it doesn't need, so a download-then-upload round-trip restores the profile with nothing dropped.

### Import from a local browser via the CLI

The [`@browserless.io/cli`](https://www.npmjs.com/package/@browserless.io/cli) tool captures cookies, `localStorage`, and `IndexedDB` directly from a local Chromium-based browser and uploads them as a cloud profile. No scripting or JSON assembly required.

```bash
npm install -g @browserless.io/cli
```

Requires Node.js ≥ 24.

```bash
browserless auth login <your-token>
```

The token is stored in your OS keychain when available and falls back to `~/.browserless/config.json`.

```bash
browserless profile upload \
  --browser chrome --profile Default --name my-chrome
```

The CLI reads the local browser's user-data directory, extracts the authentication state, and uploads it. Any Browserless session can now start already authenticated by passing `?profile=my-chrome`.

The CLI supports Chrome, Edge, Brave, Chromium, and other Chromium-based browsers. Use `browserless profile sources list` to discover available local profiles.

See the [CLI documentation](/cli) for the full command reference, including domain filtering, artifact-size management, custom browser registration, and configuration.

## Endpoint coverage

`?profile=<profile-name>` works on any request that launches a new Chromium-family browser. A few endpoints behave differently or don't support it at all which are listed below:

| Type | Endpoint | Status | Notes |
|---|---|---|---|
| WebSocket | `/`, `/chromium`, `/chrome`, `/edge`, `/chromium/stealth`, `/chrome/stealth`, `/stealth` | ✅ Supported | Auth state applied automatically |
| BrowserQL | `/chromium/bql`, `/chrome/bql`, `/stealth/bql` (POST and WS), `/chromium/agent` | ✅ Supported | Auth state applied automatically |
| REST | `/screenshot`, `/pdf`, `/unblock`, `/chromium/export`, `/content`, `/scrape`, `/function`, `/download`, `/performance`, `/smart-scrape`, `/crawl` (and `/chrome/*`, `/chromium/*`, `/edge/*` variants) | ✅ Supported | Auth state applied automatically |
| Session | `POST /session` with `profile` in the body | ✅ Supported | See the [Persisted Session](#using-a-profile) tab |
| Playwright | `/chromium/playwright`, `/chrome/playwright`, `/edge/playwright` | ⚠️ Caveat | Auth state is applied, but clients must re-use `browser.contexts()[0]`. New contexts start without cookies and storage. |
| Playwright | `/firefox/playwright`, `/webkit/playwright` | ❌ Not supported | Only Chromium-family browsers are supported |
| Reconnect | `/reconnect/*` | ❌ Not supported | `?profile=` is rejected with a `400`. The browser was already authenticated at first launch; changing its identity mid-session would invalidate the live state. |

For unsupported flows, capture and apply the auth state inside a BrowserQL or Puppeteer session instead.

## FAQ & Troubleshooting

<details id="what-are-the-limits-for-profiles">
<summary>What are the limits for profiles?</summary>

These are the per-profile and per-session constraints. Hitting any of these will prevent a profile from being saved or cause a request to be rejected.

| Limit | Value |
|---|---|
| Captured state size | 2 MB |
| Distinct origins per profile | 50 |
| IndexedDB databases per origin | 5 |
| IndexedDB entries per object store | 1,000 |
| Profile name length | 255 characters |
| Profile name uniqueness | Per token |
| Creation session lifetime | 10 minutes, or your plan's maximum session duration when shorter |
| Unused-profile retention | 30 days from last use |

Profiles unused for 30 days are removed automatically.

</details>

<details>
<summary>Does removing a saved login delete the whole profile?</summary>

No. Removing sites strips only the selected domains' cookies, `localStorage`, and `IndexedDB`. The profile and every site you didn't select stay in place, so parallel sessions keep their other logins.

Removal is exact per site and never cascades. Removing `example.com` leaves `app.example.com` or any other subdomain untouched, because each is stored as a separate login. To drop a subdomain, select that subdomain specifically.

</details>

<details>
<summary>When should I use a profile vs a persisted session?</summary>

Authenticated Profiles and [Persisted Sessions](/baas/session-management/persisting-state) solve different problems and **can be combined**:

- A **profile** is the *what*: the saved authentication state to load before your code runs.
- A **persisted session** is the *how-long*: a long-lived browser process you can disconnect from and reconnect to.

**Use a profile** when you want to reuse an authenticated state across many short-lived sessions, including in parallel. Each session gets its own working copy. Workers don't fight over a single shared browser, and changes during one session don't leak into the others.

**Use a persisted session** when you need the *same browser instance* to survive across reconnects. For example, to keep tabs open or preserve in-flight UI state across a disconnect. Profiles snapshot auth and replay it into a fresh browser; persisted sessions keep the whole browser alive.

**Use both together** when you want a long-lived browser that starts with a saved auth state. Create the session with `POST /session` and `profile: "<name>"` in the body. The profile is loaded once when the browser starts; the session then persists normally.

**Use neither** for one-off automation where re-authenticating is cheap and you don't want to manage stored state.

</details>

<details>
<summary>What does the error "No cookies found in this profile" mean when I create a profile?</summary>

This message appears when you save a profile but the browser session had no cookies at the time of capture. It typically means one of three things:

- **You saved too early.** The login flow hadn't completed yet, so the site hadn't set any authentication cookies. Make sure the page has fully loaded and the login redirect has finished before calling `Browserless.saveProfile`.
- **The site doesn't use cookies for auth.** Some applications store authentication state exclusively in `localStorage` or `IndexedDB` (e.g. JWT tokens). The profile still captures that storage, so the session may work even with zero cookies. Test the profile by connecting with `?profile=<name>` and navigating to a protected page.
- **The login happened on a different origin.** If your login form posts to a third-party identity provider and the redirect back hasn't completed, cookies may be scoped to the IdP origin rather than your app. Wait for the final redirect before saving.

If the profile has zero cookies but does contain `localStorage` or `IndexedDB` data, it can still restore a valid authenticated session. The warning is informational and doesn't mean the profile is broken.

</details>

<details>
<summary>What does enabling proxy sticky session do?</summary>

When you enable **proxy sticky session** (the `proxySticky` parameter), all network requests within a single browser session are routed through the same proxy exit node. Your session keeps the same IP address for its entire lifetime.

**Sticky is now the default on many routes.** Sticky sessions are on by default for the stealth routes (`/stealth`, `/chromium/stealth`, `/chrome/stealth`), all BrowserQL endpoints, `/unblock`, `/scrape`, and any request with `?stealth=true` — no parameter needed. Pass `proxySticky=false` (or `proxy(sticky: false)` in BrowserQL) to opt out. Plain browser connects and non-stealth REST endpoints (`/content`, `/pdf`, `/screenshot`, `/export`, `/download`, `/function`) still rotate by default, so pass `proxySticky=true` to pin the exit IP — as the profile-creation session below does. `proxySticky` is now a real boolean: `proxySticky=false` or `proxySticky=0` disables it.

Without sticky sessions, each request may be routed through a different node in the proxy pool. This causes problems:

- **Session invalidation.** Many sites tie session cookies to the originating IP. If the IP changes mid-session, the server invalidates the cookie and forces a re-login.
- **Bot detection.** Rapid IP changes within a single browsing session are a strong signal for anti-bot systems. Akamai and DataDome bind their sensor and cookies to the egress IP, so a mid-session IP rotation can re-trigger the block — which is why sticky is now the default on the stealth, BrowserQL, `/unblock`, and `/scrape` routes.
- **Geo-gated content.** If requests alternate between regions, the site may serve inconsistent content or block the session entirely.

Enable sticky sessions when creating a profile that requires a proxy, especially if the target site validates session continuity by IP. This ensures the login flow and all subsequent page loads during profile capture use the same exit IP.

See [Proxies](/baas/bot-detection/proxies) for the full proxy configuration reference.

</details>

<details>
<summary>Session times out before login completes</summary>

When using a [live session link](/baas/monitor-sessions/hybrid-automation) for manual login, there are two timeout settings. Both are capped by your plan's [maximum session duration](/baas/best-practices#session-duration-limits), and profile creation sessions also have a 10-minute ceiling. Creating a LiveURL after the session starts doesn't reset that clock, so the viewer may have less time left than the LiveURL timeout you requested.

| Timeout | Where to set it | What it controls |
|---|---|---|
| **Session timeout** | `?timeout=` query param on `POST /profile` | How long the creation browser session stays alive |
| **LiveURL timeout** | `{ timeout: ms }` param in `Browserless.liveURL` CDP call | How long the live session link stays open |

Set both explicitly when needed, without exceeding your plan maximum:

```js
// Session timeout — set on the POST URL
const session = await fetch(`${ORIGIN}/profile?token=${TOKEN}&timeout=120000`, { ... });

// LiveURL timeout — keep this shorter so there's time to verify and save.
const { liveURL } = await cdp.send('Browserless.liveURL', { timeout: 60000 });
```

</details>

<details id="captured-state-too-large">
<summary>Captured state is too large</summary>

The captured state (cookies, `localStorage`, and `IndexedDB`) is capped at 2 MB per profile. If your site stores large amounts of data in `localStorage` or `IndexedDB`, the save will fail with `{ ok: false, error: ... }` and nothing is persisted.

To fix this, try:

- Clearing unused `localStorage` keys before saving the profile
- Reducing the number of origins the session touches
- Checking the [Limits](#what-are-the-limits-for-profiles) FAQ entry for the full set of per-profile constraints

</details>

## Further reading

- [Persisting State](/baas/session-management/persisting-state) – keep a single browser instance alive across reconnects
- [Connection URL Patterns](/baas/connection-url-patterns) – full reference for connect-time query parameters
- [BrowserQL](/browserql/start) – drive the creation session through the GraphQL layer
- [`@browserless.io/cli`](https://www.npmjs.com/package/@browserless.io/cli) – capture and upload profiles from your local browser via the command line

## Next steps

  <a href="/baas/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BaaS Quickstart</h3>
      get started with Puppeteer or Playwright on Browserless
    
  </a>
  <a href="/baas/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure browser behavior and environment
    
  </a>

---

# Autologin

URL: https://docs.browserless.io/baas/features/autologin

# Autologin

Autologin turns a [1Password service account](/baas/features/1password) into a ready-to-use [Authenticated Profile](/baas/features/authenticated-profiles) with a single request. You give it a service account and a profile name; Browserless opens a session, signs in to your sites using the credentials from your vault, and saves the logged-in state as a profile you can reuse everywhere with `?profile=<name>`.

It automates the "log in once, then reuse a profile" flow you would otherwise script by hand with `Browserless.loadSecret` + `Browserless.saveProfile` — the whole sign-in is driven for you, and as with all 1Password credential filling **the secret values never reach your code**.

> **When to use Autologin**
> Use **Autologin** when you just want a logged-in profile and don't want to write the login steps yourself. Use [`loadSecret`](/baas/features/1password#filling-a-secret-in-a-session) directly when you need fine-grained control over the sign-in flow inside your own automation. Both resolve secrets from the same service account.

- A Browserless API token (available in your [account dashboard](https://browserless.io/account/))
- A [1Password service account](/baas/features/1password) already connected to your API token
- **Login** items in the connected vault, each with a website URL and a username/password (add a one-time-password field for sites with TOTP-based MFA)

## How it works

You send one request naming a service account and a profile name. Browserless then runs the login server-side:

1. **Open a session** — Browserless starts a stealth browser session with the service account authorized, and signs in from a residential IP so the login isn't treated as datacenter traffic.
2. **Discover targets** — it reads the **Login** items in the connected vault. Each item's website field is the origin to sign in to, and its username/password (and one-time-password, if present) are the fields to fill. Pass `allowedDomains` to log in to only a subset.
3. **Sign in** — for each site, an automated login agent navigates to the login page and fills the credentials by reference, exactly like [`loadSecret`](/baas/features/1password#filling-a-secret-in-a-session): the value is resolved from 1Password and typed inside the session, and is **never** returned to your code, logged, or captured.
4. **Save a profile** — once a site is signed in, Browserless captures the session's authenticated state (cookies, `localStorage`, `IndexedDB`) into an [Authenticated Profile](/baas/features/authenticated-profiles) with the name you chose — the same capture `Browserless.saveProfile` performs.

The request returns immediately with a `taskId`; the login runs in the background. You [poll the task](#tracking-progress) for live progress and the final result.

> **The profile holds session state, not your password**
> The saved profile contains the signed-in cookies and storage — not your 1Password credentials. Reusing it replays that state; no secret is filled (or resolvable) from a session started with `?profile=<name>`.

## Starting an autologin

  
**Dashboard:**

Go to **Profiles** in the dashboard sidebar. Under **1Password Service Accounts**, find the service account you want to sign in with — its vault must hold the **Login** items for the sites you want.
Click **Create profile** on that service account. In the **Create profile** dialog, set a **Profile name** (this is what you'll pass as `?profile=`) and pick the **Sites to log into** — every discovered site is selected by default; uncheck any you want to skip.

For sites that email a login code, choose an **Email OTP inbox** for each site, or use **Apply one inbox to all sites** and then override individual sites. Leave it at **None** for the existing username/password and TOTP flow. The inbox is a separate Login item containing the mailbox's IMAP credentials; it does not need a website URL. See [inbox setup](/baas/features/1password#filling-an-emailed-one-time-code). The picker lists candidate items, not verified mailboxes; Gmail is tested and other IMAP providers are best-effort.

Start it and watch each site sign in, with a live status line. On success the dialog shows a **connect URL** you can copy (a session URL with `?profile=<name>`), and the profile appears under **Authenticated Profiles**, ready to reuse.

  
  
**API:**

Create the task with a `name` for the resulting profile and the `integrationId` to sign in with. `allowedDomains` is optional — omit it to sign in to every Login item in the service account, or pass a subset of its origins.

```bash
curl -X POST "https://production-sfo.browserless.io/integrations/onepassword/profile?token=YOUR_API_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "acme-prod",
    "integrationId": "op_int_0a1b2c3d4e5f6a7b8c9d0e1f",
    "allowedDomains": ["https://app.example.com"]
  }'
```

The response is the task handle — poll it to follow progress:

```json
{
  "taskId": "autologin_eef23c512148b7198564ff49"
}
```

- **`name`** — the profile name to create; reused later as `?profile=<name>`. Must be unique for your token.
- **`integrationId`** — the service account to sign in with (from its [service-account record](/baas/features/1password#adding-a-service-account)).
- **`allowedDomains`** — optional subset of the service account's origins; every value must be one of the service account's login origins.

To enable emailed codes, add an optional `emailOtp` array to the create request:

```json
{
  "emailOtp": [
    { "origin": "https://app.example.com", "vault": "Acme", "item": "Login inbox" }
  ]
}
```

Use the vault and item names returned in `inboxCandidates` by `GET /integrations/onepassword/autologin/form?integrationId=...&token=...`. Only item identities and labels are returned, never credential values. Each selected inbox must belong to that integration and contain username/password fields; each origin must be a selected login target. Invalid selections return `400` before a task starts. Omit `emailOtp` to retain the existing behavior.

> **One task per profile name**
> Starting a second autologin for a name that's already in flight returns the **existing** `taskId` instead of starting a duplicate — polling either way follows the same task.

  

## Recovering an existing profile

Use recovery when a saved login has expired or stopped working. Recovery signs in again and refreshes the existing profile without changing its name or ID. Only successfully recovered sites replace their saved state; unselected or failed sites keep their previous state. You don't need to delete the profile first.

### Recover from the dashboard

1. Open **Profiles** and find the saved profile under **Authenticated Profiles**. Open its actions menu and choose **Recover with autologin**.
2. Choose a **1Password integration**. Only login targets matching sites already captured in this profile are offered. Uncheck any sites you don't want to recover.
3. For sites that email a login code, select an **Email OTP inbox** per site, or use **Apply one inbox to all sites** and override individual choices. Choose the integration and inboxes for each new recovery run; the profile doesn't remember the choices used to create it. **None** leaves email OTP disabled for that site. See [inbox setup](/baas/features/1password#filling-an-emailed-one-time-code) for the required IMAP credentials.
4. Set optional **Proxy** settings if needed, then click **Start recovery**. Watch the live rationale and per-site results.

You can close the dialog and choose **View recovery progress** from the same profile's actions menu to reopen the current task. This progress view is retained while you stay on the Profiles page, not across navigation or a page reload. If status checks stop, **Retry status** reconnects to the same task instead of starting another recovery.

### Recover through the API

Send the existing profile's `name` with `recover: true` to the same endpoint. Choose an integration that can sign in to its captured sites. Replace the example name, integration ID and inbox references with your own values.

```bash
curl --request POST \
  --url "https://production-sfo.browserless.io/integrations/onepassword/profile?token=YOUR_API_TOKEN_HERE" \
  --header "Content-Type: application/json" \
  --data '{
    "name": "acme-prod",
    "integrationId": "op_int_0a1b2c3d4e5f6a7b8c9d0e1f",
    "recover": true,
    "allowedDomains": ["https://app.example.com"],
    "emailOtp": [
      { "origin": "https://app.example.com", "vault": "Acme", "item": "Login inbox" }
    ]
  }'
```

**Response:**

```json
{
  "taskId": "autologin_eef23c512148b7198564ff49"
}
```

Poll this task using the [progress endpoint](#tracking-progress). To discover eligible origins and inboxes before starting, call `GET /integrations/onepassword/autologin/form` with `integrationId`, `profileName`, and `token` query parameters. URL-encode their values. Use its `domains` and `inboxCandidates` to build the request.

`allowedDomains` narrows recovery to selected captured sites; it can't add a new site to the profile. Omit `emailOtp` for sites that don't need emailed codes. Recovery uses the same inbox validation and server-side code filling as profile creation.

## Tracking progress

Poll the task by id. It reports its overall `state`, a live `currentRationale` (what the agent is doing right now), and the per-domain progress.

```bash
curl "https://production-sfo.browserless.io/integrations/onepassword/profile/autologin_eef23c512148b7198564ff49?token=YOUR_API_TOKEN_HERE"
```

```json
{
  "taskId": "autologin_eef23c512148b7198564ff49",
  "state": "success",
  "currentRationale": null,
  "profileId": "profile_9b5c6923a7d631cdee36e259",
  "profileName": "acme-prod",
  "domains": [
    { "origin": "https://app.example.com", "status": "success" }
  ],
  "createdAt": "2026-06-18T12:00:00.000Z",
  "updatedAt": "2026-06-18T12:02:00.000Z"
}
```

`state` moves through:

| State | Meaning |
|---|---|
| `pending` | Accepted, not started yet. |
| `running` | Signing in; watch `currentRationale` and `domains[].status`. |
| `success` | At least one domain signed in and the profile was saved — `profileName` is ready to reuse. |
| `failed` | The task ended without a saved profile — see `errorCode` / `errorMessage`. |

Each entry in `domains` carries its own `status`: `pending` (not reached), `active` (currently attempting), `tried` (moved past, outcome pending), then the terminal `success` or `failed` (with an `error`).

  
**Node:**

```js
const BASE = 'https://production-sfo.browserless.io';
const TOKEN = 'YOUR_API_TOKEN_HERE';

// Use the taskId returned by your create or recovery request.
const taskId = 'YOUR_TASK_ID_HERE';

// Poll until it settles.
let task;
do {
  await new Promise((r) => setTimeout(r, 4000));
  task = await fetch(`${BASE}/integrations/onepassword/profile/${taskId}?token=${TOKEN}`).then((r) => r.json());
  console.log(task.state, task.currentRationale ?? '');
} while (task.state === 'pending' || task.state === 'running');

if (task.state === 'success') {
  console.log('Profile ready:', task.profileName); // reuse via ?profile=<name>
} else {
  console.error('Autologin failed:', task.errorCode, task.errorMessage);
}
```

  
  
**curl:**

```bash
TASK=autologin_eef23c512148b7198564ff49
# Re-run until "state" is "success" or "failed".
curl -s "https://production-sfo.browserless.io/integrations/onepassword/profile/$TASK?token=YOUR_API_TOKEN_HERE"
```

  

## Using the saved profile

On `success`, the profile named `<name>` is a normal [Authenticated Profile](/baas/features/authenticated-profiles). Reuse it on any session by adding `?profile=<name>` — the browser loads its cookies and storage before navigating, so the page opens already signed in.

```js

const browser = await puppeteer.connect({
  browserWSEndpoint:
    'wss://production-sfo.browserless.io/chromium?token=YOUR_API_TOKEN_HERE&profile=acme-prod',
});

const page = await browser.newPage();
await page.goto('https://app.example.com'); // already logged in
```

Because a reused profile fills no secret, the session isn't locked down — screenshots, PDFs, and page reads all work normally. When credentials rotate or the session expires, [recover the existing profile](#recovering-an-existing-profile) to refresh its saved state.

## Security

Autologin inherits the [1Password security model](/baas/features/1password#security-model):

- **Secrets stay server-side** — credentials are resolved from 1Password and typed inside the session; they're never returned to your code, logged, or captured.
- **Origin safety** — credentials are only ever typed into public `https://` origins; `http://` and private/internal addresses are always refused. If the service account sets an allow-list, sign-ins are limited to it (by default, with no list, any public https origin is allowed); `allowedDomains` on the request can narrow a run further.
- **Locked-down capture during login** — once a credential is filled, the sign-in session blocks screenshots, PDFs, and page reads, tears down live URLs, aborts any in-progress screen recording, and scrubs the filled values from logs and the session replay — so the typed value can't leak. The saved profile is captured through the same trusted path.
- **Audit log** — each credential resolve is recorded (reference, origin, outcome) — never the value.
- **Token-scoped** — tasks and profiles are scoped to your API token; other tokens can't see or reuse them.

## Failure modes

If the request is rejected, it returns an error before a task starts:

| Status | When |
|---|---|
| `400` | `name` or `integrationId` missing, `allowedDomains` isn't an array of origins, an origin isn't an eligible login target, a profile with that `name` already exists and `recover` isn't `true`, or an `emailOtp` entry references an invalid inbox, an inbox missing the required IMAP username/password fields, or an origin not selected for this run. |
| `404` | The `integrationId` is unknown, expired, or unreachable, or the profile to recover doesn't exist for this API token. |
| `503` | The service account or 1Password is temporarily unavailable — retry. |

Once running, a task that can't finish ends in `state: "failed"` with an `errorCode`:

| `errorCode` | Meaning | What to do |
|---|---|---|
| `AllDomainsFailed` | Every targeted site failed to sign in — check each `domains[].error`. | Verify the Login items resolve and the credentials/selectors are valid; retry. |
| `AutologinTimeout` | The sign-in didn't complete within the time budget. | Retry; a slow site or an extra step (MFA, interstitial) can exceed the budget. |
| `AgentError` | The login couldn't be completed or the profile couldn't be saved. | Retry; if it persists, contact support. |

Per-domain `error` values reuse the [`loadSecret` failure codes](/baas/features/1password#failure-modes) (for example `CredentialNotResolved`, `DomainNotAllowed`, `SelectorNotFound`) so you can see exactly why a site didn't sign in.

When no fresh emailed code arrives, the affected domain carries `errorCode: "EmailOtpTimeout"`; the dashboard displays **Email code didn't arrive**. This differs from an unsupported MFA challenge. Check the mailbox credentials and delivery, then retry. Other domains can still succeed and produce a profile.

## FAQ & Troubleshooting

<details>
<summary>Why does recovery ask me to re-authenticate manually for an emailed code?</summary>

Select an **Email OTP inbox** for that site before starting recovery, or include its inbox mapping in `emailOtp` when using the API. Having configured an inbox during profile creation doesn't configure later recovery runs. Without an inbox selection, Browserless can't retrieve the emailed code. SMS, push approvals, and other unsupported challenges still need manual re-authentication.

If the result says **Email code didn't arrive** (`EmailOtpTimeout`), check the selected mailbox's IMAP credentials and code delivery instead. If recovery reports `SharedCookieConflict`, no saved state was changed: replacing a shared cookie would affect an unselected or failed site. Finish re-authentication manually rather than deleting the profile.

</details>

<details>
<summary>How is Autologin different from <code>loadSecret</code>?</summary>

`loadSecret` fills one referenced field into a selector you choose, inside your own automation. Autologin drives the entire sign-in for you across the service account's Login items and saves a reusable profile at the end. Both resolve secrets from the same service account and never expose the value.

</details>

<details>
<summary>Does it handle multi-factor authentication?</summary>

If the Login item has a one-time-password (TOTP) field, autologin can use it. For emailed codes, select an Email OTP inbox: the agent triggers the site's send-code step, then fills the code server-side using the distinct `emailOtp` credential. It never sees the code value. SMS codes and push approvals still require manual completion. You can also drive emailed-code filling yourself with [`loadSecret`](/baas/features/1password#filling-an-emailed-one-time-code).

</details>

<details>
<summary>The profile name already exists — what happens?</summary>

A normal create request is rejected with `400`. Use a new name to create a separate profile. To refresh the existing profile, choose **Recover with autologin** in the dashboard or send its name with `recover: true`. Recovery preserves the profile's name and ID; deletion isn't required.

</details>

<details>
<summary>Can I sign in to only some of the service account's sites?</summary>

Yes. Pass `allowedDomains` with the subset of origins you want. Each value must be one of the service account's login origins.

</details>

## Next steps

  <a href="/baas/features/1password" className="next-step-card-link">
    
      <h3 className="next-step-card-title">1Password integration</h3>
      Configure credentials and inbox items for server-side sign-in
    
  </a>
  <a href="/baas/features/authenticated-profiles" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Authenticated profiles</h3>
      Reuse and manage saved authentication state across sessions
    
  </a>

---

# Browser Extensions

URL: https://docs.browserless.io/baas/features/browser-extensions

Browserless allows you to upload and use your own browser extensions in your automation sessions. Extensions can be managed through your account dashboard or, for Browserless-managed dedicated deployments, the API and then referenced by name in your launch options.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Puppeteer or Playwright installed locally

> **Note**
> Extension support is only available when using the Chromium browser.
> If you don't specify a browser, Chromium is used by default.

## Extension Upload

1. Navigate to the **Extensions** section in your account dashboard
2. Upload your extension as a ZIP file containing the extension directory (max 100 MB)
3. Once uploaded, extensions can be referenced by their assigned name

> **Demo Extension**
> You can find a demo extension in [this link](https://public-assets-browserless.s3.us-east-1.amazonaws.com/test_extension_all_pages.zip). This extension adds the `"Hello from extension"` text to the `body` element of every page, making it easy to verify that your extension is working correctly.

### Manage extensions from CI/CD

Browserless-managed dedicated deployments can manage extensions from a build
pipeline with an account API token. Fully self-hosted deployments do not expose
this account API.

Upload an extension:

```shell
: "${BROWSERLESS_TOKEN:?Set BROWSERLESS_TOKEN to your API token}"
: "${BROWSERLESS_API_URL:=https://api.browserless.io}"
: "${EXTENSION_NAME:=ci-extension}"

curl --fail-with-body --silent --show-error --max-time 60 --request POST \
  --url "${BROWSERLESS_API_URL}/extension" \
  --header "Authorization: Bearer ${BROWSERLESS_TOKEN}" \
  --form-string "name=${EXTENSION_NAME}" \
  --form 'description=Uploaded by CI' \
  --form 'extension=@extension.zip;type=application/zip' || exit 1
```

Store `BROWSERLESS_TOKEN` as a masked CI secret, and disable shell command
tracing while authenticated commands run so the expanded header is not logged.

Uploading the same name again replaces the existing extension. The replacement
is temporarily unavailable while malware scanning runs, so wait for its status
to become `active` before starting sessions that depend on it:

This polling function requires Bash and `jq`:

```bash
await_extension() {
  deadline=$((SECONDS + 300))
  while :; do
    remaining=$((deadline - SECONDS))
    ((remaining > 0)) || break
    if ! response=$(curl --fail-with-body --silent --show-error \
      --connect-timeout 10 --max-time "$remaining" \
      --url "${BROWSERLESS_API_URL}/extensions" \
      --header "Authorization: Bearer ${BROWSERLESS_TOKEN}"); then
      [ -z "$response" ] || printf '%s\n' "$response" >&2
      return 1
    fi
    if ! status=$(printf '%s' "$response" | jq --exit-status --raw-output \
      --arg name "$EXTENSION_NAME" \
      'first(.extensions[] | select(.name == $name) | .status) // "missing"'); then
      echo 'Invalid extensions API response' >&2
      return 1
    fi
    case "$status" in
      active) echo 'Extension is active'; return 0 ;;
      pending) ;;
      invalid) echo 'Extension failed malware scanning' >&2; return 1 ;;
      missing) echo "Extension not found: $EXTENSION_NAME" >&2; return 1 ;;
      *) echo "Unexpected extension status: $status" >&2; return 1 ;;
    esac
    ((SECONDS + 5 < deadline)) || break
    sleep 5
  done
  echo 'Timed out waiting for extension scanning' >&2
  return 1
}

await_extension || exit 1
```

`GET /extensions` returns each extension's `name`, `description`, `status`, and
`createdAt`. A new or replaced extension starts as `pending` and becomes
`active` after it passes scanning. This example checks every five seconds for
up to five minutes and fails the pipeline if scanning rejects the extension or
does not finish in time.

Delete an extension by name when it is no longer needed:

```shell
curl --fail-with-body --silent --show-error --max-time 60 --request DELETE \
  --url "${BROWSERLESS_API_URL}/extension/${EXTENSION_NAME}" \
  --header "Authorization: Bearer ${BROWSERLESS_TOKEN}" || exit 1
```

## Extension Launch Parameter

Use the `extensions` parameter in your launch options to load extensions:

**Puppeteer:**

```javascript

// Define launch options
const launchArgs = {
  extensions: ['extension_name_01', 'extension_name_02']
};

// Create query parameters
const queryParams = new URLSearchParams({
  token: 'YOUR_API_TOKEN_HERE',
  timeout: '180000',
  launch: JSON.stringify(launchArgs)
});

const browser = await puppeteer.connect({
  browserWSEndpoint: `wss://production-sfo.browserless.io?${queryParams.toString()}`,
});
const page = await browser.newPage();
```

**Playwright CDP:**

```javascript

// Define launch options
const launchArgs = {
  extensions: ['extension_name_01', 'extension_name_02']
};

// Create query parameters
const queryParams = new URLSearchParams({
  token: 'YOUR_API_TOKEN_HERE',
  timeout: '180000',
  launch: JSON.stringify(launchArgs)
});

const browser = await playwright.chromium.connectOverCDP(
  `wss://production-sfo.browserless.io?${queryParams.toString()}`
);

// Extensions are loaded in the default context
// Always use the default context to access extensions
const context = await browser.contexts()[0];
const page = await context.newPage();
```

**BQL:**

```shell
# You need to add the launch argument to the url
# URL: https://production-sfo.browserless.io/chromium/bql?token=YOUR_TOKEN&launch={"extensions":["test_extension"]}
# Encoded URL: https://production-sfo.browserless.io/chromium/bql?token=YOUR_TOKEN&launch=%7B%22extensions%22%3A%5B%22test_extension%22%5D%7D

curl --request POST \
  --url 'https://production-sfo.browserless.io/chromium/bql?token=YOUR_TOKEN&launch=%7B%22extensions%22%3A%5B%22test_extension%22%5D%7D' \
  --header 'Content-Type: application/json' \
  --data '{"query":"mutation extensions {\n  goto(url: \"https://checkip.amazonaws.com/\", waitUntil: networkIdle) {\n    status\n  }\n  html(selector: \"body\") {\n    html\n  }\n}","variables":{},"operationName":"extensions"}'

```

> **Important Implementation Detail**
> Extensions are loaded in the **default context** only. When using **Playwright**, you must create your pages from the default context (`browser.contexts()[0]`) to access the loaded extensions. Creating new contexts will not have access to the extensions.

## Extension Requirements

- **ZIP structure**: Upload extensions as ZIP files. The `manifest.json` must sit at the ZIP root directory, not nested inside a subdirectory. A ZIP containing `my-extension/manifest.json` fails; it must be `manifest.json` at the top level.
- **Required manifest fields**: `manifest.json` must include `name`, `version`, and `manifest_version`. Missing any of these returns an upload error.
- **Extension naming**: Names must match the pattern `[a-zA-Z0-9_-]` and be 1 to 99 characters long.
- **File size**: Maximum 100 MB per extension.
- **Account limits**: Maximum 10 extensions per account. Uploading an existing name replaces it without consuming another slot.
- **Malware scanning**: Uploaded extensions go through automated malware scanning before becoming available.
- **Supported routes**: Extensions only work on `/chromium` and `/` (the default path). Paths containing `chrome`, `edge`, or `playwright` block extensions. Using an unsupported path returns: `"Extensions are only supported for CDP-based libraries with Chromium"`.

## Extensions with Authenticated Proxies

Extensions require CDP mode and the default browser context. Authenticated proxies in Playwright typically use `browser.newContext({ proxy })`, which creates a new context where extensions are not available. To use both together, pass the proxy server as a Chrome launch argument and handle authentication through the CDP `Fetch` domain.

```javascript

const launchArgs = {
  extensions: ["my_extension"],
};

const queryParams = new URLSearchParams({
  token: "YOUR_API_TOKEN_HERE",
  "--proxy-server": "proxy-host:port",
  launch: JSON.stringify(launchArgs),
});

const browser = await playwright.chromium.connectOverCDP(
  `wss://production-sfo.browserless.io?${queryParams.toString()}`
);

const context = browser.contexts()[0];
const page = context.pages()[0] || (await context.newPage());

// Enable proxy authentication via CDP Fetch domain
const client = await context.newCDPSession(page);
await client.send("Fetch.enable", {
  handleAuthRequests: true,
  patterns: [{ urlPattern: "*" }],
});

client.on("Fetch.authRequired", async (event) => {
  await client.send("Fetch.continueWithAuth", {
    requestId: event.requestId,
    authChallengeResponse: {
      response: "ProvideCredentials",
      username: "proxy_user",
      password: "proxy_pass",
    },
  });
});

client.on("Fetch.requestPaused", async (event) => {
  await client.send("Fetch.continueRequest", {
    requestId: event.requestId,
  });
});

await page.goto("https://example.com");
await browser.close();
```

This pattern keeps everything in the default context where extensions are loaded, while routing traffic through the authenticated proxy. For a deeper comparison of connection methods, see [connect vs connectOverCDP](/baas/advanced-configurations/playwright-customizations#connect-vs-connectovercdp).

## Best Practices

- **Test Locally First**: Always test your extension in a local Chrome browser before uploading
- **Keep Extensions Small**: Larger extensions increase session startup time
- **Use Descriptive Names**: Give your extensions clear, descriptive names for easier management
- **Version Control**: Consider versioning your extensions if you make frequent updates

## FAQ & Troubleshooting

<details>
<summary>My extension is not loading</summary>

Verify the extension name matches exactly what is shown in your dashboard. Check that the extension ZIP file contains the proper directory structure and that the extension is compatible with Chromium.

</details>

<details>
<summary>Sessions are taking too long to start</summary>

Reduce the number of extensions loaded simultaneously and check extension file sizes. Sessions with extensions take more time to start because the extension is loaded in real time. The startup time increases with the number and size of extensions.

</details>

<details>
<summary>My extension is not working as expected</summary>

Test the extension in a local Chrome browser first. Check the browser console for any extension errors and verify the extension's `manifest.json` is valid.

</details>

<details>
<summary>Why am I getting a <code>403 Forbidden</code> error?</summary>

Your API token is missing or expired. Pass it as a `?token=` query parameter in the WebSocket or HTTP URL. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My script works locally but fails on Browserless</summary>

Local browser settings may differ from the Browserless environment. Use `launch` parameters to match your local setup (viewport, user agent, timezone). See [launch parameters](/baas/launch-options) for the full list.

</details>

## Next steps

  <a href="/baas/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BaaS Quickstart</h3>
      get started with Puppeteer or Playwright on Browserless
    
  </a>
  <a href="/baas/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure browser behavior and environment
    
  </a>

---

# File Transfers

URL: https://docs.browserless.io/baas/features/file-transfers

File uploads and downloads require specific handling when the browser runs on a remote server instead of your local machine. This page covers both directions.

These methods work on any CDP connection (Puppeteer, or a Playwright CDP session), regardless of which region the browser runs in.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Puppeteer or Playwright installed locally

## Uploading files

`Browserless.uploadFile` attaches base64-encoded file data to an `<input type="file">` on the page. Send the bytes from your machine and Browserless builds the `File` in the browser context and fires the `input`/`change` events a real selection would. The page's upload handlers run as if a user picked the file.

```javascript

const TOKEN = "YOUR_API_TOKEN_HERE";
const BROWSER_WS_ENDPOINT = `wss://production-sfo.browserless.io?token=${TOKEN}`;

const fileToUpload = {
  name: "image.png",
  content: fs
    .readFileSync(path.join(process.cwd(), "image.png"))
    .toString("base64"),
  mimeType: "image/png",
};

const browser = await puppeteer.connect({
  browserWSEndpoint: BROWSER_WS_ENDPOINT,
});
const page = await browser.newPage();
await page.goto("https://jimpl.com/");
await page.waitForSelector("input[type=file]");

const cdp = await page.createCDPSession();
const result = await cdp.send("Browserless.uploadFile", {
  selector: "input[type=file]",
  files: [fileToUpload],
});

console.log(result); // { ok: true }
await browser.close();
```

**Parameters**

| Parameter          | Type     | Default              | Description                                                                                                         |
| ------------------ | -------- | -------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `selector`         | string   | —                    | CSS selector for the target `<input type="file">`. Must resolve to a file input or the call returns `InvalidTarget` |
| `files`            | object[] | —                    | One or more files to attach. Pass several to populate a multi-file input                                            |
| `files[].content`  | string   | —                    | Base64-encoded file contents                                                                                        |
| `files[].name`     | string   | `"file"`             | Filename reported to the page                                                                                       |
| `files[].mimeType` | string   | inferred from `name` | MIME type. When omitted, Browserless infers it from the filename extension                                          |

The combined decoded size of all files in one call is capped at 50 MB. The bytes ride the CDP WebSocket as base64, so an unbounded payload would stall the connection. Over the limit returns `{ ok: false, error: "FileTooLarge" }`.

**Response**

```json
{ "ok": true }
```

On failure, `ok` is `false` and `error` carries the reason: `SelectorNotFound` (no element matched), `InvalidTarget` (the element isn't a file input), `FileTooLarge`, or a validation message for a missing `selector`/`files`.

## Downloading files

Enable `Browserless.setDownloadEnabled`, then listen for `Browserless.fileDownloaded`. When a download finishes on the remote browser, Browserless reads it back off disk and streams it to you as base64. You never have to know the download directory or watch the filesystem.

Download events are off by default, since the file bytes count against your data transfer. You must opt in per session.

```javascript

const TOKEN = "YOUR_API_TOKEN_HERE";
const BROWSER_WS_ENDPOINT = `wss://production-sfo.browserless.io?token=${TOKEN}`;

const browser = await puppeteer.connect({
  browserWSEndpoint: BROWSER_WS_ENDPOINT,
});
const page = await browser.newPage();
const cdp = await page.createCDPSession();

await cdp.send("Browserless.setDownloadEnabled", { enabled: true });

cdp.on("Browserless.fileDownloaded", ({ filename, data }) => {
  const buffer = Buffer.from(data, "base64");
  fs.writeFileSync(path.join(process.cwd(), filename), buffer);
  console.log(`saved: ${filename} (${buffer.length} bytes)`);
});

await page.goto("https://scraping-sandbox.netlify.app/downloadsamples");
await page.click('a[href="/samples/sample.json"]');

// Give the download time to finish before closing the connection.
await new Promise((res) => setTimeout(res, 3000));
await browser.close();
```

**`Browserless.fileDownloaded` event**

| Field      | Type   | Description                                       |
| ---------- | ------ | ------------------------------------------------- |
| `filename` | string | The browser's suggested filename for the download |
| `mimeType` | string | MIME type inferred from the filename extension    |
| `size`     | number | Decoded file size in bytes                        |
| `data`     | string | Base64-encoded file contents                      |

The event fires once per completed download. Register the listener before the click that triggers the download so you don't miss it.

### Playwright

Playwright's `Download.saveAs()` already handles remote browsers natively, so you don't need the CDP method. Register a `download` listener and save directly.

```javascript

const TOKEN = "YOUR_API_TOKEN_HERE";
const BROWSER_WS_ENDPOINT = `wss://production-sfo.browserless.io/chromium/playwright?token=${TOKEN}`;

const browser = await playwright.chromium.connect(BROWSER_WS_ENDPOINT);
const page = await browser.newPage();
await page.goto("https://scraping-sandbox.netlify.app/downloadsamples");

// Register the listener before clicking so the download can't be missed.
const downloadPromise = page.waitForEvent("download");
await page.click('a[href="/samples/sample.json"]');
const download = await downloadPromise;

// Saves on the machine running this script, not the remote browser.
await download.saveAs(download.suggestedFilename());
await browser.close();
```

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>403 Forbidden</code> error?</summary>

Your API token is missing or expired. Pass it as a `?token=` query parameter in the WebSocket or HTTP URL. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My script works locally but fails on Browserless</summary>

Local browser settings may differ from the Browserless environment. Use `launch` parameters to match your local setup (viewport, user agent, timezone). See [launch parameters](/baas/launch-options) for the full list.

</details>

## Next steps

  <a href="/baas/session-management" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Session Management</h3>
      Manage browser sessions and persisted state
    
  </a>
  <a href="/baas/bot-detection/overview" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Bot Detection</h3>
      Bypass anti-bot protections with stealth and unblock modes
    
  </a>
  <a href="/baas/monitor-sessions/hybrid-automation" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Hybrid Automation</h3>
      Combine automated scripts with manual browser interaction
    
  </a>

---

# Load Balancers

URL: https://docs.browserless.io/baas/infrastructure/load-balancers

# Load Balancers

The Cloud Subscription plans and Dedicated plans have load balancing mechanisms built-in. This documentation will provide further information on how they work on each plan.

If you're looking for instructions on how to construct connection URLs and include your API token, see [Connection URLs](/baas/connection-url-patterns).

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Puppeteer or Playwright installed locally

## How Browserless Routing Works

```mermaid
graph LR
    A[Developer] --> B[Regional Endpoint]
    B --> C[Regional Load Balancer]
    C --> D[Browserless Instance]
```

This diagram shows the typical flow: your application sends requests to a regional endpoint, which routes through that region's load balancer to a Browserless instance in our fleet.

## Cloud subscriptions' load balancing

When you fire a request to our endpoints, it will go through our load balancers and will be sent to one of our instances to be handled. These are the cloud subscription endpoints available:

1. SFO endpoint: `https://production-sfo.browserless.io`
2. LON endpoint: `https://production-lon.browserless.io`
3. AMS endpoint: `https://production-ams.browserless.io`

**Choosing the right region greatly improves performance** by reducing latency and ensuring your requests are handled by the closest available instance.

We list our shared fleet's [IP addresses for whitelisting](/baas/troubleshooting/whitelisting-ips) purposes.

Our cloud-unit fleet is comprised of many machines running Browserless, and our load-balancer will automatically determine which machine to handle the request. If, say, a machine is too overloaded to handle the inbound connection then our load-balancer routes the request to another. In very plain terms we use a modified least-connected algorithm to ensure your request is handled appropriately.

## See Also

**[Connection URLs](/baas/connection-url-patterns)**: step-by-step instructions on building the connection string you need based on your library, preferred regions, flags and token.

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>403 Forbidden</code> error?</summary>

Your API token is missing or expired. Pass it as a `?token=` query parameter in the WebSocket or HTTP URL. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My script works locally but fails on Browserless</summary>

Local browser settings may differ from the Browserless environment. Use `launch` parameters to match your local setup (viewport, user agent, timezone). See [launch parameters](/baas/launch-options) for the full list.

</details>

## Next steps

  <a href="/baas/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BaaS Quickstart</h3>
      get started with Puppeteer or Playwright on Browserless
    
  </a>
  <a href="/baas/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure browser behavior and environment
    
  </a>

---

# Supported Browsers

URL: https://docs.browserless.io/baas/infrastructure/supported-browsers

# Supported Browsers

Browserless supports multiple browser engines to meet different automation needs and library requirements. Browser availability depends on your chosen automation library and the specific features you need.

> **Tip**
> Use the [Connection URL Builder](/baas/connection-url-patterns) to generate the right connection URL for your browser and library combination.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Puppeteer or Playwright installed locally

## Browser Support Matrix

| Browser | Puppeteer | Playwright | Stealth Mode | Description | Best For |
|---------|-----------|------------|--------------|-------------|----------|
| **Chromium** | ✅ | ✅ | ✅ | Open-source version of Chrome, providing excellent compatibility with most web automation tasks. Default browser and recommended for most use cases. | General web scraping and automation, cross-platform compatibility, consistent behavior |
| **Chrome** | ✅ | ✅ | ✅ | Latest stable version of Google Chrome with all proprietary features included. Offers the most up-to-date web standards support. | Testing against latest web standards, sites requiring Chrome features, enhanced bot detection evasion |
| **Stealth** | ✅ | ✅ | N/A | Privacy-hardened browser with fingerprint randomization and ad blocking built in. Use the `/stealth` endpoint to connect. | Sites with aggressive bot detection, bypassing browser fingerprinting, e-commerce and social media automation |
| **Firefox** | ❌ | ✅ | ❌ | Mozilla's Firefox browser engine, available exclusively through Playwright's native protocol. | Testing cross-browser compatibility, sites that behave differently in Firefox, Gecko engine-specific testing |
| **WebKit** | ❌ | ✅ | ❌ | Safari's browser engine, available exclusively through Playwright's native protocol. | iOS/Safari compatibility testing, WebKit-specific behavior testing, mobile web application testing |
| **Edge** | ❌ | ✅ | ❌ | Microsoft Edge browser, available exclusively through Playwright's native protocol via `/edge/playwright`. | Testing Edge-specific behavior, Microsoft ecosystem compatibility, Chromium-based Edge testing |

## When to Use Chrome over Chromium

Browserless defaults to Chromium binaries. Chrome binaries include additional proprietary codecs that Chromium does not ship with.

If a website shows a "browser not currently supported" message, the site likely requires codecs unavailable in Chromium. Switch to Chrome when:

- Accessing streaming platforms (Twitch, YouTube, Netflix)
- Using video conferencing tools
- Targeting sites that explicitly require proprietary codecs

**Default (Chromium):**
```bash
wss://production-sfo.browserless.io?token=YOUR_TOKEN
```

**Chrome (with additional codecs):**
```bash
wss://production-sfo.browserless.io/chrome?token=YOUR_TOKEN
```

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>403 Forbidden</code> error?</summary>

Your API token is missing or expired. Pass it as a `?token=` query parameter in the WebSocket or HTTP URL. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My script works locally but fails on Browserless</summary>

Local browser settings may differ from the Browserless environment. Use `launch` parameters to match your local setup (viewport, user agent, timezone). See [launch parameters](/baas/launch-options) for the full list.

</details>

## Next steps

  <a href="/baas/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BaaS Quickstart</h3>
      get started with Puppeteer or Playwright on Browserless
    
  </a>
  <a href="/baas/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure browser behavior and environment
    
  </a>

---

# Launch Parameters

URL: https://docs.browserless.io/baas/launch-options

Launch parameters configure how browsers start and behave in your sessions. You can pass them as individual query parameters or as a single JSON `launch` parameter in the connection URL.

If you've used Puppeteer or Playwright locally, these are the cloud equivalent of `browser.launch()` options.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Puppeteer or Playwright installed locally

## Passing Launch Parameters

There are two ways to pass options to the browser: as individual query parameters in the URL, or as a JSON object via the `launch` parameter.

- **Query parameters** are passed directly in the connection URL (e.g., `&headless=false&stealth=true`). Best for simple, standalone settings.
- **The `launch` object** is a JSON string passed as a single `launch` query parameter, either URL-encoded or base64-encoded. Use it for browser-level options like `headless` and `stealth`, or array flags like `args: [...]` whose brackets, quotes, and commas require encoding.

Both methods can be used together. Browserless merges them, with individual query parameters taking precedence.

## Query Parameters

These are passed directly in the connection URL. The example below passes a token, a US residential proxy, and a 1-minute timeout:

```js

const browser = await puppeteer.connect({
  browserWSEndpoint: `wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us&timeout=60000`,
});
```

For the full list of query parameters, see the [BaaS API Reference](/open-api/overview).

### Restricting ad-block rulesets (`blockAdsInclude`)

When you pass `blockAds=true`, Browserless loads uBlock Origin Lite and compiles
all **57 filter rulesets (~69,000 rules)** on **every fresh browser launch** — a
CPU-bound cost that adds up when many browsers start at once.

`blockAdsInclude` lets a session load **only the rulesets it needs**, so Chrome
compiles fewer of them and the browser starts faster.

It is **opt-in**: if you don't pass it, `blockAds=true` behaves exactly as before —
the full extension with all 57 rulesets. Pass it as a query parameter on the
connection URL, alongside `blockAds`:

```js

// no blockAdsInclude — full extension, unchanged
const full = await puppeteer.connect({
  browserWSEndpoint: `wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&blockAds=true`,
});

// only the recommended core lists — faster launch
const fast = await puppeteer.connect({
  browserWSEndpoint: `wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&blockAds=true&blockAdsInclude=ublock-filters,easylist,easyprivacy,pgl,ublock-badware,urlhaus-full`,
});
```

- Applies only when `blockAds=true`.
- Value is a comma-separated list **or** a JSON array of ruleset ids.
- Unknown ids are rejected with `400 Bad Request`.

#### Recommended set

These 6 language-neutral lists cover general ads, trackers, and malware — enough
for typical traffic. Pass them for a faster launch:

```text
blockAdsInclude=ublock-filters,easylist,easyprivacy,pgl,ublock-badware,urlhaus-full
```

#### What each group blocks

| Group | What it blocks | When to include |
|---|---|---|
| **Recommended (6)** | general ads, third-party trackers, malicious/badware URLs (language-neutral) | almost always — the recommended set |
| **Locale (37)** | region/language-specific ad lists (e.g. German, Japanese) | only if you scrape sites in that language |
| **Optional (14)** | cookie-consent banners, overlays, social/chat/AI widgets, notifications, experimental | only if you want annoyance removal |

#### Full ruleset reference

Include only the locales your traffic actually targets — the locale lists are the
bulk of the rules and therefore most of the startup cost. (This list reflects the
bundled uBlock Origin Lite build and can change when the extension is updated.)

**Recommended (6)** — the core set.

| id | list |
|---|---|
| `ublock-filters` | uBlock filters – Ads, trackers, and more |
| `easylist` | EasyList |
| `easyprivacy` | EasyPrivacy |
| `pgl` | Peter Lowe – Ads, trackers, and more |
| `ublock-badware` | uBlock filters – Badware risks |
| `urlhaus-full` | Malicious URL Blocklist |

<details>
<summary>Optional (14) — opt-in (annoyances, mobile, experimental)</summary>

| id | list |
|---|---|
| `adguard-mobile` | AdGuard/uBO – Mobile Ads |
| `block-lan` | Block Outsider Intrusion into LAN |
| `dpollock-0` | Dan Pollock's hosts file |
| `adguard-spyware-url` | AdGuard/uBO – URL Tracking Protection |
| `annoyances-ai` | EasyList – AI Widgets |
| `annoyances-cookies` | EasyList/uBO – Cookie Notices |
| `annoyances-overlays` | EasyList/uBO – Overlay Notices |
| `annoyances-social` | EasyList – Social Widgets |
| `annoyances-widgets` | EasyList – Chat Widgets |
| `annoyances-others` | EasyList – Other Annoyances |
| `annoyances-notifications` | EasyList – Notifications |
| `ublock-experimental` | uBlock filters – Experimental |
| `ubol-tests` | uBO Lite Test Filters |
| `rus-1` | RU AdList: Counters |

</details>

<details>
<summary>Locale (37) — region/language-specific ad lists</summary>

| id | list | languages |
|---|---|---|
| `alb-0` | Adblock List for Albania | sq |
| `ara-0` | Liste AR | ar kab |
| `bgr-0` | Bulgarian Adblock list | bg mk |
| `chn-0` | AdGuard Chinese | ug zh |
| `cze-0` | EasyList Czech and Slovak | cs sk |
| `deu-0` | EasyList Germany | de dsb hsb lb rm |
| `est-0` | Eesti saitidele kohandatud filter | et |
| `fin-0` | Adblock List for Finland | fi |
| `fra-0` | AdGuard Français | ar br ff fr lb oc son |
| `grc-0` | Greek AdBlock Filter | el |
| `hrv-0` | Serbo-Croatian filters | bs hr sr |
| `hun-0` | hufilter | hu |
| `idn-0` | ABPindo | id ms |
| `ind-0` | IndianList | as bn gu hi kn ml mr ne pa si ta te |
| `irn-0` | PersianBlocker | fa ps tg |
| `isl-0` | Icelandic ABP List | is |
| `isr-0` | EasyList Hebrew | he |
| `ita-0` | EasyList Italy | it lij |
| `jpn-1` | AdGuard Japanese | ja |
| `kor-1` | List-KR Classic | ko |
| `ltu-0` | EasyList Lithuania | lt |
| `lva-0` | Latvian List | lv |
| `mkd-0` | Macedonian adBlock Filters | mk |
| `nld-0` | AdGuard Dutch | af fy nl |
| `nor-0` | Nordiske filtre | nb nn no da is |
| `pol-0` | Oficjalne Polskie Filtry | szl pl |
| `pol-3` | CERT.PL's Warning List | szl pl |
| `rou-1` | Romanian Ad (ROad) Block List Light | ro md |
| `rus-0` | RU AdList | be kk tt ru uz |
| `spa-0` | EasyList Spanish | es ca eu gl … |
| `spa-1` | AdGuard Spanish/Portuguese | es pt ca … |
| `svn-0` | Slovenian List | sl |
| `swe-1` | Frellwit's Swedish Filter | sv |
| `tha-0` | EasyList Thailand | th |
| `tur-0` | AdGuard Turkish | tr |
| `ukr-0` | AdGuard Ukrainian | uk |
| `vie-1` | ABPVN List | vi |

</details>

## The `launch` Object

Options in the `launch` object are passed as an encoded JSON string. This is the cloud equivalent of Puppeteer's `launch({ options })` or Playwright's `launch({ options })`, passed as a query parameter to the connection URL. See the [full list of options](/open-api/overview) in the API reference.

### Encoding the `launch` Value

The `launch` value must be encoded before being appended to the URL. You can use URL encoding or base64. Both work the same way. Base64 is simpler because it avoids manually escaping brackets, quotes, and commas.

**URL encoding**

  
**Puppeteer:**

```js
import puppeteer from "puppeteer-core";

const launch = JSON.stringify({
  args: ["--window-size=1280,720"],
});

const browser = await puppeteer.connect({
  browserWSEndpoint: `wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&launch=${encodeURIComponent(launch)}`,
});
```
  
  
**Playwright CDP:**

```js
import playwright from "playwright";

const launch = JSON.stringify({
  args: ["--window-size=1280,720"],
});

const browser = await playwright.chromium.connectOverCDP(
  `wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&launch=${encodeURIComponent(launch)}`
);
```
  
  
**Playwright Connect:**

```js
import playwright from "playwright";

const launch = JSON.stringify({
  args: ["--window-size=1280,720"],
});

const browser = await playwright.chromium.connect(
  `wss://production-sfo.browserless.io/chromium/playwright?token=YOUR_API_TOKEN_HERE&launch=${encodeURIComponent(launch)}`
);
```
  
  
**Python:**

```python
import json
import urllib.parse
from playwright.sync_api import sync_playwright

launch = json.dumps({
    "args": ["--window-size=1280,720"],
})

with sync_playwright() as p:
    browser = p.chromium.connect_over_cdp(
        f"wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&launch={urllib.parse.quote(launch)}"
    )
```
  

**Base64 encoding**

  
**JavaScript:**

```js
import puppeteer from "puppeteer-core";

const launch = btoa(JSON.stringify({
  args: ["--window-size=1280,720"],
}));

const browser = await puppeteer.connect({
  browserWSEndpoint: `wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&launch=${launch}`,
});
```
  
  
**Python:**

```python
import json
import base64
from playwright.sync_api import sync_playwright

launch = base64.b64encode(json.dumps({
    "args": ["--window-size=1280,720"],
}).encode()).decode()

with sync_playwright() as p:
    browser = p.chromium.connect_over_cdp(
        f"wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&launch={launch}"
    )
```
  

### Chrome Flags

Chrome flags are passed via the `args` array inside the `launch` object. Encode the value before use (see [Encoding the `launch` Value](#encoding-the-launch-value) above).

Before encoding:
```json
{"args":["--window-size=1920,1080","--lang=en-US"]}
```

Encoded:
```bash
?launch=%7B%22args%22%3A%5B%22--window-size%3D1920%2C1080%22%2C%22--lang%3Den-US%22%5D%7D
```

Enterprise plans support the full set of [Chrome command-line switches](https://peter.sh/experiments/chromium-command-line-switches/). The following flags are available to all accounts:

- `--disable-features`
- `--disable-setuid-sandbox`
- `--disable-site-isolation-trials`
- `--disable-web-security`
- `--enable-features`
- `--font-render-hinting`
- `--force-color-profile`
- `--lang`
- `--proxy-bypass-list`
- `--proxy-server`
- `--window-size`

## Puppeteer-Specific Options

Some `launch` object options behave differently under `puppeteer.connect`. Here are additional notes for Puppeteer users.

### Ignore Default Args

On stealth routes (`/stealth`, `/chromium/stealth`, `/chrome/stealth`), you can pass an array to `ignoreDefaultArgs` to remove specific stealth-applied flags like `--disable-dev-shm-usage`. See [Customizing Stealth Flags](/baas/bot-detection/stealth#customizing-stealth-flags) for details.

Disabling default args can make Chromium unstable or prevent launch entirely.

```js
// Remove all default args
let launch = JSON.stringify({ ignoreDefaultArgs: true });

// Or remove specific args
launch = JSON.stringify({ ignoreDefaultArgs: ["--disable-dev-shm-usage"] });
```

### Accept Insecure Certificates

> **Warning**
> `acceptInsecureCerts` replaces the older `ignoreHTTPSErrors` parameter, which has been [renamed and deprecated by the Puppeteer team](https://github.com/puppeteer/puppeteer/issues/12341). Both are accepted for backwards compatibility, but use `acceptInsecureCerts` in new code.

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>403 Forbidden</code> error?</summary>

Your API token is missing or expired. Pass it as a `?token=` query parameter in the WebSocket or HTTP URL. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My script works locally but fails on Browserless</summary>

Local browser settings may differ from the Browserless environment. Use `launch` parameters to match your local setup (viewport, user agent, timezone). See [launch parameters](/baas/launch-options) for the full list.

</details>

## Next steps

  <a href="/baas/bot-detection/proxies" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Proxies</h3>
      Configure residential proxies and third-party proxy servers
    
  </a>

  <a href="/baas/bot-detection/stealth" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Bot Detection</h3>
      Use stealth mode and humanlike behavior to avoid detection
    
  </a>

  <a href="/baas/session-management/persisting-state" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Session Management</h3>
      Persist cookies, storage, and browser state across sessions
    
  </a>

---

# Beautiful Soup (Python)

URL: https://docs.browserless.io/baas/libraries/beautifulsoup

Beautiful Soup is one of the most popular scraping libraries for Python. It parses HTML strings so you can extract structured data. Use it alongside the [/content API](/rest-apis/content) or [BrowserQL](/browserql/start) to scrape any website.

Both APIs render the page in a browser before returning HTML. BrowserQL adds advanced stealth techniques to bypass bot detectors first.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Beautiful Soup installed (`pip install beautifulsoup4`)

## Basic Usage

Like [Cheerio](https://www.npmjs.com/package/cheerio), Beautiful Soup is _only_ a parser: it does not fetch HTML on its own. To get HTML from a website, you would use the `requests` library to download the page:

```py

from bs4 import BeautifulSoup

url = 'https://browserless.io/'
response = requests.get(url)
soup = BeautifulSoup(response.text, 'html.parser')
entries = soup.find_all('a')

for entry in entries:
    print(entry.text.strip())

```

The problem: `requests` downloads only the HTML source, **so it cannot return content that depends on JavaScript or user interaction to render**.

The `/content` API solves this. It renders and evaluates the page in a browser before returning the HTML. You can call it with the `requests` library:

```python

from bs4 import BeautifulSoup

response = requests.post("https://production-sfo.browserless.io/content",
    params={ "token": "YOUR_API_TOKEN_HERE"},
    json={
        "waitForTimeout": 5000,
        "url": "https://puppeteer.github.io/pptr.dev/"
    }
)

soup = BeautifulSoup(response.text, 'html.parser')
entries = soup.find_all("a", class_="pptr-sidebar-item")

for entry in entries:
    print(entry.text.strip())

```

In the example, we are using the [old Puppeteer doc site](https://puppeteer.github.io/pptr.dev/), which relies heavily on JS to render its content. With a usual `requests` or `cURL` request, it would only download the page's source code, the JavaScript wouldn't be interpreted and the content wouldn't be rendered.

Using our API, you can use [all the options available in the `/content` API](/rest-apis/content), use stealth mode, our residential proxies and more! For more reference, please check our [OpenAPI](/open-api/chrome-content).

## Bypass bot-blockers using `/unblock`

In cases where websites implement aggressive bot-detection mechanisms, you can use the `/unblock` API to bypass these. The `/unblock` API uses a variety of tools and strategies to override and hide the footprints that headless browsers leave behind, allowing you to access bot-protected websites from a remote interface.

Similar to the `/content` API, the `/unblock` API renders and evaluates the page in a browser, but with extra stealth features. This makes it ideal for scraping highly protected websites.

```py

from bs4 import BeautifulSoup

response = requests.post("https://production-sfo.browserless.io/unblock",
    params={ "token": "YOUR_API_TOKEN_HERE"},
    json={
        "waitForTimeout": 5000,
        "url": "https://puppeteer.github.io/pptr.dev/"
    }
)

html_content = json.loads(response.text)['content']
soup = BeautifulSoup(html_content, 'html.parser')

entries = soup.find_all("a", class_="pptr-sidebar-item")
for entry in entries:
    print(entry.text.strip())
```

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>403 Forbidden</code> error?</summary>

Your API token is missing or expired. Pass it as a `?token=` query parameter in the WebSocket or HTTP URL. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My script works locally but fails on Browserless</summary>

Local browser settings may differ from the Browserless environment. Use `launch` parameters to match your local setup (viewport, user agent, timezone). See [launch parameters](/baas/launch-options) for the full list.

</details>

## Next steps

  <a href="/baas/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BaaS Quickstart</h3>
      get started with Puppeteer or Playwright on Browserless
    
  </a>
  <a href="/baas/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure browser behavior and environment
    
  </a>

---

# Go (chromedp)

URL: https://docs.browserless.io/baas/libraries/chromedp

Browserless supports the [`chromedp`](https://pptr.dev/api/puppeteer.cdpsession) Go library in beta. The usage mirrors a local setup: use the `RemoteAllocator` to connect to the Browserless API instead of a local browser. For available connection parameters, see [connection URL patterns](/baas/connection-url-patterns).

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Go with the `chromedp` package installed (`go get github.com/chromedp/chromedp`)

## Basic Usage

The example below navigates to example.com and retrieves the page title:

```go
package main

    "context"
    "flag"
    "log"

    "github.com/chromedp/chromedp"
)

func main() {
    devtoolsWsURL := flag.String("devtools-ws-url", "wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE", "DevTools WebSsocket URL")
    flag.Parse()

    allocatorContext, cancel := chromedp.NewRemoteAllocator(context.Background(), *devtoolsWsURL,chromedp.NoModifyURL)
    defer cancel()

    ctx, cancel := chromedp.NewContext(allocatorContext)
    defer cancel()

    var title string
    if err := chromedp.Run(ctx,
        chromedp.Navigate("https://example.com"),
        chromedp.Title(&title),
    ); err != nil {
        log.Fatalf("Failed getting title of example.com: %v", err)
    }

    log.Println("Got title of:", title)
}

```

For launch options you can pass as query parameters, see [launch parameters](/baas/launch-options). To learn more about session management and reconnection, see [session management](/baas/session-management).

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>403 Forbidden</code> error?</summary>

Your API token is missing or expired. Pass it as a `?token=` query parameter in the WebSocket or HTTP URL. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My script works locally but fails on Browserless</summary>

Local browser settings may differ from the Browserless environment. Use `launch` parameters to match your local setup (viewport, user agent, timezone). See [launch parameters](/baas/launch-options) for the full list.

</details>

## Next steps

  <a href="/baas/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BaaS Quickstart</h3>
      get started with Puppeteer or Playwright on Browserless
    
  </a>
  <a href="/baas/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure browser behavior and environment
    
  </a>

---

# PHP

URL: https://docs.browserless.io/baas/libraries/php

Browserless works with PHP through cURL, Guzzle, or community frameworks like Laravel and Symfony.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- PHP with the cURL extension enabled

## Basic Usage

The quickest way to call Browserless from PHP is with cURL:

```php
<?php

$curl = curl_init();

curl_setopt_array($curl, [
    CURLOPT_URL => "https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE",
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_ENCODING => "",
    CURLOPT_MAXREDIRS => 10,
    CURLOPT_TIMEOUT => 30,
    CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
    CURLOPT_CUSTOMREQUEST => "POST",
    CURLOPT_POSTFIELDS => json_encode([
        "url" => "https://example.com/",
        "options" => [
            "fullPage" => true,
            "encoding" => "base64"
        ]
    ]),
    CURLOPT_HTTPHEADER => [
        "Content-Type: application/json"
    ],
]);

$response = curl_exec($curl);
$err = curl_error($curl);

curl_close($curl);

if ($err) {
    echo "cURL Error #:" . $err;
} else {
    // Save the base64 encoded screenshot
    file_put_contents('screenshot.png', base64_decode($response));
    echo "Screenshot saved successfully!";
}
```

## Laravel SDK (Community Supported)

> **Community Package**
> The [Laravel Browserless SDK](https://packagist.org/packages/millerphp/laravel-browserless) is a **community-supported package** created and maintained by [Christopher Miller](https://christophermiller.tech). While not officially supported by Browserless, it provides an excellent Laravel-native way to integrate with our services.
> 
> Learn more about the SDK:
> - 📦 [Package on Packagist](https://packagist.org/packages/millerphp/laravel-browserless)
> - 📖 [Official Browserless Blog: Scaling Laravel Dusk](https://www.browserless.io/blog/scaling-laravel-dusk-with-browserless)
> - 🚀 [Community Blog: SDK Launch Post](https://christophermiller.tech/blog/laravel-browserless-sdk-launch)

### Installation

```bash
composer require millerphp/laravel-browserless
```

### Laravel SDK Examples

The Laravel SDK provides a fluent, Laravel-native API for all Browserless features:

  
**PDF Generation:**

```php
use MillerPHP\LaravelBrowserless\Facades\Browserless;

// Generate a PDF from a URL
$pdf = Browserless::pdf()
    ->url('https://example.com/invoice/123')
    ->printBackground()
    ->format('A4')
    ->send()
    ->save('invoice-123.pdf');
```
  
  
**Screenshots:**

```php
// Capture a full-page screenshot
$screenshot = Browserless::screenshot()
    ->url('https://example.com')
    ->fullPage()
    ->send()
    ->save('homepage.png');
```
  
  
**Content Scraping:**

```php
// Scrape structured data
$result = Browserless::scrape()
    ->url('https://example.com/products')
    ->element('.product', [
        'text' => true,
        'attributes' => ['id', 'href'],
    ])
    ->send();

$products = $result->results('.product');
```
  
  
**BrowserQL:**

```php
// Run advanced browser automation with BQL
$result = Browserless::bql()
    ->query('
        mutation VerifyUser {
            goto(url: "https://example.com/protected") {
                status
            }
            solve(type: cloudflare) {
                found
                solved
                time
            }
        }
    ')
    ->stealth()
    ->humanLike()
    ->send();
```
  

## Modern PHP Examples (PHP 8.x)

### Using Guzzle HTTP Client

For better HTTP handling, use Guzzle:

```php
<?php

use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;

class BrowserlessClient
{
    private Client $client;
    private string $apiToken;
    private string $baseUrl;

    public function __construct(string $apiToken, string $region = 'sfo')
    {
        $this->apiToken = $apiToken;
        $this->baseUrl = "https://production-{$region}.browserless.io";
        $this->client = new Client([
            'timeout' => 60,
            'headers' => [
                'Content-Type' => 'application/json',
                'User-Agent' => 'PHP-Browserless-Client/1.0'
            ]
        ]);
    }

    public function screenshot(string $url, array $options = []): string
    {
        try {
            $response = $this->client->post("{$this->baseUrl}/screenshot", [
                'query' => ['token' => $this->apiToken],
                'json' => [
                    'url' => $url,
                    'options' => array_merge([
                        'fullPage' => true,
                        'type' => 'png'
                    ], $options)
                ]
            ]);

            return $response->getBody()->getContents();
        } catch (RequestException $e) {
            throw new Exception("Screenshot failed: " . $e->getMessage());
        }
    }

    public function generatePdf(string $url, array $options = []): string
    {
        try {
            $response = $this->client->post("{$this->baseUrl}/pdf", [
                'query' => ['token' => $this->apiToken],
                'json' => [
                    'url' => $url,
                    'options' => array_merge([
                        'format' => 'A4',
                        'printBackground' => true
                    ], $options)
                ]
            ]);

            return $response->getBody()->getContents();
        } catch (RequestException $e) {
            throw new Exception("PDF generation failed: " . $e->getMessage());
        }
    }

    public function getContent(string $url, int $waitFor = 0): array
    {
        try {
            $response = $this->client->post("{$this->baseUrl}/content", [
                'query' => ['token' => $this->apiToken],
                'json' => [
                    'url' => $url,
                    'waitForTimeout' => $waitFor
                ]
            ]);

            return json_decode($response->getBody()->getContents(), true);
        } catch (RequestException $e) {
            throw new Exception("Content retrieval failed: " . $e->getMessage());
        }
    }
}
```

### Usage Example with Error Handling

```php
<?php

try {
    $browserless = new BrowserlessClient('YOUR_API_TOKEN_HERE');

    // Generate a PDF report
    $pdfData = $browserless->generatePdf('https://example.com/report', [
        'format' => 'A4',
        'margin' => [
            'top' => '1cm',
            'bottom' => '1cm',
            'left' => '1cm',
            'right' => '1cm'
        ]
    ]);

    file_put_contents('report.pdf', $pdfData);
    echo "PDF generated successfully!\n";

    // Take a mobile screenshot
    $screenshotData = $browserless->screenshot('https://example.com', [
        'viewport' => [
            'width' => 375,
            'height' => 667,
            'deviceScaleFactor' => 2
        ]
    ]);

    file_put_contents('mobile-screenshot.png', $screenshotData);
    echo "Mobile screenshot captured!\n";

} catch (Exception $e) {
    error_log("Browserless operation failed: " . $e->getMessage());
}
```

## Advanced Use Cases

### Web Scraping with Content API

```php
<?php

class WebScraper
{
    private BrowserlessClient $client;

    public function __construct(BrowserlessClient $client)
    {
        $this->client = $client;
    }

    public function scrapeProductPrices(string $url): array
    {
        // Wait for dynamic content to load
        $content = $this->client->getContent($url, 3000);

        $dom = new DOMDocument();
        @$dom->loadHTML($content);
        $xpath = new DOMXPath($dom);

        $prices = [];
        $priceNodes = $xpath->query('//span[contains(concat(" ", normalize-space(@class), " "), " price ")]');

        foreach ($priceNodes as $node) {
            $prices[] = trim($node->textContent);
        }

        return $prices;
    }

    public function scrapeBatch(array $urls): array
    {
        $results = [];

        foreach ($urls as $url) {
            try {
                $results[$url] = $this->scrapeProductPrices($url);
                // Add delay to be respectful
                sleep(1);
            } catch (Exception $e) {
                $results[$url] = ['error' => $e->getMessage()];
            }
        }

        return $results;
    }
}
```

### Using with Proxy Support

```php
<?php

$curl = curl_init();

curl_setopt_array($curl, [
    CURLOPT_URL => "https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE&proxy=residential",
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 60,
    CURLOPT_CUSTOMREQUEST => "POST",
    CURLOPT_POSTFIELDS => json_encode([
        "url" => "https://example.com/",
        "options" => [
            "fullPage" => true
        ]
    ]),
    CURLOPT_HTTPHEADER => [
        "Content-Type: application/json"
    ],
]);

$response = curl_exec($curl);
curl_close($curl);
```

### Function Execution for Custom Logic

```php
<?php

function executeCustomBrowserFunction(string $url, string $jsCode): array
{
    $curl = curl_init();

    curl_setopt_array($curl, [
        CURLOPT_URL => "https://production-sfo.browserless.io/function?token=YOUR_API_TOKEN_HERE",
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 60,
        CURLOPT_CUSTOMREQUEST => "POST",
        CURLOPT_POSTFIELDS => json_encode([
            "code" => $jsCode,
            "context" => [
                "url" => $url
            ]
        ]),
        CURLOPT_HTTPHEADER => [
            "Content-Type: application/json"
        ],
    ]);

    $response = curl_exec($curl);
    if ($response === false) {
        $error = curl_error($curl);
        curl_close($curl);
        throw new RuntimeException("Request failed: {$error}");
    }
    curl_close($curl);

    $decoded = json_decode($response, true);
    if (!is_array($decoded)) {
        throw new RuntimeException('Invalid JSON response');
    }

    return $decoded;
}

// Example: Get page performance metrics
$performanceCode = '

  await page.goto(context.url);

  const metrics = await page.evaluate(() => {
    const navigation = performance.getEntriesByType("navigation")[0];
    return {
      loadTime: navigation.loadEventEnd - navigation.loadEventStart,
      domContentLoaded: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart,
      firstPaint: performance.getEntriesByType("paint")[0]?.startTime || 0
    };
  });

  return metrics;
};
';

$metrics = executeCustomBrowserFunction('https://example.com', $performanceCode);
echo "Page load time: " . $metrics['loadTime'] . "ms\n";
```

## Framework Integration Examples

### Symfony Integration

```php
<?php

namespace App\Service;

use Symfony\Component\HttpClient\HttpClient;
use Symfony\Contracts\HttpClient\HttpClientInterface;

class BrowserlessService
{
    private HttpClientInterface $client;
    private string $apiToken;

    public function __construct(string $apiToken)
    {
        $this->apiToken = $apiToken;
        $this->client = HttpClient::create([
            'timeout' => 60,
            'headers' => [
                'Content-Type' => 'application/json'
            ]
        ]);
    }

    public function generateInvoicePdf(int $invoiceId): string
    {
        $response = $this->client->request('POST',
            'https://production-sfo.browserless.io/pdf', [
            'query' => ['token' => $this->apiToken],
            'json' => [
                'url' => "https://yourapp.com/invoice/{$invoiceId}",
                'options' => [
                    'format' => 'A4',
                    'printBackground' => true
                ]
            ]
        ]);

        return $response->getContent();
    }
}
```

### WordPress Plugin Integration

```php
<?php

class WP_Browserless_Plugin
{
    private string $apiToken;

    public function __construct()
    {
        $this->apiToken = get_option('browserless_api_token');
        add_action('init', [$this, 'init']);
    }

    public function init(): void
    {
        // Only expose the handler to authenticated users. Do not add
        // wp_ajax_nopriv_* unless public, unauthenticated access is intended.
        add_action('wp_ajax_generate_pdf', [$this, 'generatePdf']);
    }

    public function generatePdf(): void
    {
        check_ajax_referer('browserless_generate_pdf');

        if (!current_user_can('edit_posts')) {
            wp_die('Forbidden', 'Forbidden', ['response' => 403]);
        }

        $post_id = intval($_POST['post_id'] ?? 0);
        $post_url = get_permalink($post_id);

        $response = wp_remote_post('https://production-sfo.browserless.io/pdf', [
            'headers' => ['Content-Type' => 'application/json'],
            'body' => json_encode([
                'url' => $post_url,
                'options' => [
                    'format' => 'A4',
                    'printBackground' => true
                ]
            ]),
            'timeout' => 60
        ]);

        if (is_wp_error($response)) {
            wp_die('PDF generation failed');
        }

        $pdf_data = wp_remote_retrieve_body($response);

        header('Content-Type: application/pdf');
        header('Content-Disposition: attachment; filename="post-' . $post_id . '.pdf"');
        echo $pdf_data;
        exit;
    }
}

new WP_Browserless_Plugin();
```

## Best Practices

### Error Handling and Retry Logic

```php
<?php

class RobustBrowserlessClient
{
    private const MAX_RETRIES = 3;
    private const RETRY_DELAY = 1; // seconds

    private function executeWithRetry(callable $operation, int $maxRetries = self::MAX_RETRIES): mixed
    {
        $lastException = null;

        for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
            try {
                return $operation();
            } catch (Exception $e) {
                $lastException = $e;

                if ($attempt < $maxRetries) {
                    sleep(self::RETRY_DELAY * $attempt); // Exponential backoff
                    continue;
                }
            }
        }

        throw $lastException;
    }

    public function reliableScreenshot(string $url): string
    {
        return $this->executeWithRetry(function() use ($url) {
            // Your screenshot logic here
            return $this->client->screenshot($url);
        });
    }
}
```

### Configuration Management

```php
<?php

class BrowserlessConfig
{
    public readonly string $apiToken;
    public readonly string $baseUrl;
    public readonly int $timeout;
    public readonly array $defaultOptions;

    public function __construct(
        ?string $apiToken = null,
        string $region = 'sfo',
        int $timeout = 60
    ) {
        $this->apiToken = $apiToken ?? $_ENV['BROWSERLESS_API_TOKEN'] ?? throw new InvalidArgumentException('API token required');
        $this->baseUrl = "https://production-{$region}.browserless.io";
        $this->timeout = $timeout;
        $this->defaultOptions = [
            'stealth' => true,
            'blockAds' => true
        ];
    }
}
```

## Performance Optimization

### Concurrent Requests with ReactPHP

```php
<?php

use React\Http\Browser;
use React\Promise\Promise;

function concurrentScreenshots(array $urls): Promise
{
    $browser = new Browser();
    $promises = [];

    foreach ($urls as $url) {
        $promises[] = $browser->post('https://production-sfo.browserless.io/screenshot', [
            'Content-Type' => 'application/json'
        ], json_encode([
            'url' => $url,
            'options' => ['fullPage' => true]
        ]));
    }

    return \React\Promise\all($promises);
}
```

## Security Considerations

- **Never expose your API token** in client-side code or public repositories
- **Use environment variables** to store sensitive configuration
- **Implement rate limiting** in your application to avoid hitting API limits
- **Validate and sanitize URLs** before sending them to Browserless
- **Use HTTPS** for all communications with the Browserless API

## Troubleshooting Common Issues

### Timeout Issues
If you're experiencing timeouts, increase the timeout values:

```php
// Increase cURL timeout
curl_setopt($curl, CURLOPT_TIMEOUT, 120);

// Or add timeout parameter to URL
$url = "https://production-sfo.browserless.io/pdf?token=YOUR_TOKEN&timeout=120000";
```

### Memory Issues with Large Responses
For large PDFs or screenshots, consider streaming:

```php
$handle = fopen('large-file.pdf', 'w');
curl_setopt($curl, CURLOPT_FILE, $handle);
curl_exec($curl);
fclose($handle);
```

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>403 Forbidden</code> error?</summary>

Your API token is missing or expired. Pass it as a `?token=` query parameter in the WebSocket or HTTP URL. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My script works locally but fails on Browserless</summary>

Local browser settings may differ from the Browserless environment. Use `launch` parameters to match your local setup (viewport, user agent, timezone). See [launch parameters](/baas/launch-options) for the full list.

</details>

## Next steps

Be sure to [let us know if you have questions or issues](https://www.browserless.io/contact).

  <a href="/rest-apis/intro" className="next-step-card-link">
    
      <h3 className="next-step-card-title">REST APIs</h3>
      Complete API reference for every endpoint
    
  </a>
  <a href="/browserql/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BrowserQL</h3>
      Advanced browser automation with a GraphQL API
    
  </a>
  <a href="/baas/session-management/standard-sessions" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Session management</h3>
      Keep browser sessions alive across requests
    
  </a>
  <a href="/baas/best-practices" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Best practices</h3>
      Production deployment recommendations
    
  </a>

---

# Go (playwright-go)

URL: https://docs.browserless.io/baas/libraries/playwright-go

[`playwright-go`](https://github.com/playwright-community/playwright-go) is the community-maintained Go port of Playwright. It supports the same `ConnectOverCDP` method the other Playwright SDKs use, so connecting to Browserless works the same way. Point it at a `wss://` endpoint with your API token as a query parameter. For available connection parameters, see [connection URL patterns](/baas/connection-url-patterns).

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Go with the `playwright-go` package installed (`go get github.com/playwright-community/playwright-go`)

## Basic usage

The example below connects to Browserless, navigates to example.com, and prints the page title:

```go
package main

	"fmt"
	"log"

	"github.com/playwright-community/playwright-go"
)

const token = "YOUR_API_TOKEN_HERE" // Replace with your actual token.

func main() {
	pw, err := playwright.Run()
	if err != nil {
		log.Fatalf("could not start playwright: %v", err)
	}
	defer pw.Stop()

	// Connect to Browserless over CDP.
	browser, err := pw.Chromium.ConnectOverCDP(
		fmt.Sprintf("wss://production-sfo.browserless.io?token=%s", token),
	)
	if err != nil {
		log.Fatalf("could not connect to browserless: %v", err)
	}
	defer browser.Close()

	page, err := browser.NewPage()
	if err != nil {
		log.Fatalf("could not create page: %v", err)
	}

	if _, err = page.Goto("https://www.example.com/"); err != nil {
		log.Fatalf("could not navigate: %v", err)
	}

	title, err := page.Title()
	if err != nil {
		log.Fatalf("could not get title: %v", err)
	}
	fmt.Printf("The page's title is: %s\n", title)
}
```

**Output**
```text
The page's title is: Example Domain
```

## Using an external proxy

When you pass an external proxy URL as a query parameter, any special characters in the credentials (like `@`, `:`, or `#`) need to be percent-encoded or the URL parser will misinterpret them. In Go, use `url.QueryEscape` for this:

```go
package main

	"fmt"
	"log"
	"net/url"

	"github.com/playwright-community/playwright-go"
)

const (
	token     = "YOUR_API_TOKEN_HERE"
	proxyHost = "proxy.example.com"
	proxyPort = "8080"
	proxyUser = "username"
	proxyPass = "p@ssw0rd#123" // Special chars that need encoding.
)

func main() {
	pw, err := playwright.Run()
	if err != nil {
		log.Fatalf("could not start playwright: %v", err)
	}
	defer pw.Stop()

	// Build and encode the proxy URL so special characters don't break the query string.
	proxyURL := fmt.Sprintf("http://%s:%s@%s:%s", proxyUser, proxyPass, proxyHost, proxyPort)
	encodedProxy := url.QueryEscape(proxyURL)

	wsEndpoint := fmt.Sprintf(
		"wss://production-sfo.browserless.io?token=%s&externalProxyServer=%s",
		token,
		encodedProxy,
	)

	browser, err := pw.Chromium.ConnectOverCDP(wsEndpoint)
	if err != nil {
		log.Fatalf("could not connect: %v", err)
	}
	defer browser.Close()

	page, err := browser.NewPage()
	if err != nil {
		log.Fatalf("could not create page: %v", err)
	}

	if _, err = page.Goto("https://httpbin.org/ip"); err != nil {
		log.Fatalf("could not navigate: %v", err)
	}

	body, err := page.Locator("body").TextContent()
	if err != nil {
		log.Fatalf("could not read body: %v", err)
	}
	fmt.Println(body)
}
```

**Output**
```json
{
  "origin": "proxy-ip-here"
}
```

For more proxy options (built-in residential/datacenter proxies, context-level proxy config), see [Proxies](/baas/bot-detection/proxies).

## Stealth mode

To bypass bot detection, swap the base path to `/stealth` (or `/chromium/stealth`). Everything else stays the same:

```go
wsEndpoint := fmt.Sprintf(
	"wss://production-sfo.browserless.io/stealth?token=%s",
	token,
)
```

You can combine stealth with any other query parameters:

```go
wsEndpoint := fmt.Sprintf(
	"wss://production-sfo.browserless.io/stealth?token=%s&externalProxyServer=%s&timeout=300000",
	token,
	encodedProxy,
)
```

See [Stealth Routes](/baas/bot-detection/stealth) for the full list of stealth options.

## Passing launch parameters

`launch` parameters and Browserless-specific options go directly in the query string. Because `fmt.Sprintf` builds a plain string, you can append as many parameters as you need:

```go
wsEndpoint := fmt.Sprintf(
	"wss://production-sfo.browserless.io?token=%s&--window-size=1920,1080&blockAds=true&timeout=60000",
	token,
)
```

For the full list of available parameters, see [`launch` parameters](/baas/launch-options).

## Go-specific tips

- **Always call `url.QueryEscape`** on values that contain special characters before inserting them into the query string. Go's `fmt.Sprintf` does no escaping on its own, so unencoded `@` or `#` characters will corrupt the URL.
- **Use `defer browser.Close()`** to make sure the remote browser is released even if your program panics or returns early. Leaked sessions count against your concurrency limit.
- **`ConnectOverCDP` vs `Connect`**: `ConnectOverCDP` uses the Chrome DevTools Protocol and is the recommended method for Browserless. `Connect` uses Playwright's native protocol, which requires a Playwright-specific server and won't work with Browserless endpoints. Stick with `ConnectOverCDP`.

## Related docs

- [Connection URL patterns](/baas/connection-url-patterns) – endpoint construction and query parameters
- [`launch` parameters](/baas/launch-options) – browser flags and Browserless-specific settings
- [Proxies](/baas/bot-detection/proxies) – built-in and external proxy configuration
- [Stealth Routes](/baas/bot-detection/stealth) – bot detection bypass
- [Session management](/baas/session-management) – reconnection and persistent sessions
- [Playwright customizations](/baas/advanced-configurations/playwright-customizations) – advanced Playwright configuration

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>403 Forbidden</code> error?</summary>

Your API token is missing or expired. Pass it as a `?token=` query parameter in the WebSocket or HTTP URL. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My script works locally but fails on Browserless</summary>

Local browser settings may differ from the Browserless environment. Use `launch` parameters to match your local setup (viewport, user agent, timezone). See [launch parameters](/baas/launch-options) for the full list.

</details>

## Next steps

  <a href="/baas/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BaaS Quickstart</h3>
      get started with Puppeteer or Playwright on Browserless
    
  </a>
  <a href="/baas/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure browser behavior and environment
    
  </a>

---

# Pyppeteer (Python) - UNMAINTAINED

URL: https://docs.browserless.io/baas/libraries/pyppeteer

> **Warning**
> We suggest not using this library anymore as it is now unmaintained, please consider [playwright-python](https://www.browserless.io/docs/playwright#python-playwright) as an alternative

Connect your Python automation to Browserless using [Pyppeteer](https://github.com/pyppeteer/pyppeteer), an unofficial Python port of Puppeteer.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Pyppeteer installed (`pip install pyppeteer`)

## Basic Usage

```python

async def main():
  browser = await pyppeteer.launcher.connect(
    browserWSEndpoint='wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE'
    )
  page = await browser.newPage()
  url = "https://www.example.com"
  await page.goto(url)
  values = await page.evaluate('''() => document.querySelector('h1').innerHTML
    ''')
  print(values)
  await browser.close()

asyncio.get_event_loop().run_until_complete(main())
```

This example navigates to a URL and returns the page's `h1` content.

For connection URL options and query parameters, see the [connection URL patterns](/baas/connection-url-patterns) reference. If you need help getting your API token, see [API key management](/overview/api-keys).

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>403 Forbidden</code> error?</summary>

Your API token is missing or expired. Pass it as a `?token=` query parameter in the WebSocket or HTTP URL. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My script works locally but fails on Browserless</summary>

Local browser settings may differ from the Browserless environment. Use `launch` parameters to match your local setup (viewport, user agent, timezone). See [launch parameters](/baas/launch-options) for the full list.

</details>

## Next steps

  <a href="/baas/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BaaS Quickstart</h3>
      get started with Puppeteer or Playwright on Browserless
    
  </a>
  <a href="/baas/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure browser behavior and environment
    
  </a>

---

# Scrapy (Python)

URL: https://docs.browserless.io/baas/libraries/scrapy

Use Scrapy alongside the [/content API](/rest-apis/content) or [BrowserQL](/browserql/start) to scrape data. Implement the `start_requests()` method to query `/content` or BrowserQL, and keep your scraping logic the same.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Scrapy installed (`pip install scrapy`)

## Basic Usage

Scrapy uses HTTP requests to download HTML and build a DOM-like abstraction, **so it can only return the initial render of a page without interacting with it**. The `/content` API ensures the HTML is rendered and evaluated inside a browser, not downloaded raw. BrowserQL goes further, using advanced stealth techniques to bypass bot detectors before rendering.

Implement the `start_requests()` method to query the `/content` API, and keep your scraping logic the same.

```py

class PptrDocsSpider(scrapy.Spider):
    name = "pptr-docs"

    def start_requests (self):
        options = {
            "url": "https://puppeteer.github.io/pptr.dev/",
            "waitForTimeout": 5000
        }

        yield scrapy.Request(
            url="https://production-sfo.browserless.io/content?token=YOUR_API_TOKEN_HERE",
            method='POST',
            dont_filter=True,
            headers={"Content-Type": "application/json"},
            body=json.dumps(options)
        )

    def parse(self, response):
        entries = response.css('sidebar-component a.pptr-sidebar-item')
        for entry in entries:
            yield{
                'title' : entry.css('::text').get(),
                'url' : entry.css('::attr(href)').get(),
            }

```

You can use [all the options available in the `/content` API](/rest-apis/content), use stealth mode, our residential proxies and more! For more reference, please see this [blogpost](https://www.browserless.io/blog/2024/02/15/scrapy-headless/).

## Bypass bot-blockers using `/unblock`

In cases where websites implement aggressive bot-detection mechanisms, you can use the `/unblock` API to bypass these. The `/unblock` API uses a variety of tools and strategies to override and hide the footprints that headless browsers leave behind, allowing you to access bot-protected websites from a remote interface.

Similar to the `/content` API, the `/unblock` API renders and evaluates the page in a browser, but with extra stealth features. This makes it ideal for scraping highly protected websites.

Make the `/unblock` request by yielding `scrapy.Request()`, and **extract the HTML content from the JSON response in the `parse()` method**. Here's an example:

```py

class PptrDocsSpider(scrapy.Spider):
    name = "pptr-docs"

    def start_requests (self):
        options = {
            "url": "https://puppeteer.github.io/pptr.dev/",
            "waitForTimeout": 5000
        }

        yield scrapy.Request(
            url="https://production-sfo.browserless.io/unblock?token=YOUR_API_TOKEN_HERE",
            method='POST',
            dont_filter=True,
            headers={"Content-Type": "application/json"},
            body=json.dumps(options)
        )

    def parse(self, response):
        # Extracts the content from the response
        html_content = json.loads(response.text)['content']
        dom = scrapy.Selector(text=html_content)

        # Continue as normal
        entries = dom.css('sidebar-component a.pptr-sidebar-item')
        for entry in entries:
            yield{
                'title' : entry.css('::text').get(),
                'url' : entry.css('::attr(href)').get(),
            }

```

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>403 Forbidden</code> error?</summary>

Your API token is missing or expired. Pass it as a `?token=` query parameter in the WebSocket or HTTP URL. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My script works locally but fails on Browserless</summary>

Local browser settings may differ from the Browserless environment. Use `launch` parameters to match your local setup (viewport, user agent, timezone). See [launch parameters](/baas/launch-options) for the full list.

</details>

## Next steps

  <a href="/baas/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BaaS Quickstart</h3>
      get started with Puppeteer or Playwright on Browserless
    
  </a>
  <a href="/baas/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure browser behavior and environment
    
  </a>

---

# Migration Guide

URL: https://docs.browserless.io/baas/migrate

<!-- markdownlint-disable no-trailing-punctuation -->

> **Important Note for Shared Fleet Users**
> **For users on the Shared Fleet**: Migration from v1 to v2 is primarily a connection URL change. Most of the changes described in this guide are relevant for self-hosted deployments. If you're using the shared fleet, you'll mainly need to:
> 
> 1. Update your connection URLs as explained in our [Quickstart page](https://docs.browserless.io/baas/start)
> 2. Ensure you're using a compatible Puppeteer/Playwright version (see our [versions page](https://docs.browserless.io/baas/versions))

Browserless 2.0 is finally here! It's a complete rewrite of the core of Browserless, and many things have changed. To keep things short, this rewrite allows us to support many new things which you can read about [in our blog](https://www.browserless.io/blog/2023/12/06/browserless-2-0/).

This document serves as a guide to migrate existing systems from Browserless 1.x to 2.x. New features are not covered in this guide, but we do recommend checking the new things outBelow is a table of contents from which you can pick to better aide in migrating your existing codebase or REST calls.

For the most comprehensive API documentation, feel free to visit the built-in Swagger doc-site located at your Browserless instance `/docs` route. Browserless also logs this on startup for help.

**Prerequisites:**

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Puppeteer or Playwright installed locally

## List of Major and Potentially Breaking Changes

- Drop support for Selenium and Webdriver
- Many docker environment variable changes (see below).
- Drop support for `DEFAULT_*` arguments.
- Drop support for pre-booting and keep-alive.
- `TOKEN` is not generated automatically. If none is set, every endpoint (including `/function`) is unauthenticated.
- When using custom launch flags for APIs and Libraries: please update to the new format which is a consolidated `&launch` parameter.
- Playwright's Chromium path is now `/chromium/playwright` in order to reflect other browsers in different paths.
- Unknown query parameters or JSON POST parameters will now respond with a 4xx error.
- New `/function` API environment and uses Ecmascript modules. We no longer run /function calls in the NodeJS environment, and instead run inside the browser's JavaScript runtime. `import` does work and loads modules over HTTP instead of locally.
- `blockAds` now uses Ublock Origin to facilitate ad-blocking. No more request interception.
- The prior `/stats` API is now located at `/performance`.

## Design Changes and overall goals

Browserless 2.xx was designed and developed for the sole purpose of making browser behavior more deterministic. We want to make the process of operating a headless browser stack more developer-friendly since these workflows can often be frustrating to work with. What do we mean by this? Here's a few points

- Unknown parameters will fail with 4xx errors since they're unrecognized.
- No more pre-booting or keep-alive as they can cause so many problems.
- A typescript-first workflow. All routes are strongly typed with a prescriptive approach so you can add your own.
- Better logging and built-in doc-site with all parameters and definitions.
- Enhanced and comprehensive security.

This, in combination with the past 5+ years experience in headless, means there's several things that are different in Browserless 2.xx. Please refer to the above Table of Contents to find the most relevant information for your API, library or use-case.

Finally, Browserless does its best to be friendly and helpful by logging things like out-of-date parameters and configuration. Please be sure to read through logs when migrating existing workflows over and we'll continue to improve these messages as time goes on.

## Docker

For a complete reference on Docker environment variable changes, removed variables, renamed variables, and the Docker registry change, see the [Enterprise Migrate from V1](/enterprise/migrate-from-v1#docker-environment-variables) guide.

## Cloud Service

Browserless v2 is available as a cloud service and offers several advanced features, including:

- [**BQL**](https://docs.browserless.io/browserql/start "BrowserQL Quickstart | Browserless.io")
- [**Unblock API**](https://docs.browserless.io/rest-apis/unblock "/unblock API | Browserless.io")
- [**Captcha Solving**](https://docs.browserless.io/baas/bot-detection/captchas "Solving Captchas | Browserless.io")
- [**Live Url**](https://docs.browserless.io/baas/session-management/hybrid#how-to-stream-a-remote-headless-browsers "Hybrid automation - human in the loop | Browserless.io")
- [and more](https://docs.browserless.io/ "Quickstart | Browserless.io")

To ensure optimal performance, we provide three fleets that you can choose from, based on your geographic location:

- `https://production-sfo.browserless.io/` (Based in San Francisco, USA).
- `https://production-lon.browserless.io/` (Based in London, England).
- `https://production-ams.browserless.io/` (Based in Amsterdam, Netherlands).

All Cloud plans have access to Browserless v2. Feature availability still varies by plan. Our [Pricing page](https://www.browserless.io/pricing) provides detailed information on each plan.

Screen Recording, State Proxying, City Proxying, and Captcha Solving are the features that remain plan-specific.

## Libraries

We tried to keep library changes as little as possible since the compromise the core of the platform. However, one change is the consolidation of *all* launch options into a single query string parameter of a JSON-stringified "launch". Connect calls are now more strict with query parameters. Any unknown parameter will cause connect calls to fail since they aren't supported by Browserless. In version 1.xx unknown parameters were simply ignored.

Browserless 2.xx shims old launch options query parameters internally, so it'll fix 1.xx requests for you. Here's a few examples of this so you can make any changes in code.

You may also optionally base64 encode these JSON stringified `launch` parameter as well.

### Launch flags:

**Before**
`ws://localhost:3000?token=ABCD&--window-size=1920,1080`

**After**
`ws://localhost:3000?token=ABCD&launch={"args":["--window-size=1920,108"]}`

### Headless flags:

**Before**
`ws://localhost:3000?token=ABCD&headless=shell`

**After**
`ws://localhost:3000?token=ABCD&launch={"headless":"shell"}`

## /function API

The biggest difference in the function API is that it no longer operates inside of the NodeJS runtime, but in the browser. It also supports ECMAScript modules, so you'll have to tweak existing code to work inside 2.xx. This is a fairly large change, and any /function calls should be well tested prior to deploying them into production.

The function API is still hybrid in that it can accept a JavaScript file (with content-type application/javascript) OR a JSON file with `code` and `context` properties. Be sure to read more about it on the built-in doc-site.

Browserless also now infers the appropriate response type so you no longer need to specify it. Simply return whatever data you wish and it'll write the request appropriately.

Browserless 2.xx shims old launch options query parameters internally, so it'll fix 1.xx-style requests for you.

### Basic Request

**Before**

```js
// CommonJS no longer supported
module.exports = async({ page }) => {
  await page.goto('https://example.com');

  // No longer need to response with this object schematic
  return {
    data: await page.screenshot(),
    type: 'image/png',
  };
}
```

**After**

```js
// Use the "export default" keywords

  await page.goto('https://example.com');
  // No longer need to tell what type
  return await page.screenshot();
}
```

### Request with requires

**Before**

```js
// npm packages are no longer supported
const url = require('url');

module.exports = async({ page }) => {
  await page.goto('https://example.com');
  const links = await page.evaluate(() => [...document.querySelectorAll('a')].map(l => l.href));
  const parsed = links.map((link) => url.parse(link));

  return {
    data: parsed,
    type: 'application/json',
  };
}
```

**After**

```js

  await page.goto('https://example.com');
  const links = await page.evaluate(() => [...document.querySelectorAll('a')].map(l => l.href));

  // Can use URL and other browser-based APIs as well as load them
  // with `import` syntax from hosts like unpkg
  const parsed = links.map((link) => new URL(link));

  return parsed;
}
```

## /pdf API

The PDF API operates in a similar fashion as the in Browserless 1.xx. The biggest difference is how launch flags are handled, which now use a consolidated `launch` object to hold all CLI arguments and flags.

`waitFor` has now been removed and deprecated in favor of puppeteer's discrete API methods: `waitForEvent`, `waitForFunction`, `waitForSelector` and `waitForTimeout`.

`rotate` has been removed due to lack of usage and included 3rd party dependencies. `safeMode` has also been removed in favor of using puppeteer's streaming capabilities that are much less error-prone.

## /screenshot API

The /screenshot API operates very similarly to how it did in Browserless 1.xx. A few properties and options have been removed due to their infrequent usage and 3rd party dependencies.

`waitFor` has now been removed and deprecated in favor of puppeteer's discrete API methods: `waitForEvent`, `waitForFunction`, `waitForSelector` and `waitForTimeout`.

`manipulate` has also been removed since it was infrequently used and required numerous other dependencies in order to run properly.

## /scrape API

The /scrape API operates similarly to how it did in Browserless 1.xx. A few properties and options have been removed due to their infrequent usage and 3rd party dependencies.

`waitFor` has now been removed and deprecated in favor of puppeteer's discrete API methods: `waitForEvent`, `waitForFunction`, `waitForSelector` and `waitForTimeout`.

## /stats API

The /stats API has been moved to /performance now to better reflect the action its doing since the word "stats" in this context can be ambiguous. Internally, it still runs lighthouse reports and you can provide various configurations to it.

## /screencast API

The /screencast API has been removed in favor of a library-based approach. Browserless 2.xx now ships with what we call an "embedded" API which you can use to initiate a recording of a page and get the response back (with audio!).

Please refer to the built-in doc-site for how to do screencasting or consult your library of choice for how to screencast.

## /config API

The /config API now returns more meta-data about the instance including more parameters. Please visit the internal doc-site page to see the JSON response and all the properties.

## FAQ & Troubleshooting

<details>
<summary>Where are the BaaS v1 docs?</summary>

The full v1 documentation is still available at [/baas/v1/start](/baas/v1/start), including the v1 Docker configuration and API references. It's deprecated and kept for reference only; v1 pages are intentionally excluded from search indexes, so bookmark the link if you still run v1. For new work, use [BaaS v2](/baas/start) or [BrowserQL](/browserql/start).

</details>

<details>
<summary>Why am I getting a <code>403 Forbidden</code> error?</summary>

Your API token is missing or expired. Pass it as a `?token=` query parameter in the WebSocket or HTTP URL. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My script works locally but fails on Browserless</summary>

Local browser settings may differ from the Browserless environment. Use `launch` parameters to match your local setup (viewport, user agent, timezone). See [launch parameters](/baas/launch-options) for the full list.

</details>

## Next steps

  <a href="/baas/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BaaS Quickstart</h3>
      get started with Puppeteer or Playwright on Browserless
    
  </a>
  <a href="/baas/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure browser behavior and environment
    
  </a>

---

# Embed a live browser in your app

URL: https://docs.browserless.io/baas/monitor-sessions/embedding-live-url

# Embed a live browser in your app

Live URLs are regular HTTPS pages, so you can embed the live browser inside your own UI with an iframe. This guide covers the required iframe permissions, sizing, view-only mode, and connection behavior.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- A Puppeteer or Playwright session connected to Browserless, with a page-level CDP session
- Familiarity with the [Hybrid Automation guide](/baas/monitor-sessions/hybrid-automation)
- Permission to add an iframe and update the Content Security Policy (CSP) for your app

## Overview

Create a Live URL from your automation, then use the returned `liveURL` as the iframe source. The URL contains a short-lived viewer ID instead of your Browserless API token.

## Step 1: Create and configure the Live URL

Set Live URL options when you mint the URL:

```js
const { liveURL } = await cdp.send('Browserless.liveURL', {
  interactable: false,
  resizable: false,
  showBrowserInterface: true,
  timeout: 120000,
});
```

### View-only embedding

Use `interactable: false` when viewers should only watch. Browserless drops viewer input on the server before it reaches the remote browser.

### Iframe sizing

With the default `resizable: true`, the remote viewport follows the iframe size. With `resizable: false`, the viewer letterboxes the stream to preserve the viewport and aspect ratio set by your automation.

Give the iframe a stable height through CSS. A percentage height only works when its parent also has an explicit height.

## Step 2: Add the iframe

Render the Live URL in your app:

```jsx
<iframe
  src={liveURL}
  sandbox="allow-same-origin allow-scripts"
  allow="clipboard-read; clipboard-write"
  style={{ width: '100%', height: '100%', border: 0 }}
  title="Browserless live browser"
/>
```

The viewer is a script-driven canvas application, so it needs `allow-same-origin` and `allow-scripts`. The `allow` attribute enables copy and paste between the viewer and the remote page.

On success, the iframe loads the remote browser canvas and starts a same-origin WebSocket from the Live URL page to the Browserless host.

### Keyboard focus

The iframe must have focus before it can forward keyboard input. This is especially important in Safari. Focus it from a user-initiated event:

```js
openViewerButton.addEventListener('click', () => {
  liveViewIframe.focus();
});
```

## Step 3: Allow the Browserless origin

If your app sends a CSP header, add the Browserless region you connect to under `frame-src`:

```http
Content-Security-Policy: frame-src 'self' https://production-sfo.browserless.io;
```

Use your actual Browserless host, such as `production-lon.browserless.io`, `production-ams.browserless.io`, or your self-hosted Enterprise origin. The stream WebSocket runs inside the framed page on that same origin, so your parent page doesn't need a separate `connect-src` entry for it. Corporate firewalls and proxies must still allow WSS traffic to that host.

## Viewer behavior and limits

- Each Live URL accepts up to five concurrent viewers. A sixth connection receives HTTP `429`; closing a viewer frees its slot.
- `showBrowserInterface: true` adds tabs with live titles and favicons, tab switching and closing, back and forward controls, the current URL, and a draggable page scrollbar.
- Copy and paste work in both directions. The viewer supports Ctrl-based shortcuts in Windows and Linux, and Command-based shortcuts on macOS.
- When the stream drops unexpectedly, the viewer shows **Reconnecting…** and retries three times, before asking the viewer to reload.
- Minting a Live URL keeps the browser available for the requested Live URL timeout, but never past the browser session's absolute [duration limit](/baas/best-practices#session-duration-limits).
- After Browserless fills a stored credential, it closes active viewers and refuses new Live URL connections. See the [1Password security model](/baas/features/1password#security-model).

## FAQ & Troubleshooting

<details>
<summary>Why is the iframe blank or blocked?</summary>

Allow the Browserless host in your page's `frame-src` CSP directive and make sure HTTPS and WSS traffic to `/live/*` can pass through your firewall or proxy. See [Network Endpoints & VPN/Proxy Tips](/baas/monitor-sessions/hybrid-automation#network-endpoints--vpnproxy-tips).

</details>

<details>
<summary>Why doesn't typing work inside the iframe?</summary>

Mint the URL with `interactable: true`, then focus the iframe from a click or another user-initiated event. Input remains blocked when the URL was created with `interactable: false`, regardless of client-side CSS or JavaScript.

</details>

<details>
<summary>Why doesn't copy or paste work?</summary>

Add `allow="clipboard-read; clipboard-write"` to the iframe. The browser can still require the viewer to grant clipboard permission or interact with the frame before clipboard access succeeds.

</details>

## Next steps

  <a href="/baas/monitor-sessions/hybrid-automation" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Hybrid automation</h3>
      Create and manage Live URLs from Puppeteer or Playwright
    
  </a>
  <a href="/baas/advanced-configurations/hybrid-automation-configurations" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Advanced configuration</h3>
      Configure tabs, viewports, clipboard behavior, and multi-stage workflows
    
  </a>
  <a href="/api-reference/cdp-extensions" className="next-step-card-link">
    
      <h3 className="next-step-card-title">CDP extensions</h3>
      Reference for Live URL commands, options, and events
    
  </a>

---

# Hybrid Automation

URL: https://docs.browserless.io/baas/monitor-sessions/hybrid-automation

Hybrid automation lets you pause an automated script, hand control to a human via a secure live URL, and resume automation once they finish. Use it for login flows, 2FA, CAPTCHAs, or any step that requires human judgment.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Puppeteer or Playwright installed locally

> **Session limits**
> Minting a LiveURL keeps the browser available for the requested LiveURL timeout, but it doesn't override the session's original timeout. The viewer still closes at the browser session's absolute [maximum duration](/baas/best-practices#session-duration-limits).

  ![hybrid automation preview](/img/hybrid_automation.gif)

## How It Works

Create a `liveURL` through CDP and Browserless returns a short-lived link that opens in a browser tab, with no API token embedded. You can also [embed the Live URL in your app](/baas/monitor-sessions/embedding-live-url). For multi-stage workflows, read-only monitoring, and bandwidth optimization, see [Advanced Hybrid Automation Configurations](/baas/advanced-configurations/hybrid-automation-configurations).

| Behavior | Default | Override |
|---|---|---|
| **Viewport** | Resizes to match the end user's screen | `resizable: false` to keep the current viewport |
| **Interaction** | Click, type, scroll, touch, and tap enabled | `interactable: false` to block all input (view-only mode) |
| **Stream quality** | Full quality compressed video | `quality: 1-100` to reduce bandwidth (useful for mobile) |
| **Tab scope** | Starts on the page used to create the URL and switches to new tabs as they open | One link per tab: see [Multi-Tab Workflows](/baas/advanced-configurations/hybrid-automation-configurations#multi-tab-workflows). All tabs in one link: `showBrowserInterface: true` (may increase CAPTCHA challenges) |
| **Events** | None | Listen for `Browserless.liveComplete` and `Browserless.captchaFound` |

## Basic Implementation

  
**Puppeteer:**

```js
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint: 'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE',
});
const page = await browser.newPage();
await page.goto('https://practicetestautomation.com/practice-test-login/');

const cdp = await page.createCDPSession();
const { liveURL } = await cdp.send('Browserless.liveURL');

console.log('Share this URL:', liveURL);

// Wait for the live URL to complete, including timeout or cancellation
await new Promise((r) => cdp.on('Browserless.liveComplete', r));

// Continue automation...
await browser.close();
```
  
  
**Playwright:**

```js
import { chromium } from 'playwright-core';

const browser = await chromium.connectOverCDP(
  'wss://production-sfo.browserless.io/chromium/stealth?token=YOUR_API_TOKEN_HERE'
);
const [context] = await browser.contexts();
const page = await context.newPage();
await page.goto('https://practicetestautomation.com/practice-test-login/');

const cdpSession = await context.newCDPSession(page);
const { liveURL } = await cdpSession.send('Browserless.liveURL');

console.log('Share this URL:', liveURL);

// Wait for the live URL to complete, including timeout or cancellation
await new Promise((r) => cdpSession.on('Browserless.liveComplete', r));

// Continue automation...
await browser.close();
```
  

## Close LiveURL Programmatically

This script waits for Live URL completion or a specific selector on the page. When the selector appears, it programmatically closes the Live URL. Completion alone does not prove the user finished the task.

  
**Puppeteer:**

```js
import puppeteer from "puppeteer-core";

const browser = await puppeteer.connect({
  browserWSEndpoint: `wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE`,
});
const page = await browser.newPage();
await page.goto("https://practicetestautomation.com/practice-test-login/");

const cdp = await page.createCDPSession();
const { liveURL, liveURLId } = await cdp.send("Browserless.liveURL", {
  timeout: 120000,
});

console.log("Share this URL:", liveURL);

// Wait for live URL completion OR a successful login
const result = await Promise.race([
  new Promise((r) =>
    cdp.once("Browserless.liveComplete", () => r("live_completed"))
  ),
  page.waitForSelector("h1.post-title", { timeout: 0 })
    .then(() => "login_detected"),
]);

if (result === "login_detected") {
  await cdp.send("Browserless.closeLiveURL", { liveURLId });
}

console.log(`LiveURL closed via: ${result}`);
await browser.close();
```
  
  
**Playwright:**

```js
import { chromium } from "playwright-core";

const browser = await chromium.connectOverCDP(
  `wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE`
);
const [context] = browser.contexts();
const page = await context.newPage();
await page.goto("https://practicetestautomation.com/practice-test-login/");

const cdpSession = await context.newCDPSession(page);
const { liveURL, liveURLId } = await cdpSession.send("Browserless.liveURL", {
  timeout: 120000,
});

console.log("Share this URL:", liveURL);

// Wait for live URL completion OR a successful login
const result = await Promise.race([
  new Promise((r) =>
    cdpSession.once("Browserless.liveComplete", () => r("live_completed"))
  ),
  page.waitForSelector("h1.post-title", { timeout: 0 })
    .then(() => "login_detected"),
]);

if (result === "login_detected") {
  await cdpSession.send("Browserless.closeLiveURL", { liveURLId });
}

console.log(`LiveURL closed via: ${result}`);
await browser.close();
```
  

## Handle Completion Reasons

`Browserless.liveComplete` supplies `{ liveURLId, reason }`. The example below uses the page's `cdp` session from the Puppeteer example above (use `cdpSession` for Playwright).

**Arm the listener after the mint response and before sharing the link.** Re-minting on the same page reuses the ID and completes the old mint with `closed`. A listener armed earlier can receive that old completion instead.

```js
const { liveURL, error } = await cdp.send('Browserless.liveURL', {
  timeout: 60000,
  interactable: true,
});
if (error) throw new Error(error);
const completion = new Promise((resolve) =>
  cdp.once('Browserless.liveComplete', resolve)
);
console.log('Share this link:', liveURL);
const { reason } = await completion;

switch (reason) {
  case 'timeout':
    console.log('The link expired; retry the handoff if needed.');
    break;
  case 'closed':
    console.log('The link was closed or replaced; check the page state.');
    break;
  case 'viewerDisconnected':
    console.log('The viewer left; verify the task before continuing.');
    break;
  // Reserved for future viewer actions; not currently produced.
  case 'userDone':
    console.log('The viewer reported done; verify the result.');
    break;
  case 'userFailed':
    console.log('The viewer reported a problem; handle it before continuing.');
    break;
}
```

Completion fires at most once per mint, not once per viewer. `viewerDisconnected` fires only after the last viewer stays gone for two seconds; an ordinary viewer reconnect within that grace cancels it. The URL remains reopenable until expiry while the browser session is available, but reopening does not re-arm completion. **Credential-protection teardown blocks new viewers for the session**, so reopening cannot cancel or re-arm that completion. Verify the expected page state rather than treating disconnect as success. See the [full reason contract](/api-reference/cdp-extensions#browserlesslivecomplete).

## Mint a Live URL for a running browser over HTTP

You can use this browser-management route to create a view-only Live URL for a browser you own:

```bash
curl -X POST \
  "https://production-sfo.browserless.io/browser/BROWSER_ID/live?token=YOUR_API_TOKEN_HERE"
```

```json
{
  "liveURL": "https://production-sfo.browserless.io/live/index.html?i=...",
  "liveURLId": "..."
}
```

The route selects the focused page, shows the browser interface, and sets `interactable: false`. The URL lasts for up to 15 minutes, capped by the browser session's remaining lifetime. It returns `404` when the browser doesn't exist or is not accessible. See the [`POST /browser/{id}/live` reference](/open-api/browser-live).

## End-User Authentication

LiveURL links authenticate using a short-lived `?i=<id>` parameter that is unique to the session. This ID expires automatically after the LiveURL timeout elapses or when `Browserless.closeLiveURL` is called. LiveURL paths do not require your API token. Do not embed your token in LiveURL links shared with end users.

After Browserless fills a stored credential, it immediately closes active Live URL streams and refuses new viewers or new Live URLs for that session. See the [1Password security model](/baas/features/1password#security-model).

## Network Endpoints & VPN/Proxy Tips

The LiveURL page loads over HTTPS and then upgrades a same-origin WebSocket to stream frames and input. When diagnosing corporate firewalls, VPNs, or upstream proxies, both of the URLs below must be reachable from the end user's browser:

| Surface | URL pattern | Protocol |
|---|---|---|
| LiveURL page | `https://<your-browserless-host>/live/index.html?i=<id>` | HTTPS |
| LiveURL stream | `wss://<your-browserless-host>/live/<id>` | WebSocket (WSS, TLS) |

For the Browserless-managed fleet, `<your-browserless-host>` resolves to `production-sfo.browserless.io`, `production-lon.browserless.io`, or `production-ams.browserless.io` depending on the region you connected to. Self-hosted Enterprise deployments use the same `/live/*` paths on whatever host they are deployed to.

If end users report an intermittent `Couldn't establish a secure connection to the server.` error on the LiveURL page:

- **Allowlist WebSockets to `/live/*`**, not just HTTPS. Some VPNs and TLS-inspecting proxies pass HTTPS traffic but terminate WebSocket upgrades, which produces exactly this error.
- **Some VPN exit nodes interfere with specific regions.** Switching VPN node/region – or routing LiveURL traffic directly – usually resolves it when the failure is tied to a particular upstream proxy.
- **The server sends WebSocket pings every 30s** to keep the connection alive through idle-timeout-sensitive middleboxes. After an initial connection failure or an established stream drops, the LiveURL page shows **Reconnecting…** and retries three times after 1, 2, and 4 seconds before showing a terminal error.
- **Prefer a stable region close to the end user** (`production-sfo`, `production-lon`, `production-ams`) for the underlying Browserless connection. The LiveURL WSS runs on the same host, so a closer region reduces the number of hops where VPN/proxy interference can occur.

## FAQ & Troubleshooting

<details>
<summary>How do I enable liveURL for my account or token?</summary>

There's nothing to enable. Call the `liveURL` [BQL mutation](/bql-schema/operations/mutations/live-url) or the `Browserless.liveURL` CDP command shown above.

</details>

<details>
<summary>Can a human interact with the session without writing code?</summary>

Yes, that's what the live URL is for. Request it with `interactable: true` and share the link; anyone who opens it can watch the browser and click, type, and scroll in it directly, with no code or Browserless account needed. Useful for hand-completing logins or reviewing what an automation is doing.

</details>

<details>
<summary>Why was a viewer rejected with <code>429</code>?</summary>

Each Live URL accepts five concurrent viewers. Close an existing viewer before connecting another one. Reloads and reconnects can briefly overlap, so wait for the old connection to close before retrying.

</details>

<details>
<summary>Why didn't <code>Browserless.liveComplete</code> fire?</summary>

Register the listener on the same CDP session before sharing the URL; earlier events are not replayed. The event fires at most once for a CDP-created link: on timeout, `Browserless.closeLiveURL`, replacement by another live URL on the same page, or about two seconds after the last viewer disconnects (including credential-protection teardown). A reconnect within that grace period cancels the pending viewer-disconnect completion. Links nobody opens still complete on timeout, close, or replacement. Completion does not prove the human task succeeded; verify the expected page state before continuing.

</details>

<details>
<summary>Why am I getting a <code>403 Forbidden</code> error?</summary>

Your API token is missing or expired. Pass it as a `?token=` query parameter in the WebSocket or HTTP URL. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My script works locally but fails on Browserless</summary>

Local browser settings may differ from the Browserless environment. Use `launch` parameters to match your local setup (viewport, user agent, timezone). See [launch parameters](/baas/launch-options) for the full list.

</details>

## Next steps

  <a href="/baas/monitor-sessions/embedding-live-url" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Embed a Live URL</h3>
      Place the live browser inside your own application
    
  </a>
  <a href="/baas/advanced-configurations/hybrid-automation-configurations" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Advanced Hybrid Automation Configurations</h3>
      Configure stream quality, viewport control, event handling, and programmatic session management
    
  </a>

  <a href="/baas/monitor-sessions/screen-recording" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Screen Recording</h3>
      Capture browser sessions as WebM video files
    
  </a>

  <a href="/baas/monitor-sessions/session-replay" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Session Replay</h3>
      Record DOM-based sessions viewable in your dashboard
    
  </a>

---

# Screen Recording

URL: https://docs.browserless.io/baas/monitor-sessions/screen-recording

Screen Recording generates on-demand WebM video files from your Puppeteer or Playwright automation scripts using CDP commands. It supports both video and audio capture, and requires a paid plan. This feature is not to be confused with [Session Replay](/baas/monitor-sessions/session-replay), which records DOM events for playback in your dashboard.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Puppeteer or Playwright installed locally

> **Warning**
> The Screen Recording feature is **not available** on the `/chrome` route due to restrictions from Chrome's runtime environment. Use the `/stealth` endpoint instead.

## Basic Usage

Add `record=true` to your WebSocket connection string and use CDP commands (`Browserless.startRecording` / `Browserless.stopRecording`) to control capture. Screen Recording works with Puppeteer, Playwright, and any library that supports CDP connections.

Each example below shows the complete workflow: connecting to Browserless with recording enabled, starting a recording, performing automation tasks, stopping the recording, and saving the resulting WebM file.

  
**Puppeteer:**

```js
import fs from "fs";
import puppeteer from "puppeteer-core";

const sleep = (ms) => new Promise((res) => setTimeout(res, ms));
const token = "YOUR_API_TOKEN_HERE";

const wsEndpoint = `wss://production-sfo.browserless.io?token=${token}&headless=false&stealth&record=true`;
const browser = await puppeteer.connect({ browserWSEndpoint: wsEndpoint });
const page = await browser.newPage();

// Set custom viewport for recording dimensions
await page.setViewport({ width: 1280, height: 720 });
await page.goto("https://browserless.io");

// Start recording session
const cdp = await page.createCDPSession();
await cdp.send("Browserless.startRecording");

// Perform your automation actions here
await sleep(15000);

// Stop recording and save. Request base64 so the value crosses CDP as a
// portable string regardless of client language.
const response = await cdp.send("Browserless.stopRecording", { encoding: "base64" });
const file = Buffer.from(response.value, "base64");
await fs.promises.writeFile("./recording.webm", file);

await browser.close();
```
  
  
**Playwright:**

    
      
**Javascript:**

        :::warning
        On Playwright, you **must** use the already existing context and page and avoid creating new ones.
        :::

```js
import playwright from "playwright-core";
import fs from "fs/promises";

const sleep = (ms) => new Promise((res) => setTimeout(res, ms));
const token = "YOUR_API_TOKEN_HERE";

const wsEndpoint = `wss://production-sfo.browserless.io?token=${token}&headless=false&stealth&record=true`;

(async () => {
  // Connect to the remote browser
  const browser = await playwright.chromium.connectOverCDP(wsEndpoint);

  // Reuse the context and page
  const context = browser.contexts()[0];
  const page = context.pages()[0];
  await page.setViewportSize({ width: 1280, height: 720 });
  await page.goto("https://browserless.io");

  // Start recording
  const cdpSession = await page.context().newCDPSession(page);
  await cdpSession.send("Browserless.startRecording");

  // Wait for some time while recording
  await sleep(15000);

  // Stop recording and get the response. Request base64 so the value
  // crosses CDP as a portable string regardless of client language.
  const response = await cdpSession.send("Browserless.stopRecording", { encoding: "base64" });
  const file = Buffer.from(response.value, "base64");

  // Save the recording as a webm file
  await fs.writeFile("./recording.webm", file);

  await browser.close();
})();
```
      
      
**Python:**

        :::warning
        On Playwright, you **must** use the already existing context and page and avoid creating new ones.
        :::

```python
import base64
import time
from playwright.sync_api import sync_playwright

TOKEN = "YOUR_API_TOKEN_HERE"
WS_ENDPOINT = (
    f"wss://production-sfo.browserless.io?token={TOKEN}&headless=false&stealth&record=true"
)

with sync_playwright() as playwright:
    # Connect to the remote browser
    browser = playwright.chromium.connect_over_cdp(WS_ENDPOINT)

    # Reuse the context and page
    context = browser.contexts[0]
    page = context.pages[0]
    page.set_viewport_size({"width": 1280, "height": 720})
    page.goto("https://browserless.io")

    # Start recording
    cdp_session = context.new_cdp_session(page)
    cdp_session.send("Browserless.startRecording")

    # Wait for some time while recording
    time.sleep(15)

    # Stop recording and request base64 so the value crosses CDP as a
    # string instead of a JSON-serialized Buffer.
    response = cdp_session.send(
        "Browserless.stopRecording",
        {"encoding": "base64"},
    )

    # Decode and write to a file
    with open("recording.webm", "wb") as file:
        file.write(base64.b64decode(response["value"]))

    browser.close()
```
      
      
**Java:**

        :::warning
        On Playwright, you **must** use the already existing context and page and avoid creating new ones.
        :::

```java
import com.microsoft.playwright.*;
import java.nio.file.*;
import java.util.Base64;
import java.util.Map;

public class App {
  public static void main(String[] args) {
    String TOKEN = "YOUR_API_TOKEN_HERE";
    String WS_ENDPOINT = String.format("wss://production-sfo.browserless.io?token=%s&headless=false&stealth&record=true",
        TOKEN);

    try (Playwright playwright = Playwright.create()) {
      // Connect to the remote browser
      Browser browser = playwright.chromium().connectOverCDP(WS_ENDPOINT);

      // Reuse the context and page
      Page page = browser.contexts().get(0).pages().get(0);
      page.setViewportSize(1280, 720);
      page.navigate("https://browserless.io");

      // Start recording
      CDPSession cdpSession = browser.contexts().get(0).newCDPSession(page);
      cdpSession.send("Browserless.startRecording", null);

      // Wait for 15 seconds
      Thread.sleep(15000);

      // Stop recording. Request base64 so the value crosses CDP as a
      // portable string instead of a JSON-serialized Buffer.
      var response = cdpSession.send("Browserless.stopRecording", Map.of("encoding", "base64"));
      String recordingData = response.get("value").getAsString();

      // Decode and write to a file
      byte[] fileData = Base64.getDecoder().decode(recordingData);
      Files.write(Paths.get("recording.webm"), fileData);

      System.out.println("Recording saved as 'recording.webm'");
      browser.close();
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}
```
      
      
**C#:**

        :::warning
        On Playwright, you **must** use the already existing context and page and avoid creating new ones.
        :::

```csharp
using Microsoft.Playwright;
using System;
using System.Collections.Generic;
using System.IO;

class BrowserlessRecording
{
    public static async Task Main(string[] args)
    {
        string TOKEN = "YOUR_API_TOKEN_HERE";
        string WS_ENDPOINT = $"wss://production-sfo.browserless.io?token={TOKEN}&headless=false&stealth&record=true";

        using var playwright = await Playwright.CreateAsync();
        var browser = await playwright.Chromium.ConnectOverCDPAsync(WS_ENDPOINT);

        // Reuse the context and page
        var context = browser.Contexts[0];
        var page = context.Pages[0];

        await page.SetViewportSizeAsync(1280, 720);
        await page.GotoAsync("https://browserless.io");

        // Start recording
        var cdpSession = await context.NewCDPSessionAsync(page);
        await cdpSession.SendAsync("Browserless.startRecording");

        // Wait for 15 seconds
        await Task.Delay(15000);

        // Stop recording. Request base64 so the value crosses CDP as a
        // portable string instead of a JSON-serialized Buffer.
        var response = await cdpSession.SendAsync(
            "Browserless.stopRecording",
            new Dictionary<string, object> { ["encoding"] = "base64" }
        );
        string recordingData = response.Value<string>("value");

        // Decode and write to a file
        byte[] fileData = Convert.FromBase64String(recordingData);
        await File.WriteAllBytesAsync("recording.webm", fileData);

        Console.WriteLine("Recording saved as 'recording.webm'");
        await browser.CloseAsync();
    }
}
```
      
    
  

## Key Features

- High-quality WebM output with audio captured automatically
- Video dimensions inherited from the browser viewport at recording start
- Works with Puppeteer, Playwright, and other CDP-compatible libraries
- Can be combined with LiveURL for human-in-the-loop recording workflows

## Video Dimensions Configuration

The resulting WebM dimensions match the page viewport size at the moment recording starts. If you run `await page.setViewport({ width: 2048, height: 720 });` before `Browserless.startRecording`, the video will be 2048x720 px.

You can set viewport dimensions in several ways:

- **Puppeteer**: Use `page.setViewport({ width: 1280, height: 720 })`
- **URL Parameters**: Add `--window-size=1280,720` and `headless=false` to your connection string
- **Launch Arguments**: Configure viewport in browser launch options

> **CDP Fallback for Viewport Sizing**
> If your automation library does not expose a `page.setViewport` or `page.setViewportSize` method, you can set dimensions directly with the CDP `Emulation.setDeviceMetricsOverride` method. Most libraries use this CDP command under the hood when updating viewport size. See the [DevTools Protocol reference](https://chromedevtools.github.io/devtools-protocol/tot/Emulation/#method-setDeviceMetricsOverride).

> **Avoid Resizing During Recording**
> Do not change the viewport size while recording is in progress. Mid-recording viewport changes can distort rendered content and produce a visibly stretched or otherwise unusual recording.

## Recording with LiveURL

You can combine Screen Recording with LiveURL to create human-in-the-loop recording workflows. This captures a WebM video file while a human interacts with the browser in real time. For full LiveURL documentation, see [Hybrid Automation](/baas/monitor-sessions/hybrid-automation).

```javascript

const token = 'YOUR_API_TOKEN_HERE';

const queryParams = new URLSearchParams({
  token,
  timeout: 180000,
  headless: true,
}).toString();

(async () => {
  let browser = null;

  try {
    // Connect with recording enabled
    browser = await puppeteer.connect({
      browserWSEndpoint: `wss://production-sfo.browserless.io?record=true&${queryParams}`,
    });

    const page = await browser.newPage();
    await page.goto('https://example.com/login', {
      waitUntil: 'networkidle2'
    });

    // Initialize CDP session
    const cdp = await page.createCDPSession();

    // Start recording
    await cdp.send("Browserless.startRecording");

    // Generate LiveURL for user interaction
    const { liveURL } = await cdp.send('Browserless.liveURL', {
      timeout: 30000
    });
    console.log('Share this URL:', liveURL);

    // Wait for user interaction
    await new Promise(resolve => setTimeout(resolve, 30000));

    // Stop recording and save. Request base64 so the value crosses CDP as a
    // portable string regardless of client language.
    const response = await cdp.send("Browserless.stopRecording", { encoding: "base64" });
    const recordingBuffer = Buffer.from(response.value, "base64");
    await fs.promises.writeFile("./session-recording.webm", recordingBuffer);

  } catch (error) {
    console.error('Recording failed:', error);
  } finally {
    if (browser) await browser.close();
  }
})();
```

### Advanced Session Monitoring

For workflows that need visual feedback, you can implement monitoring logic that notifies users when a task completes:

```javascript
// Enhanced monitoring with visual feedback
await page.waitForFunction(() => {
  const currentUrl = window.location.href;
  const isSuccess = currentUrl.includes('logged-in') ||
                   currentUrl.includes('dashboard') ||
                   document.querySelector('.post-login') !== null;

  if (isSuccess) {
    // Create success notification
    const notification = document.createElement('div');
    notification.style.cssText = `
      position: fixed; top: 20px; left: 50%;
      transform: translateX(-50%); background-color: #4CAF50;
      color: white; padding: 15px 30px; border-radius: 5px;
      z-index: 9999; font-family: Arial, sans-serif;
      box-shadow: 0 2px 5px rgba(0,0,0,0.2);
    `;
    notification.textContent = 'Login successful - you can close this tab';
    document.body.appendChild(notification);
    return true;
  }
  return false;
}, { timeout: 60000 });
```

### Custom Recording Triggers

Start recording only after specific conditions are met:

```javascript
await page.waitForSelector('.login-form');
await cdp.send("Browserless.startRecording");
```

### Conditional Recording

Record only if a specific element is present:

```javascript
const hasLoginForm = await page.$('.login-form');
if (hasLoginForm) {
  await cdp.send("Browserless.startRecording");
}
```

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>403 Forbidden</code> error?</summary>

Your API token is missing or expired. Pass it as a `?token=` query parameter in the WebSocket or HTTP URL. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My script works locally but fails on Browserless</summary>

Local browser settings may differ from the Browserless environment. Use `launch` parameters to match your local setup (viewport, user agent, timezone). See [launch parameters](/baas/launch-options) for the full list.

</details>

## Next steps

  <a href="/baas/monitor-sessions/hybrid-automation" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Hybrid Automation</h3>
      Enable human-in-the-loop workflows with shareable LiveURL sessions
    
  </a>

  <a href="/baas/monitor-sessions/session-replay" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Session Replay</h3>
      Capture DOM-based recordings viewable in your dashboard
    
  </a>

---

# Session Replay

URL: https://docs.browserless.io/baas/monitor-sessions/session-replay

**Session Replay** captures browser sessions as interactive replays viewable in your Browserless dashboard. It uses RRWeb technology to record DOM mutations, mouse movements, clicks, scrolling, keyboard input, console logs, and network requests. The result is a lightweight, high-fidelity recording you can play back at any speed. This feature requires a paid plan and is not to be confused with [Screen Recording](/baas/monitor-sessions/screen-recording), which captures WebM video files returned directly to your code.

To enable it, add `replay=true` to your WebSocket connection string. Recording starts automatically on connect and stops when you call `Browserless.stopSessionRecording` or close the browser.

- A paid Browserless plan (Prototyping, Starter, Scale, or Enterprise) — Session Replay is not available on the free plan
- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Node.js installed (for the code examples below)
- Install the required npm package: `npm install puppeteer-core`

![Session Replay Demo](/img/session_replay.gif)

## Recording a Session

  
    Connect to Browserless and enable session replay by adding the `replay=true` query parameter to your connection URL. Recording starts automatically upon connection.

    
      
**Puppeteer:**

```javascript
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint: `wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&timeout=300000&headless=false&replay=true`,
});
```
      
      
**Playwright:**

```javascript
import { chromium } from 'playwright-core';

const browser = await chromium.connectOverCDP(
  'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&timeout=300000&headless=false&replay=true'
);
```
      
    
  

  
    Create a page, navigate to your target website, and perform the actions you want to record. The recording captures all interactions on this page.

    
      
**Puppeteer:**

```javascript
const page = await browser.newPage();
await page.goto("https://www.example.com", { waitUntil: "networkidle0" });

const sleep = (ms) => new Promise((res) => setTimeout(res, ms));

// Example: Random mouse movements for demonstration
const randomMouseMovements = async (page, moves = 20) => {
  const viewport = page.viewport() || { width: 1280, height: 720 };
  for (let i = 0; i < moves; i++) {
    const x = Math.floor(Math.random() * viewport.width);
    const y = Math.floor(Math.random() * viewport.height);

    await page.mouse.move(x, y, {
      steps: 5 + Math.floor(Math.random() * 10),
    });
    await sleep(100 + Math.random() * 300);
  }
};

await randomMouseMovements(page, 2);
await sleep(6000);
```
      
      
**Playwright:**

```javascript
const [context] = await browser.contexts();
const page = await context.newPage();
await page.goto("https://www.example.com", { waitUntil: "networkidle" });

const sleep = (ms) => new Promise((res) => setTimeout(res, ms));

// Example: Random mouse movements for demonstration
const randomMouseMovements = async (page, moves = 20) => {
  const viewport = page.viewportSize() || { width: 1280, height: 720 };
  for (let i = 0; i < moves; i++) {
    const x = Math.floor(Math.random() * viewport.width);
    const y = Math.floor(Math.random() * viewport.height);

    await page.mouse.move(x, y);
    await sleep(100 + Math.random() * 300);
  }
};

await randomMouseMovements(page, 2);
await sleep(6000);
```
      
    
  

  
    Create a CDP session, stop the session recording, and close the browser connection. When you call `Browserless.stopSessionRecording`, the recorded data uploads automatically to your Browserless account for viewing in the dashboard.

    
      
**Puppeteer:**

```javascript
const cdp = await page.createCDPSession();

// Stop the recording - data uploads automatically
await cdp.send("Browserless.stopSessionRecording");
await browser.close();
```
      
      
**Playwright:**

```javascript
const cdpSession = await context.newCDPSession(page);

// Stop the recording - data uploads automatically
await cdpSession.send("Browserless.stopSessionRecording");
await browser.close();
```
      
    

    :::tip Alternative: Close Without Explicit Stop
    If you close the browser without calling `Browserless.stopSessionRecording`, the recording still saves automatically. Explicitly stopping the recording ensures all data flushes before disconnection.
    :::
  

## What Gets Recorded

Session Replay captures the following data:

**DOM Events**: All visual changes to the page including element mutations, style changes, and layout shifts, recorded using RRWeb's snapshot technology.

**User Interactions**: Mouse movements, clicks, scrolling, and keyboard input with precise timing for accurate playback.

Input values are masked while an interactive LiveURL viewer is connected, so credentials and one-time codes entered during a human handoff do not appear in the replay. Input recording returns to its normal behavior after the last interactive viewer disconnects. After Browserless fills a stored credential, subsequent replay input values remain masked for the browser session.

**Console Logs**: All console output (log, warn, error, info) with timestamps, making it easy to correlate visual events with application state.

**Network Requests**: HTTP requests and responses, helping you understand data flow during the session.

## Viewing Replays

After your session ends, the replay uploads automatically to your Browserless dashboard. Navigate to the Session Replay section to find your recorded sessions. Each replay includes a timeline showing all captured events, console logs, and network activity. You can play back the session at different speeds and jump to specific moments.

> **BQL Users**
> For BrowserQL-specific Session Replay documentation, see [Session Replay with BQL](/browserql/session-management/session-replay).

## Navigation Handling

Session Replay handles page navigations automatically within your session. When the browser navigates to a new page, the recording system reinjects the RRWeb recorder and continues capturing events. All data from multiple page navigations aggregates into a single replay, giving you the complete user journey across different pages.

## Limitations

- Cross-origin iframes may not be fully captured due to browser security restrictions.
- Very long sessions with high DOM activity may produce large replay files.
- Dynamic content loaded via WebGL or canvas may not reproduce perfectly in playback.

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>403 Forbidden</code> error?</summary>

Your API token is missing or expired. Pass it as a `?token=` query parameter in the WebSocket or HTTP URL. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My script works locally but fails on Browserless</summary>

Local browser settings may differ from the Browserless environment. Use `launch` parameters to match your local setup (viewport, user agent, timezone). See [launch parameters](/baas/launch-options) for the full list.

</details>

## Next steps

  <a href="/baas/monitor-sessions/screen-recording" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Screen Recording</h3>
      Generate WebM video files from your automation scripts
    
  </a>

  <a href="/baas/monitor-sessions/hybrid-automation" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Hybrid Automation</h3>
      Enable human-in-the-loop workflows with shareable LiveURL sessions
    
  </a>

---

# Quickstart

URL: https://docs.browserless.io/baas/quick-start

Connect to Browserless using either Puppeteer or Playwright through Chrome's DevTools Protocol (CDP) using websockets. This is the primary and recommended way to connect to Browserless, as it provides a stable and reliable connection.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Puppeteer or Playwright installed locally

Follow these steps to get started with Browserless:

  
    <ul>
      <li>Go to the <a href="https://browserless.io/account/">Browserless dashboard</a></li>
      <li>Sign up or log in to your account</li>
      <li>Copy your API token from the dashboard</li>
    </ul>

    ![API Key Dashboard](/img/browserql/api-key-dashboard.png)
  

  
    Choose your preferred automation library and install it:

    
      
**Puppeteer:**

```bash
npm install puppeteer-core
```
      
      
**Playwright:**

        
          
**JavaScript:**

```bash
npm install playwright-core
```
          
          
**Python:**

```bash
pip install playwright
```
          
          
**Java:**

```xml
<dependency>
  <groupId>com.microsoft.playwright</groupId>
  <artifactId>playwright</artifactId>
  <version>1.40.0</version>
</dependency>
```
          
          
**C#:**

```bash
dotnet add package Microsoft.Playwright
```
          
          
**Go:**

```bash
go get github.com/playwright-community/playwright-go
```

            You also need to install the Playwright browser binaries once:

```bash
go run github.com/playwright-community/playwright-go/cmd/playwright@latest install --with-deps
```
          
        
      
    
  

  
    Here's a complete working example:

    
      
**Puppeteer:**

```js
import puppeteer from "puppeteer-core";

const TOKEN = "YOUR_API_TOKEN_HERE"; // Replace with your actual token

async function getPageTitle() {
  // Connect to Browserless using WebSocket endpoint
  const browser = await puppeteer.connect({
    browserWSEndpoint: `wss://production-sfo.browserless.io?token=${TOKEN}`,
  });

  const page = await browser.newPage();

  // Navigate to the target website
  await page.goto("https://www.example.com/");

  // Get the page title
  const title = await page.title();
  console.log(`The page's title is: ${title}`);

  // Clean up resources
  await browser.close();
}

getPageTitle().catch(console.error);
```

        **Output**
```text
The page's title is: Example Domain
```
      
      
**Playwright:**

        
          
**JavaScript:**

```js
import { chromium } from "playwright-core";

const TOKEN = "YOUR_API_TOKEN_HERE"; // Replace with your actual token

async function getPageTitle() {
  // Connect to Browserless using CDP (recommended method)
  const browser = await chromium.connectOverCDP(
    `wss://production-sfo.browserless.io?token=${TOKEN}`
  );

  const context = await browser.newContext();
  const page = await context.newPage();

  // Navigate to the target website
  await page.goto("https://www.example.com/");

  // Get the page title
  const title = await page.title();
  console.log(`The page's title is: ${title}`);

  // Clean up resources
  await browser.close();
}

getPageTitle().catch(console.error);
```

            **Output**
```text
The page's title is: Example Domain
```
          
          
**Python:**

```python
from playwright.sync_api import sync_playwright

TOKEN = "YOUR_API_TOKEN_HERE"  # Replace with your actual token

def get_page_title():
    with sync_playwright() as p:
        # Connect to Browserless using CDP (recommended method)
        browser = p.chromium.connect_over_cdp(
            f"wss://production-sfo.browserless.io?token={TOKEN}"
        )

        context = browser.new_context()
        page = context.new_page()

        # Navigate to the target website
        page.goto("https://www.example.com/")

        # Get the page title
        title = page.title()
        print(f"The page's title is: {title}")

        # Clean up resources
        browser.close()

if __name__ == "__main__":
    get_page_title()
```

            **Output**
```text
The page's title is: Example Domain
```
          
          
**Java:**

```java
package org.example;

import com.microsoft.playwright.*;

public class Main {
    public static void main(String[] args) {
        try (Playwright playwright = Playwright.create()) {
            final String TOKEN = "YOUR_API_TOKEN_HERE"; // Replace with your actual token

            // Connect to Browserless using CDP (recommended method)
            Browser browser = playwright.chromium().connectOverCDP(
                String.format("wss://production-sfo.browserless.io?token=%s", TOKEN)
            );

            BrowserContext context = browser.newContext();
            Page page = context.newPage();

            // Navigate to the target website
            page.navigate("https://www.example.com/");

            // Get the page title
            String title = page.title();
            System.out.println("The page's title is: " + title);

            // Clean up resources
            browser.close();
        }
    }
}
```

            **Output**
```text
The page's title is: Example Domain
```
          
          
**C#:**

```csharp
using Microsoft.Playwright;

namespace PlaywrightExample
{
    class Program
    {
        private const string TOKEN = "YOUR_API_TOKEN_HERE"; // Replace with your actual token

        public static async Task Main(string[] args)
        {
            using var playwright = await Playwright.CreateAsync();

            // Connect to Browserless using CDP (recommended method)
            var browser = await playwright.Chromium.ConnectOverCDPAsync(
                $"wss://production-sfo.browserless.io?token={TOKEN}"
            );

            var context = await browser.NewContextAsync();
            var page = await context.NewPageAsync();

            // Navigate to the target website
            await page.GotoAsync("https://www.example.com/");

            // Get the page title
            var title = await page.TitleAsync();
            Console.WriteLine($"The page's title is: {title}");

            // Clean up resources
            await browser.CloseAsync();
        }
    }
}
```

            **Output**
```text
The page's title is: Example Domain
```
          
          
**Go:**

```go
package main

import (
    "fmt"
    "log"

    "github.com/playwright-community/playwright-go"
)

const token = "YOUR_API_TOKEN_HERE" // Replace with your actual token.

func main() {
    // Start Playwright without launching a local browser.
    pw, err := playwright.Run()
    if err != nil {
        log.Fatalf("could not start playwright: %v", err)
    }
    defer pw.Stop()

    // Connect to Browserless using CDP (recommended method).
    browser, err := pw.Chromium.ConnectOverCDP(
        fmt.Sprintf("wss://production-sfo.browserless.io?token=%s", token),
    )
    if err != nil {
        log.Fatalf("could not connect to browserless: %v", err)
    }

    context, err := browser.NewContext()
    if err != nil {
        log.Fatalf("could not create context: %v", err)
    }

    page, err := context.NewPage()
    if err != nil {
        log.Fatalf("could not create page: %v", err)
    }

    // Navigate to the target website.
    if _, err = page.Goto("https://www.example.com/"); err != nil {
        log.Fatalf("could not navigate: %v", err)
    }

    // Get the page title.
    title, err := page.Title()
    if err != nil {
        log.Fatalf("could not get title: %v", err)
    }
    fmt.Printf("The page's title is: %s\n", title)

    // Clean up resources.
    if err = browser.Close(); err != nil {
        log.Fatalf("could not close browser: %v", err)
    }
}
```

            **Output**
```text
The page's title is: Example Domain
```
          
        
      
    
  

> **Tip**
> The examples above connect to the US West region (`production-sfo`). Browserless is also available in Europe. See [Regional Endpoints](/baas/connection-url-patterns#regional-endpoints) to choose the region closest to your target sites.

## FAQ & Troubleshooting

<details>
<summary>How do I connect Puppeteer to Browserless?</summary>

Replace your local browser launch with a WebSocket connection to Browserless. Use puppeteer.connect() with the Browserless WebSocket URL and your API token as a query parameter.

</details>

<details>
<summary>Does Browserless support Playwright?</summary>

Yes. Connect Playwright to Browserless using browserType.connect() with the Browserless WebSocket URL. Both Chromium and Firefox browsers are supported for Playwright connections.

</details>

<details>
<summary>Do I need to change my existing automation code?</summary>

Minimal changes are needed. Replace your browser launch call with a connect call pointing to Browserless. The rest of your Puppeteer or Playwright code runs unchanged against the remote browser.

</details>

<details>
<summary>Is there a free tier to try Browserless?</summary>

Yes. Sign up at browserless.io for a free account that includes a limited number of browser sessions per month. This lets you test your automation code against managed browsers before scaling up.

</details>

## Next steps

  <a href="/baas/session-management/standard-sessions" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Persistent sessions</h3>
      long-running browser sessions for complex workflows
    
  </a>
  <a href="/baas/bot-detection/unblocking" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Site unblocking</h3>
      bypass anti-bot measures
    
  </a>
  <a href="/baas/bot-detection/proxies" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Proxy configuration</h3>
      built-in residential and third-party proxies
    
  </a>

---

# Session Management Overview

URL: https://docs.browserless.io/baas/session-management

Browserless gives you two ways to keep state (cookies, localStorage, cache, authentication) across browser connections. Which one to use depends on how long you need to keep the browser alive and whether you know upfront that you need to persist state.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Puppeteer or Playwright installed locally

  ![Persisting browser sessions demonstration](/img/persistingsessions.gif)

## How Session Persistence Works

When a Puppeteer script connects to Browserless, it gets a remote browser process. By default, Browserless closes that process the moment your connection drops. Session persistence changes that behavior.

### Standard Sessions

Call the CDP command `Browserless.reconnect` mid-script to flag the browser as reconnectable. The server holds the browser process open for a configurable timeout window. Reconnecting within that window drops you back into the live browser exactly where you left it: same page, same scroll position, same in-memory state. The browser is cleaned up once the TTL expires or you close it.

### Persisting State

The Session API lets you create browser sessions via REST, returning a set of URLs and a WebSocket endpoint you use to connect and manage the session lifecycle. Data (cookies, localStorage, cache) persists for days across browser restarts in an isolated `userDataDir`, the same Chrome mechanism used for user profiles, provisioned per-session by Browserless for isolation across shared fleets.

## Choosing an Approach

| | Standard Sessions | Persisting State |
|---|---|---|
| **How you set it up** | CDP command called mid-script | REST API call before connecting |
| **State survives** | While browser process is running (up to 5 min timeout) | For days, across full browser restarts |
| **Keep browser process alive** | Built-in, up to 5 min | Optional via `processKeepAlive` |
| **Library support** | Puppeteer only (requires `browser.disconnect()`) | Puppeteer, Playwright, or any other library |
| **Session lifetime** | Short: seconds to a few minutes | Long: hours to days |
| **Control** | Decide mid-execution whether to persist | Define lifecycle upfront with explicit TTL |
| **Management** | No separate create/delete step | Create and delete via HTTP |

**Use Standard Sessions when:**
- You're already in a running Puppeteer script and want to conditionally keep the browser alive mid-execution.
- Your disconnection window is short: you'll reconnect within seconds or a few minutes.
- You don't need state to outlast the browser process.

**Use Persisting State when:**
- You need cookies, localStorage, or auth state to survive for hours or days.
- You want to create, manage, and terminate sessions through a REST API.
- You need to predefine session configuration (TTL, stealth mode, proxy) before a browser connects.
- You're using Playwright or another library. Playwright lacks `browser.disconnect()`, making Standard Sessions unreliable.
- You want to keep the browser process alive between connections using `processKeepAlive`. See [Keeping Browser Sessions Alive](/baas/session-management/persisting-state#keeping-browser-sessions-alive).

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>403 Forbidden</code> error?</summary>

Your API token is missing or expired. Pass it as a `?token=` query parameter in the WebSocket or HTTP URL. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My script works locally but fails on Browserless</summary>

Local browser settings may differ from the Browserless environment. Use `launch` parameters to match your local setup (viewport, user agent, timezone). See [launch parameters](/baas/launch-options) for the full list.

</details>

## Next steps

  <a href="/baas/session-management/standard-sessions" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Standard Sessions</h3>
      Keep a browser alive mid-script using the Browserless.reconnect CDP command.
    
  </a>

  <a href="/baas/session-management/persisting-state" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Persisting State</h3>
      Create long-lived sessions via REST API with data that survives browser restarts for days.
    
  </a>

---

# Persisting State

URL: https://docs.browserless.io/baas/session-management/persisting-state

The Session API lets you create browser sessions via REST, returning a set of URLs and a WebSocket endpoint you use to connect and manage the session lifecycle. Data (cookies, localStorage, cache) persists for days across browser restarts in an isolated `userDataDir`, the same Chrome mechanism used for user profiles, provisioned per-session by Browserless for isolation across shared fleets.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Puppeteer or Playwright installed locally

## Persisting State Workflow

You'll need an API token. Grab one from your [Browserless account](https://www.browserless.io/account/home). Create a session via REST with your desired configuration. Save the returned object: it contains the URLs you need for the rest of the session lifecycle.

```javascript

const TOKEN = "YOUR_API_TOKEN_HERE";

async function createSession(sessionConfig) {
    const response = await fetch(`https://production-sfo.browserless.io/session?token=${TOKEN}`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(sessionConfig),
    });

    if (!response.ok) {
        throw new Error(`Failed to create session: ${response.status}`);
    }

    return response.json();
}

const session = await createSession({
    ttl: 180000, // Required: session lifetime in ms (3 minutes here)
});

// Save session.connect and session.stop: you'll need them below
```

Use the `connect` URL from Step 1 to attach Puppeteer to the running session. The URL already includes your token.

```javascript

async function connectToSession(connectUrl) {
  const browser = await puppeteer.connect({
    browserWSEndpoint: connectUrl,
  });
  const page = (await browser.pages())[0];
  return { browser, page };
}

// connectUrl is session.connect from Step 1
const { browser, page } = await connectToSession(session.connect);

await page.goto("https://docs.browserless.io/");
await page.setViewport({ width: 1000, height: 800 });
await page.screenshot({ path: "before-toggle.jpg" }); // 1. Before click

await page.click('#toggleIcon button');
await page.screenshot({ path: "after-toggle.jpg" }); // 2. After click
```

When finished, disconnect from the browser and terminate the session using the `stop` URL from Step 1. This permanently deletes the session and all its data.

```javascript

async function stopSession(stopUrl) {
    // Append &force=true to terminate immediately even if a client is connected
    const response = await fetch(`${stopUrl}&force=true`, { method: 'DELETE' });

    if (response.ok) {
        console.log('Session stopped.');
        return true;
    }

    const errorText = await response.text();
    console.error(`Failed to stop session (${response.status}):`, errorText);
    return false;
}

await browser.close();

// stopUrl is session.stop from Step 1
await stopSession(session.stop);
```

## Session Response Shape

A successful `POST /session` returns a JSON object with these fields:

| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Unique session identifier |
| `connect` | string | WebSocket URL (`wss://`) for Puppeteer, Playwright, or any CDP-capable library. Token is pre-embedded. |
| `stop` | string | HTTPS URL to issue a `DELETE` request to terminate and clean up the session. Token is pre-embedded. |
| `browserQL` | string | HTTPS URL to run BrowserQL queries against this session. Token is pre-embedded. See [BrowserQL Session Management](/browserql/session-management/persisting-state) for more info. |
| `ttl` | number | The TTL value you provided, in milliseconds |
| `cloudEndpointId` | string \| null | Cloud routing ID; `null` on self-hosted enterprise |

Save the `connect` and `stop` URLs after session creation. They are your primary handles for the session lifecycle.

## Complete Example

**JavaScript (Puppeteer):**

```javascript

const TOKEN = "YOUR_API_TOKEN_HERE";

async function main() {
  const session = await createSession({ ttl: 180000 });

  // First connection
  const { browser, page } = await connectToSession(session.connect);

  await page.goto("https://docs.browserless.io/");
  await page.setViewport({ width: 1000, height: 800 });
  await page.screenshot({ path: "before-toggle.jpg" }); // 1. Before click

  await page.click('#toggleIcon button');
  await page.screenshot({ path: "after-toggle.jpg" }); // 2. After click

  await browser.close(); // Closes the browser; session data is saved to disk

  // Second connection: a new browser process starts on reconnect.
  // The browser opens to a blank page; navigate to the site before reading localStorage (it's origin-scoped).
  const { browser: browser2, page: page2 } = await connectToSession(session.connect);
  await page2.goto("https://docs.browserless.io/");
  await page2.setViewport({ width: 1000, height: 800 });
  await page2.screenshot({ path: "after-reconnect.jpg" }); // 3. After reconnect: toggle state restored from disk

  await browser2.close();
  await stopSession(session.stop); // Only stop the session when you want to discard the session data and no longer need to reconnect to it in the future.
}

main().catch(console.error);

// --- Helpers ---

async function createSession(sessionConfig) {
  const response = await fetch(
    `https://production-sfo.browserless.io/session?token=${TOKEN}`,
    {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(sessionConfig),
    }
  );
  if (!response.ok) throw new Error(`Failed to create session: ${response.status}`);
  return response.json();
}

async function connectToSession(connectUrl) {
  const browser = await puppeteer.connect({ browserWSEndpoint: connectUrl });
  const page = (await browser.pages())[0];
  return { browser, page };
}

async function stopSession(stopUrl) {
  const response = await fetch(`${stopUrl}&force=true`, { method: "DELETE" });
  if (!response.ok) throw new Error(`Failed to stop session: ${response.status}`);
  console.log("Session stopped.");
}
```

**Python (Playwright):**

```python

from playwright.async_api import async_playwright

TOKEN = "YOUR_API_TOKEN_HERE"

async def main():
    async with async_playwright() as p:
        session = await create_session({
            "ttl": 180000,
        })

        # First connection
        browser = await p.chromium.connect_over_cdp(session["connect"])
        page = browser.contexts[0].pages[0]

        await page.goto("https://docs.browserless.io/")
        await page.set_viewport_size({"width": 1000, "height": 800})
        await page.screenshot(path="before-toggle.jpg")  # 1. Before click

        await page.click('#toggleIcon button')
        await page.screenshot(path="after-toggle.jpg")   # 2. After click

        await browser.close()

        # Reconnect: a new browser process starts on reconnect.
        # Navigate to the site before reading localStorage (it's origin-scoped).
        browser2 = await p.chromium.connect_over_cdp(session["connect"])
        page2 = browser2.contexts[0].pages[0]
        await page2.goto("https://docs.browserless.io/")
        await page2.set_viewport_size({"width": 1000, "height": 800})
        await asyncio.sleep(1)
        await page2.screenshot(path="after-reconnect.jpg")  # 3. After reconnect: toggle state restored from disk

        await browser2.close()
        await stop_session(session["stop"])  # Only stop the session when you want to discard the session data and no longer need to reconnect to it in the future.

# --- Helpers ---

async def create_session(config):
    async with aiohttp.ClientSession() as http:
        async with http.post(
            f"https://production-sfo.browserless.io/session?token={TOKEN}",
            headers={"Content-Type": "application/json"},
            json=config,
        ) as r:
            if not r.ok:
                raise Exception(f"Failed to create session: {r.status}")
            return await r.json()

async def stop_session(stop_url):
    async with aiohttp.ClientSession() as http:
        async with http.delete(f"{stop_url}&force=true") as r:
            if not r.ok:
                raise Exception(f"Failed to stop session: {r.status}")
            print("Session stopped.")

if __name__ == "__main__":
    asyncio.run(main())
```

## Keeping Browser Sessions Alive

By default, when all connections to a session close, the underlying browser process is terminated. The next time you connect, a **new browser process** starts. The browser restores cookies, localStorage, and cache from disk, but opens to a blank page.

Set `processKeepAlive` when creating the session to add a grace period. While the timer is running the browser process stays alive, so reconnecting restores the full live state: open pages, scroll position, navigation history, in-memory inputs.

```javascript
const session = await createSession({
    ttl: 180000,
    processKeepAlive: 60000, // Browser stays alive for 60s after disconnect
});
```

After the grace period expires, the next connection starts a fresh browser process. The browser still loads persisted data (cookies, localStorage) from disk.

> **Puppeteer only**
> `processKeepAlive` requires `browser.disconnect()` to detach without terminating the remote browser. Playwright does not expose a `disconnect()` method, so this feature is not supported with Playwright.

## Persisted Session Data Duration

The data that persists (cache, cookies, localStorage) survives for the duration of the session `ttl`, up to the plan maximum:

| Plan | Maximum Session Lifetime |
|------|--------------------------|
| Free | 1 day |
| Prototyping | 7 days |
| Starter | 30 days |
| Scale | 90 days |
| Enterprise | Custom |

Set `ttl` to control exactly when session data expires. Sessions are permanently deleted when the `ttl` elapses.

## Session Configuration Options

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `ttl` | number | **Required** | Session lifetime in ms. When reached, the session and all its data are permanently deleted. Must be a non-negative number. Throws `400` if omitted. |
| `processKeepAlive` | number | `0` | Time in ms to keep the browser process alive after the last client disconnects. Within this window, reconnecting restores the full live browser state. After expiry, a new process starts on reconnect but cookies and localStorage are retained. Must be ≤ `ttl`. |
| `stealth` | boolean | `false` | Enable stealth mode to avoid bot detection |
| `headless` | boolean | `false` | Run browser in headless mode. An omitted value resolves to `false`, so a session launches headful unless you pass `true`. Ignored when `stealth: true` (the stealth browser always runs headful). |
| `blockAds` | boolean | `false` | Enable ad blocking |
| `args` | string[] | `[]` | Additional Chrome launch arguments |
| `browser` | string | `'chromium'` | Browser engine: `'chrome'`, `'chromium'`, or `'stealth'`. The `'stealth'` browser cannot be combined with the `stealth` property. |
| `url` | string | — | Optional target URL. Some sites require this for specialized handling. |
| `replay` | boolean | `false` | Enable rrweb session recording for later replay |
| `proxy` | object | `null` | Proxy configuration (see [proxy docs](/baas/launch-options)) |

For the complete OpenAPI reference, see the [Session API documentation](/open-api/session-create).

## Common Pitfalls

**`ttl` is required.** Unlike most parameters, `ttl` has no default. Omitting it returns `400: ttl must be a non-negative number greater than 0`.

**`processKeepAlive` must not exceed `ttl`.** The API returns `400` if `processKeepAlive > ttl`. A safe ratio is `processKeepAlive` at 10–20% of `ttl`.

**Without `processKeepAlive`, you must re-navigate on reconnect.** When `processKeepAlive` is `0` (the default), a new browser process starts on every reconnect. The persistent session directory restores cookies and localStorage, but the browser opens to a blank page. Navigate to your target URL before reading localStorage.

**Use `browser.disconnect()` not `browser.close()` when using `processKeepAlive`.** `browser.close()` terminates the remote browser immediately, bypassing the keep-alive window. Use `browser.disconnect()` to detach locally while leaving the browser process running.

**`stop` is permanent.** Calling the `stop` URL permanently deletes the session and all its stored data. Save it, but only call it when you are completely done with the session.

**Wrong field names.** The session response uses `connect` (not `browserWSEndpoint`) and `stop` (not a manually constructed delete URL). The `connect` and `stop` URLs already embed your token.

**Only one client can connect to a session at a time.** If a second client attempts to connect while another is attached, the server returns HTTP 429 with `"Session is already being accessed by another client."` There is no built-in queuing for concurrent session access. Disconnect or close the first client before connecting the next one.

**`timeout` goes on the connection URL, not on session creation.** The `timeout` query parameter controls how long a single WebSocket connection stays open. Append it to the WebSocket URL when you connect. This is separate from `ttl`, which controls the overall session lifetime.

```javascript
// timeout = 2 minutes for this connection
const browser = await puppeteer.connect({
  browserWSEndpoint: `${session.connect}&timeout=120000`,
});
```

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>403 Forbidden</code> error?</summary>

Your API token is missing or expired. Pass it as a `?token=` query parameter in the WebSocket or HTTP URL. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My script works locally but fails on Browserless</summary>

Local browser settings may differ from the Browserless environment. Use `launch` parameters to match your local setup (viewport, user agent, timezone). See [launch parameters](/baas/launch-options) for the full list.

</details>

## Next steps

  <a href="/baas/bot-detection/overview" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Bot Detection</h3>
      Bypass anti-bot measures with stealth mode and advanced evasion techniques.
    
  </a>

  <a href="/baas/features/human-in-loop-automation" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Hybrid Automation</h3>
      Combine automated scripts with human interaction for complex workflows.
    
  </a>

  <a href="/baas/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Custom Launch Arguments</h3>
      Configure browser with custom Chrome launch arguments for specialized use cases.
    
  </a>

---

# Standard Sessions

URL: https://docs.browserless.io/baas/session-management/standard-sessions

Browserless lets you keep a browser running between connections using the `Browserless.reconnect` CDP command. Call it before disconnecting and the browser stays alive. Reconnect within the timeout window and your page state (localStorage, cookies, navigation history) is exactly where you left it.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Puppeteer installed locally (Standard Sessions don't support Playwright — see note below)

> **Puppeteer only**
> Standard Sessions require `browser.disconnect()` to detach from the browser without terminating it. Playwright does not expose a `disconnect()` method, making this pattern unreliable with Playwright. If you're using Playwright, use [Persisting State](/baas/session-management/persisting-state) instead.

## Standard Sessions Workflow

You'll need an API token. Grab one from your [Browserless account](https://www.browserless.io/account/home). Connect using Puppeteer, open a CDP session on the page, then run your automation and set up any state you want to persist across reconnections:

```javascript

const TOKEN = "YOUR_API_TOKEN_HERE";
const BROWSERLESS_URL = `wss://production-sfo.browserless.io?token=${TOKEN}`;

async function connectToBrowserless() {
  const browser = await puppeteer.connect({
    browserWSEndpoint: BROWSERLESS_URL,
  });

  const page = (await browser.pages())[0];
  const cdp = await page.createCDPSession();

  return { browser, page, cdp };
}

const { browser, page, cdp } = await connectToBrowserless();

await page.goto("https://docs.browserless.io/");
await page.setViewport({ width: 1000, height: 800 });
await page.screenshot({ path: "before-toggle.jpg" }); // 1. Before click

await page.click('#toggleIcon button');
await page.screenshot({ path: "after-toggle.jpg" }); // 2. After click
```

Call `Browserless.reconnect` before disconnecting. This registers the browser as reconnectable for the given `timeout` duration (in milliseconds). After calling it, disconnect. The browser stays alive on the server.

```javascript
async function prepareReconnection(cdp, browser, timeout = 30000) {
  const { error, browserWSEndpoint } = await cdp.send("Browserless.reconnect", {
    timeout, // How long (ms) the browser stays alive waiting for a reconnect
  });

  if (error) throw new Error(error);
  console.log("Reconnection endpoint:", browserWSEndpoint);

  await browser.disconnect(); // Detaches locally; browser keeps running on server

  return browserWSEndpoint;
}

const browserWSEndpoint = await prepareReconnection(cdp, browser);
```

Use the `browserWSEndpoint` from the previous step to reconnect. Append your API token to the URL:

```javascript
async function reconnectToSession(browserWSEndpoint, token) {
  const reconnectUrl = `${browserWSEndpoint}?token=${token}`;

  const browser = await puppeteer.connect({
    browserWSEndpoint: reconnectUrl,
  });

  const pages = await browser.pages();
  const page = pages[0];

  const cdp = await page.createCDPSession();

  return { browser, page, cdp };
}

const reconnected = await reconnectToSession(browserWSEndpoint, TOKEN);

// Browser stayed alive: page is exactly where we left it, no re-navigation needed
await reconnected.page.screenshot({ path: "after-reconnect.jpg" }); // 3. After reconnect
```

When finished, close the browser to release resources:

```javascript
await reconnected.browser.close();
```

## Reconnect Response Shape

`Browserless.reconnect` returns a JSON object with two fields:

| Field | Type | Description |
|-------|------|-------------|
| `browserWSEndpoint` | string \| null | The URL to reconnect to. `null` if reconnect failed. |
| `error` | string \| null | Error message if the request was rejected, otherwise `null`. |

Always check `error` before using `browserWSEndpoint`.

## Complete Example

```javascript

const TOKEN = "YOUR_API_TOKEN_HERE";
const BROWSERLESS_URL = `wss://production-sfo.browserless.io?token=${TOKEN}`;

async function main() {
  console.log("Connecting to Browserless...");
  const { browser, page, cdp } = await connectToBrowserless();

  await page.goto("https://docs.browserless.io/");
  await page.setViewport({ width: 1000, height: 800 });
  await page.screenshot({ path: "before-toggle.jpg" }); // 1. Before click

  await page.click('#toggleIcon button');
  await page.screenshot({ path: "after-toggle.jpg" }); // 2. After click

  console.log("Disconnecting browser and preparing for reconnection...");
  const browserWSEndpoint = await prepareReconnection(cdp, browser);

  // Browser is live on the server within the TTL. Reconnect from any process when ready. We reconnect immediately here.
  console.log("Reconnecting to session...");
  const reconnected = await reconnectToSession(browserWSEndpoint, TOKEN);

  // Browser stayed alive: page is exactly where we left it, no re-navigation needed
  await reconnected.page.screenshot({ path: "after-reconnect.jpg" }); // 3. After reconnect

  console.log("Closing session...");
  await reconnected.browser.close();

  console.log("Session workflow complete!");
}

main().catch(console.error);

// --- Helpers ---

async function connectToBrowserless() {
  const browser = await puppeteer.connect({
    browserWSEndpoint: BROWSERLESS_URL,
  });
  const page = (await browser.pages())[0];
  const cdp = await page.createCDPSession();
  return { browser, page, cdp };
}

async function prepareReconnection(cdp, browser, timeout = 30000) {
  const { error, browserWSEndpoint } = await cdp.send("Browserless.reconnect", {
    timeout,
  });
  if (error) throw new Error(error);
  await browser.disconnect();
  return browserWSEndpoint;
}

async function reconnectToSession(browserWSEndpoint, token) {
  const browser = await puppeteer.connect({
    browserWSEndpoint: `${browserWSEndpoint}?token=${token}`,
  });
  const pages = await browser.pages();
  const page = pages[0];
  const cdp = await page.createCDPSession();
  return { browser, page, cdp };
}

// No cleanup helper needed: browser.close() terminates the remote process.
```

## Reconnection Timeout Limits

The `timeout` parameter passed to `Browserless.reconnect` controls how long the browser stays alive waiting for a reconnect. If you omit it, Browserless falls back to the connection timeout configured for your deployment.

## Common Pitfalls

**Using `browser.close()` instead of `browser.disconnect()`.** `browser.close()` terminates the remote browser. The session is gone. You must call `browser.disconnect()` to detach locally while keeping the browser alive on the server.

**Not appending the token to the reconnect URL.** The `browserWSEndpoint` returned by `Browserless.reconnect` requires your API token: `${browserWSEndpoint}?token=${TOKEN}`. Connecting without it returns a `401 Unauthorized`.

**`Browserless.reconnect` must be called before disconnecting.** The CDP command must complete and return a valid `browserWSEndpoint` before you call `browser.disconnect()`. Disconnecting first means you cannot request reconnection.

**Reconnecting after the TTL expires.** Once the timeout elapses, the browser is cleaned up. Connecting to a stale `browserWSEndpoint` will fail. Always reconnect within the TTL window, and remember the window ends early if your plan's maximum session duration is reached first.

**Timeout exceeds plan maximum.** Always check the `error` field in the `Browserless.reconnect` response before proceeding.

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>403 Forbidden</code> error?</summary>

Your API token is missing or expired. Pass it as a `?token=` query parameter in the WebSocket or HTTP URL. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My script works locally but fails on Browserless</summary>

Local browser settings may differ from the Browserless environment. Use `launch` parameters to match your local setup (viewport, user agent, timezone). See [launch parameters](/baas/launch-options) for the full list.

</details>

## Next steps

  <a href="/baas/bot-detection/overview" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Bot Detection</h3>
      Bypass anti-bot measures with stealth mode and advanced evasion techniques.
    
  </a>

  <a href="/baas/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Custom Launch Arguments</h3>
      Configure browser with custom Chrome launch arguments for specialized use cases.
    
  </a>

  <a href="/baas/session-management/persisting-state" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Persisting State</h3>
      Learn about Session API for explicit session lifecycle control through REST endpoints.
    
  </a>

---

# Browsers as a Service

URL: https://docs.browserless.io/baas/start

Browsers as a Service (BaaS) lets you connect Puppeteer or Playwright to managed browsers over WebSocket. Run your existing automation code by changing the connection URL to point at Browserless. No infrastructure management required.

If you want a declarative GraphQL workflow, browser-side reconnection, or an IDE-driven session model, use [BrowserQL](/browserql/start) instead. If you only need one-off HTTP tasks like screenshots, PDFs, or scraping, use [REST APIs](/rest-apis/intro).

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Puppeteer or Playwright installed locally

## How It Works

BaaS exposes a WebSocket endpoint. You pass your API token and any launch parameters in the URL, then use the standard Puppeteer `connect()` or Playwright `connectOverCDP()` methods. Browserless starts a managed browser and hands control to your script.

```javascript

const browser = await puppeteer.connect({
  browserWSEndpoint: "wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE",
});

const page = await browser.newPage();
await page.goto("https://example.com");

console.log(await page.title());

await browser.close();
```

Browserless is available in multiple regions. Replace `production-sfo` in the examples above with your preferred regional endpoint. See [Regional Endpoints](/baas/connection-url-patterns#regional-endpoints) for available regions.

See the [Quickstart guide](/baas/quick-start) for full setup instructions, including how to get your API token.

## When to Use BaaS v2

BaaS is the right choice when you already have Puppeteer or Playwright code and want to run it in the cloud without rewriting it. It gives you full programmatic control over the browser, which is useful for complex workflows with branching logic, dynamic interactions, or tightly coupled multi-step sequences.

Running browsers at scale introduces real operational overhead: Chrome instances leak memory over time, concurrent sessions compete for CPU and RAM, and self-hosted infrastructure requires ongoing security patching and capacity planning. BaaS offloads all of that. Browserless manages the browser pool, handles resource isolation between sessions, and keeps the runtime up to date. Your automation scales without the infrastructure burden.

## Key Features

- **[Persistent sessions](/baas/session-management)**: Reconnect to the same browser across requests and maintain state between operations.
- **[Stealth mode](/baas/bot-detection/stealth)**: Enable bot detection evasion by passing stealth launch parameters in the connection URL.
- **[LiveURL](/baas/monitor-sessions/hybrid-automation)**: Hand off a browser session to a human for interaction, then resume automation.
- **[Regional load balancing](/baas/infrastructure/load-balancers)**: Distribute sessions across multiple geographic regions.
- **[Launch parameters](/baas/launch-options)**: Control timeouts, concurrency limits, and queuing behavior.

## Browser Endpoints

Six browsers are available. Connect by changing the path in your WebSocket URL:

| Path | Browser | Notes |
|------|---------|-------|
| `/chromium` | Chromium | Open-source build. Recommended for most automation tasks. |
| `/chrome` | Chrome | Google Chrome binary. Use when a site requires Chrome-specific features or codecs. |
| `/stealth` | Stealth | Privacy-hardened browser with fingerprint randomization and ad blocking. |
| `/firefox/playwright` | Firefox | Playwright native protocol only. |
| `/webkit/playwright` | WebKit | Playwright native protocol only. |
| `/edge/playwright` | Edge | Playwright native protocol only. |

For protocol variants, stealth combinations, and a full feature matrix, see [Connection URL Patterns](/baas/connection-url-patterns) and [Supported Browsers](/baas/infrastructure/supported-browsers).

## Authentication

BaaS connections require an API token included in the WebSocket URL. See the [Quickstart guide](/baas/quick-start) for connection examples.

## Session Limits

BaaS session durations depend on your subscription plan:

## FAQ & Troubleshooting

<details>
<summary>What is BaaS (Browsers as a Service)?</summary>

BaaS lets you run Puppeteer or Playwright against managed headless browsers in the cloud over WebSocket. You write automation code locally, and Browserless handles the browser infrastructure.

</details>

<details>
<summary>Which automation libraries does Browserless support?</summary>

Browserless supports Puppeteer, Playwright, and other CDP-based libraries including chromedp (Go) and Python Pyppeteer. Use a library that speaks the protocol your endpoint expects: CDP on `/chromium` and `/chrome`, and Playwright's native protocol on the `/playwright` routes. Connecting a CDP client to a `/playwright` route fails, and the reverse fails too.

Languages without a CDP client, such as PHP, can call the [REST APIs](/rest-apis/intro) over plain HTTP instead.

Selenium and WebDriver aren't supported in BaaS v2. Support was dropped in the v2 rewrite because v2 speaks the Chrome DevTools Protocol, not the WebDriver protocol. If you're moving off Selenium, see [migrating Selenium to Playwright](https://www.browserless.io/blog/migrating-selenium-to-playwright) and [migrating from Selenium Grid to BrowserQL](https://www.browserless.io/blog/how-to-migrate-from-selenium-grid-to-browserql).

</details>

<details>
<summary>How does BaaS differ from BrowserQL?</summary>

BaaS provides remote browsers you control with existing libraries like Puppeteer or Playwright. BrowserQL is a declarative GraphQL API with built-in stealth and CAPTCHA solving. Choose BaaS to reuse existing code, or BrowserQL for new scraping workflows.

</details>

## Next steps

Explore these areas to get started:

---

# Whitelisting IP Addresses

URL: https://docs.browserless.io/baas/troubleshooting/whitelisting-ips

If your target infrastructure restricts inbound traffic by IP address, you need to whitelist the outbound IPs used by Browserless's shared fleet. The IPs listed below are automatically updated from our live infrastructure and may change periodically as we scale or perform maintenance.

> **Info**
> The shared fleet's IP addresses may change every few months as we scale capacity or perform maintenance. We recommend checking this page or using the GraphQL API for the latest list.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Puppeteer or Playwright installed locally

## Current IP Address Lists

Below is the most recent list of IP addresses on our shared fleet, automatically updated from our live infrastructure.

## Programmatically Fetch IPs

If you prefer to fetch the latest IP addresses programmatically rather than referring to the table above, you can use our GraphQL API:

```bash
curl --location 'https://api.browserless.io/graphql' \
--header 'Content-Type: application/json' \
--data '{"query":"query {\n  workerIps {\n    sfo\n    lon\n    ams\n    lastUpdate\n  }\n}","variables":{}}'
```

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>403 Forbidden</code> error?</summary>

Your API token is missing or expired. Pass it as a `?token=` query parameter in the WebSocket or HTTP URL. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My script works locally but fails on Browserless</summary>

Local browser settings may differ from the Browserless environment. Use `launch` parameters to match your local setup (viewport, user agent, timezone). See [launch parameters](/baas/launch-options) for the full list.

</details>

## Next steps

  <a href="/baas/best-practices#http-error-codes" className="next-step-card-link">
    
      <h3 className="next-step-card-title">HTTP Error Codes</h3>
      Common HTTP errors and how to resolve them.
    
  </a>

  <a href="/baas/best-practices" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Best Practices</h3>
      Patterns for reliable browser automation.
    
  </a>

---

# BaaS V1 Documentation

URL: https://docs.browserless.io/baas/v1

This page points to the [BaaS V1 documentation](/baas/v1/start), which is deprecated but still available for reference.

For new projects, we recommend using [BaaS V2](/baas/start) or [BrowserQL](/browserql/start).

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>403 Forbidden</code> error?</summary>

Your API token is missing or expired. Pass it as a `?token=` query parameter in the WebSocket or HTTP URL. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My script works locally but fails on Browserless</summary>

Local browser settings may differ from the Browserless environment. Use `launch` parameters to match your local setup (viewport, user agent, timezone). See [launch parameters](/baas/launch-options) for the full list.

</details>

## Next steps

  <a href="/baas/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BaaS Quickstart</h3>
      get started with Puppeteer or Playwright on Browserless
    
  </a>
  <a href="/baas/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure browser behavior and environment
    
  </a>

---

# /config API

URL: https://docs.browserless.io/baas/v1/apis/config

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/enterprise/utility-functions/config) or [BrowserQL](/browserql/start).

The config API allows you to retrieve the current configuration of your container **and is only available for dedicated and self-hosted accounts.**

Check out the [config API schema](https://chrome.browserless.io/docs/#/Management%20API/get_config) defined in our Swagger page.

## Gathering your container configuration

To see the configuration of your container, simply issue a `GET` request to `/config`:

```bash
curl -X GET \
  https://chrome.browserless.io/config?token=MY_API_TOKEN
```

> **Warning**
> Running this in the browser exposes your API token.

Running this cURL request will result in an output like:

```json
{
  "allowFileProtocol": true,
  "chromeRefreshTime": 0,
  "connectionTimeout": 0,
  "disabledFeatures": ["string"],
  "enableAPIGet": true,
  "enableCors": true,
  "errorAlertURL": "string",
  "exitOnHealthFailure": true,
  "functionBuiltIns": ["string"],
  "functionEnableIncognitoMode": true,
  "functionExternals": ["string"],
  "healthFailureURL": "string",
  "sessionCheckFailURL": "string",
  "keepAlive": true,
  "maxCPU": 0,
  "maxConcurrentSessions": 0,
  "maxMemory": 0,
  "maxQueueLength": 0,
  "metricsJSONPath": "string",
  "port": 0,
  "prebootChrome": true,
  "queuedAlertURL": "string",
  "rejectAlertURL": "string",
  "singleRun": true,
  "timeoutAlertURL": "string",
  "token": "string",
  "workspaceDir": "string",
  "socketBehavior": "string"
}
```

---

# /content API

URL: https://docs.browserless.io/baas/v1/apis/content

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/rest-apis/content) or [BrowserQL](/browserql/start).

The content API allows for simple navigation to a site and capturing the page's content (including the `<head>` section). Browserless will respond with a `Content-Type` of `text/html`, and string of the site's HTML. This is useful for capturing the content of a page that has a lot of JavaScript or other interactivity.

If you want to see all the options check out the [schema for it here on GitHub](https://github.com/browserless/browserless/blob/v1/src/schemas.ts#L177-L192) or check out this API schema defined in [Swagger](https://chrome.browserless.io/docs/#/Browser%20API/post_content).

## Capture the content of example.com

**JSON payload**

```js
{
  "url": "https://example.com/"
}
```

**cURL request**

```bash
curl -X POST \
  https://chrome.browserless.io/content?token=MY_API_TOKEN \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/json' \
  -d '
{
  "url": "https://example.com/"
}'
```

---

# /download API

URL: https://docs.browserless.io/baas/v1/apis/download

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/rest-apis/download) or [BrowserQL](/browserql/start).

Much like the `/function` API, the `/download` API allows you to `POST` code for Browserless to execute. The only difference with /download is that, instead of returning the result of your function, we instead return a file that the browser downloads.

Check out this API schema defined in [Swagger](https://chrome.browserless.io/docs/#/Browser%20API/post_download).

> If your download request doesn't result in a file being downloaded, Browserless will likely time out the function.

## Basic Usage

Here's an example of downloading a file created in the browser (a CSV file):

```js
const puppeteer = require('puppeteer');

async function run() {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();

  // Here we generate a CSV file and have the browser download it
  await page.evaluate(() => {
    const rows = [
      ['name1', 'city1', 'some other info'],
      ['name2', 'city2', 'more info'],
    ];
    let csvContent = 'data:text/csv;charset=utf-8,';
    rows.forEach(function (rowArray) {
      let row = rowArray.join(',');
      csvContent += row + '\r\n';
    });
    const encodedUri = encodeURI(csvContent);
    const link = document.createElement('a');
    link.setAttribute('href', encodedUri);
    link.setAttribute('download', 'data.csv');
    document.body.appendChild(link);

    return link.click();
  });
}

run();
```

This might come as a surprise, but unfortunately in puppeteer there's no way to know if the file was downloaded, or an API to even get it. You'll have to _know_ ahead of time where files are kept, and watch the file system for it to complete. Instead of wiring up all that code you can send Browserless an HTTP request and it takes care of all the underlying file-system calls:

```bash
curl -X POST \
  https://chrome.browserless.io/download?token=YOUR-API-TOKEN \
  -H 'Content-Type: application/javascript' \
  -d 'module.exports = async ({ page }) => {
    await page.evaluate(() => {
        const rows = [
            ["name1", "city1", "some other info"],
            ["name2", "city2", "more info"]
        ];
        let csvContent = "data:text/csv;charset=utf-8,";
        rows.forEach(function(rowArray){
            let row = rowArray.join(",");
            csvContent += row + "\r\n";
        });
        const encodedUri = encodeURI(csvContent);
        const link = document.createElement("a");
        link.setAttribute("href", encodedUri);
        link.setAttribute("download", "data.csv");
        document.body.appendChild(link);

        return link.click();
    });
};'
```

This API is sensitive to the downloaded file and will return an appropriate `Content-Type` with the response.

## Parameterizing your /download call

You can also post an `application/json` body, as opposed to an `application/javascript` one, if you'd like to re-use your function with new parameters. In this case, let's change our CSV file to be dynamic (note that our script is now minified for portability):

```bash
curl -X POST \
  https://chrome.browserless.io/download?token=YOUR-API-TOKEN \
  -H 'Content-Type: application/json' \
  -d '{
  "code": "module.exports=async({page:a,context:b})=>{const{rows:c}=b;await a.evaluate(()=>{let d='\''data:text/csv;charset=utf-8,'\'';c.forEach(function(g){let h=g.join('\'','\'');d+=h+'\''\r\n'\''});const e=encodeURI(d),f=document.createElement('\''a'\'');return f.setAttribute('\''href'\'',e),f.setAttribute('\''download'\'','\''data.csv'\''),document.body.appendChild(f),f.click()})};",
  "context": {
    "rows": [
      ["Some", "Cool", "CSV"],
            ["With", "Rows", "And Stuff"]
        ]
  }
}'
```

This lets you define re-usable functions and invoke them over HTTP in a more flexible way.

---

# /function API

URL: https://docs.browserless.io/baas/v1/apis/function

<!-- markdownlint-disable no-emphasis-as-heading -->

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/rest-apis/function) or [BrowserQL](/browserql/start).

A quick way to run commands with Browserless is via our `/function` API. You can use Python, NodeJS or any other language that supports making HTTP requests, so you don't need to install any libraries on your end.

The `/function` endpoint allows for `POST`ing of custom `code` and `context` to run them with. The `code` function, which only supports puppeteer code, gets called with an object containing several properties: a `page` property, [which is a puppeteer page object](https://pptr.dev/api/puppeteer.page), and `context` which is the context you provide in the JSON body.

Functions should return an object with two properties: `data` and `type`. `data` can be whatever you'd like (`Buffer`, `JSON`, or plain `text`) and `type` is a string describing the content-type of `data`. Browserless reads both of these from your functions return value and resolves the request appropriately.

Check out this API schema defined in [Swagger](https://chrome.browserless.io/docs/#/Browser%20API/post_function).

> If you want to see more examples, checkout how other [REST API's are handled in our GitHub project](https://github.com/browserless/browserless/tree/v1/functions).

## Basic Usage

**JS code**

```javascript
// Read the `url` from context, goto the page and return the results
module.exports = async ({ page, context }) => {
  const { url } = context;
  await page.goto(url);

  const data = await page.content();

  return {
    data,
    // Make sure to match the appropriate content here
    // You'll likely want 'application/json'
    type: 'application/html',
  };
};
```

**context**

```json
{
  "url": "https://example.com"
}
```

Since you can't have multi-line strings in JSON we've minified our above code with the [online babel repl](https://babeljs.io/repl) and use it in the below `curl` call.

**cURL request**

```bash
curl -X POST \
  'https://chrome.browserless.io/function?TOKEN=YOUR-API-TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
  "code": "module.exports=async({page:a,context:b})=>{const{url:c}=b;await a.goto(c);const d=await a.content();return{data:d,type:\"application/html\"}};",
  "context": {
    "url": "https://example.com/"
  }
}'
```

## Detached functions

Functions that are "detached" resolve _immediately_ with a JSON payload exposing the job's ID. These are useful if you don't want to hold onto the connection with Browserless while it's running, but are relying on the side-effects of having a Browserless run the function. You can also call external services with any results (like webhooks or your own API's). Since it's harder to track detached functions, Browserless will call them with another parameter if `id` which is the same `id` that they immediately resolve with.

In order to detach a function you'll need to pass in a third parameter of `detached: true` in the JSON POST body.

> You can currently require 'url', 'util', 'path', 'querystring', 'lodash', 'node-fetch', and 'request' in your functions. Please contact us for adding a module

**code**

```js
const fetch = require('node-fetch');

// ID here is dynamic, and matches up with the immediate response from
// browserless but allows you to track it in third-party systems
module.exports = async ({ page, context, id }) => {
  const { url } = context;
  await page.goto(url);

  const data = await page.content();

  // POST the content to a third-party service
  return fetch('https://my-third-party-service.com/content', {
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      pageContent: data,
      sessionId: id,
    }),
    method: 'POST',
  });
};
```

**context**

```json
{
  "url": "https://example.com"
}
```

To force functions to operate in a detached way, simply add a `detached: true` to the POSTed body.

**cURL request**

```bash
curl -X POST \
  https://chrome.browserless.io/function?token=MY_API_TOKEN \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/json' \
  -d '{
    "code": "const fetch=require('\''node-fetch'\'');module.exports=async({page:a,context:b,id:c})=>{const{url:d}=b;await a.goto(d);const e=await a.content();return fetch('\''https://my-third-party-service.com/content'\'',{headers:{'\''Content-Type'\'':'\''application/json'\''},body:JSON.stringify({pageContent:e,sessionId:c}),method:'\''POST'\''})};",
    "context": {
        "url": "https://example.com/"
    },
    "detached": true
}'
```

---

# /kill API

URL: https://docs.browserless.io/baas/v1/apis/kill

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/rest-apis/intro) or [BrowserQL](/browserql/start).

The kill API allows you to kill a specific session or all the open sessions **and is only available for dedicated and self-hosted accounts.**

Check out the [kill API schema](https://chrome.browserless.io/docs/#/Management%20API/get_kill__id_) defined in our Swagger page.

> **Danger**
> These actions **cannot** be undone

### Examples

1. [Killing a specific session](#killing-a-specific-session)
2. [Killing all the sessions](#killing-all-the-sessions)

## Killing a specific session

To kill a specific session, simply issue a `GET` request to `/kill/{id}`, you can set the id with the browser ID or tracking-ID, you can find this with the help of the [sessions API](https://chrome.browserless.io/docs/#/Management%20API/get_sessions)

You can run this with a cURL request as so:

```bash
curl -X GET \
  https://chrome.browserless.io/kill/{id}?token=MY_API_TOKEN
```

## Killing all the sessions

To kill all the open sessions, simply issue a `GET` request to `/kill/all`

You can run this with a cURL request as so:

```bash
curl -X GET \
  https://chrome.browserless.io/kill/all?token=MY_API_TOKEN
```

> **Warning**
> Running this in the browser exposes your API token.

---

# /metrics API

URL: https://docs.browserless.io/baas/v1/apis/metrics

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/enterprise/utility-functions/metrics) or [BrowserQL](/browserql/start).

The metrics API allows you to retrieve an array of session statistics **and is only available for dedicated and self-hosted accounts.** This API is different to the [pressure API](https://chrome.browserless.io/docs/#/Management%20API/get_pressure) since the metrics API returns a collection of all the metrics data with one week of history whereas the pressure API only returns one object (the current metrics).

Check out the [metrics API schema](https://chrome.browserless.io/docs/#/Management%20API/get_metrics) defined in our Swagger page.

## Gathering metrics array from your workers

To see statistics regarding your workers, simply issue a `GET` request to `/metrics`:

```bash
curl -X GET \
  https://chrome.browserless.io/metrics?token=MY_API_TOKEN
```

> **Warning**
> Running this in the browser exposes your API token.

Running this cURL request will result in an output like:

```json
[
  {
    "date": 0,
    "successful": 0,
    "queued": 0,
    "rejected": 0,
    "unhealthy": 0,
    "memory": 0,
    "cpu": 0,
    "timedout": 0,
    "totalTime": 0,
    "meanTime": 0,
    "maxTime": 0,
    "minTime": 0,
    "maxConcurrent": 0,
    "sessionTimes": [0]
  }
]
```

### /metrics/all

This route will output a similar payload, but with the total stats of all sessions.

---

# Browserless API Index

URL: https://docs.browserless.io/baas/v1/apis/overview

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/rest-apis/intro) or [BrowserQL](/browserql/start).

Browserless has production ready APIs that you can use depending on your use-case. Each API accepts different parameters and options; their schemas are displayed in the [Swagger UI page](https://chrome.browserless.io/docs/).

There are [Browser](https://chrome.browserless.io/docs/#/Browser%20API) and [Management](https://chrome.browserless.io/docs/#/Management%20API) APIs. The browser APIs help you execute specific tasks for your use-case. The management APIs allow you to access things like open sessions, metrics, and kill sessions among many more functions to monitor the health of your worker.

> The management APIs are not available for usage-based or cloud unit-based accounts.

### Browser APIs

- [/content](./content)
- [/download](./download)
- [/function](./function)
- [/pdf](./pdf)
- [/screencast](./screencast)
- [/screenshot](./screenshot)
- [/scrape](./scrape)
- [/stats](./stats)

### Management APIs

- [/workspace](./workspace)
- [/sessions](./sessions)
- [/metrics](./metrics)
- [/pressure](./pressure)
- [/kill](./kill)
- [/config](./config)

Remember to check out all the APIs available from the [Swagger UI page](https://chrome.browserless.io/docs/)

---

# /pdf API

URL: https://docs.browserless.io/baas/v1/apis/pdf

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/rest-apis/pdf-api) or [BrowserQL](/browserql/start).

The pdf API allows for simple navigation to a site and capturing a pdf. Browserless will respond with a `Content-Type` of `application/pdf`, and a Buffer of the pdf file. Similar to screenshots, this also exposes puppeteer's `pdf` options via an `options` property in the JSON body for granular control.

If you want to see all the options check out this API schema defined in [Swagger](https://chrome.browserless.io/docs/#/Browser%20API/post_pdf).

> This route requires either a `url` or `html` property in the JSON body. When `html` is present Browserless will render the HTML for you and capture the PDF

## Basic Usage

```js
// JSON body
// `options` are the options available via puppeteer's Page.pdf() method
// (see https://pptr.dev/api/puppeteer.pdfoptions)
{
  "url": "https://example.com/",
  "options": {
    "displayHeaderFooter": true,
    "printBackground": false,
    "format": "A0"
  }
}
```

**cURL request**

```bash
curl -X POST \
  https://chrome.browserless.io/pdf?token=MY_API_TOKEN \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/json' \
  -d '{
  "url": "https://example.com/",
  "options": {
    "displayHeaderFooter": true,
    "printBackground": false,
    "format": "A0"
  }
}'
```

> We do not allow to set a `options.path`

## Custom options

The `/pdf` route has a few special custom options that make it more usable and configurable. We've added these options based on feedback from you in hope that it will help gather best-practices in a single place.

### `gotoOptions`

The `gotoOptions` is an object that's passed directly into puppeteer's `page.goto()` call so that you can specify things like alternative loaded events. See [puppeteer's goto options](https://pptr.dev/api/puppeteer.page.goto#remarks) for more information.

**Example cURL request**

```bash
curl -X POST \
  https://chrome.browserless.io/pdf?token=MY_API_TOKEN \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/json' \
  -d '{
  "url": "https://example.com/",
  "gotoOptions": {
    "waitUntil": "networkidle2",
  },
  "options": {
    "displayHeaderFooter": true,
    "printBackground": false,
    "format": "A0"
  }
}'
```

## Custom styling

If you need to inject custom CSS code to the page, you can use the `addStyleTag` property.

This property takes an array of objects, each with either a `content` property with valid CSS code, or a `url` property that loads the stylesheet from the web.

**Example**

```json
{
  "url": "https://example.com/",
  "gotoOptions": {
    "waitUntil": "networkidle2"
  },
  "addStyleTag": [
    {
      "content": "body { height: 100vh;  background: linear-gradient(45deg, #da5a44, #a32784);}"
    },
    {
      "url": "https://interactive-examples.mdn.mozilla.net/live-examples/css-examples/text-decoration/text-decoration-color.css"
    }
  ]
}
```

## safeMode

Because longer pages can crash trigger a "Page Crashed!" error when doing a `page.pdf` invocation, we've implemented a "safe" way to generate PDF's of bigger websites. When set to `true`, `safeMode` will:

- Capture PDF's _one_ page at a time in-memory.
- Once all pages are in memory we connect them together.
- Compress the PDF so it's more efficiently transported over the network.
- Return the resulting PDF as a binary via `application/pdf`.

> Please note that when using `safeMode` that it will likely take a while longer to process the request, and the resulting PDF file may be larger.

**Example cURL request**

```bash
curl -X POST \
  https://chrome.browserless.io/pdf?token=MY_API_TOKEN \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/json' \
  -d '{
  "url": "https://example.com/",
  "safeMode": true,
  "options": {
    "displayHeaderFooter": true,
    "printBackground": false,
    "format": "A0"
  }
}'
```

---

# /pressure API

URL: https://docs.browserless.io/baas/v1/apis/pressure

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/rest-apis/intro) or [BrowserQL](/browserql/start).

The pressure API allows you to retrieve the current metrics of your worker(s) **and is only available for dedicated and self-hosted accounts.** This API is different to the [metrics API](https://chrome.browserless.io/docs/#/Management%20API/get_metrics) since the pressure API only returns one object (the current metrics) whereas the metrics API returns a collection of all the metrics data with one week of history.

Check out the [pressure API schema](https://chrome.browserless.io/docs/#/Management%20API/get_pressure) defined in our Swagger page.

## Gathering metrics from your workers

To see statistics regarding your workers, simply issue a `GET` request to `/pressure`:

```bash
curl -X GET \
  https://chrome.browserless.io/pressure?token=MY_API_TOKEN
```

> **Warning**
> Running this in the browser exposes your API token.

Running this cURL request will result in an output like:

```json
{
  "date": 0,
  "successful": 0,
  "queued": 0,
  "rejected": 0,
  "unhealthy": 0,
  "memory": 0,
  "cpu": 0,
  "timedout": 0,
  "totalTime": 0,
  "meanTime": 0,
  "maxTime": 0,
  "minTime": 0,
  "maxConcurrent": 0,
  "sessionTimes": [0]
}
```

---

# /scrape API

URL: https://docs.browserless.io/baas/v1/apis/scrape

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/rest-apis/scrape) or [BrowserQL](/browserql/start).

The scrape API allows for getting the contents a page, by specifying selectors you are interested in, and returning a structured JSON response. We also allow for setting a timeout option for asynchronously added elements.

The default behavior is to navigate to the URL specified, wait for the page to load (including parsing and executing of JavaScript), then waiting for the elements for a maximum of 30 seconds. All of these are configurable, and documented in detail below.

At a minimum you'll need to specify at least a `url` and an `elements` array.

Check out this API schema defined in [Swagger](https://chrome.browserless.io/docs/#/Browser%20API/post_scrape).

## Basic Usage

Below is the most basic usage, where we'll navigate to the example.com website (waiting for page-load) and parse out all `a` elements.

Internally we use `document.querySelectorAll` to retrieve all matches on a page. Using a more specific selector can narrow down the returned results.

**Get the a elements on example.com**

```json
{
  "url": "https://example.com/",
  "elements": [
    {
      "selector": "a"
    }
  ]
}
```

**cURL request**

```bash
curl -X POST \
  https://chrome.browserless.io/scrape?token=MY_API_TOKEN \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/json' \
  -d '{
  "url": "https://example.com/",
  "elements": [{
      "selector": "a"
  }]
}'
```

Once ran, this will return a JSON payload of the following. We return the `innerHTML` and `innerText` of all matched selectors, as well as all their `attributes`. In the case of the above call, this will return:

```json
{
  "data": [
    {
      "selector": "a",
      "results": [
        {
          "html": "More information...",
          "text": "More information...",
          "attributes": [
            {
              "name": "href",
              "value": "https://www.iana.org/domains/example"
            }
          ]
        }
      ]
    }
  ]
}
```

## Specifying page-load behavior

The scrape API allows for setting specific page-load behaviors by setting a `gotoOptions` in the JSON body. This is passed directly into [puppeteer's `goto()` method](https://pptr.dev/api/puppeteer.page.goto).

In the example below, we'll set a `waitUntil` property and a `timeout`

**Get the H1 elements on example.com with custom goto options**

```json
{
  "url": "https://example.com/",
  "elements": [
    {
      "selector": "h1"
    }
  ],
  "gotoOptions": {
    "timeout": 10000,
    "waitUntil": "networkidle2"
  }
}
```

This will now force selectors to be collected _after_ the network has been idle for 2 seconds, and we'll timeout the request after 10 seconds.

## Custom behavior with `waitFor`

Sometimes it's helpful to do further actions, or wait for custom events on the page before getting data. We allow this behavior with the `waitFor` property. We closely follow [puppeteer's `waitFor()` method](https://github.com/puppeteer/puppeteer/blob/7b768838023bcb79051f5d34c96949ef253ad9a2/docs/api.md#pagewaitforselectororfunctionortimeout-options-args).

This property can accept one of three options:

- A function to be ran within the page's context, inside of the browser.
- A `number` indicating the time in milliseconds to wait.
- A valid selector to wait for.

**Waiting for a selector**

```json
{
  "url": "https://example.com/",
  "elements": [
    {
      "selector": "h1"
    }
  ],
  "waitFor": "h1"
}
```

**Waiting for 10 seconds**

```json
{
  "url": "https://example.com/",
  "elements": [
    {
      "selector": "h1"
    }
  ],
  "waitFor": 10000
}
```

**Waiting for a function**

```json
{
  "url": "https://example.com/",
  "elements": [
    {
      "selector": "h1"
    }
  ],
  "waitFor": "() => document.querySelector('h1')"
}
```

**Error handling**

We do catch and return errors for invalid functions inside of `waitFor`. For instance: `() => document.querySelector('h1'))` as a `waitFor` will return an HTTP code of `400` and the following text:

```text
Evaluation failed: SyntaxError: Unexpected token ')'
at new Function (<anonymous>)
  at waitForPredicatePageFunction (__puppeteer_evaluation_script__:2:21)
```

## Cookies, headers and other options

As with our other APIs, you can inject things like `cookies`, `headers` and other options, including intercepting requests and responses. These follow the pattern set by [puppeteer's `setCokkie()`](https://pptr.dev/15.3.0/api/puppeteer.page.setcookie) and [`setExtraHTTPHeaders()` methods](https://pptr.dev/15.3.0/api/puppeteer.page.setextrahttpheaders).

**Setting a special cookie**

```json
{
  "url": "https://example.com/",
  "elements": [
    {
      "selector": "h1"
    }
  ],
  "cookies": [
    {
      "name": "my-special-cookie",
      "value": "foo-bar",
      "url": "https://www.example.com",
      "path": "/",
      "sameSite": "Strict"
    }
  ]
}
```

**Adding an authentication header**

```json
{
  "url": "https://example.com/",
  "elements": [
    {
      "selector": "h1"
    }
  ],
  "setExtraHTTPHeaders": {
    "Authentication": "Basic foo-bar"
  }
}
```

**Intercepting requests**

```json
{
  "url": "https://example.com/",
  "elements": [
    {
      "selector": "h1"
    }
  ],
  "rejectRequestPattern": ["png", "jpg"]
}
```

## Element timeouts

Browserless will wait for the elements specified for up to 30 seconds to be inserted in the page. This is useful for single-page applications that can load data on the fly. You can conditionally change this timer by specifying a `timeout` property per-element.

**Using a custom timeout of 10 seconds**

```json
{
  "url": "https://example.com/",
  "elements": [
    {
      "selector": "h1",
      "timeout": 10000
    },
    {
      "selector": "a",
      "timeout": 5000
    }
  ]
}
```

## Debugging

Without a doubt, one of the most frustrating aspects of scraping is debugging broken scripts. This is nothing new for Browserless, and though we offer [excellent debugging tools](https://www.browserless.io/blog/2019/04/18/live-debugger/), we thought it'd be appropriate to add some extra information in the JSON response to help make debugging even easier.

As of today, we offer five payloads to help debug the page: `html`, `screenshot`, `console`, `cookies` and `network`. Each is listed below.

**HTML**: This is the raw HTML of the webpage, after all page-load and `waitFor` functions have ran.

**Screenshot**: A full-page JPEG of the page encoded in `base64`.

**Console**: An array of all the various `console` messages the page has written out.

**Cookies**: An array of objects specifying all the cookies currently set on the page.

**Network**: An object with two properties: `inbound` and `outbound`, representing the outgoing requests as well as their inbound responses.

In order to get this debugging information, you'll need to specify that you want it in your JSON POST payload. This is made optional due to the fact that generating these properties can take additional resources away from other concurrent work. You'll need to specify what fields you want, by adding a `debug` object and declaring what fields you want returned. Here's an example of getting all the fields back:

**Requesting all debug fields**

```json
{
  "url": "https://example.com/",
  "elements": [
    {
      "selector": "h1",
      "timeout": 10000
    },
    {
      "selector": "a",
      "timeout": 5000
    }
  ],
  "debug": {
    "screenshot": true,
    "console": true,
    "network": true,
    "cookies": true,
    "html": true
  }
}
```

---

# /screencast API

URL: https://docs.browserless.io/baas/v1/apis/screencast

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/rest-apis/intro) or [BrowserQL](/browserql/start).

> The screencast API is still under beta, and isn't recommended yet for production usage.

The screencast API allows for generating video files, on demand, of your puppeteer scripts. This simple REST API allows for POSTing of an `application/json` _or_ `application/javascript` body of your code you'd like to record. The resulting response is a `webm` file with the video attached. Audio is now supported in our screencast API as of docker version `1.8.0`.

The resulting video's dimensions are _inherited_ by the width and height of the browser. You can change this very easily by calling `page.setViewport` with your dimensions in your scripts as well as setting `--window-size` in the URL.

Check out this API schema defined in [Swagger](https://chrome.browserless.io/docs/#/Browser%20API/post_screencast).

## Basic Usage

**Example body**

```js
module.exports = async ({ page }) => {
  const sleep = (ms) => new Promise((res) => setTimeout(res, ms));

  await page.setViewport({ width: 1920, height: 1080 });
  await page.goto('https://www.youtube.com/watch?v=GnEmD17kYsE');
  await sleep(10000);
};
```

And the resulting `cURL` call:

**Full cURL request**

```bash
curl -X POST \
  https://chrome.browserless.io/screencast?token=MY-API-TOKEN&--window-size=1920,1080 \
  -o video.webm \
  -H 'Content-Type: application/javascript' \
  -d '
    module.exports = async ({ page }) => {
      const sleep = (ms) => new Promise((res) => setTimeout(res, ms));

      await page.setViewport({ width: 1920, height: 1080 });
      await page.goto(`https://www.youtube.com/watch?v=GnEmD17kYsE`);
      await sleep(10000);
    };
  '
```

## Using `application/javascript` content-type

The `application/javascript` content-type is great for testing quick scripts and seeing their output and works with many tools like postman for faster feedback. When using this API you'll need to specify an appropriate content-type for the body of the request. When the type is `application/json` the /screencast API follows a similar semantic of the `/function` API, where you'll apply both a `code` property and a `context` property.

This makes your request more reusable as you can specify arguments in the `context` property that your code can reference. The only limitation is that all parameters will need to be hard-coded.

**Example code body**

```js
module.exports = async ({ page, context }) => {
  const sleep = (ms) => new Promise((res) => setTimeout(res, ms));

  const { url, wait, width, height } = context;
  await page.setViewport({ width, height });
  await page.goto(url);
  await sleep(wait);
};
```

**context object example**

```js
// JSON body
// Code is minified here!
{
  "code": "module.exports = async ({ page, context }) => { const sleep = (ms) => new Promise((res) => setTimeout(res, ms)); const { url, wait, width, height } = context; await page.setViewport({ width, height }); await page.goto(url); await sleep(wait); };",
  "context": {
    "url": "https://www.youtube.com/watch?v=GnEmD17kYsE",
    "wait": 1000,
    "width": 1920,
    "height": 1080
  }
}
```

**Full cURL request**

```bash
curl -X POST \
  https://chrome.browserless.io/screencast?token=YOUR-API-TOKEN&--window-size=1920,1080 \
  -o video.webm \
  -H 'Content-Type: application/json' \
  -d '{
  "code": "module.exports = async ({ page, context }) => { const sleep = (ms) => new Promise((res) => setTimeout(res, ms)); const { url, wait, width, height } = context; await page.setViewport({ width, height }); await page.goto(url); await sleep(wait); };",
  "context": {
    "url": "https://www.youtube.com/watch?v=GnEmD17kYsE",
    "wait": 5000,
    "width": 1920,
    "height": 1080
  }
}'
```

## Programmatic control

By default, Browserless will start recording the session immediately and will stop once your function completes execution. You can change this behavior by using two functions in your code: `startScreencast` and `stopScreencast`:

**Starting the screencast after load**

```js
module.exports = async ({ page, startScreencast, stopScreencast }) => {
  const sleep = (ms) => new Promise((res) => setTimeout(res, ms));

  await page.goto('https://vclock.com/stopwatch');
  await page.click('#btn-resume');
  startScreencast();
  await sleep(5000);
  stopScreencast();
};
```

**Full cURL request**

```bash
curl -X POST \
  https://chrome.browserless.io/screencast?token=YOUR-API-TOKEN \
  -H 'Content-Type: application/javascript' \
  -d 'module.exports = async ({ page, startScreencast, stopScreencast }) => {
    const sleep = (ms) => new Promise((res) => setTimeout(res, ms));

    await page.goto("https://vclock.com/stopwatch");
    await page.click("#btn-resume");
    startScreencast();
    await sleep(5000);
    stopScreencast();
}'
```

---

# /screenshot API

URL: https://docs.browserless.io/baas/v1/apis/screenshot

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/rest-apis/screenshot-api) or [BrowserQL](/browserql/start).

The screenshot API allows for simple navigation to a site and capturing a screenshot. Browserless will respond with either a binary or base64 encode of a `png` or `jpg` (depending on parameters). This API exposes most of puppeteer's screenshot API through the posted JSON payload.

If you want to see all the options check out this API schema defined in [Swagger](https://chrome.browserless.io/docs/#/Browser%20API/post_screenshot).

## Basic Usage

```js
// JSON body
// `options` are the options available via puppeteer's Page.screenshot() method
// (see https://pptr.dev/api/puppeteer.screenshotoptions)
{
  "url": "https://example.com/",
  "options": {
    "fullPage": true,
    "type": "jpeg",
    "quality": 75
  }
}
```

**cURL request**

```bash
curl -X POST \
  https://chrome.browserless.io/screenshot?token=MY_API_TOKEN \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/json' \
  -d '{
  "url": "https://example.com/",
  "options": {
    "fullPage": true,
    "type": "jpeg",
    "quality": 75
  }
}'
```

## Screenshot options

We support Puppeteer's [screenshot options](https://pptr.dev/api/puppeteer.screenshotoptions/#properties) almost in its entirety, and you can easily pass options to the JSON payload using the `options` property.

**Example**

```json
{
  "url": "https://example.com/",
  "options": {
    "type": "jpeg",
    "clip": {
      "height": 200,
      "width": 600,
      "x": 90,
      "y": 100
    },
    "fullPage": false,
    "encoding": "binary"
  }
}
```

> We do not allow to set a `options.path` nor to return a `webp`

## Custom options

The `/screenshot` route has a few special custom options that make it more usable and configurable. We've added these options based on feedback from you in hope that it will help gather best-practices in a single place.

### `gotoOptions`

The `gotoOptions` is an object that's passed directly into puppeteer's `page.goto()` call so that you can specify things like alternative loaded events. See [puppeteer's goto options](https://pptr.dev/api/puppeteer.page.goto#remarks) for more information.

**cURL request**

```bash
curl -X POST \
  https://chrome.browserless.io/screenshot?token=MY_API_TOKEN \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/json' \
  -d '{
  "url": "https://example.com/",
  "gotoOptions": {
    "waitUntil": "networkidle2",
  }
}'
```

## Custom styling

If you need to inject custom CSS code to the page, you can use the `addStyleTag` property.

This property takes an array of objects, each with either a `content` property with valid CSS code, or a `url` property that loads the stylesheet from the web.

**Example**

```json
{
  "url": "https://example.com/",
  "gotoOptions": {
    "waitUntil": "networkidle2"
  },
  "addStyleTag": [
    {
      "content": "body { height: 100vh;  background: linear-gradient(45deg, #da5a44, #a32784);}"
    },
    {
      "url": "https://interactive-examples.mdn.mozilla.net/live-examples/css-examples/text-decoration/text-decoration-color.css"
    }
  ]
}
```

## Custom behavior with `waitFor`

If you need to wait for a selector to appear in the DOM, to wait for a timeout to happen, or to execute a custom function before screenshotting, you can use the `waitFor` property in your JSON payload.

We closely follow [puppeteer's `waitFor()` method](https://github.com/puppeteer/puppeteer/blob/7b768838023bcb79051f5d34c96949ef253ad9a2/docs/api.md#pagewaitforselectororfunctionortimeout-options-args).

This property can accept one of three options:

- A valid CSS selector to wait for.
- A `number` indicating the time in milliseconds to wait.
- A function to be ran within the page's context.

**Waiting for a selector**

```json
{
  "url": "https://example.com/",
  "waitFor": "h1"
}
```

**Waiting for a timeout**

```json
{
  "url": "https://example.com/",
  "waitFor": 5000
}
```

**Waiting for a function**

```json
{
  "url": "https://example.com/",
  "waitFor": "() => document.querySelector(`h1`).innerText = `Hello World!`"
}
```

---

# /sessions API

URL: https://docs.browserless.io/baas/v1/apis/sessions

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/enterprise/utility-functions/sessions) or [BrowserQL](/browserql/start).

The sessions API allows you to gather information about the currently running sessions **and is only available for dedicated and self-hosted accounts**

Check out this API schema defined in [Swagger](https://chrome.browserless.io/docs/#/Management%20API/get_sessions).

## Gathering information from your running sessions

To see information regarding the running sessions, simply issue a `GET` request to `/sessions`:

```bash
curl -X GET \
  https://chrome.browserless.io/sessions?token=MY_API_TOKEN
```

> **Warning**
> Running this in the browser exposes your API token.

Running this cURL request will result in an output like:

```json
[
  {
    "description": "",
    "devtoolsFrontendUrl": "/devtools/inspector.html?ws=0.0.0.0:3000/devtools/page/7B491986CD0B6408DF15A339A7621226",
    "id": "7B491986CD0B6408DF15A339A7621226",
    "title": "Example Domain",
    "type": "page",
    "url": "https://www.example.com",
    "webSocketDebuggerUrl": "ws://0.0.0.0:3000/devtools/page/7B491986CD0B6408DF15A339A7621226",
    "port": "41685",
    "browserId": "b95cd82a-76b1-495e-8092-b040de780d0f",
    "trackingId": null,
    "browserWSEndpoint": "ws://0.0.0.0:3000/devtools/browser/b95cd82a-76b1-495e-8092-b040de780d0f"
  }
]
```

---

# /stats API

URL: https://docs.browserless.io/baas/v1/apis/stats

<!-- markdownlint-disable no-emphasis-as-heading -->

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/rest-apis/intro) or [BrowserQL](/browserql/start).

The stats API reveals key metrics about a site's accessibility, best practices, performance, PWA information, and SEO, from a performance standpoint. It includes useful metrics like latency, time-to-interaction, design contrast other recommendations. This API is powered by [Google's Lighthouse](https://github.com/GoogleChrome/lighthouse) project, and it's compatible with its CLI options.

In order to gather this data, send a `POST` request to `https://chrome.browserless.io/stats` if on a hosted plan, with a simple JSON payload containing a valid URL.

> **Warning**
> Due to the number of checks gathered it can take anywhere from several seconds to minutes depending on the site and size of the worker.

If you want to see all the options check out this API schema defined in [Swagger](https://chrome.browserless.io/docs/#/Browser%20API/post_stats).

### Examples

1. [Basic Usage](#basic-usage)
2. [Gathering metrics for a category](#gathering-metrics-for-a-category)
3. [Gathering metrics from a single audit](#gathering-metrics-from-a-single-audit)

## Basic Usage

**JSON**

```js
{
  "url": "https://example.com/"
}
```

**cURL request**

```bash
curl -X POST \
  https://chrome.browserless.io/stats?token=MY_API_TOKEN \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/json' \
  -d '
{
  "url": "https://example.com/"
}'
```

By default, the `/stats` API will gather **all** the metrics. This will not only result in a **really** big JSON response (350kb to 800kb on average) and it **will** take some seconds (to minutes) to complete.

Each test has a score that indicates how well it performed, where 1 is the maximum and 0 is the lowest, generating a response like this:

```js
  // ...
  "audits": {
    "is-on-https": {
      "title": "Uses HTTPS",
      "score": 1
      // ...
    },
    "viewport": {
      "title": "Has a `<meta name=\"viewport\">` tag with `width` or `initial-scale`",
      "score": 1
      // ...
    },
    "first-contentful-paint": {
      "score": 1,
      "displayValue": "0.8 s"
      // ...
    }
    // ...
  }
```

## Gathering metrics for a category

You can get metrics from specific categories, using the `config` property. [Just like Lighthouse](https://github.com/GoogleChrome/lighthouse/blob/master/readme.md#cli-options), available categories are `accessibility`, `best practices`, `performance`, `pwa`, and `seo`

**JSON**

```json
{
  "url": "https://example.com",
  "config": {
    "extends": "lighthouse:default",
    "settings": {
      "onlyCategories": ["accessibility"]
    }
  }
}
```

**cURL request**

```bash
curl --request POST \
  --url 'https://chrome.browserless.io/stats?token=MY_API_TOKEN' \
  --header 'Cache-Control: no-cache' \
  --header 'Content-Type: application/json' \
  --data '{
  "url": "https://browserless.io",
  "config": {
    "extends": "lighthouse:default",
    "settings": {
      "onlyCategories": ["accessibility"]
    }
  }
}'
```

This will gather the predefined metrics defined by Lighthouse for that category:

```js
  // ...
  "audits": {
    "aria-command-name": {
      "title": "`button`, `link`, and `menuitem` elements have accessible names",
      "score": 1
      // ...
    },
    "aria-hidden-body": {
      "title": "`[aria-hidden=\"true\"]` is not present on the document `<body>`",
      "score": 1
      // ...
    },
    "aria-hidden-focus": {
      "title": "`[aria-hidden=\"true\"]` elements do not contain focusable descendents",
      "score": 1
      // ...
    },
    // ...
  }
```

## Gathering metrics from a single audit

You can get any specific metrics [valid in Lighthouse](https://github.com/GoogleChrome/lighthouse/tree/master/core/audits)

```json
{
  "url": "https://browserless.io",
  "config": {
    "extends": "lighthouse:default",
    "settings": {
      "onlyAudits": ["unminified-css"]
    }
  }
}
```

**cURL request**

```bash
curl --request POST \
  --url 'https://chrome.browserless.io/stats?token=MY_API_TOKEN' \
  --header 'Cache-Control: no-cache' \
  --header 'Content-Type: application/json' \
  --data '{
  "url": "https://browserless.io",
  "config": {
    "extends": "lighthouse:default",
    "settings": {
      "onlyCategories": ["accessibility"]
    }
  }
}'
```

**Response example**

```js
  // ...
  "audits": {
    "unminified-css": {
      "title": "Minify CSS",
      "score": 1
      // ...
    },
    // ...
  }
```

---

# /workspace API

URL: https://docs.browserless.io/baas/v1/apis/workspace

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/rest-apis/intro) or [BrowserQL](/browserql/start).

> **Info**
> If you're using the hosted service, please refer to our GraphQL "workspace" query for better documentation. We also collect workspace results across your entire fleet in GraphQL. See [https://api.browserless.io/graphql](https://api.browserless.io/graphql)

When dealing with both file downloads and uploads, Chrome needs access to the local file-system. In most docker-based environments this can be challenging since each container runs with its own file-system, unless you attach a network-mounted file system (and subsequently grant it access in your `docker run` command). Because of these challenges we've implemented a series of API's that make it easier to work with files on disk, under the `/workspace` route.

Check out this API schema defined in [Swagger](https://chrome.browserless.io/docs/#/Management%20API/post_workspace).

> **Info**
> The workspace API is only available for dedicated and self-hosted accounts

## Listing your files

To get files on your running container, simply issue a `GET` request to `/workspace`:

```bash
curl -X GET \
  https://chrome.browserless.io/workspace?token=MY_API_TOKEN
```

The following cURL will result in an output like:

```json
[
  {
    "created": "2019-07-15T22:11:40.157Z",
    "isDirectory": false,
    "name": "data.csv",
    "path": "/workspace/data.csv",
    "size": 52,
    "workspaceId": null
  }
]
```

Please note that this _will_ print any files in the current directory, but won't recursively look into them. It will also show files that Browserless itself hasn't had any interaction with (in cases where you assign `WORKSPACE_DIR` to a non-empty directory in docker). Because of this, **Browserless won't delete or remove any files automatically**, so take care that files are removed swiftly when no longer needed.

With this information returned, we can now download a file from the system.

## Downloading files

Once you know the name of the file, you can simply issue a `GET` request for that particular file. We currently don't support downloading of all files in an archived format, so you'll need to issue a download request for every file you want.

```bash
curl -X GET \
  https://chrome.browserless.io/workspace/file-one.csv?token=MY_API_TOKEN
```

Depending on the file, and its extension, Browserless will stream the file back. In this case since the file is a CSV file, it'll return a `text/csv; charset=UTF-8` Content-Type.

```csv
"Wednesday, October 10, 2018"
"Thursday, October 4, 2018"
"Monday, October 1, 2018"
"Wednesday, September 26, 2018"
"Monday, September 24, 2018"
"Wednesday, September 19, 2018"
"Monday, September 17, 2018"
"Wednesday, September 12, 2018"
```

## Saving files

If you need to have a file locally for Chrome to reference, then you'll need to upload it into Browserless for consumption. This is done by `POST`ing a requests to the `/workspace` API with a `multipart/form-data` Content-Type. This allows for uploading of _multiple_ files, and is more flexible to meet a variety of use-cases. Once uploaded, Browserless will return an array of file information based upon the content.

Please refer to your language or libraries documentation on how to handle `multipart/form-data` uploads. cURL, for instance, adds these headers automatically, as does many other libraries.

```bash
curl -X POST \
  https://chrome.browserless.io/workspace?token=MY_API_TOKEN \
  -F 'file=@/Users/browserless/Documents/file-one.csv'
```

Responses have the following output, and are always returned in an array:

```json
[
  {
    "fieldname": "file",
    "originalname": "file-one.csv",
    "encoding": "7bit",
    "mimetype": "text/csv",
    "destination": "/Users/jgriffith/Downloads",
    "filename": "file-one.csv",
    "path": "/Users/jgriffith/Downloads/file-one.csv",
    "size": 2649
  }
]
```

## Deleting files

> Be careful when deleting files as the operation cannot be undone

When work is complete, or you simply want to cleanup the workspace, you can issue a `DELETE` request for the file(s) you want to remove. Following upon our previous examples, you'll need to tell Browserless which file to delete:

```bash
curl -X DELETE \
  https://chrome.browserless.io/workspace/file-one.csv?token=MY_API_TOKEN
```

Once this is complete, Browserless will either respond with a `404` if the file isn't available, or a `204` if the operation was successful.

By default our dedicated plans automatically get their workspace contents deleted seven (7) days after creation.

## Scoping downloads

> Scoping downloads is available in version 1.11.0 of our docker images.

Often times workloads involve downloading the same files as previous sessions did, which can result in a filename conflict. You might also want to just extract _all_ the files that Chrome downloads in a particular session instead of a single file at a time. For instances such as these we recommend using a `trackingId` in your connect request.

`trackingID` treats the behavior of file downloads slightly differently. Instead of saving files in the default workspace directory, Browserless will instead create a sub-directory with the name of the `trackingId` you provide. For instance, if you connect with the following, and run some work, the result of the /workspace GET call will include files in a sub-directory.

```javascript
// Connect with a "trackingId" in the browserWSEndpoint:
puppeteer.connect({
  browserWSEndpoint:
    'wss://chrome.browserless.io?token=YOUR-API-TOKEN&trackingId=AwENCwMHDQoHBgwKBAUODQ',
});

// Do some work, download files, catch errors!
browser.close();
```

Now, with that session complete, a `GET /workspace` will reveal these contents:

```js
[
  {
    // Date of file creation
    created: "2019-07-15T22:11:40.157Z",

    // If the download is a directory
    isDirectory: false,

    // The resulting files name
    name: "data.csv",

    // The path to retrieve the file
    path: "/workspace/AwENCwMHDQoHBgwKBAUODQ/data.csv",

    // The size of the file in bytes
    size: 52,

    // The tracking-id that this file belongs to
    workspaceId: "AwENCwMHDQoHBgwKBAUODQ",
  },
];
```

Also, if you wish to get _all_ the files of a particular session, simply do a GET call with the sessions tracking ID in place. Following our previous example, that will look like:

```bash
curl -X GET \
  https://chrome.browserless.io/workspace/AwENCwMHDQoHBgwKBAUODQ?token=MY_API_TOKEN
```

This will return a ZIP archive of all the files downloaded from that particular session. Finally, you can do a DELETE call on that tracking-id to remove all files:

```bash
curl -X DELETE \
  https://chrome.browserless.io/workspace/AwENCwMHDQoHBgwKBAUODQ?token=MY_API_TOKEN
```

We strongly recommend using a `tracingId` that has a low chance of collision with another (like GUID or UUID). This prevents one session from overwriting another's request.

---

# Docker Configuration

URL: https://docs.browserless.io/baas/v1/docker/config

<!-- markdownlint-disable commands-show-output -->

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/enterprise/docker/config) or [BrowserQL](/browserql/start).

### All Options

- [Chrome information](#chrome-information)
- [Port](#port)
- [Max Concurrent Sessions](#max-concurrent-sessions)
- [Connection Timeout](#connection-timeout)
- [Max Queue Length](#max-queue-length)
- [Pre-booting Chrome](#pre-booting-chrome)
- [Pre-boot quantity](#pre-boot-quantity)
- [Demo Mode](#demo-mode)
- [Defining a host bind](#defining-a-host-bind)
- [Defining a workspace location](#defining-a-workspace-location)
- [Deleting workspace files automatically](#deleting-workspace-files-automatically)
- [Defining a time to delete workspace files](#defining-a-time-to-delete-workspace-files)
- [Disable the Debugger](#disabling-the-debugger)
- [Disable download behavior](#disable-download-behavior)
- [Securing your Instance](#securing-your-instance)
- [Logging](#logging)
- [Enable CORS](#enable-cors)
- [Enable XVFB](#enable-xvfb)
- [Exit on health failure](#exit-on-health-failure)
- [Persisting Metrics](#persisting-metrics)
- [Exposing Built-in Modules for `/function`](#exposing-built-in-modules-to-function)
- [Exposing External Modules for `/function`](#exposing-external-modules-to-function)
- [Keeping Chrome Alive](#keeping-chrome-alive)
- [Chrome Refresh Time](#chrome-refresh-time)
- [Single Run mode](#single-run-mode)
- [Default Block Ads](#default-block-ads)
- [Default Headless](#default-headless)
- [Default Launch Args](#default-launch-args)
- [Default Ignore Default Args](#default-ignore-default-args)
- [Default Ignore HTTPS](#default-ignore-https)
- [Default User Data Dir](#default-user-data-dir)
- [Disabling Routes](#disable-routes)
- [Enable API GET Calls](#enable-api-get)
- [Function Incognito Mode](#function-incognito-mode)
- [Pre-boot Chrome](#pre-booting-chrome)
- [Using a proxy](#using-a-proxy)

The Browserless docker container is highly-configurable, and accepts parameters through environment variables when starting. You can set parameters such as port, connection-timeout, queueing and more. Below is a description of each parameter, what they mean, and what they default to.

## Chrome information

Browserless automatically builds labels with each release regarding important information like the current browser version, the version of puppeteer that's supported, and even the debugger version protocol. You can access this information by running the command below:

```bash
$ docker inspect browserless/chrome
```

## Port

By default Browserless listens on port `3000`. You can configure this in docker by using it's port mapping functionality. As an example, let's say you want to have it listen port `8080`, you would run it like so:

```bash
$ docker run -p 8080:3000 --restart always -d --name browserless browserless/chrome
```

This should allow you some flexibility in how to run the image.

## Max Concurrent Sessions

Since running Chrome can be rather resource intensive you'll probably want to limit the number of concurrent sessions. This is, by default, set to 5 when not specified. Once the limit is reached then queueing begins to take place and requests will wait until more workers are ready.

To set this to a different number, say 10, issue your run like so:

```bash
$ docker run -e "MAX_CONCURRENT_SESSIONS=10" -p 3000:3000 --restart always -d --name browserless browserless/chrome
```

It's best to keep this limit small and grow it over time as it's easy to starve resources when running on restricted hardware.

## Connection Timeout

Connection timeout is a parameter that sets how long any session can run for. This is in place to prevent scripts that don't cleanup properly, or run into errors tha cause them to hang. The value of which can be set in milliseconds, and defaults to `30000`, or 30 seconds.

To allow more time for sessions, simply change it by setting `CONNECTION_TIMEOUT` when running.

> You can opt-out of timers by setting this value to `-1` for _no_ session timer. Be sure to close connections when not in use!

```bash
$ docker run -e "CONNECTION_TIMEOUT=60000" -p 3000:3000 --restart always -d --name browserless browserless/chrome
```

## Max Queue Length

This value determines how many items in the queue are allowed before requests are issued a `429` response code and closed. This mechanism is in place to prevent consumers from accidentally (or purposefully) triggering a denial-of-service. By default the image only allows a queue of 5 requests before beginning to fail more. As an example, if you have a `MAX_CONCURRENT_SESSIONS` of 5 and a `MAX_QUEUE_LENGTH` of 5, then 10 concurrent connections are allowed (5 running then 5 pending).

To adjust this value, simply set it via `MAX_QUEUE_LENGTH`.

```bash
$ docker run -e "MAX_QUEUE_LENGTH=10" -p 3000:3000 --restart always -d --name browserless browserless/chrome
```

## Pre-booting Chrome

> Prebooting is an experimental feature, and its usage is generally discouraged if your workers don't have a lot of resources or if you are going to perform resource-intensive tasks (PDF rendering, scrapping JS-heavy webpages, screenshotting).

You can, optionally, pre-boot Chrome and keep it in a pool of instances in order to cut-down on the boot time. To enable this, run your docker command as you normally would plus the `PREBOOT_CHROME=true` flag. Unless specified otherwise using `PREBOOT_QUANTITY`, `PREBOOT_CHROME` will create a swarm of browsers of lenght `MAX_CONCURRENT_SESSIONS`.

```bash
$ docker run -e "PREBOOT_CHROME=true" -p 3000:3000 --restart always -d --name browserless browserless/chrome
```

## Pre-boot quantity

> Prebooting is an experimental feature, and its usage is generally discouraged if your workers don't have a lot of resources or if you are going to perform resource-intensive tasks (PDF rendering, scrapping JS-heavy webpages, screenshotting).

By default, prebooting will create a swarm of browsers of lenght `MAX_CONCURRENT_SESSIONS`. `PREBOOT_QUANTITY` allows better resource management by setting the number of pre-booted and always-open browser instances. This can be useful if your Browserless instance has a large ceiling for concurrent connections but doesn't need as many always-open browsers.

If `PREBOOT_QUANTITY` gets evaluated as `0`, Browserless is going to use `MAX_CONCURRENT_SESSIONS` instead.

```bash
$ docker run -e "PREBOOT_CHROME=true" -e "PREBOOT_QUANTITY=5" -p 3000:3000 --restart always -d --name browserless browserless/chrome
```

## Demo Mode

If you want to serve the interactive debugger, but not allow it to handle `puppeteer.connect` calls, you can do so via the `DEMO_MODE` flag.

```bash
$ docker run -e "DEMO_MODE=true" -p 3000:3000 --restart always -d --name browserless browserless/chrome
```

## Defining a host bind

Browserless will, by default, bind to localhost when no host is provided. If you want to bind to another IP, or domain, then pass in a `HOST` variable to do so.

```bash
$ docker run -e "HOST=192.168.1.1" -p 3000:3000 --restart always -d --name browserless browserless/chrome
```

## Defining a workspace location

Browserless automatically has downloads from Chromium set to the `/tmp` directory inside the docker image. To change this, simply set the `WORKSPACE_DIR`:

```bash
$ docker run -e "WORKSPACE_DIR=~/downloads" -p 3000:3000 --restart always -d --name browserless browserless/chrome
```

## Deleting workspace files automatically

When downloads from Chrome are larger, it's easy for the docker image to fill it's disk-space if you forget to periodically delete files. Browserless has a mechanism to do so with `WORKSPACE_DELETE_EXPIRED`:

```bash
$ docker run -e "WORKSPACE_DELETE_EXPIRED=true" -p 3000:3000 --restart always -d --name browserless browserless/chrome
```

This will have Browserless delete files older than 30 days in the workspace, checking once a day. To change the length, simply set `WORKSPACE_EXPIRE_DAYS`.

## Defining a time to delete workspace files

When `WORKSPACE_DELETE_EXPIRED` is `true`, Browserless attempts to delete files older than 30 days in the workspace directory. To change this to something else, set a `WORKSPACE_EXPIRE_DAYS`:

```bash
$ docker run -e "WORKSPACE_DELETE_EXPIRED=true" -e "WORKSPACE_EXPIRE_DAYS=7" -p 3000:3000 --restart always -d --name browserless browserless/chrome
```

The example above will delete files older than 7 days.

## Disabling the debugger

If you want to disable the debugger (and all accompnaying HTML) you can set the `ENABLE_DEBUGGER` to `false` to only allow `puppeteer.connect` calls to succeed.

```bash
$ docker run -e "ENABLE_DEBUGGER=false" -p 3000:3000 --restart always -d --name browserless browserless/chrome
```

## Disable download behavior

By default, Browserless tells chromium to use a special directory in `/tmp` for storing files. If you want opt-out of this behavior, start the docker image with this flag:

```bash
$ docker run -e "DISABLE_AUTO_SET_DOWNLOAD_BEHAVIOR=true" -p 3000:3000 --restart always -d --name browserless browserless/chrome
```

## Securing your instance

If you're exposing your instance to the world, but don't want anyone to use it, you can optionally apply a `TOKEN` param that will restrict calls without a `token` query-string parameter. When present, Browserless will reject any calls that don't have a matching token.

```bash
$ docker run -e "TOKEN=2cbc5771-38f2-4dcf-8774-50ad51a971b8" -p 3000:3000 --restart always -d --name browserless browserless/chrome
```

Then, in your application use a query-parameter to pass that token in.

```js
const browser = await puppeteer.connect({
  browserWSEndpoint: 'ws://localhost:3000?token=2cbc5771-38f2-4dcf-8774-50ad51a971b8',
});
```

## Logging

Browserless uses the `debug` npm package to log output. By default, Browserless prints a minimal set of logs regarding sessions and other performance characteristics. To turn it off, simply set the DEBUG value to nothing:

```bash
$ docker run -e "DEBUG=-*" -p 3000:3000 --restart always -d --name browserless browserless/chrome
```

Since most other npm packages use this same module, you can set this flag to a wildcard `*` to see nearly everything that's happening internally:

NOTE: Be careful running with this command as you'll generate a lot of log output.

```bash
$ docker run -e "DEBUG=*" -p 3000:3000 --restart always -d --name browserless browserless/chrome
```

## Enable CORS

You can enable cross-origin-resource-sharing with Browserless by setting the `ENABLE_CORS=true` variable. This defaults to `false`:

```bash
$ docker run -e "ENABLE_CORS=true" -p 3000:3000 --restart always -d --name browserless browserless/chrome
```

## Enable XVFB

By default Browserless will take care of XVFB running automatically, which essentially means this is defaulted to `true`. However, in some hosting environments it's not possible to write to the appropriate folders to support XVFB and you should set this parameter to `false`.

> The screencast API depends on this setting to be `true` and will fail when turned off.

```bash
$ docker run -e "ENABLE_XVBF=false" -p 3000:3000 --restart always -d --name browserless browserless/chrome
```

## Exit on health failure

Browserless routinely checks on the health of the image as it's running. Sometimes it's helpful to have it restart automatically when CPU or Memory usage are above 100% for a period of time (by default 5 minutes). In order for Browserless to restart on health-failure checks, you'll have to set a parameter of `EXIT_ON_HEALTH_FAILURE=true`.

```bash
$ docker run -e "EXIT_ON_HEALTH_FAILURE=true" -p 3000:3000 --restart always -d --name browserless browserless/chrome
```

## Persisting Metrics

Browserless captures metrics while it's running, and exposes them via the `/metrics` endpoint. These, by default, don't persist anywhere _unless_ you tell Browserless where to persist them. When provided, the docker image will also attempt to read the metrics on startup and write to it periodically during execution.

> In this run command we're mounting the machines `/root` path so that the docker can access it at `/root`, be sure to change this to fit your use-case.

```bash
$ docker run -e "METRICS_JSON_PATH=/root/metrics.json" -v /root:/root -p 3000:3000 --restart always -d --name browserless browserless/chrome
```

## Exposing built-in modules to `/function`

The `/function` endpoint allows for user-submitted code to be ran inside the docker-image without the need for another compilation step. By default functions are _not_ allowed to require modules (both built-in's or externally). To enable Node's built-in modules you'll need to supply an array of modules available.

Here we're allowing both the `url` and `util` modules.

```bash
$ docker run -e "FUNCTION_BUILT_INS=[\"url\", \"util\"]" -p 3000:3000 --restart always -d --name browserless browserless/chrome
```

## Exposing external modules to `/function`

The `/function` endpoint allows for user-submitted code to be ran inside the docker-image without the need for another compilation step. By default functions are _not_ allowed to require modules (both built-in's or externally). To enable external modules you'll need to supply an array of modules available.

Here we're allowing both the `request` and `fetch` modules to be required in the `/function` endpoint.

```bash
$ docker run -e "FUNCTION_EXTERNALS=[\"request\", \"fetch\"]" -p 3000:3000 --restart always -d --name browserless browserless/chrome
```

## Keeping Chrome Alive

When the `PREBOOT_CHROME` flag is set, you can optionally keep Chrome "alive" after sessions are complete. This allows you to re-use Chrome instances without having to start and stop them (making for much faster execution). To enable this behavior, set the `PREBOOT_CHROME=true` and `KEEP_ALIVE=true`.

Browserless will close Chrome after 30 minutes to try and mitigate issues with Chrome consuming too many resources. This is configurable as well with the `CHROME_REFRESH_TIME` flag.

> When using KEEP_ALIVE Chrome will retain information about prior-sessions such as cookies and login contexts. **You'll also need to replace `browser.close()` with `browser.disconnect()` in your puppeteer code.**

```bash
$ docker run -e "PREBOOT_CHROME=true" -e "KEEP_ALIVE=true" -p 3000:3000 --restart always -d --name browserless browserless/chrome
```

## Chrome Refresh Time

When both `PREBOOT_CHROME` and `KEEP_ALIVE` are `true` Browserless keeps track of how long Chrome has been running, and will attempt to close it after a certain period. By default it will try and restart Chrome after 30 minutes. You can configure this behavior with the `CHROME_REFRESH_TIME` flag.

The `CHROME_REFRESH_TIME` flag accepts a value in milliseconds to keep Chrome running before it attempts to close it. Below we set this threshold to one hour.

```bash
$ docker run -e "PREBOOT_CHROME=true" -e "KEEP_ALIVE=true" -e "CHROME_REFRESH_TIME=3600000" -p 3000:3000 --restart always -d --name browserless browserless/chrome
```

## Single Run Mode

Some cloud providers let you run a docker image as a "function", where the container shuts down after all open connections close. The `SINGLE_RUN` option does the same thing. It shuts down the container after a single session completes.

> It's recommended to set MAX_CONCURRENT_SESSIONS to 1 and QUEUE_LENGTH to 0 if you want to run just a single session

```bash
$ docker run -e "SINGLE_RUN=true" -p 3000:3000 --restart always -d --name browserless browserless/chrome
```

## Default Block Ads

Starts all sessions with the ad-blocker already running (no need to use `?blockAds=true` in your API or puppeteer connect calls). If a session with `?blockAds=false` is started, that preference will take precedence, and the ad-blocker will be turned off.

```bash
$ docker run -e "DEFAULT_BLOCK_ADS=true" -p 3000:3000 --restart always -d --name browserless browserless/chrome
```

## Default Headless

Starts all sessions, that don't include a `?headless` flag in their API or connect call, with a pre-set headless behavior. When not set, Browserless sets all sessions to be headless by default.

If a session starts with a different `?headless` flag, that flag will take precedence over the pre-configured behavior.

```bash
$ docker run -e "DEFAULT_HEADLESS=false" -p 3000:3000 --restart always -d --name browserless browserless/chrome
```

## Default Launch Args

Allows for setting default launch args when none are present in an API or puppeteer.connect call. When a call is started with launch-arguments set, they take precedence over this default behavior.

```bash
$ docker run -e "DEFAULT_LAUNCH_ARGS=[\"--window-size=1920,1080\"]" -p 3000:3000 --restart always -d --name browserless browserless/chrome
```

## Default Ignore HTTPS

Tells Browserless to, by default, ignore or not ignore HTTPS errors on sites. If a sesions or API call comes starts with a different preference set, then it will override this default behavior.

```bash
$ docker run -e "DEFAULT_IGNORE_HTTPS_ERRORS=true" -p 3000:3000 --restart always -d --name browserless browserless/chrome
```

## Default Ignore Default Args

When set this will override any default arguments set by Browserless to Chrome's launch arguments. This can be a list of flags to ignore, or `true`/`false` to ignore all arguments. When a session is started with this flag set, it takes precedence over this default behavior.

```bash
$ docker run -e "DEFAULT_IGNORE_DEFAULT_ARGS=true" -p 3000:3000 --restart always -d --name browserless browserless/chrome
```

## Default User Data Dir

Sets a default directory to cache user data to (cookies, local-storage and more). This will force all sessions to use the same cache unless sessions themselves specify another path, which will override this behavior.

```bash
$ docker run -e "DEFAULT_USER_DATA_DIR=/tmp/my-profile" -p 3000:3000 --restart always -d --name browserless browserless/chrome
```

## Disable Routes

Allows for disabling routes by supplying a list of routes to disable:

```bash
$ docker run -e 'DISABLED_FEATURES=["pdfEndpoint", "contentEndpoint"]' -p 3000:3000 --restart always -d --name browserless browserless/chrome
```

## Enable API GET

Allows for enabling an expiremental GET-style operation, as opposed to POSTing bodies of code to run on our APIs. When enabled, you'll simply need to set a `body` query-string parameter, with a URL-encoded JSON string of the normal API body.

```bash
$ docker run -e 'ENABLE_API_GET=true' -p 3000:3000 --restart always -d --name browserless browserless/chrome
```

## Function Incognito Mode

When set, and `PREBOOT_CHROME` and `KEEP_ALIVE` are set, allows for generating a fresh page (incognito page) from a re-used browser. This is useful if you want to reuse a Chrome instance, but don't want to retain the cookies and other browser caches.

```bash
$ docker run -e FUNCTION_ENABLE_INCOGNITO_MODE=true -e KEEP_ALIVE=true -e PREBOOT_CHROME=true -p 3000:3000 --restart always -d --name browserless browserless/chrome
```

## Using a proxy

You'll often want to use a load-balancer, or reverse proxy, in front of Browserless for SSL termination or to load balance across a fleet of Browserless instances. In certain cases, Browserless does generate links (like in the /sessions API), in which case this link builder needs to be aware of how it should route sessions.

In order to handle this, you can specify three parameters: `PROXY_HOST`, `PROXY_PORT` and `PROXY_SSL` to Browserless. This will tell certain APIs to generate links with the prior information.

```bash
$ docker run -e "PROXY_HOST=browserless.my-domain.com" -e PROXY_PORT=443 -e PROXY_SSL=true -p 3000:3000 --restart always -d --name browserless browserless/chrome
```

Then, /sessions API calls will return properly formatted JSON resposnes:

```json
[
  {
    "description": "",
    "devtoolsFrontendUrl": "/devtools/inspector.html?wss=browserless.my-domain.com:443/devtools/page/C6962B3428FC8E42CDA6484AB5B57EAC",
    "id": "C6962B3428FC8E42CDA6484AB5B57EAC",
    "title": "Example Domain",
    "type": "page",
    "url": "https://www.example.com/",
    "webSocketDebuggerUrl": "wss://browserless.my-domain.com:443/devtools/page/C6962B3428FC8E42CDA6484AB5B57EAC",
    "browserId": "b519351a-355e-47d5-82cc-7c240cfa40f3",
    "browserWSEndpoint": "wss://browserless.my-domain.com:443/devtools/browser/b519351a-355e-47d5-82cc-7c240cfa40f3",
    "port": "42169",
    "trackingId": null
  }
]
```

---

# Extending the Docker Image

URL: https://docs.browserless.io/baas/v1/docker/extending-docker

<!-- markdownlint-disable commands-show-output -->

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/enterprise/docker/enterprise-image) or [BrowserQL](/browserql/start).

> For hosted accounts, if you want to use a third-party module please [get in touch!](https://www.browserless.io/contact)

Browserless includes a few extra packages that make it easy to get going with most of the use-cases we've seen. However if you're using the `/function` API, or other API's wherein you want to load npm modules, then you'll need to extend our image so that you can install these dependencies. The process if fairly easy in docker to do so.

## 1. Create your own Dockerfile

First, you'll want to write your own `Dockerfile`. In the example below, we're going to add the `aws-sdk` module.

```dockerfile
FROM browserless/chrome:latest

# Install the AWS SDK
RUN npm install aws-sdk
```

Feel free to change the `FROM` value here. It's generally a good practice to "pin" this version to value that won't change. For instance, if we want to use version [1.15.0](https://github.com/browserless/browserless/blob/v1/CHANGELOG.md#1150) of Browserless, and puppeteer `1.19.0`, we'd have done:

```dockerfile
FROM browserless/chrome:1.15.0-puppeteer-1.19.0

# Install the AWS SDK
RUN npm install aws-sdk
```

In either case, save your file and let's proceed.

## 2. Build the docker image

Once the you have the file saved, it's as simple as building the image.

```sh
$ docker build -t my/browserless:latest .
```

Here, we're using a "dumby" user of `my` and a repo of `browserless`, tagging it as the `latest. Feel free to change these however you see fit, especially if you want to push this built image into docker's public cloud or your own private image registry.

It'll likely take a few minutes to pull all the dependencies locally and build, however subsequent builds will be much quicker since they'll have a local cache.

## 3. Run the image

Now that you have a custom-built version you can apply all the same arguments to your start command (all environment variables and so on apply), you'll simply change the image you're running when you startup (note that we're whitelisting this new module, allowing Browserless to execute it for security purposes).

Here's an example of running the newly built image with the module whitelisted.

```sh
$ docker run -p 3000:3000 -e FUNCTION_EXTERNALS='["aws-sdk"]' my/browserless:latest
```

Now your calls to `/function` and others will have access to the `aws-sdk` module:

```sh
$ curl -X POST \
  http://localhost:3000/function \
  -H 'Content-Type: application/javascript' \
  -d 'const aws = require('\''aws-sdk'\'');

module.exports = async () => {
  const data = Object.keys(aws);

  return {
    data,
    type: '\''text/plain'\'',
  };
};'
```

That's it! If you need to add more simply install your modules in the `Dockerfile`, build the image, and run it!

---

# Docker Quickstart

URL: https://docs.browserless.io/baas/v1/docker/quickstart

<!-- markdownlint-disable no-bare-urls -->

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/enterprise/docker/enterprise-image) or [BrowserQL](/browserql/start).

The docker image that powers the core of Browserless is available for free for open-source projects. Using it is about as simple as using the Browserless service itself with the only difference being that you'll have to launch and manage the infrastructure.

If you're interested in reading about setting up a project, then checkout the [quick-start guide here](/).

At a high-level, we'll do the following:

1. Run the docker image
2. Update our app to connect to this image.

## 1. Run it with some sensible defaults

Running the docker image is similar to other `docker run` commands, and all options are passed in via environment variables. In the example below, we're starting with the image with a maximum concurrency of `10`, further requests will remain open and ran when a slot opens up.

After the image is pulled (one time only), then you'll see the first of our logs, which will print the final configuration.

```bash
$ docker run \
  --rm \
  -p 3000:3000 \
  -e "MAX_CONCURRENT_SESSIONS=10" \
  browserless/chrome:latest
```

Feel free to visit http://localhost:3000 to see if the image is running properly. You can also read more about the different variables on our [docker section](/baas/v1/docker/config).

## 2. Update your app to use Browserless

Once we have Browserless running we can simply use it inside of our application instead of launching it locally:

```javascript
const express = require('express');
const puppeteer = require('puppeteer-core');

const app = express();

app.get('/image', async (req, res) => {
  // This was puppeteer.launch()
  const browser = await puppeteer.connect({ browserWSEndpoint: 'ws://localhost:3000' });
  const page = await browser.newPage();

  await page.goto('http://www.example.com/');
  const data = await page.screenshot();
  browser.close();

  return res.end(data, 'binary');
});

app.listen(8080);
```

## What's next?

There's a lot more that you can configure and tune in Browserless to handle the needs of your application. Be sure to read about all the options it exposes and how to get the most out of the Browserless image!

---

# Docker Webhooks

URL: https://docs.browserless.io/baas/v1/docker/webhooks

<!-- markdownlint-disable commands-show-output -->

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/enterprise/docker/enterprise-image) or [BrowserQL](/browserql/start).

When launching the docker image you can configure it to "callback" to a web-based URL when certain events occur. For instance, you can have it call an HTTP URI when sessions are queued or rejected.

For each URL, when the associated event happens, Browserless will do a `GET` call on the provided URL. It doesn't listen to responses or failures, however it's a good idea to answer it back immediately to close any open connections.

Below is a list of all possible options. Be sure to check back often and see when more are added!

## Queuing

Setting a `QUEUE_ALERT_URL` will trigger the image to call a the specified URL when queuing begins to happen. You can set this to any HTTP URL as seen below:

```bash
$ docker run -e "QUEUE_ALERT_URL=https://my.website.com/queued" -p 3000:3000 --restart always -d --name browserless browserless/chrome
```

## Rejections

Setting a `REJECT_ALERT_URL` will trigger the image to call a the specified URL when rejections begins to happen. You can set this to any HTTP URL as seen below:

```bash
$ docker run -e "REJECT_ALERT_URL=https://my.website.com/rejected" -p 3000:3000 --restart always -d --name browserless browserless/chrome
```

## Timeouts

Setting a `TIMEOUT_ALERT_URL` will trigger the image to call a the specified URL when time-outs begins to happen. You can set this to any HTTP URL as seen below:

```bash
$ docker run -e "TIMEOUT_ALERT_URL=https://my.website.com/time-out" -p 3000:3000 --restart always -d --name browserless browserless/chrome
```

## Health Failure

Setting a `FAILED_HEALTH_URL` will trigger the image to call a the specified URL when the health of the instance is critical (over 99% on CPU and memory). You can use this event to potentially restart the image or replace it.

```bash
$ docker run -e "FAILED_HEALTH_URL=https://my.website.com/failed-health" -p 3000:3000 --restart always -d --name browserless browserless/chrome
```

## Need something else?

If you're looking for hooks for other events, please [let us know](https://www.browserless.io/contact).

---

# Best Practices

URL: https://docs.browserless.io/baas/v1/hosted-service/best-practices

<!-- markdownlint-disable no-emphasis-as-heading -->

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/baas/best-practices) or [BrowserQL](/browserql/start).

Aside from getting everything setup and running, there's a few best practices that'll help ensure your sessions are operating well. Below is a couple of highlights for running healthy headless scripts.

## Make sure you close your session

When finished with work, or on errors, make sure you run `browser.close` so that other sessions may start. Browserless _will_ terminate long-running sessions via the timeout setting, but it's always a good idea to close tidly whenever you're finished.

```js
const browser = await puppeteer.connect({
  browserWSEndpoint: `wss://chrome.browserless.io?token=YOUR-TOKEN-HERE`,
});

const page = await browser.newPage();

try {
  await page.goto('https://www.browserless.io/');
  await page.screenshot({ path: './browserless.png' });
  browser.close();
} catch (error) {
  console.error({ error }, 'Something happened!');
  browser.close();
}
```

## Reduce `await`'s as much as possible

Most of the puppeteer is async, meaning any command with `await` in front of it (or `.then`'s) is going to make a round-trip from your application to Browserless and back. While you can only do so much to limit this you should definitely try and do as much as possible. For instance, use `page.evaluate` over `page.$selector` as you can accomplish a lot in one `evaluate` versus multiple `$selector` calls.

**DON'T DO**

```js
const $button = await page.$('.buy-now');
const buttonText = await $button.getProperty('innerText');
const clicked = await $button.click();
```

**DO**

```js
const buttonText = await page.evaluate(() => {
  const $button = document.querySelector('.buy-now');
  const clicked = $button.click();

  return $button.innerText;
});
```

## Use the GraphQL API to do pre-session checks

If you've been getting rejections or queued, be sure to use the GraphQL API to do pre-session checks. This allows your workers to remain up and healthy, allowing sessions to go smoothly.

```graphql
# https://api.browserless.io/graphql
{
  pressure(apiToken: "YOUR-API-TOKEN") {
    running
    recentlyRejected
    queued
    isAvailable
    date
  }
}
```

This request will return a JSON object with the following payload:

```js
// JSON returned
{
  "data": {
    "pressure": {
      "running": 0,
      "recentlyRejected": 0,
      "queued": 0,
      "isAvailable": true,
      "date": 1524762532204
    }
  }
}
```

You can use the `isAvailable` boolean to check to see if the instance can handle more connections. Otherwise you're free to write any sort of custom check with the provided JSON response.

## Seen Improvements Yourself?

If you've seen improvements or have practices you think the community would benefit from, we'd [love to hear from you!](https://www.browserless.io/contact)

---

# Browserless GraphQL API

URL: https://docs.browserless.io/baas/v1/hosted-service/graphql-api

<!-- markdownlint-disable no-bare-urls -->
> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/enterprise/private-deployment/graphql-api) or [BrowserQL](/browserql/start).

Aside from providing the default `chrome.browserless.io` domain for running sessions, browserless also exposes a GraphQL API for other operations. This document goes over some critical queries.

> **Info**
> The graphql API only applies to Dedicated and Enterprise accounts.

## Pressure

The `pressure` query shows how much load your instance(s) are under, and whether they can accept more traffic or not. This is real-time, so if you need to check if your instance can take more traffic you can do a request to check prior to running your `puppeteer.connect` call.

> **Warning**
> The pressure queries are still in BETA and may experience breaking schema changes.

```graphql
# https://api.browserless.io/graphql
{
  pressure(apiToken: "YOUR-API-TOKEN") {
    running
    recentlyRejected
    queued
    isAvailable
    date
  }
}
```

This request will return a JSON object with the following payload:

```json
{
  "data": {
    "pressure": {
      "running": 0,
      "recentlyRejected": 0,
      "queued": 0,
      "isAvailable": true,
      "date": 1524762532204
    }
  }
}
```

## Metrics

The `metrics` query gives you insight to how your worker(s) are performing. It details things like successful, rejected, and timed-out sessions. Eventually this will be expanded to include things like average session-time and other helpful data-points.

Below is an example of a request for `metrics` and the responding payload.

> **Warning**
> The metrics queries are still in BETA and may experience breaking schema changes.

```graphql
# https://api.browserless.io/graphql
{
  metrics(apiToken: "YOUR-API-TOKEN") {
    successful
    rejected
    timedout
    queued
    cpu
    memory
    date
  }
}
```

This request returns an array of object detailing the metrics of your instance(s). If there's more than one instance stats are aggregated together in 5-minute intervals. CPU and Memory are averaged across instances.

```js
{
  "data": {
    "metrics": [
      {
        "successful": 0,
        "rejected": 0,
        "timedout": 0,
        "queued": 0,
        "cpu": 0.002734375,
        "memory": 0.9055320561641963,
        "date": 1524227700000
      },
      //...
    ]
  }
}
```

## Account Status

The `accountStatus` query provides information about your account's usage and worker status. This is particularly useful for:

- Checking remaining time on usage-based accounts
- Monitoring worker launch status on dedicated plans

```graphql
# https://api.browserless.io/graphql
{
  accountStatus(apiToken: "YOUR-API-TOKEN") {
    launching
    secondsAvailable
  }
}
```

This request returns an object with the following information:

```json
{
  "data": {
    "accountStatus": {
      "launching": false,
      "secondsAvailable": 3600
    }
  }
}
```

The response includes:

- `launching`: Boolean indicating if a new worker is being launched (for legacy dedicated plans)
- `secondsAvailable`: Number of seconds remaining on your account (for legacy usage-based accounts)

> **Info**
> This query is specifically for legacy accounts. For current account information, please refer to the [BaaS v2 documentation](/enterprise/private-deployment/graphql-api).

> **Warning**
> For security purposes, we limit the number of failed GraphQL request attempts. If you encounter rate limiting errors, you'll need to wait until the top of the hour before making additional requests.

---

# How it works

URL: https://docs.browserless.io/baas/v1/hosted-service/how-it-works

<!-- markdownlint-disable no-emphasis-as-heading -->

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/baas/session-management/standard-sessions) or [BrowserQL](/browserql/start).

> **Warning**
> Libraries that only allow a `host` and `port` option are not well supported by the Browserless service since you cannot properly send in API tokens. If you're looking to integrate into the hosted service, read more about our REST API's and the `/function` [endpoint here](https://www.browserless.io/blog/2018/05/26/browserless-functions/#browserless-functions).

Browserless works almost identically to how most libraries and web-drivers work when ran locally. There's no additional software you need to install on your production machines or complicated setup steps. As a matter of fact, the only thing you really need to when using the Browserless service is merely change where your code references Chrome.

Browserless runs Chrome in a cloud environment, and exposes most of the [Chrome remote protocol](https://chromedevtools.github.io/devtools-protocol/) to you. On top of exposing these commands, it also:

- Isolates your session from all others.
- Can run concurrent requests without interfering with others.
- Cleans up sessions after 30 seconds.
- Starts a clean copy of Chrome for each session.
- Restarts automatically if anything crashes.
- Queues requests if thresholds are met.

## How sessions work

### Via `browserWSEndpoint`

Libraries like puppeteer and chrome-remote-interface can hook into an existing Chrome instance by WebSocket. The hosted Browserless service only supports this type of interface since you can pass in tokens and other query-params. Typically you only need to replace how you start Chrome with a connect-like statement:

```js
// Connecting to Chrome locally
const browser = await puppeteer.launch();

// Connecting to browserless
const browser = await puppeteer.connect({
  browserWSEndpoint: 'wss://chrome.browserless.io/?token=YOUR_API_TOKEN',
});
```

After that your code should remain exactly the same.

### Via `host` and `port`

Many libraries will issue an HTTP request to one of the `/json` endpoints exposed by Chrome's dev protocol. When this request happens, Browserless will respond with the resulting payload to allow remote programs to interact with it.

If you're looking to use the Browserless service with non-Node language, it's better to use the REST API's and `/function` endpoint as Browserless _can_ run puppeteer code for you. Take a look at our blog post about this interface [here](https://www.browserless.io/blog/2018/05/26/browserless-functions/#browserless-functions).

**Introspection Request**

```bash
# curl https://chrome.browserless.io/json?token=YOUR_API_TOKEN_HERE
[
   {
      "description":"",
      "devtoolsFrontendUrl":"/devtools/inspector.html?ws=138.197.93.72:3000/devtools/page/da78a5e7-1db5-4d47-a2a5-07885088ad07",
      "id":"da78a5e7-1db5-4d47-a2a5-07885088ad07",
      "title":"about:blank",
      "type":"page",
      "url":"about:blank",
      "webSocketDebuggerUrl":"ws://138.197.93.72:3000/devtools/page/da78a5e7-1db5-4d47-a2a5-07885088ad07"
   }
]
```

The WebSocket endpoints are where commands from the protocol are emitted into, and Chrome will then emit responses back. Browserless does not modify or alter any of these messages. Once your session and underlying websocket are closed, Browserless will automatically clear that Target and session data.

---

# Load Balancers

URL: https://docs.browserless.io/baas/v1/hosted-service/load-balancers

<!-- markdownlint-disable no-emphasis-as-heading -->
> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/baas/infrastructure/load-balancers) or [BrowserQL](/browserql/start).

The Cloud Subscription plans, Dedicated plans and Enterprise plans have load balancing mechanisms built-in. This docs will provide further information on how they work on each plan. For Enterprise plans we provision you your very own load-balancer.

## Cloud subscriptions' load balancing

When you fire a request to our endpoint, be it `https://chrome.browserless.io` or `wss://chrome.browserles.io`, it will go through our load balancers and will be sent to one of our instances to be handled. We list our shared fleet's [IP addresses for whitelisting](https://www.browserless.io/docs/whitelist#current-ip-address-lists) purposes.

Our cloud-unit fleet is comprised of many machines running Browserless, and our load-balancer will automatically determine which machine to handle the request. If, say, a machine is too overloaded to handle the inbound connection then our load-balancer routes the request to another. In very plain terms we use a modified least-connected algorithm to ensure your request is handled appropriately.

## Dedicated workers' load balancing

Your worker can be located in 4 different sites: San Francisco, New York, Amsterdam, or London. If you request to `https://chrome.browserless.io` or `wss://chrome.browserles.io`, it'll travel to San Francisco then to your worker.

In order to improve the latency by having a worker located closer to your client, you'll want to use the load balancer closest to your machine, these are the load balancers available for you to point your traffic to:

1. West load balancer (SFO): `https://chrome-us-west.browserless.io`
2. East load balancer (NYC): `https://chrome-us-east.browserless.io`
3. UK load balancer (LON): `https://chrome-eu-uk.browserless.io`
4. AMS load balancer (AMS): `https://chrome-eu-ams.browserless.io`

### Multiple worker load balancing

When you have multiple instances, it'll automatically load-balance amongst them by least connections. If you want to handle load-balancing on your end, you can get the endpoint for every individual worker by clicking on the URL in the "Address" Column in the Production Workers table available in your [account dashboard](https://cloud.browserless.io/account/).

---

# Proxies

URL: https://docs.browserless.io/baas/v1/hosted-service/proxying

<!-- markdownlint-disable no-emphasis-as-heading no-space-in-code no-trailing-punctuation-->
> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/baas/bot-detection/proxies) or [BrowserQL](/browserql/start).

If you need to use proxies with v1, you can use external/third-party proxies. See our documentation on [using external proxies](/baas/v1/recipes/using-a-proxy) for more information.

> **Info**
> Our [in-built residential](/baas/bot-detection/proxies) proxies are only available on Browserless V2, which is available in production via three domains: `production-sfo.browserless.io`, `production-lon.browserless.io`, and `production-ams.browserless.io`.

---

# Browserless Terminology

URL: https://docs.browserless.io/baas/v1/hosted-service/terminology

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/enterprise/terminology) or [BrowserQL](/browserql/start).

On the main browserless.io site, and inside this documentation site, you'll notice we use a few terms like "worker" or "session". Below is a list of commonly-used terms and what they mean.

## Worker

A worker is an instance of the Browserless container, running on a virtual-machine. The size of the worker will dictate what types of sites and interactions you can do. For Browserless Enterprise customers, we go through an onboarding process where we help pick the right size and class of machine in order to properly support your workloads.

## Session

A session is anytime Chrome is actively running and connected to. Browserless, however, automatically queues up to 2X this amount without any changes in your app's code for _Enterprise plans_ only. Our cloud-unit fleet is provisioned such that you shouldn't ever encounter a sluggish connection. This allows you to set a baseline of traffic throughput, but allows for "bursts" of traffic without having to adjust any of your code (just keep using `puppeteer.connect`).

If more sessions are needed, [please reach out to us](https://www.browserless.io/contact).

## Pressure

> **Info**
> The Pressure API is only available for Enterprise plans that run on a dedicated fleet of machines.

Pressure indicates how much traffic is going on in your workers at any given time. Using the GraphQL API, you can fetch important details like `isAvailable` to see if your workers can handle another connection. Doing so will ensure your account doesn't get overloaded and become unresponsive.

```graphql
# https://api.browserless.io/graphql
{
  pressure(apiToken: "YOUR_API_KEY") {
    running
    recentlyRejected
    queued
    isAvailable
    date
  }
}
```

This request will return a JSON object with the following payload:

```js
// JSON returned
{
  "data": {
    "pressure": {
      "running": 0,
      "recentlyRejected": 0,
      "queued": 0,
      "isAvailable": true,
      "date": 1524762532204
    }
  }
}
```

## More Coming

Did we miss something? [Send us an email and let us know!](https://www.browserless.io/contact)

---

# Using your API token

URL: https://docs.browserless.io/baas/v1/hosted-service/token

<!-- markdownlint-disable no-emphasis-as-heading no-space-in-code -->
> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/baas/start) or [BrowserQL](/browserql/start).

When you sign-up for a Browserless account, we create a unique token that allows you to interact with the service. Once your worker(s) are ready you should use this token anytime you interact with the service.

You can use this token with most of our integrations by simply appending a `?token=YOUR-API-TOKEN` as a query-string parameter. **The only exception is the webdriver integration as most libraries strip query-parameters for remote servers**. For webdriver connections, use the format `https://YOUR-API-TOKEN@chrome.browserless.io/webdriver`.

For the purposes of illustrating these examples, we'll assume your API-TOKEN is `094632bb-e326-4c63-b953-82b55700b14c`.

## Example integrations

### Puppeteer with your API Token

```js
const browser = puppeteer.connect({
  browserWSEndpoint: 'wss://chrome.browserless.io?token=094632bb-e326-4c63-b953-82b55700b14c',
});
```

### REST with your API Token

#### Query parameter

The basic way to send your API token over HTTP, is to simply add it as a query parameter:

```bash
curl -X POST \
  https://chrome.browserless.io/content?token=094632bb-e326-4c63-b953-82b55700b14c \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/json' \
  -d '
{
  "url": "https://example.com/"
}'
```

#### Header Authentication

You can also add an `Authorization` header for REST API requests as well. Browserless uses the `Basic` auth scheme to get the token:

> **Warning**
> As per [the HTTP specification](https://httpwg.org/specs/rfc9110.html#authentication), the token **must** be encoded in base64, otherwise you can get `4XX` or `5XX` responses.

```bash
curl -X POST \
  https://chrome.browserless.io/content \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Basic MDk0NjMyYmItZTMyNi00YzYzLWI5NTMtODJiNTU3MDBiMTRj' \
  -d '
{
  "url": "https://example.com/"
}'
```

### Webdriver with your API Token

> **Warning**
> Webdriver and Selenium are deprecated in Browserless V2 and beyond, and will eventually be removed altogether from our cloud.

```js
const chromeCapabilities = webdriver.Capabilities.chrome();
chromeCapabilities.set('browserless:token', '094632bb-e326-4c63-b953-82b55700b14c');
chromeCapabilities.set('goog:chromeOptions', {
  args: [
    '--disable-background-timer-throttling',
    '--disable-backgrounding-occluded-windows',
    '--disable-breakpad',
    '--disable-component-extensions-with-background-pages',
    '--disable-dev-shm-usage',
    '--disable-extensions',
    '--disable-features=TranslateUI,BlinkGenPropertyTrees',
    '--disable-ipc-flooding-protection',
    '--disable-renderer-backgrounding',
    '--enable-features=NetworkService,NetworkServiceInProcess',
    '--force-color-profile=srgb',
    '--hide-scrollbars',
    '--metrics-recording-only',
    '--mute-audio',
    '--headless',
    '--no-sandbox',
  ],
});

const driver = new webdriver.Builder()
  .forBrowser('chrome')
  .withCapabilities(chromeCapabilities)
  .usingServer('https://chrome.browserless.io/webdriver')
  .build();
```

---

# Versions on the usage based service

URL: https://docs.browserless.io/baas/v1/hosted-service/versions

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/baas/versions) or [BrowserQL](/browserql/start).

> **Info**
> This information applies for users that use API tokens on the [cloud unit-based or usage-based plan](https://www.browserless.io/pricing).

Our workers that service the usage-based API tokens are updated every so often. Here are the versions that the hosted version currently has for certain libraries, global timeout, and CLI flags supported.

## Current versions used in usage based workers

Last update: November 13th, 2023.

1. puppeteer version: `21.4.1`
2. chromedriver: `118.0.5993.70`
3. playwright-core: `1.33.0`, `1.34.3`, `1.35.1`, `1.36.2`, `1.37.1`, and `1.38.1`

> **Info**
> If your code is mysteriously crashing, try using the exact version mentioned above, as some older but especially more recent versions than the above may cause conflicts.

## Global timeout

By default, the global timeout is set to 15 minutes for usage-based and cloud unit-based API Keys, if you need your sessions to run longer than that, you'll want to consider upgrading to a dedicated account in order to modify this setting.

## CLI flags supported

```text
--proxy-server,
--window-size,
--disable-web-security,
--enable-features,
--disable-web-security,
--disable-setuid-sandbox,
--lang,
--font-render-hinting,
--force-color-profile,
stealth,
ignoreDefaultArgs,
headless,
token,
blockAds,
ignoreHTTPSErrors,
slowMo,
```

## Playwright's versioning

It is important to note that Playwright's [`connect` method](https://playwright.dev/docs/api/class-browsertype#browser-type-connect) is tightly version-coupled. Your client version must be either `1.33.0`, `1.34.3`, `1.35.1`, `1.36.2`, `1.37.1`, or `1.38.1`

Playwright's `connectOverCDP` doesn't always need to match the same playwright version on our usage based workers. This method is generally backwards/forwards compatible since it communicates directly through CDP, with the caveat to be slightly slower due to having more communication over the devtools protocol.

---

# Whitelisting the IP addresses of the shared fleet

URL: https://docs.browserless.io/baas/v1/hosted-service/whitelist

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/baas/troubleshooting/whitelisting-ips) or [BrowserQL](/browserql/start).

If you restrict access to your infrastructure, you can whitelist our service through IP address to allow communicating to your systems.

## Dedicated plans

Your worker's IP addresses are listed under the Production Worker's section in the Public IP column.

## Cloud-subscription plans (Shared fleet)

The table below lists the most recent IP addresses from our shared fleet for you to whitelist.

The workers that service the cloud-subscription plans may change every few months, since we add more workers according to the demand increase, and also when there are maintenance tasks that require relaunching.

## Current IP Address lists

Below is the most recent list of IP addresses on our shared-fleet.

Date of last update: March/28/2025.

| IP address      |
| --------------- |
| 146.190.53.75   |
| 146.190.53.72   |
| 146.190.175.184 |
| 147.182.250.161 |
| 164.92.125.142  |
| 164.92.125.134  |
| 146.190.143.216 |
| 146.190.127.114 |
| 146.190.143.212 |
| 24.199.123.163  |
| 24.199.111.199  |
| 146.190.159.230 |
| 146.190.159.176 |
| 24.199.103.126  |
| 159.223.205.248 |
| 137.184.226.226 |
| 146.190.159.171 |
| 24.144.81.223   |
| 137.184.235.213 |
| 146.190.139.82  |
| 164.92.125.23   |
| 159.223.205.48  |
| 146.190.159.67  |
| 164.92.125.19   |
| 146.190.130.128 |
| 137.184.179.6   |
| 137.184.178.146 |
| 164.92.109.217  |
| 164.92.109.12   |
| 143.198.129.119 |
| 165.232.140.251 |
| 24.199.105.203  |
| 143.198.111.152 |
| 143.198.99.228  |
| 24.144.94.186   |
| 146.190.53.55   |
| 164.90.145.42   |
| 143.198.99.72   |
| 146.190.53.52   |
| 144.126.211.154 |
| 143.244.181.136 |
| 146.190.57.39   |
| 144.126.220.231 |
| 143.198.131.227 |
| 143.244.188.238 |
| 147.182.203.53  |
| 146.190.143.211 |
| 146.190.131.136 |
| 24.199.115.166  |
| 137.184.12.124  |
| 209.38.79.187   |
| 209.38.68.13    |
| 209.38.74.36    |
| 164.92.95.6     |
| 209.38.75.204   |
| 209.38.67.232   |
| 209.38.70.84    |
| 209.38.73.120   |
| 143.198.129.120 |
| 143.198.136.242 |
| 146.190.114.39  |
| 146.190.118.62  |
| 146.190.59.199  |
| 146.190.116.90  |
| 146.190.34.116  |
| 64.23.239.117   |
| 64.23.238.239   |
| 64.23.235.163   |
| 64.23.238.31    |
| 64.23.238.124   |
| 64.23.232.196   |
| 64.23.234.46    |
| 164.90.159.169  |
| 164.90.155.253  |
| 64.23.235.171   |
| 64.23.215.208   |
| 64.23.167.192   |
| 64.23.215.190   |
| 137.184.3.181   |
| 143.198.109.32  |
| 146.190.44.51   |
| 161.35.229.21   |
| 161.35.229.77   |
| 161.35.237.149  |
| 161.35.233.103  |
| 134.209.180.180 |
| 134.209.189.197 |
| 134.209.188.237 |
| 134.209.186.0   |
| 134.209.187.201 |
| 134.209.186.65  |
| 209.97.186.36   |
| 167.172.61.214  |
| 46.101.92.250   |
| 46.101.87.212   |
| 134.209.25.165  |
| 134.209.25.209  |
| 134.209.25.215  |
| 134.209.17.68   |
| 134.209.17.100  |
| 134.209.25.201  |
| 134.209.17.116  |
| 134.209.25.53   |
| 144.126.204.196 |
| 134.209.25.97   |
| 152.42.138.139  |
| 157.245.73.238  |
| 167.71.77.118   |
| 167.71.74.138   |
| 157.245.70.98   |

---

# Worker settings

URL: https://docs.browserless.io/baas/v1/hosted-service/worker-settings

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/enterprise/private-deployment/worker-settings) or [BrowserQL](/browserql/start).

> **Info**
> This information applies for users that have Dedicated or Enterprise worker(s) in the capacity-based plan.

To customize your worker settings, log in to your [browserless account](https://cloud.browserless.io/account/) and click on the the gear icon in the Production Workers tab.
There are several settings that you can tweak for your dedicated workers in the popup that appears.

## Puppeteer version and global timeout

The first two dropdown fields will allow you to pick the chrome/puppeteer version and the global timeout.

### Supported chrome/puppeteer versions

By default, you will have the latest version. If a newer version is released, we notify through email and your workers will update automatically the next time your workers are relaunched or restarted. If you want to pin your workers to a specific version, you'll have to select one of these versions from the dropdown.

### Global timeout

Once the duration of your session reaches the global timeout, it will be terminated.

If your session runs into an exception and your script doesn't handle it properly, your browser process might not exit properly, causing it to hang and remain open, which leads to consuming unnecessary resources, until it reaches the global timeout. It's always a best practice to [implement exception handling in your scripts](https://hackernoon.com/7-tips-to-making-your-puppeteer-scripts-more-bulletproof) to avoid this.

## Concurrency and Queue

The max concurrency and queue can be modified by using the slider.

### Concurrency tips

If you experience many timeouts/errors and rarely see queued sessions, your worker may be running out of resources and your worker is getting gridlocked. You can limit the max concurrency so that sessions don't run out of resources and hit the queue faster.

### Queue tips

If you use an external queueing system, you'll want to set your queue to 0, so that we return an error immediately after your concurrency limit has been reached. Bear in mind this feature may not be 100% flawless, due to the async implementation, a request may slip through the cracks if two requests are being received by the server exactly at the same time, so don't panic if a request gets queued even when set to 0.

## Session health checks

If this feature is checked on, your worker will check the health of your worker before a session connects, be it an api call or library call; if the load balancer determines your worker(s) can't handle another request, it will throw a 5xx response code. Disabling this will send the requests to your worker blindly, forcing it to take the request independently of the health of the worker.

## Stealth mode

This feature implements puppeteer-extra-plugin-stealth, which injects user behavior to your page, helping your sessions avoid basic bot detecting techniques.

## Experimental features

The following features are experimental, which means they are to be used with caution, since they can have caveats you should consider before enabling them.

### Pre-boot chrome

This feature will launch browser instances and keep them in a pool of instances in order to cut-down on the boot time. Read more about [pre-booting chrome](https://www.browserless.io/docs/docker.html#pre-booting-chrome).

> **The caveat to this feature is that it may impact performance, since there will be more browsers open than necessary at a given point, which take up resources, hence reducing the available resources for your sessions that are actually running.**
> 

### Enable GET APIs

This feature allows you to enable special /GET handlers for our APIs. This is handy for cases where you need to directly call our services from a cache-layer like a CDN. Read more information on the [/GET APIs](https://www.browserless.io/docs/api-get).

> **Please do not call the hosted service from the browser as it will reveal your secret token!**
> 

### Automatic health restarts

This feature will restart your worker automatically when CPU or Memory usage are above 100% for a period of time (by default 5 minutes). Read more about [exiting on health failure](https://www.browserless.io/docs/docker.html#exit-on-health-failure)

---

# Capybara

URL: https://docs.browserless.io/baas/v1/libraries/capybara

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/baas/start) or [BrowserQL](/browserql/start).

You can now use the `Capybara` library with Browserless! Similar to most libraries based on the webdriver protocol, you simply need to tell `Capybara` _where_ to talk with Chrome. This is a pretty quick change to make:

```rb
require 'capybara'
require 'selenium-webdriver'

# Done for demo purposes here!
Capybara.server = :puma, { Silent: true }

Capybara.register_driver :remote_chrome do |app|
  Capybara::Selenium::Driver.new(app,
    :browser              => :remote,
    :url                  => 'https://YOUR_API_TOKEN_HERE@chrome.browserless.io/webdriver',
    :options => Selenium::WebDriver::Remote::Capabilities.chrome("goog:chromeOptions" => {
      "args" => [
        "--headless",
        "--no-sandbox"
      ]
    }),
  )
end

Capybara.default_driver = :remote_chrome

browser = Capybara.current_session
browser.visit 'https://browserless.io/'
puts browser.html
browser.driver.quit

```

This will simply log out the HTML from the Example demo, however it'll give you a great starting place for using Capybara alongside Browserless.

Be sure to [let us know if you have questions or issues](https://www.browserless.io/contact).

## Pausing for the debugger

You can use the built-in `desired_capabilities` parameter in Selenium's API to tell Browserless to pause your script on startup, allowing you to visit the account page and click the Session link to view it as it runs. This is helpful if your script executes extremely quickly, making debugging harder to do.

```rb
# Tell browserless to pause on startup:
Capybara.register_driver :remote_chrome do |app|
  Capybara::Selenium::Driver.new(app, {
    :browser              => :remote,
    :url                  => 'https://YOUR_API_TOKEN_HERE@chrome.browserless.io/webdriver',
    :desired_capabilities => Selenium::WebDriver::Remote::Capabilities.chrome(
      "goog:chromeOptions" => { "args" => [ "--headless", "--no-sandbox" ] },
      # PAUSE!
      "browserless.pause": true
    ),
  })
end
```

## Specifying a different timeout

You can also override the global timeout for sessions by specifying a `browserless.timeout` capability as well:

```rb
# Set timeout to 30 seconds
Capybara.register_driver :remote_chrome do |app|
  Capybara::Selenium::Driver.new(app, {
    :browser              => :remote,
    :url                  => 'https://YOUR_API_TOKEN_HERE@chrome.browserless.io/webdriver',
    :desired_capabilities => Selenium::WebDriver::Remote::Capabilities.chrome(
      "goog:chromeOptions" => { "args" => [ "--headless", "--no-sandbox" ] },
      # 30 seconds in milliseconds
      "browserless.timeout": 30000
    ),
  })
end
```

---

# Go (chromedp)

URL: https://docs.browserless.io/baas/v1/libraries/go-cdp

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/baas/libraries/chromedp) or [BrowserQL](/browserql/start).

We're happy to offer beta support of the excellent Go library [chromedp](https://pptr.dev/api/puppeteer.cdpsession)!

Usage of the library remains similar to how you'd run your application locally, simply use the `RemoteAllocator` to connect to Browserless API.

An example of navigating to the Example.com site, and getting the title, is listed below as a jumping off point:

```go
package main

    "context"
    "flag"
    "log"

    "github.com/chromedp/chromedp"
)

func main() {
    devtoolsWsURL := flag.String("devtools-ws-url", "wss://chrome.browserless.io?token=****", "DevTools WebSsocket URL")
    flag.Parse()

    allocatorContext, cancel := chromedp.NewRemoteAllocator(context.Background(), *devtoolsWsURL,chromedp.NoModifyURL)
    defer cancel()

    ctx, cancel := chromedp.NewContext(allocatorContext)
    defer cancel()

    var title string
    if err := chromedp.Run(ctx,
        chromedp.Navigate("https://example.com"),
        chromedp.Title(&title),
    ); err != nil {
        log.Fatalf("Failed getting title of example.com: %v", err)
    }

    log.Println("Got title of:", title)
}

```

Be sure to [let us know if you have questions or issues](https://www.browserless.io/contact).

---

# Java (Selenium)

URL: https://docs.browserless.io/baas/v1/libraries/java

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/baas/start) or [BrowserQL](/browserql/start).

Browserless now supports the Selenium library for Java, provided by OpenQA! Similar to all our selenium integrations you can specify your authorization token by specifying an additional capability of `browserless:token`. You'll also want to ensure `--headless` and `--no-sandbox` are set as well.

Here's a small example:

```java

public class Browserless {
  public static void main() throws MalformedURLException {

    final ChromeOptions chromeOptions = new ChromeOptions();

    chromeOptions.addArguments("--no-sandbox");
    chromeOptions.addArguments("--headless");
    chromeOptions.addArguments("--disable-dev-shm-usage");
    chromeOptions.setCapability("browserless:token", "YOUR-API-TOKEN");

    WebDriver driver = new RemoteWebDriver(
        new URL("https://chrome.browserless.io/webdriver"),
        chromeOptions
    );

    driver.get("https://www.example.com");
    driver.quit();
  }
}
```

This will simply navigate to the Example website, however it'll give you a great starting place for using Java Selenium alongside Browserless.

Be sure to [let us know if you have questions or issues](https://www.browserless.io/contact).

---

# PHP (cURL)

URL: https://docs.browserless.io/baas/v1/libraries/php

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/baas/libraries/php) or [BrowserQL](/browserql/start).

Browserless can run in PHP by using cURL.
Here's a small example:

```php
<?php

$curl = curl_init();

curl_setopt_array($curl, [
  CURLOPT_URL => "https://chrome.browserless.io/screenshot?token=xxxxxx",
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_ENCODING => "",
  CURLOPT_MAXREDIRS => 10,
  CURLOPT_TIMEOUT => 30,
  CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
  CURLOPT_CUSTOMREQUEST => "POST",
  CURLOPT_POSTFIELDS => "{\n  \"url\": \"https://example.com/\",\n\t\"options\": {\n\t\t\"fullPage\": true,\n\t\t\"encoding\": \"base64\"\n\t}\n}",
  CURLOPT_HTTPHEADER => [
    "Content-Type: application/json"
  ],
]);

$response = curl_exec($curl);
$err = curl_error($curl);

curl_close($curl);

if ($err) {
  echo "cURL Error #:" . $err;
} else {
  echo $response;
}
```

This will simply take a screenshot of the Example website and return it encoded in base64 format, however it'll give you a great starting place for using our APIs through cURL in PHP.

Be sure to [let us know if you have questions or issues](https://www.browserless.io/contact).

---

# Playwright

URL: https://docs.browserless.io/baas/v1/libraries/playwright

<!-- markdownlint-disable link-fragments no-emphasis-as-heading -->

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/baas/start) or [BrowserQL](/browserql/start).

`playwright` is a new cross-browser library written by Microsoft to aide in cross-browser testing and development. This page helps you get started quickly connecting remotely to Browserless instead of launching browsers locally. You can find more detailed documentation on [playwright's documentation site](https://playwright.dev/docs/api/class-browsertype#browser-type-connect-over-cdp).

We support playwright in several languages:

- [Javascript](#javascript-playwright)
- [Java](#java-playwright)
- [C#.NET](#cnet-playwright)
- [Python](#python-playwright)

> NOTE: To avoid errors with no apparent reason, please make sure your playwright version is compatible with one of these [versions.](https://www.browserless.io/docs/versions#playwrights-versioning).

## Javascript playwright

We support `playwright` for Javascript out of the box via their `pw.chromium.connect` method. As of now, we only support their `chromium` option, but we're working on other browsers as well.

Browserless supports two different methods for connecting via playwright, each with its own benefits and drawbacks. In short, there are two API methods that we support:

### `playwright.chromium.connect(wsEndpoint: string)`

The standard `connect` method uses playwright's built-in browser-server to handle the connection. This, generally, is a faster and more fully-featured method since it supports most of the playwright parameters (such as using a proxy and more). However, since this requires the usage of playwright in our implementation, things like ad-blocking and stealth aren't supported. In order to utilize those, you'll need to see our integration with `connectOverCDP`.

> NOTE: If you are using a usage-based or cloud unit-based API token, your playwright version must match one of our supported versions, read more info on [playwright compatible versions.](https://www.browserless.io/docs/versions)

**Take a screenshot in playwright**

```js
const pw = require('playwright');

(async () => {
  const browser = await pw.chromium.connect(
    'wss://chrome.browserless.io/playwright?token=YOUR-API-TOKEN'
  );
  const context = await browser.newContext();
  const page = await context.newPage();

  await page.goto('https://www.example.com/');
  await page.screenshot({ path: 'example.png' });

  await browser.close();
})();
```

### `playwright.chromium.connectOverCDP(wsEndpoint: string)`

The "connectOverCDP" endpoint allows playwright to connect through Chrome's DevTools Protocol. While this is more functionally similar to how puppeteer operates, it does come with a slight performance hit since sessions are more "chatty" over the network versus playwright's `connect`. Because it's similar to puppeteer, you can use one of our built-in helpers like ad-blocking or stealth to your session. If you're wanting to use playwright's proxy helper then you'll need to use the `connect` method instead.

**Take a screenshot in playwright**

```js
const pw = require('playwright');

(async () => {
  const browser = await pw.chromium.connectOverCDP(
    'wss://chrome.browserless.io?token=YOUR-API-TOKEN'
  );
  const context = await browser.newContext();
  const page = await context.newPage();

  await page.goto('https://www.example.com/');
  await page.screenshot({ path: 'example.png' });

  await browser.close();
})();
```

## Java playwright

We support `playwright`for Java out of the box via their `playwright.chromium().connectOverCDP` method. As of now, we only support their `chromium` option, but we're working on other browsers as well.

Here's a simple snippet of its implementation.

**Scrape a site title with playwright**

```java

 public class Main {
   public static void main(String[] args) {
    try (Playwright playwright = Playwright.create()) {
      BrowserType chromium = playwright.chromium();
      Browser browser = playwright.chromium().connectOverCDP("wss://chrome.browserless.io?token=YOUR-API-TOKEN");
      Page page = browser.newPage();
      page.navigate("https://example.com");
      System.out.println(page.title());
      browser.close();
    }
     catch(Exception exception){
       System.out.println(exception);
     }
  }
}
```

### Alternate method that allows using authenticated proxies

Here's a slightly more complex way to instantiate the page through a context. This technique is useful because we can apply options to the context, such as a proxy.

**Use a proxy with remote playwright**

```java

public class Main {
  public static void main(String[] args) {
    try {
      Playwright playwright = Playwright.create();
      Browser browser = playwright.chromium().connectOverCDP("wss://chrome.browserless.io?token=YOUR-API-TOKEN");
      var context = browser.newContext(new Browser.NewContextOptions()
        .setProxy(new Proxy("https://YOUR-PROXY-SERVER-DOMAIN:PORT")
          .setUsername("proxyUsername")
          .setPassword("proxyPassword")));

      Page page = context.newPage();
      page.navigate("https://www.whatismyip.com/", new Page.NavigateOptions()
                    .setWaitUntil(WaitUntilState.DOMCONTENTLOADED));
      String myIP = page.innerHTML(".the-ipv4");
      System.out.println(myIP);
      browser.close();
    } catch (Exception exception) {
      exception.printStackTrace();
    }
  }
}
```

## C#.NET Playwright

We support `playwright`for C#.NET out of the box via their `playwright.Chromium.connectOverCDPAsync` method. As of now, we only support their `chromium` option, but we're working on other browsers as well.

**Take a screenshot in playwright**

```cs
using Microsoft.Playwright;
using System.Threading.Tasks;
using System;

class Program
{
    public static async Task Main()
    {
        try{
          using var playwright = await Playwright.CreateAsync();
          await using var browser = await playwright.Chromium.ConnectOverCDPAsync("wss://chrome.browserless.io?token=YOUR-API-TOKEN");
          var page = await browser.NewPageAsync();
          await page.GotoAsync("https://example.com/", new PageGotoOptions { WaitUntil = WaitUntilState.NetworkIdle });
          await page.ScreenshotAsync(new PageScreenshotOptions { Path = "example.png" });
        }
        catch (Exception e){
        Debug.WriteLine(e.ToString());
      }
    }
}
```

## Python playwright

We support `playwright`for Python out of the box via their `playwright.chromium().connect_over_cdp` method.

Here's a simple snippet of its implementation, we can add additional features through the context, such as a proxy, shown below as well, feel free to remove that if you're not going to use one.

Find more of the [playwright documentation for python here.](https://playwright.dev/python/docs/api/class-browsercontext)

```py
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
  browser = p.chromium.connect_over_cdp('wss://chrome.browserless.io?token=your_token')
  context = browser.new_context()
  page = context.new_page()
  page.goto('http://www.example.com',wait_until='domcontentloaded')
  print(page.content())
  context.close()
```

**Use a proxy with remote playwright in Python**

Simply add the proxy parameter to the `new_context` as shown below.

```py
  context = browser.new_context(
    proxy={"server": "http://proxyprovider.com:porthere", "username": "yourusername", "password": "yourpassword"}
  )
```

You can use all the same APIs and helper functions directly with Browserless, and begin to scale out your `playwright` applications and tests today.

What's more, [is that you can use all of the same launch arguments for puppeteer](/baas/v1/options/chrome-flags), directly with `playwright` and `connectOverCDP`!

---

# Python (Playwright)

URL: https://docs.browserless.io/baas/v1/libraries/playwright-python

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/baas/start) or [BrowserQL](/browserql/start).

We support `playwright`for Python out of the box via their `playwright.chromium().connect_over_cdp` method.

Here's a simple snippet of its implementation, we can add additional features through the context, such as a proxy, shown below as well, feel free to remove that if you're not going to use one.

Find more of the [playwright documentation for python here.](https://playwright.dev/python/docs/api/class-browsercontext)

```py
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
  browser = p.chromium.connect_over_cdp('wss://chrome.browserless.io?token=your_token')
  context = browser.new_context()
  page = context.new_page()
  page.goto('http://www.example.com',wait_until='domcontentloaded')
  print(page.content())
  context.close()
```

## Built-in proxy

Use our in-built residential proxies, which consume units from your subscription. See [proxy options](/baas/v1/hosted-service/proxying) for details.
Your connection would look like this

```py
  browser = p.chromium.connect_over_cdp('wss://chrome.browserless.io?token=your_token&proxy=residential&proxySticky=true&proxyCountry=us')
```

You can set your IP address to be geolocated in a specific country with the [&proxyCountry flag](https://www.browserless.io/docs/proxying#all-proxy-options).

## Third party proxies

If you want to bring your own third party proxy, add these parameters to the `new_context` as shown below.

```py
  context = browser.new_context(
    proxy={"server": "http://proxyprovider.com:porthere", "username": "yourusername", "password": "yourpassword"}
  )
```

---

# Puppeteer

URL: https://docs.browserless.io/baas/v1/libraries/puppeteer

<!-- markdownlint-disable no-emphasis-as-heading -->

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/baas/start) or [BrowserQL](/browserql/start).

Puppeteer is well-supported by Browserless, and is easy to upgrade an existing service or app to use it. In order to use the Browserless service, simply change the following:

**Before Browserless**

```js

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();

  // Remember to catch errors and close!
})();
```

**After Browserless**

```js

(async () => {
  const browser = await puppeteer.connect({
    browserWSEndpoint: `wss://chrome.browserless.io/`,
  });
  const page = await browser.newPage();

  // Remember to catch errors and close!
})();
```

Please note that if you're running the docker container to replace the location of `wss://chrome.browserless.io/` to wherever your container is running.

## Specifying launch flags

You can specify launch-arguments through query-string parameters inside the `browserWSEndpoint`. As an example, if you want to start the browser with a pre-defined width and height you can specify it like so:

**Setting width and height**

```js

(async () => {
  const browser = await puppeteer.connect({
    browserWSEndpoint: `wss://chrome.browserless.io?--window-size=1200,900`,
  });
  const page = await browser.newPage();

  // Remember to catch errors and close!
})();
```

---

# Pyppeteer (Python) - UNMAINTAINED

URL: https://docs.browserless.io/baas/v1/libraries/pyppeteer

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the maintained [BaaS v2 Quickstart](/baas/quick-start), [Playwright configurations](/baas/advanced-configurations/playwright-customizations), or [BrowserQL](/browserql/start).

> NOTE: We suggest not using this library anymore as it is now unmaintained. Please consider [Quickstart](/baas/quick-start) and [Playwright configurations](/baas/advanced-configurations/playwright-customizations) as the maintained alternatives.

You can use your existing python infrastructure today with browserless by using [Pyppeteer](https://github.com/pyppeteer/pyppeteer), an unofficial python port of puppeteer.

Below is a small example of getting started:

```python

async def main():
  browser = await pyppeteer.launcher.connect(
    browserWSEndpoint='wss://chrome.browserless.io?token='+YOUR_API_TOKEN_HERE
    )
  page = await browser.newPage()
  url = "https://www.example.com"
  await page.goto(url)
  values = await page.evaluate('''() => document.querySelector('h1').innerHTML
    ''')
  print(values)
  await browser.close()

asyncio.get_event_loop().run_until_complete(main())
```

This will simply navigate to the example website and return the title, however it'll give you a great starting place for using Pyppeteer alongside Browserless.

Be sure to [let us know if you have questions or issues](https://www.browserless.io/contact).

---

# Scrapy (Python)

URL: https://docs.browserless.io/baas/v1/libraries/scrapy

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/baas/libraries/scrapy) or [BrowserQL](/browserql/start).

You can use Scrapy alongside [our `/content` API](/baas/v1/apis/content) to scrape data. All you need to do is use the  start_requests() method to make a query to the `/content` API, while  keeping your scraping code the same.

Since Scrapy uses HTTP requests to just download the HTML page and build a DOM-like abstraction to be scrapped, **it can only return the initial render of a page without interacting with it**. On the other hand, the `/content` API asure the HTML content is not just downloaded, but rendered and evaluated inside a browser.

All you need to do is implement the `start_requests()` method to make a query to the `/content` API, while keeping your scraping code the same.

```py

class PptrDocsSpider(scrapy.Spider):
    name = "pptr-docs"

    def start_requests (self):
        options = {
            "url": "https://puppeteer.github.io/pptr.dev/",
            "gotoOptions": { "waitUntil": "networkidle0" }
        }

        yield scrapy.Request(
            url="https://chrome.browserless.io/content?token=YOUR_API_TOKEN_HERE",
            method='POST',
            dont_filter=True,
            headers={"Content-Type": "application/json"},
            body=json.dumps(options)
        )

    def parse(self, response):
        entries = response.css('sidebar-component a.pptr-sidebar-item')
        for entry in entries:
            yield{
                'title' : entry.css('::text').get(),
                'url' : entry.css('::attr(href)').get(),
            }

```

You can use [all the options available in the `/content` API](https://chrome.browserless.io/docs/#/Browser%20API/post_content), use stealth mode, our residential proxies and more! For more reference, please see this [blogpost](https://www.browserless.io/blog/2024/02/15/scrapy-headless/).

---

# Selenium

URL: https://docs.browserless.io/baas/v1/libraries/selenium

<!-- markdownlint-disable no-emphasis-as-heading -->

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/baas/start) or [BrowserQL](/browserql/start).

We're happy to support the web-driver protocol, largely used by Selenium. You'll likely need to consult your libraries documentation since there's many options and alternatives for remote connections. Below is a small example of using the `chrome-webdriver` library and how to use Browserless with it.

**Before**

```js
const webdriver = require('selenium-webdriver');
const fs = require('fs');

const chromeCapabilities = webdriver.Capabilities.chrome();
chromeCapabilities.set('goog:chromeOptions', {
  // Set args similar to puppeteer's for best performance
  args: [
    '--disable-background-timer-throttling',
    '--disable-backgrounding-occluded-windows',
    '--disable-breakpad',
    '--disable-component-extensions-with-background-pages',
    '--disable-dev-shm-usage',
    '--disable-extensions',
    '--disable-features=TranslateUI,BlinkGenPropertyTrees',
    '--disable-ipc-flooding-protection',
    '--disable-renderer-backgrounding',
    '--enable-features=NetworkService,NetworkServiceInProcess',
    '--force-color-profile=srgb',
    '--hide-scrollbars',
    '--metrics-recording-only',
    '--mute-audio',
    '--headless',
    '--no-sandbox',
  ],
});

const driver = new webdriver.Builder()
  .forBrowser('chrome')
  .withCapabilities(chromeCapabilities)
  .build();

(async () => {
  try {
    await driver.get('https://www.example.com/');

    // Take screenshot of results page. Save to disk.
    const base64png = await driver.takeScreenshot();
    fs.writeFileSync('.no-git/screenshot.png', new Buffer(base64png, 'base64'));
  } finally {
    driver.quit();
  }
})();
```

**After**

```js
const webdriver = require('selenium-webdriver');
const fs = require('fs');

const chromeCapabilities = webdriver.Capabilities.chrome();
chromeCapabilities.set('browserless:token', 'YOUR-API-TOKEN');
chromeCapabilities.set('goog:chromeOptions', {
  args: [
    '--disable-background-timer-throttling',
    '--disable-backgrounding-occluded-windows',
    '--disable-breakpad',
    '--disable-component-extensions-with-background-pages',
    '--disable-dev-shm-usage',
    '--disable-extensions',
    '--disable-features=TranslateUI,BlinkGenPropertyTrees',
    '--disable-ipc-flooding-protection',
    '--disable-renderer-backgrounding',
    '--enable-features=NetworkService,NetworkServiceInProcess',
    '--force-color-profile=srgb',
    '--hide-scrollbars',
    '--metrics-recording-only',
    '--mute-audio',
    '--headless',
    '--no-sandbox',
  ],
});

const driver = new webdriver.Builder()
  .forBrowser('chrome')
  .withCapabilities(chromeCapabilities)
  // Specify browserless for the server
  .usingServer('https://chrome.browserless.io/webdriver')
  .build();

(async () => {
  try {
    await driver.get('https://www.example.com/');

    // Take screenshot of results page. Save to disk.
    const base64png = await driver.takeScreenshot();
    fs.writeFileSync('.no-git/screenshot.png', new Buffer(base64png, 'base64'));
  } finally {
    driver.quit();
  }
})();
```

If you're using the hosted service, be sure to set the extra option for the Browserless token:

**Token Auth**

```js
chromeCapabilities.set('browserless:token', 'YOUR-API-TOKEN');
```

The reason you can't append use the standard `?token=YOUR-API-TOKEN` is due to limitations in many of the Selenium libraries where query-string parameters are dropped from the server URL.

---

# .NET Selenium (C#)

URL: https://docs.browserless.io/baas/v1/libraries/selenium-dotnet

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/baas/start) or [BrowserQL](/browserql/start).

Browserless now supports the .NET Selenium library provided by OpenQA! Similar to all our selenium integrations, you can specify your authorization token by specifying an additional capability of `browserless:token`. Note that this is _not_ a capability that Chrome understands natively, and will throw an error, so you must specify `true` as your 3rd argument.

Here's a full example:

```cs
using System;
using OpenQA.Selenium;
using OpenQA.Selenium.Remote;
using OpenQA.Selenium.Chrome;

namespace BrowserlessSelenium
{
    class Program
    {
        static void Main(string[] args)
        {
            IWebDriver driver;
            ChromeOptions options = new ChromeOptions();

            // Set launch args similar to puppeteer's for best performance
            options.AddArgument("--disable-background-timer-throttling");
            options.AddArgument("--disable-backgrounding-occluded-windows");
            options.AddArgument("--disable-breakpad");
            options.AddArgument("--disable-component-extensions-with-background-pages");
            options.AddArgument("--disable-dev-shm-usage");
            options.AddArgument("--disable-extensions");
            options.AddArgument("--disable-features=TranslateUI,BlinkGenPropertyTrees");
            options.AddArgument("--disable-ipc-flooding-protection");
            options.AddArgument("--disable-renderer-backgrounding");
            options.AddArgument("--enable-features=NetworkService,NetworkServiceInProcess");
            options.AddArgument("--force-color-profile=srgb");
            options.AddArgument("--hide-scrollbars");
            options.AddArgument("--metrics-recording-only");
            options.AddArgument("--mute-audio");
            options.AddArgument("--headless");
            options.AddArgument("--no-sandbox");

            options.AddAdditionalOption("browserless:token", "YOUR-API-TOKEN");
            options.AddAdditionalOption("browserless:timeout", 60000);//optional flag to set a different timeout value than the default
            options.AddAdditionalOption("browserless:stealth", True);//optional flag to enable Stealth
            options.AddAdditionalOption("browserless:blockAds", True);//optional flag to enable blockAds
            options.AddAdditionalOption("browserless:trackingId", "AwENCwMHDQoHBgwKBAUODQ");//optional flag to enable trackingId
            options.AddAdditionalOption("browserless:pauseOnConnect", True);//optional flag to enable pauseOnConnect

            driver = new RemoteWebDriver(
              new Uri("https://chrome.browserless.io/webdriver"), options.ToCapabilities()
            );

            driver.Navigate().GoToUrl("https://example.com");
            Console.WriteLine(driver.Title);

            // Always call `quit` to ensure your session cleans up properly and you're not charged for unused time
            driver.Quit();
        }
    }
}
```

This will simply log out the Title from the Example website, however it'll give you a great starting place for using .NET alongside Browserless.

Be sure to [let us know if you have questions or issues](https://www.browserless.io/contact).

---

# Python (Selenium)

URL: https://docs.browserless.io/baas/v1/libraries/selenium-python

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/baas/start) or [BrowserQL](/browserql/start).

You can use your existing python infrastructure today with Browserless by using the selenium webdriver library.

As with most selenium libraries, you'll need to install the webdriver dependency, and set Browserless as your remote webdriver. You'll also use `set_capability` to specify your browserless.io token when making requests to the hosted service.

Below there are some small examples of getting started:

## Selenium 4

```python
# Selenium 4
from selenium import webdriver

chrome_options = webdriver.ChromeOptions()
chrome_options.set_capability('browserless:token', 'YOUR-API-TOKEN')
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--headless")

driver = webdriver.Remote(
    command_executor='https://chrome.browserless.io/webdriver',
    options=chrome_options
)

driver.get("https://www.example.com")
print(driver.title)
driver.quit()
```

## Selenium 3

```python
# Selenium 3
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities

chrome_options = webdriver.ChromeOptions()
chrome_options.set_capability('browserless:token', 'YOUR-API-TOKEN')
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--headless")

driver = webdriver.Remote(
  command_executor='https://chrome.browserless.io/webdriver',
  desired_capabilities=chrome_options.to_capabilities()
)

driver.get("https://www.example.com")
print(driver.title)
driver.quit()
```

This will simply navigate to the Example website and print the title, however it'll give you a great starting place for using Java Selenium alongside Browserless.

Be sure to [let us know if you have questions or issues](https://www.browserless.io/contact).

---

# Using /get API

URL: https://docs.browserless.io/baas/v1/options/api-get

<!-- markdownlint-disable no-emphasis-as-heading -->

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/baas/start) or [BrowserQL](/browserql/start).

In recent version of Browserless we've enabled special /GET handlers for our APIs. This is handy for cases where you need to directly call our services from a cache-layer like a CDN.

> Please do not call the hosted service from the browser as it will reveal your secret token!

In order to use this feature on the hosted service, you'll need to restart or update your worker(s) to do so. Simply click the "Restart Workers" button, and toggle the "Enable API GET" switch. Once restart you should be able to issue GET calls.

In docker, you'll need to set the `ENABLE_API_GET` environment variable to `true`.

## Making the request

Similar to our POST APIs (/content, /screenshot, /pdf, /scrape and /stats), you'll need to send a JSON payload to Browserless. However, since we're issuing a GET call instead of a POST, you'll need to stringify your JSON body and append it to the URL of the call as a query-string `body` parameter.

Here's a simple example of doing a screenshot with the example.com website.

**Before (POST)**

```bash
curl -X POST \
  https://chrome.browserless.io/screenshot?token=MY_API_TOKEN \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/json' \
  -d '{
  "url": "https://example.com/",
  "options": {
    "fullPage": true,
    "type": "jpeg",
    "quality": 75
  }
}'
```

**After (GET)**

```bash
curl -X GET \
  https://chrome.browserless.io/screenshot?token=MY_API_TOKEN&body=%7B%22url%22%3A%22https%3A%2F%2Fexample.com%2F%22%2C%22options%22%3A%7B%22fullPage%22%3Atrue%2C%22type%22%3A%22jpeg%22%2C%22quality%22%3A75%7D%7D
```

You'll note that we've both stringified the JSON, and encoded it for URL transmission. Because of how much space this takes, it's generally recommended to use the POST version, however certain use-cases might benefit from this GET feature.

---

# Launch Parameters

URL: https://docs.browserless.io/baas/v1/options/chrome-flags

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/baas/start) or [BrowserQL](/browserql/start).

- [Launching with a proxy](#launching-with-a-proxy)
- [Blocking Ads](#blocking-ads)
- [Running in Stealth](#running-in-stealth)
- [Specifying a user-data-dir](#caching-with-user-data-dir)
- [Keeping the browser alive](#keeping-the-browser-alive)
- [Setting a width and height](#starting-chrome-with-a-width-and-height)
- [Ignoring default flags](#ignoring-default-flags)
- [Setting headless](#setting-headless)
- [Overriding puppeteer launch args](#overriding-puppeteers-launch-args)
- [Setting a Tracking ID](#setting-a-tracking-id)
- [Overriding the session timer](#overriding-the-session-timer)
- [Other parameters](#other-parameters)

Browserless let's you control how Chrome is started via query-string parameters in your `puppeteer.connect`'s `browserWSEndpoint` URL, or in your REST API. This is useful in cases where you might need to start Chrome at a certain width and height, or need to use a proxy for visiting pages behind a firewall. Browserless (both hosted and self-hosted) lets you send launch arguments by specifying them as query-string parameters.

> If you've launched workers in a different region, be sure to use the appropriate load-balancer on your account page for a faster connection.

## Launching with a proxy

> Please note that many proxies only support Chromium revisions in less than puppeteer 1.18.0. This is due to Chromium dropping support for Proxy-Authorization headers. Check your proxies documentation for how to properly authenticate, and ensure that you don't need to set a Proxy-Authorization header.

```text
wss://chrome.browserless.io/?token=YOUR_API_KEY&--proxy-server=https://my-proxy.com
```

You can start Chrome and have it issues requests via a proxy by using the query-string parameter of `--proxy-server=${server}`. This will have Chrome issues requests through the proxy server as opposed to issuing plain network requests.

NOTE: If using an authenticated proxy, where you must provide a username and password, refer to your libraries documentation on how to handle this.

## Blocking ads

> Available in all accounts in Browserless version 1.7.0 and beyond

You can use our built-in ad-blocker for both `puppeteer.connect()` and REST API calls. You'll simply need to add a query-string parameter of `?blockAds` to your URL when connecting:

```text
wss://chrome.browserless.io/?token=YOUR_API_KEY&blockAds
```

Or for REST API calls:

```text
https://chrome.browserless.io/?token=YOUR_API_KEY&blockAds
```

When new pages are created, Browserless will inject a request-interceptor. If a domain matches a known site, we'll abort the call. This greatly reduces the number of inbound networks calls, and can significantly improve performance.

## Running in Stealth

Newer versions of our docker image, and in our cloud environment, now include a `stealth` argument that utilizes `puppeteer-extra-stealth` to help ensure your requests run stealthfully. As with all of our launch options, simply connect or query our APIs with a `stealth` argument:

```text
wss://chrome.browserless.io/?token=YOUR_API_KEY&stealth
```

Or for REST API calls:

```text
https://chrome.browserless.io/?token=YOUR_API_KEY&stealth
```

## Caching with user-data-dir

> Setting a user-data-dir is only available for Dedicated and self-hosted accounts (not on usage-based or cloud unit-based).

You can launch Chrome and specify a "user-data-dir" on each request to cache and make future sessions faster:

```text
wss://chrome.browserless.io/?token=YOUR_API_KEY&--user-data-dir=~/browserless-cache-123
```

This can be used in conjuction with all our API's and integrations.

## Keeping the browser alive

> This feature is only supported in our puppeteer integration, and only on Dedicated and self-hosted deployments.

By default, Browserless automatically closes chrome when the session disconnects. This generally meets most of the use-cases we've seen out there. However, there may be times when you want to keep the browser open even after your finished. You can do this with a `keepalive` parameter.

`keepalive` specifies how long Browserless should keep the browser process open for possible reconnection later.

**Keep chrome open for 5 minutes after disconnecting**

```js
puppeteer.connect({
  browserWSEndpoint: 'wss://chrome.browserless.io/?token=YOUR_API_KEY&keepalive=300000',
});
```

Should you use `keepalive` in your connect call, you'll also want to ensure that `browser.disconnect` is called _and not_ `browser.close`, as the latter will kill the Chrome process.

Once your app disconnects, either by design or in case of an interuppted session, we will keep the browser open for the keepalive specified (in the above example this would be 5 minutes). After that, regardless of any reconnected sessions, Browserless will kill the chromium process.

In order to reconnect to an existing session, simply call the `/session` endpoint, or for Dedicated accounts use the GraphQL `sessions` query. This call will return all active sessions back, including a `browserWSEndpoint` payload you can use to re-connect puppeteer back to the browser. When combined with a `trackingId` this gives you a flexible integration with full control over session lifecycle.

## Starting Chrome with a width and height

```text
wss://chrome.browserless.io/?token=YOUR_API_KEY&--window-size=1200,900
```

You can also specify the dimensions Chrome should start with via the `--window-size` parameter. The value here should be a string of X and Y dimensions which translate to width and height (IE: '800,600').

## Ignoring default flags

By default puppeteer specifies a certain number of launch flags in order to provide the best experience. In order to turn these off you'll need to tell Browserless to ignore them. Be careful ignoring these args as it might cause chromium to become unstable or refuse to launch.

```text
wss://chrome.browserless.io/?token=YOUR_API_KEY&ignoreDefaultArgs=--hide-scrollbars,--disable-default-apps
```

## Setting headless

In many cases setting headless won't matter, as Browserless will default to this to `true`. In some special circumastances it's important to set this to `false`. As an example, our screencast API sets this to `false` in order to appropriately load an extension for testing.

```text
wss://chrome.browserless.io/?token=YOUR_API_KEY&headless=false
```

## Overriding Puppeteer's launch args

Since `puppeteer.connect` differs greatly from `puppeteer.launch`, Browserless a way to pass in certain parameters to "override" puppeteer's default launch arguments. These are specified through query-string parameters, and can be applied like so:

**Ignore HTTPS Errors**

```js
// Via puppeteer.launch()
puppeteer.launch({
  ignoreHTTPSErrors: true,
});

// Via puppeteer.connect()
puppeteer.connect({
  browserWSEndpoint: 'wss://chrome.browserless.io?token=YOUR-API-TOKEN',
  ignoreHTTPSErrors: true,
});
```

**Slow Mo**

```js
// Via puppeteer.launch()
puppeteer.launch({
  slowMo: 1000,
});

// Via puppeteer.connect()
puppeteer.connect({
  browserWSEndpoint: 'wss://chrome.browserless.io?token=YOUR-API-TOKEN',
  slowMo: 1000,
});
```

**Ignore default args**

```js
// Via puppeteer.launch()
puppeteer.launch({
  ignoreDefaultArgs: true,
});

// Via puppeteer.connect()
puppeteer.connect({
  browserWSEndpoint: 'wss://chrome.browserless.io?token=YOUR-API-TOKEN&ignoreDefaultArgs=true',
});
```

## Setting a tracking ID

> Tracking ID's, and the workspace queries, are only available for Dedicated and self-hosted accounts (not usage-based or cloud unit-based).

Sometimes it's useful to associate particular sessions of Browserless with some other part of your application. For those instances we have mechanism called `trackingId`.

When this query-string parameter is present in a REST API call, or `puppeteer.connect`, Browserless will scope both file downloads and `GET /sessions` calls to reveal the `trackingId` of the original call. For instance:

```js
// Connect with a "trackingId" in the browserWSEndpoint:
puppeteer.connect({
  browserWSEndpoint:
    'wss://chrome.browserless.io?token=YOUR-API-TOKEN&trackingId=AwENCwMHDQoHBgwKBAUODQ',
});

// Do some work, download files
browser.close();
```

Will save downloads to a sub-directory of `AwENCwMHDQoHBgwKBAUODQ` in the workspace. Thus, a `GET /workspace` call results in the following:

```js
[
  {
    created: '2019-07-15T22:11:40.157Z',
    isDirectory: false,
    name: 'data.csv',
    path: '/workspace/AwENCwMHDQoHBgwKBAUODQ/data.csv',
    size: 52,
    workspaceId: 'AwENCwMHDQoHBgwKBAUODQ',
  },
];
```

Similarly, a `GET /session` call will reveal a property to identify that particular session:

```json
[
  {
    "description": "",
    "devtoolsFrontendUrl": "/devtools/inspector.html?ws=127.0.0.1:3000/devtools/page/6875C09959939E88D9BD30EC1FE1AAF0",
    "id": "6875C09959939E88D9BD30EC1FE1AAF0",
    "title": "Example Domain",
    "type": "page",
    "url": "https://example.com/",
    "webSocketDebuggerUrl": "ws://127.0.0.1:3000/devtools/page/6875C09959939E88D9BD30EC1FE1AAF0",
    "port": "62932",
    "trackingId": "AwENCwMHDQoHBgwKBAUODQ"
  }
]
```

We'll be adding more behavioral and logging changes to surface this tracking-ID as it makes sense, so be sure to check back often to see how this can help you in your application development.

## Overriding the session timer

By default, your sessions (REST API, puppeteer.connect _and_ webdriver) are governed by a timeout. This is set via your account page for the hosted service, or by specifying `CONNECTION_TIMEOUT` in the `docker run` command. You can, on a per-job basis, override this.

For `puppeteer.connect` and REST API's, simply specify a `?timeout` query-string parameter, with the value being the time in milliseconds for the session to execute:

```sh
# REST API (10-second limit):
https://chrome.browserless.io?token=YOUR-API-TOKEN&timeout=10000

# puppeteer.connect URL (10-second limit):
wss://chrome.browserless.io?token=YOUR-API-TOKEN&timeout=10000
```

For webdriver, you must add an additional capability:

```js
const webdriver = require('selenium-webdriver');

const chromeCapabilities = webdriver.Capabilities.chrome();
// Set the session to a 10-second limit
chromeCapabilities.set('browserless.timeout', 10000);
```

For other runtimes and selenium libraries be sure to consult your libraries documentation, or [contact us](https://www.browserless.io/contact)

## Other parameters

By default Browserless looks for query-string parameters beginning with `--` and passes them to Chrome upon boot. This means that most flags are supported even if they aren't documented. You can also see how we parse parameters by [reading through our code here](https://github.com/browserless/browserless/blob/v1/src/chrome-helper.ts#L88).

At this time, only certain flags and query-parameters are allowed for usage-based accounts and cloud unit-based, and are listed below:

- --proxy-server,
- --window-size,
- --disable-web-security,
- --enable-features,
- --disable-web-security,
- --disable-setuid-sandbox,
- --lang,
- --font-render-hinting,
- --force-color-profile,
- stealth,
- ignoreDefaultArgs,
- headless,
- token,
- blockAds,
- ignoreHTTPSErrors,
- slowMo,

---

# How to bypass bot detection

URL: https://docs.browserless.io/baas/v1/recipes/bot-detection

<!-- markdownlint-disable no-emphasis-as-heading -->

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/baas/bot-detection/stealth) or [BrowserQL](/browserql/start).

Websites can have a different level of anti-bot mechanisms depending on the sensitivity of their data and budget. If your automations about being blocked, setting these three flags will bypass most anti-bot mechanisms.

## Flags to bypass bot detection

### Use the `&headless=false` flag

Most bot detectors will check your user-agent, which by default explicitly claims you're running headless chrome. This is a dead giveaway. It can be changed by setting a specific user-agent but we highly recommend you use the &headless=false flag instead, which changes your user-agent to a more credible one.

### Use the `&stealth` flag

The stealth flag implements Puppeteer's puppeteer-extra-plugin-stealth plugin which applies various techniques to make detection of headless puppeteer harder. This flag may backfire and be easily detected by some sites, so consider avoiding it as well.

### Use our [in-built proxy](https://www.browserless.io/docs/proxying) or use a [third-party proxy](https://www.browserless.io/docs/using-a-proxy)

Finally, the hardest sites to crack down check your IP address; there are two type of bocks that can occur, those based on type of IP, and those based on frequency of requests (rate-limits).

- Sites checking the type of IP address will detect your data-center IP addresses when using Browserless. To overcome this, using a proxy with residential IP addresses will be the best option.
- Sites that work the first few times and then stop working, are probably rate-limiting and it's not the residential part of it that blocks us. For these cases, you don't necessarily need a residential proxy and data-center IP addresses that rotate should be enough.

## Additional strategies

If none of these do the trick, get in touch with us at [support@browserless.io](mailto:support@browserless.io)>.

We have more trick up our sleeves we can show you, such as captcha solving and our newest BQL tool on our current v2 build, upgrade to the latest build to learn [new bypassing methods](https://docs.browserless.io/baas/bot-detection/stealth).

---

# Dealing with file downloads in puppeteer

URL: https://docs.browserless.io/baas/v1/recipes/downloading-files

<!-- markdownlint-disable no-emphasis-as-heading -->

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/baas/start) or [BrowserQL](/browserql/start).

Because of the tight coupling between Chrome and the local file-system it can be hard to determine _where_ files are being downloaded, and subsequently process/remove them. Because of this complex nature we offer two mechanisms in which to deal with file downloads in Browserless: using our `/download` API, or manage the downloads via the `/workspace` API.

## Using the /download API

Much like the `/function` API, the `/download` API allows you to `POST` a module to run that will return the file the browser downloads. Here's an example of a puppeteer script that will return a CSV file:

```js
const puppeteer = require('puppeteer');

async function run() {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();

  // Here we generate a CSV file and have the browser download it
  await page.evaluate(() => {
    const rows = [
      ['name1', 'city1', 'some other info'],
      ['name2', 'city2', 'more info'],
    ];
    let csvContent = 'data:text/csv;charset=utf-8,';
    rows.forEach(function (rowArray) {
      let row = rowArray.join(',');
      csvContent += row + '\r\n';
    });
    const encodedUri = encodeURI(csvContent);
    const link = document.createElement('a');
    link.setAttribute('href', encodedUri);
    link.setAttribute('download', 'data.csv');
    document.body.appendChild(link);

    return link.click();
  });
}

run();
```

This might come as a surprise, but unfortunately in puppeteer there's no way to know if the file downloaded, or an API to even get it. You'll have to _know_ ahead of time where files are kept, and watch the file-system for it to complete!

The `/download` API handles all of this for you. Here's that same example:

```sh
curl -X POST \
  https://chrome.browserless.io/download?token=YOUR-API-TOKEN \
  -H 'Content-Type: application/javascript' \
  -d 'module.exports = async ({ page }) => {
    await page.evaluate(() => {
        const rows = [
            ["name1", "city1", "some other info"],
            ["name2", "city2", "more info"]
        ];
        let csvContent = "data:text/csv;charset=utf-8,";
        rows.forEach(function(rowArray){
            let row = rowArray.join(",");
            csvContent += row + "\r\n";
        });
        const encodedUri = encodeURI(csvContent);
        const link = document.createElement("a");
        link.setAttribute("href", encodedUri);
        link.setAttribute("download", "data.csv");
        document.body.appendChild(link);

        return link.click();
    });
};'
```

This will process your code, create a new temporary directory, execute your script, watch for the download to complete and return it with an appropriate Content-Type (plus delete it from within the container). If there's an issue in the process, Browserless will return semantic HTTP codes, and messages relaying what exactly happened to help with debugging.

This can save you numerous hassles as it takes care of all the low-level work involved in dealing with file-downloads, but if you're curious on how to accomplish this without this API read below!

## Using the `/workspace` API

The /workspace API allows for users to manually upload and download files that Browserless (and thus Chrome) can access. In our prior example, we didn't care about the file-system in any capacity, however that changes if you wish to continue to use puppeteer in your code-base and call it programatically.

### 1. Mount the `WORKSPACE_DIR` someplace

By default, the internal `WORKSPACE_DIR` in Browserless is set to the operating-system's temporary directory. It's best if you can mount it in an ephemeral location, but any directory with write-privileges is fine:

```sh
docker run -e "WORKSPACE_DIR=/home/browserless" -p 3000:3000 --restart always -d --name browserless browserless/chrome:latest
```

Now that is done we can instrument puppeteer to use it as the directory to download files.

### 2. Tell puppeteer where to download files

Chrome defaults to downloading files in various places, depending on the operating system. In our case, we want to tell it where since it provides greater flexibility on sharing files and processing them in an expedited manner.

In order to tell puppeteer where to store files you'll have to reach into some internals so that things work appropriately:

```js
await page._client.send('Page.setDownloadBehavior', {
  behavior: 'allow',
  // This path must match the WORKSPACE_DIR in Step 1
  downloadPath: '/home/browserless',
});
```

Unfortunately you can't use the the [CDP Session](https://pptr.dev/#?product=Puppeteer&version=v1.11.0&show=api-class-cdpsession) manager to do this as it'll only apply to _that_ newly created CDP session, hence why it's required to to talk directly with the `_client` module on the `page` object.

Once this is set you can now emit whatever action to download the file!

### 3. Download the file in the page

By whatever means, this is the step where you'll download the file in the browser. Using our prior example we'll set this to generate a file for illustration purposes:

```js
await page._client.send('Page.setDownloadBehavior', {
  behavior: 'allow',
  // This path must match the WORKSPACE_DIR in Step 1
  downloadPath: '/home/browserless',
});
await page.evaluate(() => {
  const rows = [
    ['name1', 'city1', 'some other info'],
    ['name2', 'city2', 'more info'],
  ];
  let csvContent = 'data:text/csv;charset=utf-8,';
  rows.forEach(function (rowArray) {
    let row = rowArray.join(',');
    csvContent += row + '\r\n';
  });
  const encodedUri = encodeURI(csvContent);
  const link = document.createElement('a');
  link.setAttribute('href', encodedUri);
  link.setAttribute('download', 'data.csv');
  document.body.appendChild(link);

  return link.click();
});
```

This will trigger the file to download to the file-system _inside of browserless_.

### 4. Download the file

Unfortunately you'll have to know the file's name in order to deal with it appropriately, but since Browserless ships with a `/workspace` API we can use a simple `GET` request to see what's on the disk!

**Get downloads on Browserless**

```bash
# Curl browserless and see what files it has
curl -X GET \
  https://chrome.browserless.io/workspace?token=MY_API_TOKEN
```

This will return a list of files, which we can then use a subsequent `GET` call to download.

```json
[
  {
    "isDirectory": false,
    "name": "data.csv",
    "size": 2649
  }
]
```

Now that we know the file is there, and the name of it, we can issue our `GET` call to download it:

```sh
curl -X GET \
  https://chrome.browserless.io/workspace/data.csv?token=MY_API_TOKEN
```

Once we have it in our possession, a simple `DELETE` call will remove it from Browserless' file-system:

```sh
curl -X DELETE \
  https://chrome.browserless.io/workspace/data.csv?token=MY_API_TOKEN
```

As you can see there's a lot involved in dealing with file-downloads: managing the directory where they reside, telling puppeteer _where_ to download them, and interacting with the file-system to get/delete them once we're done. If this seems like too much of a task then we highly recommend using the `/download` API and letting Browserless deal with all the low-level internals.

---

# Setting a user agent

URL: https://docs.browserless.io/baas/v1/recipes/setting-a-user-agent

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/baas/bot-detection/user-agent-masking) or [BrowserQL](/browserql/start).

The User-Agent request header is a characteristic string that lets servers and network peers identify the application, operating system, vendor, and/or version of the requesting user agent.

Many sites use this information to render the site differently for each user, and sometimes even for rudimentary bot detection. If you run chrome headless and want to take a screenshot of a page, it's a good idea to set a user agent so that web fonts load properly.

## Code

You can set the user agent from code, in puppeteer it would be like this:

```js
await page.setUserAgent(
  'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36'
);
```

## REST API

If you're using our APIs like /screenshot, /content, /pdf, or /scrape, you can also set the user agent in the body of your request.

```json
{
  "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
}
```

## Hosted service

If you're on a usage-based, cloud unit-based or capacity-based account, you can change the user agent as so

```js
const browser = await puppeteer.connect({
  browserWSEndpoint:
    'wss://chrome.browserless.io?token=YOUR_API_KEY&--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36',
});
```

## Self-hosting

If you're self hosting, you can set the user agent as a default launch argument.

```bash
    docker run -e "DEFAULT_LAUNCH_ARGS=[\"--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36\"]" -p 3000:3000 --restart always -d --name browserless browserless/chrome
```

---

# Using a proxy in Puppeteer

URL: https://docs.browserless.io/baas/v1/recipes/using-a-proxy

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/baas/bot-detection/proxies) or [BrowserQL](/browserql/start).

:::success
We now have our own built-in residential proxy! [See this document here for how to use it.](/baas/bot-detection/proxies)
:::

Both Browserless, and Chrome itself, support the usage of external proxies. All cloud plans, including free, have access to a built-in proxy which requires no additional work on your part. Built-in proxy traffic consumes units per MB. [See more about that here.](/baas/bot-detection/proxies).

If you want to use an external, or 3rd party proxy, please continue to read below. In general there's two things you'll have to do:

- Specify the address of where the proxy is with the `--proxy-server` switch.
- Optionally, you'll also need to send in your username and password if the proxy is authenticated.

## Specifying the proxy

Regardless of whether or not you're using our REST API's or the puppeteer integration, you'll need to specify _where_ the proxy is. Chrome has a command-line flag to do this, and we support this in Browserless via the following query-string parameter:

```text
?--proxy-server=https://YOUR-PROXY-SERVER-DOMAIN:PORT
```

You can set this parameter in our [live debugger](https://chrome.browserless.io/) by clicking on the gear icon on the left panel and modifying the Browser URL field.

If you're using a proxy that doesn't require a password (maybe just an IP address filter), then that's it! You're free to now use this proxy going forward! Otherwise read on.

## Using username and password

### Method 1: page.authenticate

Most proxies will require some means of authentication. There's generally two ways you can do this in Puppeteer, and also in Browserless. The first more common method is the `page.authenticate`:

```js
await page.authenticate({
  username: 'joel',
  password: 'browserless-rocks',
});
```

Doing this will apply these parameters to your network requests going forward.

In our REST API's you can specify these fields with the following in your POST JSON body. These parameters work for the `pdf`, `content` and `screenshot` APIs:

```json
{
  "authenticate": {
    "username": "joel",
    "password": "browserless-rocks"
  }
}
```

### Method 2: page.setExtraHTTPHeaders

The other mechanism is to use HTTP headers to send in extra authorization information. Puppeteer makes this pretty easy by allowing us to send in new HTTP headers via `page.setExtraHTTPHeaders`:

> NOTE: This is deprecated in most libraries now, so it's worth keeping in mind that using authentication methods in each library is now the standard.

```js
// Remember to base64 encode your username:password!
await page.setExtraHTTPHeaders({
  'Proxy-Authorization': 'Basic username:password',
  // OR
  Authorization: 'Basic username:password',
});
```

Refer to your libraries documentation on what the name of the headers is, and how to properly encode it.

We also allow this in our REST APIs as well, via the `setExtraHTTPHeaders` property:

```json
{
  "setExtraHTTPHeaders": {
    "Proxy-Authorization": "Basic username:password",
    // OR
    "Authorization": "Basic username:password"
  }
}
```

This will allow your REST APIs to utilize the prior provided proxy!

---

# Watching your sessions

URL: https://docs.browserless.io/baas/v1/recipes/watch-sessions

<!-- markdownlint-disable no-emphasis-as-heading commands-show-output no-bare-urls-->

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/enterprise/watching-sessions) or [BrowserQL](/browserql/start).

Both the Browserless docker image, and Dedicated hosted plans, have the ability to watch sessions as they run. As with most of our APIs and features, they're strongly geared towards engineers building on top of Browserless. It's with these engineers in mind that we've written this document on how to watch your sessions as you work, broken up into three different methods.

1. [Watching sessions on the docker image](#watching-sessions-on-the-docker-image)
2. [Using the Sessions tab in the Dedicated plans](#using-the-sessions-tab-in-the-dedicated-plans)
3. Programmatically watching your sessions on Dedicated plans

## Watching sessions on the docker image

To use the session viewer on the docker image, you don't need to do anything special in your code or how you connect to Browserless. The only change you might make is add a `trackingId` to your connect call in order to sort or filter sessions to the one you care about.

In any case, the first step is to start the container as usual:

```sh
$ docker run --name browserless --rm -p 3000:3000 browserless/chrome:latest
```

Then, in your application or script connect to it.

```js
const puppeteer = require('puppeteer');

(async () => {
  let browser = null;

  try {
    // If your script executes too quickly, you can add a ?pause query parameter
    // to the connect call to pause the script from running until you're watching it
    browser = await puppeteer.connect({
      browserWSEndpoint: `ws://localhost:3000`,
    });
    const page = await browser.newPage();
    await page.goto('https://example.com/');
  } catch (error) {
    console.log(error);
  } finally {
    browser && browser.close();
  }
})();
```

In your browser, you can visit http://localhost:3000, click on the Settings icon in the top left, and click the link for the session:

<!-- ![Session debugging](/img/sessions.gif) -->

If your session is immediately closing, then you can keep it open by _not_ calling browser.close at the end (just remember to terminate your script with CTRL+C or similar).

## Using the Sessions tab in the Dedicated plans

In Dedicated plans, you can see your active sessions at any time by going to the Sessions portion of the account page.

<!-- ![Running sessions](/img/running-sessions.png) -->

Pressing the "Fetch Running Sessions" button will retrieve actively running sessions across all your workers, and show them here. The "Debugger" Link will then open up the page that you can run and view these sessions.

Anytime you connect, you can immediately see the results here after pushing the "Fetch Running Sessions" button.

## Programmatically watching your sessions on Dedicated plans

You can programmatically retrieve your running sessions with a simple GraphQL API call. This lets you build integrations where end-users can witness their automation running, engineers can debug live production issues, and you can more easily pinpoint problems in your scripts.

When setting this up, you'll want to ensure that you don't accidentally expose your sessions to other users. It's for this reason that we've create a `trackingId`, which allows you to filter and more easily distinguish between running sessions.

For those that want to see the full example, see below. We'll break it down step-by-step afterwards.

**TL;DR the goods**

```js
const puppeteer = require('puppeteer');
const fetch = require('node-fetch');
const apiToken = 'YOUR-API-TOKEN';
const trackingId = 'some-unique-id';

const getSessionsById = (trackingId) => {
  return fetch('https://api.browserless.io/graphql', {
    method: 'POST',
    headers: {
      'content-type': 'application/json',
    },
    body: JSON.stringify({
      query: `
        query getSessions($trackingId: String!, $apiToken: String!) {
          sessions(apiToken: $apiToken, trackingId: $trackingId) {
            description
            devtoolsFrontendUrl
            id
            title
            url
            trackingId
            browserId
            browserWSEndpoint
          }
        }
      `,
      variables: {
        trackingId,
        apiToken,
      },
    }),
  })
    .then((res) => res.json())
    .then((res) => res.data.sessions)
    .catch((error) => {
      console.log(`Error retrieving sessions ${error.message}`);
      return null;
    });
};

const run = async () => {
  const sleep = (ms) => new Promise((res) => setTimeout(res, ms));

  let browser = null;

  try {
    browser = await puppeteer.connect({
      browserWSEndpoint: `wss://chrome.browserless.io?token=${apiToken}&trackingId=${trackingId}`,
    });

    const page = await browser.newPage();
    await page.goto('https://example.com');

    const [session] = await getSessionsById(trackingId);

    if (!session) {
      throw new Error(`Error retrieving session!`);
    }

    console.log(`Open: ${session.devtoolsFrontendUrl} in your browser!`);

    // Let the page stay open for 30 seconds so we can debug!
    await sleep(30000);
  } catch (error) {
    console.error(`Saw error when running: ${error.message}`);
  } finally {
    if (browser) {
      console.log(`Shutting down the browser.`);
      browser.close();
    }
  }
};

run();
```

### Selecting the modules to use

For this automated example, we're using the `puppeteer` module as well as the `node-fetch` package. Puppeteer is used for running the browser, and `node-fetch` for doing the API calls to Browserless to get sessions.

You're free to use whatever modules you like, including Selenium or even axios, however we've found that puppeteer and node-fetch are the most common across the web.

Once done, let's setup the function to get sessions from the GraphQL endpoint!

### Fetching sessions

Our function below takes a single argument: the trackingId we intend to use for the session. In a production system, you should _always_ make this ID unique as it'll prevent you from accidentally watching the wrong session. If you need a recommendation, we suggest something like a UUID or a GUID library.

```js
const getSessionsById = (trackingId) => {
  return fetch('https://api.browserless.io/graphql', {
    method: 'POST',
    headers: {
      'content-type': 'application/json',
    },
    body: JSON.stringify({
      query: `
        query getSessions($trackingId: String!, $apiToken: String!) {
          sessions(apiToken: $apiToken, trackingId: $trackingId) {
            description
            devtoolsFrontendUrl
            id
            title
            url
            trackingId
            browserId
            browserWSEndpoint
          }
        }
      `,
      variables: {
        trackingId,
        apiToken,
      },
    });
  })
  .then((res) => res.json())
  .then((res) => res.data.sessions)
  .catch((error) => {
    console.log(`Error retrieving sessions ${error.message}`);
    return null;
  });
};
```

The below query is the underlying GraphQL request, that says we want to retrieve the following properties for our session. In our example we only make use of the `devtoolsFrontendUrl`, so you're free to remove the others if you wish, they're there for educational purposes.

```graphql
query getSessions($trackingId: String!, $apiToken: String!) {
  sessions(apiToken: $apiToken, trackingId: $trackingId) {
    description
    devtoolsFrontendUrl
    id
    title
    url
    trackingId
    browserId
    browserWSEndpoint
  }
}
```

In GraphQL terms, we're setting up a "function" here that retrieves sessions with your API Token and trackingId. The response will contain something like:

```js
{
  "data": {
    "sessions": [
      {
        "description": "",
        "devtoolsFrontendUrl": "https://chrome.browserless.io/p/53616c7465645...",
        "id": "CE39322DD6F46305A46FF92A0B41E4F2",
        "title": "Example Site",
        "url": "https://example.com",
        "trackingId": "some-unique-id",
        "browserId": "a69f18cf-23d9-4b5e-82c0-b817f2fd4e82",
        "browserWSEndpoint": "wss://chrome.browserless.io/p/53616c7465645f5f325..."
      }
    ]
  }
}
```

> In all of our returned links we _omit_ your API token, so you'll need to add it to the link manually to visit it. This is done for security purposes.

**Hooking up puppeteer**

In our puppeteer setup, the `run` function, we start the browser and begin to write our script. Once we know the page is open, we use our GraphQL fetching function to get the active sessions (filtered by trackingId), and log the response.

In reality you can do whatever you'd like with this response: save it in a database or load it in a user's page. However, once the session is done the link will no longer respond properly, and the session will return a `404`.

Here's that code again:

```js
const run = async () => {
  const sleep = (ms) => new Promise((res) => setTimeout(res, ms));

  let browser = null;

  try {
    browser = await puppeteer.connect({
      browserWSEndpoint: `wss://chrome.browserless.io?token=${apiToken}&trackingId=${trackingId}`,
    });

    const page = await browser.newPage();
    await page.goto('https://example.com');

    const [session] = await getSessionsById(trackingId);

    if (!session) {
      throw new Error(`Error retrieving session!`);
    }

    console.log(
      `Open: ${session.devtoolsFrontendUrl} in your browser (be sure to add your &token to it!)`
    );

    // Let the page stay open for 30 seconds so we can debug!
    await sleep(30000);
  } catch (error) {
    console.error(`Saw error when running: ${error.message}`);
  } finally {
    if (browser) {
      console.log(`Shutting down the browser.`);
      browser.close();
    }
  }
};
```

You'll note that we have a `page.waitFor` call in here as well: this is to ensure you have time to visit the debugging page. In a production system, the page interactions can happen quickly, and so it's easy to go to a debug page and miss all of the interactivity. If it's crucial that you witness the script as it runs we recommend pausing the script on startup with the `&pause` query-string parameter in your `puppeteer.connect` call.

That's it! You've successfully setup a running puppeteer session, and have a way to view the session as needed! If you've found this helpful, or need help with an issue, please [let us know](https://www.browserless.io/contact)

---

# Quickstart

URL: https://docs.browserless.io/baas/v1/start

<!-- markdownlint-disable no-emphasis-as-heading link-fragments commands-show-output no-bare-urls -->

> **DEPRECATION WARNING**
> This documentation is for BaaS v1, which is no longer actively supported. If you are a new user, please refer to the updated documentation for [BaaS v2](/baas/start) or [BrowserQL](/browserql/start).

Welcome to the quick-start! Below are the top three integration we support, with quick directions on getting up and running with popular methods. See the sidebar for all the other supported API's and REST methods!

**Choose your library**

- [Using Puppeteer](#using-puppeteer)
- [Using Playwright](/baas/v1/libraries/playwright)
- [Using Selenium or WebDriver](#using-selenium-or-webdriver)

## Using Puppeteer

Whether you're looking to get started, or already have an established codebase, Browserless aims to make the transition as easy as possible. At a high-level, you'll need to do the following:

1. [Install puppeteer](#1-install-puppeteer)
2. [Setup your app](#2-setup-your-app)
3. [Update your app to use Browserless](#3-update-your-app-to-use-browserless)

### 1. Install puppeteer

If you haven't chosen a library yet we highly recommend [puppeteer](https://github.com/puppeteer/puppeteer/) as it's fairly active and has many maintainers. It's also built by the developers of Chrome, so it's one of the highest quality libraries around.

**Installing puppeteer**

```bash
$ npm i --save puppeteer
# or
> yarn add puppeteer
```

In development you'll likely want to install puppeteer's bundled version of Chromium, however you won't need this in production as Chrome is all taken care of for you by Browserless. To disable the downloading of puppeteer, simply add an environment variable when installing:

**Install for production**

```bash
# Linux
$ PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
# Windows
set PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true

$ npm install --production
# or yarn install --production
```

You can read more about puppeteer's environment variables [here](https://pptr.dev/#environment-variables).

### 2. Setup your app

As an example let's write a screenshot service that takes a picture of the website and downloads it. We'll setup a route on the `/image` path and take a picture of a static webpage (though you can make this configurable if you wish):

**Screenshot service**

```javascript
const express = require('express');
const puppeteer = require('puppeteer');

const app = express();

app.get('/image', async (req, res) => {
  // puppeteer.launch() => Chrome running locally (on the same hardware)
  let browser = null;

  try {
    browser = await puppeteer.launch();
    const page = await browser.newPage();

    await page.goto('http://www.example.com/');
    const screenshot = await page.screenshot();

    res.end(screenshot, 'binary');
  } catch (error) {
    if (!res.headersSent) {
      res.status(400).send(error.message);
    }
  } finally {
    if (browser) {
      browser.close();
    }
  }
});

app.listen(8080, () => console.log('Listening on PORT: 8080'));
```

With that in place, let's re-work some of our functionality to use Browserless in production.

### 3. Update your app to use Browserless

Once we have Browserless account setup, or have it running via docker, we can simply "connect" to it in our application instead of launching Chrome.

> **Warning**
> It's very important to close the browser for unit-based, otherwise you'll spend seconds on the browser running. The best practice is to implement try/catch blocks and set a custom timeout to last less than the 15 minute global timeout set by default, per your requirements.

**Screenshot service with Browserless**

```javascript
const express = require('express');
const puppeteer = require('puppeteer');
const IS_PRODUCTION = process.env.NODE_ENV === 'production';

const app = express();

const getBrowser = () =>
  IS_PRODUCTION
    ? // Connect to browserless so we don't run Chrome on the same hardware in production
      puppeteer.connect({ browserWSEndpoint: 'wss://chrome.browserless.io?token=YOUR-API-TOKEN' })
    : // Run the browser locally while in development
      puppeteer.launch();

app.get('/image', async (req, res) => {
  let browser = null;

  try {
    browser = await getBrowser();
    const page = await browser.newPage();

    await page.goto('http://www.example.com/');
    const screenshot = await page.screenshot();

    res.end(screenshot, 'binary');
  } catch (error) {
    if (!res.headersSent) {
      res.status(400).send(error.message);
    }
  } finally {
    if (browser) {
      browser.close();
    }
  }
});

app.listen(8080, () => console.log('Listening on PORT: 8080'));
```

That's it! Now you don't have to worry about bundling Chrome or it's dependencies in production and can continue to develop your application.

Below is a sample cURL call that will use your new service to ensure it's all working:

```sh
$ curl -X GET -o temp.png http://localhost:8080/image
```

## Using Selenium or WebDriver

Browserless exposes the WebDriver protocol at https://chrome.browserless.io/webdriver. Updating your tests, continuous-integration tests, or application to use it is as simple as specifying a remote connection.

1. [Setup WebDriver.](#setup-webdriver)
2. [Update the builder to use Browserless.](#update-the-builder-to-use-browserless)

### Setup WebDriver

For the purposes of this example we'll be using the Ruby bindings, but almost every language has an accompanying library for WebDriver. Be sure to consult your runtime's library for more direct instructions.

```ruby
# You must specify --headless and --no-sandbox
options = Selenium::WebDriver::Chrome::Options.new
options.add_argument('--headless')
options.add_argument('--no-sandbox')

# Use the webdriver for going to sites, taking pictures, anything else. Make sure you close the browser when done.
driver = Selenium::WebDriver.for :chrome, options: options
```

Once that's setup, be sure to verify your script works locally before proceeding to rule out any unwanted behavior.

### Update the builder to use Browserless

Once you have script working locally, the next step is to specify Browserless as the remote server. This will give you the ability to run multiple browsers in parallel, greatly reducing the amount of time your tests take.

```rb
# You must specify --headless and --no-sandbox
caps = Selenium::WebDriver::Remote::Capabilities.chrome("goog:chromeOptions" => {
  "args" => [
    "--disable-background-timer-throttling",
    "--disable-backgrounding-occluded-windows",
    "--disable-breakpad",
    "--disable-component-extensions-with-background-pages",
    "--disable-dev-shm-usage",
    "--disable-extensions",
    "--disable-features=TranslateUI,BlinkGenPropertyTrees",
    "--disable-ipc-flooding-protection",
    "--disable-renderer-backgrounding",
    "--enable-features=NetworkService,NetworkServiceInProcess",
    "--force-color-profile=srgb",
    "--hide-scrollbars",
    "--metrics-recording-only",
    "--mute-audio",
    "--headless",
    "--no-sandbox"
  ]
})
caps['browserless:token'] = 'YOUR-API-TOKEN'
driver = Selenium::WebDriver.for :remote, url: "https://chrome.browserless.io/webdriver", desired_capabilities: caps

# Use the webdriver for going to sites, taking pictures. Make sure you close the browser when done.
driver = Selenium::WebDriver.for :chrome, options: options
```

That's it! Once you've verified it's all working properly, the next step is to conditionally use Browserless only when running in a continuous integration environment.

### What's next?

There's a lot more that you can configure and tune in Browserless to handle the needs of your application. Be sure to read about all the options it exposes and how to get the most out of Browserless.

---

# Version Compatibility

URL: https://docs.browserless.io/baas/versions

Browserless workers are updated periodically with new browser and library versions. Matching your client library version to the versions running on our workers is important, especially for Playwright's `connect` method, which is tightly version-coupled. If your code is unexpectedly crashing, version mismatch is a common cause.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Puppeteer or Playwright installed locally

## Current versions used in usage based workers

Last update: August 11, 2026.

1. **Puppeteer**:  `25.4.0`
2. **Playwright**:  `1.62.1`, `1.61.1`, `1.60.0`, `1.59.1`, `1.58.2`, `1.57.0`, `1.56.1`, `1.55.1`, `1.54.2`, `1.53.2`, `1.52.0`, `1.51.1`, `1.50.1`, `1.49.1`, `1.48.2`, `1.47.2`, `1.46.1`, `1.45.3`, `1.44.1`
3. **Chromium**:  `151.0.7922.34`
4. **Chrome**: `151.0.7922.71`
5. **Firefox**:  `153.0`
6. **Webkit**:  `26.5`
7. **Edge**:  `151.0.4129.59`

> **Info**
> If your code is mysteriously crashing, try using the exact version mentioned above, as some older but especially more recent versions than the above may cause conflicts.

## Playwright's versioning

Playwright's [`connect` method](https://playwright.dev/docs/api/class-browsertype#browser-type-connect) is tightly version-coupled. Your client version must be one of the versions listed above.

Playwright's `connectOverCDP` does not always need to match the same Playwright version on our workers. This method is generally backwards/forwards compatible since it communicates directly through CDP, with the caveat of being slightly slower due to more communication over the DevTools protocol.

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>403 Forbidden</code> error?</summary>

Your API token is missing or expired. Pass it as a `?token=` query parameter in the WebSocket or HTTP URL. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My script works locally but fails on Browserless</summary>

Local browser settings may differ from the Browserless environment. Use `launch` parameters to match your local setup (viewport, user agent, timezone). See [launch parameters](/baas/launch-options) for the full list.

</details>

## Next steps

  <a href="/baas/best-practices" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Best Practices</h3>
      Patterns for reliable browser automation.
    
  </a>

  <a href="/baas/best-practices#timeouts" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Timeouts</h3>
      Configure session and action timeouts.
    
  </a>

---

# BAP

URL: https://docs.browserless.io/bap

# BAP

Browser Automation Protocol (BAP) is Browserless's automation layer on top of BrowserQL: managed stealth, residential proxies, CAPTCHA solving, and live debugging URLs, driven through a typed, Puppeteer- or Playwright-shaped API instead of hand-written GraphQL. Pick a language below, or send BQL documents directly if you don't want an SDK in the loop.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- npm 11.10 or later for the TypeScript SDK, or Python 3.11 or later for the Python SDK

## How it works

BAP doesn't connect over Chrome DevTools Protocol. Each `Page` method builds a BrowserQL mutation, sends it as one JSON frame over a WebSocket, and awaits the response. Operations are queued and run serially, matching BrowserQL's server-side concurrency model.

That architecture is why BAP is fast over the wire: a CDP session sends thousands of small messages for a single interaction, while BAP sends one frame per method call. It's also why the method set is a subset of Puppeteer's and Playwright's. `Page` exposes what BQL can express, and nothing that needs a live CDP session.

Each new page opens its own WebSocket connection. Nothing connects when you call `Browserless.connect()`.

## Which one should I use?

Reach for the **TypeScript** or **Python SDK** first: you get autocomplete, typed options and responses, and method names that match Puppeteer and Playwright respectively, so a mismatched argument fails at the call site instead of silently sending the wrong GraphQL variable.

In **TypeScript**, every option and response is typed, so mistakes surface as compile-time errors rather than runtime GraphQL errors. The package is isomorphic and runs in Node.js or a browser bundle. In **Python**, options and response fields are generated straight from the schema, so a mismatched keyword argument fails at call time, and the package ships matching async (`bap`) and sync (`bap.sync_api`) surfaces. The [Quickstart's runtime notes](/bap/getting-started#runtime-notes) cover both.

Use **BrowserQL directly** when you're not writing TypeScript or Python, want to generate BQL documents from another tool, or are working in the hosted IDE. Every BAP method is a thin wrapper over a BQL mutation, so anything BAP can do, BQL can do too.

Already have Puppeteer or Playwright code you don't want to rewrite? None of these three wrap BQL: connect to [Browsers as a Service](/baas/start) over CDP instead, if you don't need BQL's managed stealth, proxying, or CAPTCHA solving.

## Guides

The guides apply to both SDKs, with TypeScript and Python examples side by side. Start with the [Quickstart](/bap/getting-started), then pick the task in front of you:

## FAQ & Troubleshooting

<details>
<summary>What's the difference between BAP and BrowserQL?</summary>

BrowserQL (BQL) is the GraphQL protocol. BAP is a typed SDK, in TypeScript or Python, built on top of it. Every BAP method sends a BQL mutation under the hood, so anything you can do with BAP, you can do with BQL directly, and vice versa.

</details>

<details>
<summary>Which language should I pick if I don't have a preference?</summary>

Either is fully supported and generated from the same BrowserQL schema, so pick whichever matches the rest of your stack. There's no feature gap between the two SDKs.

</details>

---

# Custom transports in BAP

URL: https://docs.browserless.io/bap/advanced/custom-transports

# Custom transports in BAP

The TypeScript SDK sends every BrowserQL frame through a transport, and you can supply your own, the same way Puppeteer accepts a custom `transport`. That's the hook for logging frames, injecting auth at the socket layer, tunneling through infrastructure that plain WebSockets can't cross, or mocking the server in tests.

> Custom transports are a TypeScript SDK feature. The Python SDK always uses its built-in WebSocket transport.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- The TypeScript SDK installed and connecting, from the [BAP Quickstart](/bap/getting-started)

## The transport contract

A transport implements `ConnectionTransport`, modeled on Puppeteer's interface: `send()` writes one text frame, `close()` tears the connection down, and the library assigns `onmessage`, `onclose`, and `onerror` after creation. Your transport must call `onmessage` for every frame it receives. The frames are `graphql-transport-ws` envelopes; the transport is just the pipe, and the SDK does the framing on top. A WebSocket-based transport must offer `graphql-transport-ws` as its subprotocol, because a server that sees none falls back to its legacy response protocol and the handshake breaks.

You supply a **factory**, not an instance, through the `transport` option on `Browserless.connect()`. Nothing connects at `connect()` time. On each `newPage()`:

1. The endpoint URL is resolved, with `?token=` appended when a `token` is set.
2. Your factory is called once with that URL.
3. The library awaits the factory, so resolve only when the connection is open. Frames are sent the moment it resolves.
4. The handlers are assigned and the transport belongs to that page.

`browserWSEndpoint` and `transport` aren't two connections. The endpoint is the argument handed to your factory, which gives you two patterns.

## Pattern 1: wrap the endpoint connection

Set `browserWSEndpoint` as usual and read the factory's `url` argument. This decorates the default connection, which is how you add logging, metrics, or extra headers without changing anything else:

```typescript

  type ConnectionTransport,
} from "@browserless.io/bap-ts";

const TOKEN = "YOUR_API_TOKEN_HERE";

// A minimal transport, modeled on Puppeteer's ConnectionTransport.
class LoggingTransport implements ConnectionTransport {
  onmessage?: (message: string) => void;
  onclose?: () => void;
  onerror?: (error: unknown) => void;

  private constructor(private readonly ws: WebSocket) {
    ws.addEventListener("message", (e) => this.onmessage?.(String(e.data)));
    ws.addEventListener("close", () => this.onclose?.());
    ws.addEventListener("error", (e) => this.onerror?.(e));
  }

  static create(url: string): Promise {
    return new Promise((resolve, reject) => {
      // Offer the graphql-transport-ws subprotocol: the SDK's frames are that
      // protocol's envelopes, and a server seeing no subprotocol falls back to
      // its legacy protocol and the handshake breaks.
      const ws = new WebSocket(url, "graphql-transport-ws");
      // Resolve only once open: the SDK sends frames as soon as this settles.
      ws.addEventListener("open", () => resolve(new LoggingTransport(ws)), {
        once: true,
      });
      ws.addEventListener("error", reject, { once: true });
    });
  }

  send(message: string): void {
    console.log("→", message);
    this.ws.send(message);
  }

  close(): void {
    this.ws.close();
  }
}

const browser = Browserless.connect({
  browserWSEndpoint: "wss://production-sfo.browserless.io/chromium/bql",
  token: TOKEN,
  // url is the endpoint above with the token already appended.
  transport: (url) => LoggingTransport.create(url!),
});

const page = await browser.newPage();
await page.goto("https://example.com");

// close() ends the session and calls your transport's close().
await browser.close();
```

**Response**, one line per outbound frame:

```text
→ {"id":"1","type":"subscribe","payload":{"query":"mutation ..."}}
```

The built-in `WebSocketTransport` is exported, so wrapping beats hand-rolling a socket when all you want is a hook around the default behavior. `create(url, headers?, protocols?)` doesn't offer a subprotocol on its own, so pass `graphql-transport-ws` explicitly. The default factory passes that same subprotocol, along with a `User-Agent` header this wrapper leaves off:

```typescript

Browserless.connect({
  browserWSEndpoint: "wss://production-sfo.browserless.io/chromium/bql",
  token: TOKEN,
  transport: (url) =>
    WebSocketTransport.create(url!, undefined, "graphql-transport-ws"),
});
```

## Pattern 2: let the transport own the connection

Omit `browserWSEndpoint` entirely. The factory's `url` argument is then `undefined` and your transport connects wherever it likes: a tunnel, a reused socket, or a mock server in tests.

```typescript
const browser = Browserless.connect({
  // No endpoint: the transport knows where to connect.
  transport: () => MyTunnelTransport.create(),
});
```

Pick exactly one pattern. Supplying both an endpoint and a transport that ignores its `url` argument leaves the endpoint silently unused, which reads like a configuration bug to whoever maintains it next.

## One transport per page

The factory runs again for every `newPage()`, so one page maps to one transport and typically one connection. Return a new instance on every call. The library assigns `onmessage`, `onclose`, and `onerror` on whatever the factory returns, so handing back the same instance across pages overwrites the earlier pages' handlers and cross-wires their frames. Share an underlying connection across pages only if your transport multiplexes frames per page itself.

## FAQ & Troubleshooting

<details>
<summary>My transport connects but the first operation hangs</summary>

The factory resolved before the connection was open. The SDK writes frames immediately after awaiting the factory, so resolve on the socket's `open` event, not on construction.

</details>

<details>
<summary>Two pages are receiving each other's responses</summary>

The factory returned the same transport instance for both pages, so the second page's handler assignment overwrote the first's. Return a fresh instance per call, or multiplex inside the transport.

</details>

<details>
<summary>Where did my token go in pattern 2?</summary>

Token appending only happens when the SDK resolves `browserWSEndpoint`. When your transport owns the connection, authentication is your transport's job, which is often the very reason to use pattern 2.

</details>

<details>
<summary>Can I use this to correlate subscription frames?</summary>

Yes, and it's the right layer for it. BrowserQL is serial request/response, so [`page.on('message')`](/bap/advanced/raw-browserql) sees every frame but can't tell you which in-flight operation a push belongs to. A transport sits below that and can track operation ids itself.

</details>

## Next steps

---

# Dropping down to raw BrowserQL

URL: https://docs.browserless.io/bap/advanced/raw-browserql

# Dropping down to raw BrowserQL

Every BAP method is a typed wrapper around one BrowserQL mutation, and `page.send()` is the door past the wrappers: it runs any GraphQL document you hand it on the same session. Reach for it when a BQL mutation has no BAP method yet, when a new server feature lands before the SDK release that wraps it, or when you want several steps in one round trip.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- The BAP SDK installed and connecting, from the [BAP Quickstart](/bap/getting-started)
- Basic familiarity with [writing BrowserQL](/browserql/writing-bql/language-basics)

## Run a raw mutation

`send()` takes a GraphQL document string and resolves with the operation's `data` object. It rides the page's existing WebSocket, so whatever your typed calls did before it, the raw mutation sees: same page, same cookies, same session.

**TypeScript:**

```typescript

const TOKEN = "YOUR_API_TOKEN_HERE";

const browser = Browserless.connect({
  browserWSEndpoint: "wss://production-sfo.browserless.io/chromium/bql",
  token: TOKEN,
});

const page = await browser.newPage();

// The generic parameter types the resolved data object.
const data = await page.send<{ goto: { status: number } }>(
  `mutation Visit($url: String!) {
    goto(url: $url) { status }
  }`,
  { variables: { url: "https://example.com" } },
);

console.log(data.goto.status);
await browser.close();
```

**Python (Coming soon):**

```python

TOKEN = "YOUR_API_TOKEN_HERE"

with bap.Browserless.connect(
    browser_ws_endpoint="wss://production-sfo.browserless.io/chromium/bql",
    token=TOKEN,
) as browser:
    with browser.page() as page:
        data = page.send(
            """
            mutation Visit($url: String!) {
              goto(url: $url) { status }
            }
            """,
            variables={"url": "https://example.com"},
        )

        print(data["goto"]["status"])
```

**Response:**

```text
200
```

Pass values through `variables` instead of interpolating them into the document. The server coerces and validates variables by type, and the operation string stays constant, which also keeps user input from turning into query syntax.

Errors behave like every other BAP call: GraphQL errors reject with `BrowserQLError`, and the optional `timeout` option bounds the operation like any typed method, as covered in [error handling](/bap/error-handling).

## Batch steps into one round trip

Typed methods run one mutation per call. A raw document can carry several fields, and BrowserQL executes them in order inside a single operation, which cuts latency on chatty sequences and guarantees no client-side gap between steps:

**TypeScript:**

```typescript
// One round trip: navigate, wait, and extract.
const data = await page.send<{
  goto: { status: number };
  waitForSelector: { time: number };
  html: { html: string };
}>(`mutation ScrapeInOneTrip {
  goto(url: "https://example.com") { status }
  waitForSelector(selector: "h1") { time }
  html(selector: "main") { html }
}`);

console.log(data.html.html.length);
```

**Python (Coming soon):**

```python
# One round trip: navigate, wait, and extract.
data = page.send(
    """
    mutation ScrapeInOneTrip {
      goto(url: "https://example.com") { status }
      waitForSelector(selector: "h1") { time }
      html(selector: "main") { html }
    }
    """
)

print(len(data["html"]["html"]))
```

The [BrowserQL schema](/bql-schema/schema) lists every mutation and its arguments, and the [BQL IDE](/browserql/using-the-ide/ide-features) is the fastest place to prototype a document before pasting it into `send()`.

## Observe raw frames

Every frame the transport receives, including server-pushed frames that don't answer a request, is visible through `page.on('message')`. `subscribe()` is the same listener with an unsubscribe function returned. Both SDKs expose the pair. The Python spellings are `page.on("message", listener)` and `page.subscribe(listener)`:

```typescript
const page = await browser.newPage();

// Every incoming frame, including pushes.
page.on("message", (frame) => {
  console.log("frame:", frame);
});

// Same stream, unsubscribe-style.
const unsubscribe = page.subscribe((frame) => console.log(frame));

await page.send('mutation { goto(url: "https://example.com") { status } }');

unsubscribe();
// page.off("message", listener) removes a named listener too.

// The demo page holds a socket and a concurrency slot like any other.
await page.close();
```

One protocol fact to respect: BrowserQL is serial request/response, so a pushed frame that arrives while a request is in flight is consumed as that request's response. `on('message')` shows you everything, but correlating subscription traffic beyond that is up to your [custom transport](/bap/advanced/custom-transports).

## FAQ & Troubleshooting

<details>
<summary>How do I know what mutations and arguments exist?</summary>

The [BrowserQL schema reference](/bql-schema/schema) documents every mutation, argument, and response type. BAP's generated method table maps each SDK method to its source mutation, so anything in the schema without a method is `send()` territory.

</details>

<details>
<summary>My document works in the BQL IDE but <code>send()</code> rejects it</summary>

Check that the variables you pass match the declared types exactly, including required `!` markers. A `BrowserQLError` from `send()` carries the server's GraphQL errors, which name the argument or field that failed validation.

</details>

<details>
<summary>Does <code>send()</code> bypass BAP's timeouts?</summary>

No. The connection-level default applies, and the `timeout` option on `send()` overrides it per call, exactly like a typed method.

</details>

<details>
<summary>Can Python observe raw frames too?</summary>

Yes. Python has the same pair: `page.on("message", listener)` receives every transport frame, and `page.subscribe(listener)` is the same stream with an unsubscribe callable returned. The `message` event and `subscribe()` exist on both the sync and async `Page`.

</details>

## Next steps

---

# Solving CAPTCHAs with BAP

URL: https://docs.browserless.io/bap/bot-detection/captchas

# Solving CAPTCHAs with BAP

BAP solves CAPTCHAs inside the running session, so the page that was blocked is the page that continues once the challenge clears. This guide covers `solve()` for interactive challenges like Cloudflare and reCAPTCHA, and `solveImageCaptcha()` for image-plus-input forms.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- The BAP SDK installed and connecting, from the [BAP Quickstart](/bap/getting-started)
- A plan with CAPTCHA solving, since solves consume units

## Solve an interactive challenge

`solve()` waits for a CAPTCHA to appear, solves it, and resolves with what happened. With no `type` it auto-detects the challenge, which is the right default because protected sites rotate providers. Pass a `type` when you know the site and want to skip detection.

**TypeScript:**

```typescript

const TOKEN = "YOUR_API_TOKEN_HERE";

const browser = Browserless.connect({
  browserWSEndpoint: "wss://production-sfo.browserless.io/stealth/bql",
  token: TOKEN,
});

try {
  const page = await browser.newPage();
  await page.goto("https://example.com/protected");

  // Auto-detects the challenge type. timeout bounds the wait for the
  // CAPTCHA to appear, not the solve itself.
  const result = await page.solve({ timeout: 30_000 });

  if (result.solved) {
    // The session continues where the challenge was.
    console.log(await page.title());
  } else if (result.found === false) {
    // found is nullable: false means no CAPTCHA, null means no value came back.
    console.log("no captcha appeared");
  } else {
    console.error("solve failed:", result.error);
  }
} finally {
  // A rejected goto() or solve() would otherwise leave the session billing.
  await browser.close();
}
```

**Python (Coming soon):**

```python

TOKEN = "YOUR_API_TOKEN_HERE"

with bap.Browserless.connect(
    browser_ws_endpoint="wss://production-sfo.browserless.io/stealth/bql",
    token=TOKEN,
) as browser:
    with browser.page() as page:
        page.goto("https://example.com/protected")

        # Auto-detects the challenge type. timeout bounds the wait for the
        # CAPTCHA to appear, not the solve itself.
        result = page.solve(timeout=30000)

        if result["solved"]:
            # The session continues where the challenge was.
            print(page.title())
        elif result["found"] is False:
            # found is nullable: False means no CAPTCHA, None means no value came back.
            print("no captcha appeared")
        else:
            print("solve failed:", result["error"])
```

**Response** when the solve succeeds:

```text
Protected Page Title
```

### Reading the result

`solve()` doesn't throw when a challenge can't be cleared. It reports through the response, so branch on the fields:

| Field | Meaning |
| ----- | ------- |
| `found` | Whether a CAPTCHA was present at all. `false` with `solved: false` means the page wasn't challenged |
| `solved` | Whether the found CAPTCHA was cleared |
| `token` | The solve token, when the provider issues one you need to submit yourself |
| `time` | Total milliseconds to find and solve |
| `error` | What went wrong, with guidance on fixing it |

### Supported types

Auto-detection covers the common providers, including Cloudflare, reCAPTCHA v2 and v3, GeeTest, DataDome, and Akamai. When targeting explicitly, `type` accepts any value of the schema-generated [`CaptchaType`](/bap/typescript/reference/type-aliases/CaptchaType) enum. Prefer auto-detect unless a site stacks multiple widgets and you need to pick one.

### Waiting behavior

`wait` defaults to `true`, so `solve()` blocks until a CAPTCHA shows up or `timeout` elapses. That makes the click-then-solve pattern safe: trigger the action that provokes the challenge, then call `solve()`, and it catches the CAPTCHA whenever it renders. Pass `wait: false` to check for an already-visible challenge without blocking, which suits polling loops that mix solving with other work.

## Solve an image CAPTCHA

Forms that show a distorted image next to a text input need `solveImageCaptcha()`. You point it at both elements and it fills the input with the solved value.

**TypeScript:**

```typescript
await page.solveImageCaptcha({
  captchaSelector: "#captcha-image",
  inputSelector: "#captcha-answer",
  timeout: 30_000,
});

await page.click("#submit");
```

**Python (Coming soon):**

```python
page.solve_image_captcha(
    captcha_selector="#captcha-image",
    input_selector="#captcha-answer",
    timeout=30000,
)

page.click("#submit")
```

Both selectors are required. `timeout` covers the whole operation here, appearance and solve together, unlike `solve()` where it only bounds the wait for the challenge to appear.

## Avoid CAPTCHAs before solving them

Solving costs time and units, so treat it as the last layer, not the first. Sessions on the [stealth endpoint with a residential proxy](/bap/bot-detection/stealth) get challenged far less than plain Chromium, and humanized typing keeps a cleared session from being re-challenged mid-form. The cheapest CAPTCHA is the one that never renders.

## FAQ & Troubleshooting

<details>
<summary><code>solve()</code> returns <code>found: false</code> but I can see the CAPTCHA</summary>

The challenge is probably inside a frame or shadow root that rendered after your call, or it's a provider auto-detection missed. Raise `timeout` so detection has longer to catch a slow-rendering widget, or pass the provider explicitly with `type`.

</details>

<details>
<summary>The CAPTCHA was solved but the page still blocks me</summary>

Some providers bind the solve to browsing behavior after the challenge. Keep the session on the stealth endpoint, keep the same proxy IP with `sticky: true`, and interact with default humanization rather than jumping straight to a protected URL.

</details>

<details>
<summary>Do failed solves throw an exception?</summary>

No. `solve()` reports through `solved`, `found`, and `error` on the response, so check those fields. Only transport failures and timeouts raise, as covered in [error handling](/bap/error-handling).

</details>

<details>
<summary>What do I do with the <code>token</code> field?</summary>

Usually nothing: the solve is applied inside the page. Providers like reCAPTCHA v3 hand back a token your own form submission may need to include, which is when you read it from the response and inject it yourself.

</details>

## Next steps

---

# OS emulation in BAP

URL: https://docs.browserless.io/bap/bot-detection/os-emulation

# OS emulation in BAP

Browserless sessions run on Linux, so they report a Linux fingerprint by default. The `emulationOs` query parameter switches the session to a coherent Windows, macOS, or Android identity, and BAP takes it on the connection URL. This guide covers setting it at connect time, emulating a phone, and confirming what the session actually reports.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- The BAP SDK installed and connecting, from the [BAP Quickstart](/bap/getting-started)

## Set the OS on the connection URL

`emulationOs` is a query parameter on the endpoint, not a connect option or a method, so it goes straight into `browserWSEndpoint`. The SDK appends `&token=` when the endpoint already carries a query string, so the two coexist without you building the URL by hand.

There's no route to get wrong here. BAP only speaks to `/bql` endpoints, and every one of them runs stealth-hardened, which is where `emulationOs` takes effect. On plain CDP routes like `/chromium` the parameter does nothing, but BAP can't connect to those in the first place.

**TypeScript:**

```typescript

const TOKEN = "YOUR_API_TOKEN_HERE";

const browser = Browserless.connect({
  // emulationOs is a URL parameter, so it belongs on the endpoint itself.
  browserWSEndpoint:
    "wss://production-sfo.browserless.io/chromium/bql?emulationOs=windows",
  token: TOKEN,
});

try {
  const page = await browser.newPage();
  await page.goto("https://example.com");

  console.log(await page.evaluate("navigator.platform"));
} finally {
  // Close even when an operation rejects, or the session keeps billing.
  await browser.close();
}
```

**Python (Coming soon):**

```python

TOKEN = "YOUR_API_TOKEN_HERE"

with bap.Browserless.connect(
    # emulationOs is a URL parameter, so it belongs on the endpoint itself.
    browser_ws_endpoint="wss://production-sfo.browserless.io/chromium/bql?emulationOs=windows",
    token=TOKEN,
) as browser:
    with browser.page() as page:
        page.goto("https://example.com")

        print(page.evaluate("navigator.platform"))
```

**Response:**

```text
Win32
```

`connect()` opens no socket. `newPage()` is what provisions the session, so the emulated identity is already applied to the page it hands back and you never have to wait for it to settle.

## Accepted values

| Value | Emulated identity |
| ----- | ----------------- |
| `windows` | Windows / Chrome on Win64 |
| `macos` | macOS / Chrome on Intel Mac |
| `linux` | Native Linux identity (the default when omitted) |
| `android` | Mobile Chrome on Android, covered [below](#emulate-an-android-phone) |

Values are exact-match and lowercase, so `macOS` is not `macos`. iPhone, iOS, and Safari aren't supported. Setting `emulationOs` swaps a whole stack of signals at once, including the User-Agent, UA Client Hints, `navigator.platform`, GPU renderer, CPU core count, fonts, voices, and audio latency, and they stay consistent for the life of the session. The [BQL OS emulation guide](/browserql/bot-detection/os-emulation#what-gets-spoofed) has the full list.

## Emulate an Android phone

`emulationOs=android` is the one value that changes more than OS-derived signals: it also applies a phone viewport, a high device pixel ratio, touch input, and portrait orientation. The same script becomes a mobile session with no other edits, which is what makes responsive pages reflow to their mobile layout. Swap the one value on the endpoint:

```bash
wss://production-sfo.browserless.io/chromium/bql?emulationOs=android
```

Reading the device metrics back confirms the session is really a phone:

**TypeScript:**

```typescript
await page.goto("https://example.com");

// evaluate() returns a string, so serialize the signals inside the page.
console.log(await page.evaluate(`JSON.stringify({
  platform: navigator.platform,
  width: window.innerWidth,
  dpr: window.devicePixelRatio,
  touch: navigator.maxTouchPoints
})`));
```

**Python (Coming soon):**

```python
page.goto("https://example.com")

# evaluate() returns a string, so serialize the signals inside the page.
print(page.evaluate("""JSON.stringify({
  platform: navigator.platform,
  width: window.innerWidth,
  dpr: window.devicePixelRatio,
  touch: navigator.maxTouchPoints
})"""))
```

**Response:**

```text
{"platform":"Linux armv81","width":412,"dpr":3.5,"touch":5}
```

The exact numbers depend on which phone you get. Omit `emulatedDevice` and Browserless picks a real device and keeps it stable for the session, or name one to pin it:

```bash
wss://production-sfo.browserless.io/chromium/bql?emulationOs=android&emulatedDevice=pixel-8-pro
```

Device slugs are case-sensitive and matched exactly. See [supported mobile devices](/baas/bot-detection/supported-mobile-devices) for the list. `emulatedDevice` requires `emulationOs=android`, and sending it alone is rejected. Because `connect()` opens no socket, that rejection surfaces on the call that does: `newPage()` in TypeScript, entering `browser.page()` in Python.

## What it doesn't change

`emulationOs` only sets OS-derived browser signals. Timezone, locale, and `Accept-Language` follow your [proxy](/bap/bot-detection/stealth#route-traffic-through-a-proxy)'s location instead, and WebRTC handling is applied uniformly to every stealth session. A Windows identity served from a mismatched timezone is itself a detection signal, so match the proxy region to the OS you're emulating. On a session connected with `?emulationOs=macos`, call `proxy()` before the navigation you want proxied:

**TypeScript:**

```typescript
// Pair the emulated OS with a region-matched IP for a coherent profile.
await page.proxy({ country: "US", sticky: true, type: ["document"] });
await page.goto("https://example.com");
```

**Python (Coming soon):**

```python
# Pair the emulated OS with a region-matched IP for a coherent profile.
page.proxy(country="US", sticky=True, type=["document"])
page.goto("https://example.com")
```

## FAQ & Troubleshooting

<details>
<summary>I set <code>emulationOs</code> but <code>navigator.platform</code> still says Linux</summary>

Check the casing and spelling first, since values are exact-match lowercase and `macOS` isn't `macos`. Then confirm the parameter survived into the URL you passed as `browserWSEndpoint`, because it's easy to lose when the endpoint is built from string concatenation. Read the identity back with `evaluate("navigator.platform")` to confirm.

</details>

<details>
<summary>Can I change the emulated OS mid-session?</summary>

No. It's fixed when the socket opens, so switching means closing the browser and calling `connect()` again with a different endpoint. Run the two sessions separately when you need to compare a desktop and a mobile identity.

</details>

<details>
<summary>Which <code>/bql</code> route should I use it on?</summary>

Any of them: `/chromium/bql`, `/chrome/bql`, and `/stealth/bql`. Start on `/chromium/bql` and move to `/stealth/bql` when a site still challenges you, the same escalation described in [stealth and humanization](/bap/bot-detection/stealth).

</details>

<details>
<summary>Does emulating Android change how I interact with the page?</summary>

No. `click()` and `type()` work the same, and both still scroll the target into view first. What changes is the layout you're targeting: a responsive site serves its mobile DOM, so selectors written against the desktop layout may not exist.

</details>

## Next steps

---

# Stealth and humanization in BAP

URL: https://docs.browserless.io/bap/bot-detection/stealth

# Stealth and humanization in BAP

BAP inherits BrowserQL's bot-detection tooling: a managed stealth browser, interaction methods that behave like a person by default, and a proxy method with geo-targeting. This guide covers how to combine the three when a target site blocks plain automation.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- The BAP SDK installed and connecting, from the [BAP Quickstart](/bap/getting-started)
- Residential proxy access on your plan for the proxy examples

## Connect to the stealth browser

Switching to stealth is an endpoint change, not a code change. The `/stealth/bql` route runs a browser whose fingerprint Browserless manages, which is why there's no stealth flag in the SDK: patched automation libraries leave detectable traces, so the fingerprint work happens in the browser build instead of your script.

**TypeScript:**

```typescript

const TOKEN = "YOUR_API_TOKEN_HERE";

const browser = Browserless.connect({
  // Same SDK, stealth browser: only the endpoint path changes.
  browserWSEndpoint: "wss://production-sfo.browserless.io/stealth/bql",
  token: TOKEN,
});

try {
  const page = await browser.newPage();
  await page.goto("https://example.com");
  console.log(await page.title());
} finally {
  // Close even when an operation rejects, or the session keeps billing.
  await browser.close();
}
```

**Python (Coming soon):**

```python

TOKEN = "YOUR_API_TOKEN_HERE"

with bap.Browserless.connect(
    # Same SDK, stealth browser: only the endpoint path changes.
    browser_ws_endpoint="wss://production-sfo.browserless.io/stealth/bql",
    token=TOKEN,
) as browser:
    with browser.page() as page:
        page.goto("https://example.com")
        print(page.title())
```

**Response:**

```text
Example Domain
```

Start with `/chromium/bql` and move to `/stealth/bql` when a site challenges or blocks you. Stealth sessions cost more units, so there's no reason to pay for them against sites that don't check.

## Interactions are humanized by default

BAP's `type()` and `click()` run inside the browser with human-shaped behavior already on:

- `type()` delays each keystroke by a random value from the `delay` range, defaulting to `[50, 200]` milliseconds.
- Both methods scroll the element into view first (`scroll: true`), the way a person would have to.
- `interactable: true` on `type()` hovers the element and confirms it's actually reachable at that position before typing.

You tune this rather than build it. Widen the delay range on sensitive forms, or set `delay: [0, 0]` in trusted environments where speed matters more than looking human.

**TypeScript:**

```typescript
await page.goto("https://example.com/login");

// A wider delay range reads as slower, more deliberate typing.
await page.type("#email", "user@example.com", { delay: [80, 250] });
await page.type("#password", "hunter2", { delay: [80, 250] });
await page.click("#submit");
```

**Python (Coming soon):**

```python
page.goto("https://example.com/login")

# A wider delay range reads as slower, more deliberate typing.
page.type("#email", "user@example.com", delay=[80, 250])
page.type("#password", "hunter2", delay=[80, 250])
page.click("#submit")
```

## Route traffic through a proxy

`proxy()` routes matching requests through the Browserless residential proxy network with geo-targeting, or through your own proxy server. Like [`reject()`](/bap/network-control), call it before the navigation whose traffic you want proxied.

**TypeScript:**

```typescript
// Proxy only document requests through a US residential IP, and keep
// that IP for the whole session.
await page.proxy({
  country: "US",
  sticky: true,
  type: ["document"],
});

await page.goto("https://example.com");
```

**Python (Coming soon):**

```python
# Proxy only document requests through a US residential IP, and keep
# that IP for the whole session.
page.proxy(country="US", sticky=True, type=["document"])

page.goto("https://example.com")
```

The options that matter most:

| Option | Default | What it does |
| ------ | ------- | ------------ |
| `country`, `state`, `city` | — | Geo-target the exit IP. State and city are lowercase with spaces removed, such as `"newyorkcity"` |
| `network` | `residential` | Pool to use: `residential` or `datacenter`. Residential is harder to detect; datacenter is cheaper |
| `sticky` | — | Reuse the same IP for subsequent matching requests, which you want whenever the site ties session state to IP |
| `url`, `type`, `method` | — | Proxy only matching requests. Scoping to `["document"]` keeps page loads geo-correct while static assets skip the metered proxy |
| `server` | — | Use your own proxy instead. Geo-targeting and `sticky` throw when combined with this, since they only apply to the Browserless network |

Selective proxying is the cost lever: residential bandwidth is metered at a higher unit rate than datacenter, and a page's images and scripts usually don't need to come from a residential IP. Scope with `type` or `url` unless the site fingerprints asset requests too. The [BQL proxies guide](/browserql/bot-detection/proxies) owns the per-network unit pricing, and the generated [`ProxyOptions` reference](/bap/typescript/reference/interfaces/ProxyOptions) covers the remaining options, including website-specific `preset` codes.

## Layer the three together

Sites with serious detection get the full stack. Order matters only in that proxy rules should exist before navigation:

**TypeScript:**

```typescript
const browser = Browserless.connect({
  browserWSEndpoint: "wss://production-sfo.browserless.io/stealth/bql",
  token: TOKEN,
});

try {
  const page = await browser.newPage();
  await page.proxy({ country: "GB", sticky: true, type: ["document", "xhr", "fetch"] });

  await page.goto("https://example.com/account");
  await page.type("#user", "name@example.com", { delay: [80, 250] });
  await page.click("#next");
} finally {
  await browser.close();
}
```

**Python (Coming soon):**

```python
with bap.Browserless.connect(
    browser_ws_endpoint="wss://production-sfo.browserless.io/stealth/bql",
    token=TOKEN,
) as browser:
    with browser.page() as page:
        page.proxy(country="GB", sticky=True, type=["document", "xhr", "fetch"])

        page.goto("https://example.com/account")
        page.type("#user", "name@example.com", delay=[80, 250])
        page.click("#next")
```

If a CAPTCHA still appears after this, solve it in the session with [BAP's CAPTCHA methods](/bap/bot-detection/captchas) rather than starting over.

## FAQ & Troubleshooting

<details>
<summary>I'm still being detected on <code>/chromium/bql</code></summary>

That's the expected escalation path. Move to `/stealth/bql`, add a residential proxy with `sticky: true`, and keep the default humanization on `type()` and `click()`. If a CAPTCHA appears, [solve it](/bap/bot-detection/captchas) instead of retrying the navigation.

</details>

<details>
<summary>Why does <code>proxy()</code> throw when I pass <code>country</code> with <code>server</code>?</summary>

`country`, `state`, `city`, and `sticky` only apply to the Browserless proxy network. With `server` you're using your own proxy, so geo-targeting belongs to that proxy's configuration instead.

</details>

<details>
<summary>My session's IP changed between requests</summary>

Pass `sticky: true`. Without it, each matching request can exit through a different IP in the pool, which sites that bind cookies or carts to an IP will treat as suspicious.

</details>

<details>
<summary>Does typing faster risk detection?</summary>

On protected sites, yes. The default `[50, 200]` millisecond range exists because instant keystrokes are a classic automation signal. Narrow it only where you've confirmed the target doesn't score input timing.

</details>

## Next steps

---

# Error handling in BAP

URL: https://docs.browserless.io/bap/error-handling

# Error handling in BAP

BAP raises typed errors, so a dead WebSocket, a slow page, and a failed BrowserQL mutation each surface as a different class. This guide shows how to catch and branch on each type, tune the timeouts that produce them, and read the GraphQL errors BrowserQL returns.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- The BAP SDK installed and connecting, from the [BAP Quickstart](/bap/getting-started)

## The error types

Each failure gets its own class, so an `instanceof` check in TypeScript or an `except` clause in Python tells you which stage failed. Python's errors also share the `BAPError` base for catch-all handling.

**TypeScript:**

| Error | Thrown when |
| ----- | ----------- |
| `ConnectionError` | The WebSocket fails to open, drops mid-session, or is unavailable |
| `TimeoutError` | An operation exceeds its transport deadline |
| `BrowserQLError` | The BrowserQL server returns GraphQL errors. The `errors` property holds the raw GraphQL error objects |

All three are exported from `@browserless.io/bap-ts` alongside the default export.

**Python (Coming soon):**

| Error | Raised when |
| ----- | ----------- |
| `BAPError` | Base class for every error the SDK produces |
| `ConnectionError` | The transport fails to connect or disconnects |
| `TimeoutError` | An operation exceeds its transport deadline |
| `BrowserQLError` | The GraphQL operation returned one or more errors |
| `ValueError` | A public SDK argument failed client-side validation |
| `SchemaMismatchError` | The generated schema metadata and runtime call site disagree, which usually means the installed package is out of date for the server you're talking to |

`ConnectionError` and `TimeoutError` share names with Python's builtins. Import them from `bap` explicitly, because an `except` clause written for the builtin will not catch the SDK's version. The [generated errors reference](/bap/python/reference/errors) is the authoritative list.

## Catch and branch on error type

Branching on type tells you what to do next: reconnect on `ConnectionError`, raise the deadline or fix the selector on `TimeoutError`, and read the server's message on `BrowserQLError`.

**TypeScript:**

```typescript

  BrowserQLError,
  ConnectionError,
  TimeoutError,
} from "@browserless.io/bap-ts";

const TOKEN = "YOUR_API_TOKEN_HERE";

const browser = Browserless.connect({
  browserWSEndpoint: "wss://production-sfo.browserless.io/chromium/bql",
  token: TOKEN,
});

try {
  const page = await browser.newPage();
  await page.goto("https://example.com", { timeout: 5000 });
} catch (error) {
  if (error instanceof TimeoutError) {
    console.error("operation timed out");
  } else if (error instanceof ConnectionError) {
    console.error("websocket connection failed");
  } else if (error instanceof BrowserQLError) {
    // The raw GraphQL error objects from the server.
    console.error("browserql returned errors:", error.errors);
  }
} finally {
  await browser.close();
}
```

**Python (Coming soon):**

```python

from bap import BrowserQLError, ConnectionError, TimeoutError

TOKEN = "YOUR_API_TOKEN_HERE"

with bap.Browserless.connect(
    browser_ws_endpoint="wss://production-sfo.browserless.io/chromium/bql",
    token=TOKEN,
) as browser:
    try:
        with browser.page() as page:
            page.goto("https://example.com", timeout=5000)
    except TimeoutError:
        print("operation timed out")
    except ConnectionError:
        print("websocket connection failed")
    except BrowserQLError as error:
        print("browserql returned errors:", error)
```

**Response** on a timeout:

```text
operation timed out
```

## Timeouts

Every operation runs against a deadline of `30000` milliseconds unless you change it. There are three places to do that, and the more specific one wins:

1. **Per connection.** The `timeout` option on `Browserless.connect()` replaces the default for every operation on that browser.
2. **Per session.** `preferences({ timeout })` replaces the session's defaults from inside the page, which is the option to reach for when you're driving a session you didn't open, such as one you [reconnected to](/bap/session-management/reconnects).
3. **Per call.** Navigation, selector, and CAPTCHA methods accept their own `timeout` option, which overrides both of the above for that one call.

**TypeScript:**

```typescript
// Every operation on this browser gets 60 seconds.
const browser = Browserless.connect({
  browserWSEndpoint: "wss://production-sfo.browserless.io/chromium/bql",
  token: TOKEN,
  timeout: 60_000,
});

const page = await browser.newPage();

// This one call gets 10 seconds, overriding the 60 above.
await page.waitForSelector(".results", { timeout: 10_000 });

await browser.close();
```

**Python (Coming soon):**

```python
# Every operation on this browser gets 60 seconds.
with bap.Browserless.connect(
    browser_ws_endpoint="wss://production-sfo.browserless.io/chromium/bql",
    token=TOKEN,
    timeout=60000,
) as browser:
    with browser.page() as page:
        # This one call gets 10 seconds, overriding the 60 above.
        page.wait_for_selector(".results", timeout=10000)
```

Set the connection-level timeout to the slowest operation you consider healthy, then lower it per call where you want to fail fast. A short deadline on `waitForSelector` catches a missing element in seconds instead of holding the session for the full default.

## How BrowserQL failures surface

BAP is a typed wrapper over [BrowserQL](/browserql/start), and BrowserQL reports failures in a GraphQL `errors` array rather than through HTTP status codes. When a mutation your method call maps to comes back with errors, the SDK raises `BrowserQLError` carrying those raw error objects. The message text is written by the BrowserQL server, so it names the mutation and the reason, such as a selector that never matched or a navigation that was blocked.

Two consequences of the GraphQL model are worth knowing before you debug:

- **Navigation doesn't throw on HTTP error statuses.** `goto()` resolves with the response object, so a 404 or 500 from the target site is data, not an exception. Check `response.status` when you need to react to it.
- **The server decides what's fatal.** Some conditions BrowserQL reports as errors are recoverable from the session's point of view. The [BQL error handling guide](/browserql/writing-bql/error-handling) covers how the underlying protocol behaves, which is what `BrowserQLError` is wrapping.

**TypeScript:**

```typescript
const response = await page.goto("https://example.com/missing-page");

// A 404 arrives here, not in a catch block.
if (response && response.status >= 400) {
  console.warn(`target returned ${response.status}`);
}
```

**Python (Coming soon):**

```python
response = page.goto("https://example.com/missing-page")

# A 404 arrives here, not in an except block.
if response and response.status >= 400:
    print(f"target returned {response.status}")
```

## Retrying failed operations

`ConnectionError` is the type worth retrying, because a dropped socket says nothing about your script's logic. Reconnect by creating a fresh page rather than reusing the dead one, since each page owns its own WebSocket. `TimeoutError` and `BrowserQLError` usually mean the page or the script needs to change, so blind retries tend to fail the same way while spending your concurrency.

One constraint on what you retry: the server may have executed a mutation even though its response never reached you, so a retried callback can repeat a click or a form submission. Pass only operations that are safe to run twice, such as navigate-and-read, and reconcile anything non-idempotent explicitly before retrying it.

**TypeScript:**

```typescript

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

async function withRetry(run: () => Promise, attempts = 3): Promise {
  for (let attempt = 1; ; attempt++) {
    try {
      return await run();
    } catch (error) {
      // Only a dropped socket is worth retrying; other errors repeat identically.
      if (!(error instanceof ConnectionError) || attempt === attempts) throw error;
      // Back off between attempts so retries don't hammer a busy service.
      await sleep(1000 * 2 ** (attempt - 1));
    }
  }
}

const title = await withRetry(async () => {
  const page = await browser.newPage();
  try {
    await page.goto("https://example.com");
    return await page.title();
  } finally {
    // Every page holds its own socket and concurrency slot, so close it
    // whether the attempt succeeded or not.
    await page.close();
  }
});
```

**Python (Coming soon):**

```python

from bap import ConnectionError

def with_retry(run, attempts=3):
    for attempt in range(1, attempts + 1):
        try:
            return run()
        except ConnectionError:
            # Only a dropped socket is worth retrying; other errors repeat identically.
            if attempt == attempts:
                raise
            # Back off between attempts so retries don't hammer a busy service.
            time.sleep(2 ** (attempt - 1))

def visit():
    # The context manager closes the page even when the attempt fails.
    with browser.page() as page:
        page.goto("https://example.com")
        return page.title()

title = with_retry(visit)
```

The [retry with exponential backoff example](/examples/retry-backoff) expands this pattern with jitter and queue-aware handling. If the session itself must survive a client-side crash or restart, [reconnect to the running session](/bap/session-management/reconnects) instead of starting over.

## FAQ & Troubleshooting

<details>
<summary>Why did my operation time out after exactly 30 seconds?</summary>

`30000` milliseconds is the default per-operation timeout. Raise it for the whole browser with `timeout` on `Browserless.connect()`, or per call with the `timeout` option on navigation, selector, and CAPTCHA methods. The per-call value wins when both are set.

</details>

<details>
<summary>(Python) My <code>except ConnectionError</code> block never fires</summary>

You're catching Python's builtin `ConnectionError`, not the SDK's. Add `from bap import ConnectionError` to the module doing the catching. The same applies to `TimeoutError`.

</details>

<details>
<summary><code>goto()</code> didn't throw even though the page returned a 500</summary>

That's by design. BrowserQL reports HTTP status through the response object rather than as an error, so a failing target site is data you inspect with `response.status`. Only transport failures, timeouts, and GraphQL errors throw.

</details>

<details>
<summary>What's inside a <code>BrowserQLError</code>?</summary>

The raw GraphQL error objects the BrowserQL server returned, on the `errors` property in TypeScript and in the exception message in Python. Each one names the mutation that failed and why. The [BQL error handling guide](/browserql/writing-bql/error-handling) explains the underlying error format.

</details>

## Next steps

---

# Extracting content with BAP

URL: https://docs.browserless.io/bap/extracting-content

# Extracting content with BAP

BAP has four extraction methods, and picking the right one saves you a parsing step: `text()` for readable content, `html()` for markup, `markdown()` for LLM-ready output, and `mapSelector()` for structured per-element data. This guide covers all four plus the `clean` options that strip a page down before it leaves the browser.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- The BAP SDK installed and connecting, from the [BAP Quickstart](/bap/getting-started)

## Pick a method

| You want | Use | Returns |
| -------- | --- | ------- |
| Readable text, optionally scoped to a selector | `text()` | The text plus timing metadata |
| Markup, optionally scoped and cleaned | `html()` | The HTML plus timing metadata |
| LLM-ready conversion of the page | `markdown()` | The page converted to Markdown |
| One record per matched element | `mapSelector()` | `id`, `class`, `innerText`, and `innerHTML` for every match |

`text()`, `html()`, and `markdown()` accept a `selector` to scope extraction, a `timeout` for how long to wait for that selector, and `visible` to skip elements that aren't rendered. `mapSelector()` differs: its selector is the required first argument, and its options are `timeout` and `wait` only. There is no `visible` option. Extraction happens inside the browser, so what comes back over the wire is already scoped and cleaned rather than a full page you post-process.

## Extract text and HTML

**TypeScript:**

```typescript

const TOKEN = "YOUR_API_TOKEN_HERE";

const browser = Browserless.connect({
  browserWSEndpoint: "wss://production-sfo.browserless.io/chromium/bql",
  token: TOKEN,
});

const page = await browser.newPage();
await page.goto("https://example.com");

// Text of one element. The default selector is the whole document.
const { text } = await page.text({ selector: "h1" });

// Markup scoped to a selector.
const { html } = await page.html({ selector: "main" });

console.log(text, html?.length);
await browser.close();
```

**Python (Coming soon):**

```python

TOKEN = "YOUR_API_TOKEN_HERE"

with bap.Browserless.connect(
    browser_ws_endpoint="wss://production-sfo.browserless.io/chromium/bql",
    token=TOKEN,
) as browser:
    with browser.page() as page:
        page.goto("https://example.com")

        # Text of one element. The default selector is the whole document.
        text = page.text(selector="h1")["text"]

        # Markup scoped to a selector.
        html = page.html(selector="main")["html"]

        print(text, len(html or ""))
```

Playwright-style shorthands exist too: `content()` returns the full HTML as a plain string, and `eval_on_selector(selector)` returns an element's text.

**Response:**

```text
Example Domain 220
```

## Convert a page to Markdown

`markdown()` converts the rendered DOM to Markdown inside the browser, which is usually the cheapest way to feed a page to an LLM: markup noise is gone and the structure survives as headings and lists. Scope it with `selector` when you only want the article body rather than navigation and footers.

**TypeScript:**

```typescript
const { markdown } = await page.markdown({ selector: "main" });
console.log(markdown);
```

**Python (Coming soon):**

```python
markdown = page.markdown(selector="main")["markdown"]
print(markdown)
```

**Response:**

```md
# Example Domain

This domain is for use in illustrative examples in documents.
```

## Clean HTML before it leaves the browser

`text()` and `html()` accept a `clean` option that strips the page down server-side. That matters for LLM pipelines twice over: smaller payloads cross the wire, and fewer tokens reach the model. By default cleaning removes non-text nodes such as scripts, styles, and media, and collapses whitespace. The knobs:

| Option | Default | What it does |
| ------ | ------- | ------------ |
| `removeNonTextNodes` | `true` | Drops scripts, links, video, canvas, and other non-textual nodes |
| `selectors` | the non-text tags (`script`, `style`, `head`, media, …) | DOM selectors to remove while `removeNonTextNodes` is `true`. Passing your own list replaces the default, so keep any default tags you still want dropped |
| `removeAttributes` | `false` | Strips attributes from every node, keeping only structure |
| `attributes` | — | With `mode: "deny"` (default), attributes to remove; with `mode: "allow"`, the only attributes to keep. Takes effect only while `removeNonTextNodes` is `true` |
| `removeRegex` | `true` | Runs the `regexes` list over the output, replacing matches with a space |
| `regexes` | whitespace + HTML comments | Regex patterns (no surrounding `/`) applied in order |

The generated [`CleanInput` reference](/bap/typescript/reference/interfaces/CleanInput) stays current with the schema when these options change.

**TypeScript:**

```typescript
// Keep structure and href targets. This selectors list replaces the
// default non-text set, so exactly nav, footer, and ads are removed.
const { html } = await page.html({
  selector: "main",
  clean: {
    removeAttributes: true,
    mode: "allow",
    attributes: ["href"],
    selectors: ["nav", "footer", ".ads"],
  },
});
```

**Python (Coming soon):**

```python
# Keep structure and href targets. This selectors list replaces the
# default non-text set, so exactly nav, footer, and ads are removed.
# Input types are TypedDicts, so a plain dict with the schema's camelCase keys works.
html = page.html(
    selector="main",
    clean={
        "removeAttributes": True,
        "mode": "allow",
        "attributes": ["href"],
        "selectors": ["nav", "footer", ".ads"],
    },
)["html"]
```

## Get structured data with mapSelector

`mapSelector()` runs your selector against every match and returns one record per element with its `id`, `class` list, `innerText`, and `innerHTML`. One request replaces the query-then-loop pattern, which is the difference between one round trip and one per element.

**TypeScript:**

```typescript
const links = await page.mapSelector("a");

for (const link of links) {
  console.log(link.innerText, link.class);
}

// $$eval delegates to mapSelector, so this is the same call.
const items = await page.$$eval("ul li");
```

**Python (Coming soon):**

```python
links = page.map_selector("a")

for link in links:
    print(link["innerText"], link["class"])

# eval_on_selector_all delegates to map_selector, so this is the same call.
items = page.eval_on_selector_all("ul li")
```

**Response**, one line per matched element:

```text
More information... null
```

By default `mapSelector()` waits for the selector to appear (`wait: true`). Pass `wait: false` to sample whatever is in the DOM right now, which returns an empty list instead of blocking when nothing matches.

## FAQ & Troubleshooting

<details>
<summary>My extraction returns <code>null</code> instead of content</summary>

The selector didn't match within the timeout, or the element exists but isn't rendered and you passed `visible: true`. Widen the selector, raise the per-call `timeout`, or drop `visible` if hidden content is acceptable.

</details>

<details>
<summary>Why is <code>$eval()</code> not running my function?</summary>

BAP's `$eval(selector)` returns the matched element's text content and takes no function argument, unlike Puppeteer's. For structured properties across matches use `mapSelector()`. The [migration guide](/bap/migrate) lists every behavioral difference.

</details>

<details>
<summary>The cleaned HTML removed content I needed</summary>

`removeNonTextNodes` defaults to `true` and strips the whole default tag list, which includes `img`, `svg`, `picture`, and `iframe`, so an anchor wrapping an image comes back empty. `a` itself isn't on that list. `selectors` only runs while `removeNonTextNodes` is `true`, and passing your own list replaces the default one. Keep it `true` and supply a narrower `selectors` list that omits the tags you need kept. For attributes, switch `mode` to `"allow"` and list only the ones you need.

</details>

<details>
<summary>Should I use <code>markdown()</code> or <code>html()</code> with <code>clean</code> for LLM input?</summary>

Start with `markdown()`. It produces the smallest readable output and needs no tuning. Reach for `html()` with `clean` when the model needs markup semantics, such as table structure or attribute values, that Markdown conversion flattens.

</details>

## Next steps

---

# BAP Quickstart

URL: https://docs.browserless.io/bap/getting-started

# BAP Quickstart

Install the BAP SDK, connect to a BrowserQL endpoint, and run a session that navigates to a page and reads its title. By the end you'll have a working script and know which connection options control timeouts and transport.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- npm 11.10 or later for the TypeScript SDK, or Python 3.11 or later for the Python SDK

## Get your API token

Copy your token from the [account dashboard](https://browserless.io/account/) before continuing. BAP appends it to the endpoint as `?token=` when the WebSocket opens, so a missing token fails at connection time rather than on your first method call.

## Install the library

**TypeScript:**

```bash
npm install @browserless.io/bap-ts
```

**Python (Coming soon):**

> **Coming soon**
> The Python SDK isn't published to PyPI yet, so `pip install bap-py` currently fails. Use the TypeScript SDK or [BrowserQL directly](/browserql/start) in the meantime.

```bash
python -m pip install bap-py
```

## Connect and run

Swap in your token and this runs as-is:

**TypeScript:**

```typescript

const TOKEN = "YOUR_API_TOKEN_HERE";

// connect() is synchronous and opens no socket. The WebSocket opens on newPage().
const browser = Browserless.connect({
  browserWSEndpoint: "wss://production-sfo.browserless.io/chromium/bql",
  token: TOKEN,
});

const page = await browser.newPage();
await page.goto("https://example.com");

console.log(await page.title());

// close() closes every page and its socket, freeing your account concurrency.
await browser.close();
```

**Python (Coming soon):**

```python

TOKEN = "YOUR_API_TOKEN_HERE"

# connect() opens no socket. The WebSocket opens when browser.page() is entered.
with bap.Browserless.connect(
    browser_ws_endpoint="wss://production-sfo.browserless.io/chromium/bql",
    token=TOKEN,
) as browser:
    # The context manager closes the page and the browser when the block exits.
    with browser.page() as page:
        page.goto("https://example.com")
        print(page.title())
```

Async applications import `bap` instead of `bap.sync_api`. The async API uses the same class and method names with `async with` and `await`.

## Expected output

```text
Example Domain
```

## Pick an endpoint

The quickstart uses Chromium. Swap the path for a different browser:

```bash
wss://production-sfo.browserless.io/chromium/bql   # Chromium, the default
wss://production-sfo.browserless.io/chrome/bql     # Google Chrome
wss://production-sfo.browserless.io/stealth/bql    # Managed stealth browser
```

The endpoint must end in `/bql`. The same host also serves CDP endpoints such as `/chromium`, and those don't speak BrowserQL. See [Connection URLs and Endpoints](/overview/connection-urls) for every region and route.

## Connection options

**TypeScript:**

| Option | Type | Default | Description |
| ------ | ---- | ------- | ----------- |
| `browserWSEndpoint` | string | — | BrowserQL WebSocket endpoint. Optional only when you supply a custom `transport` that owns its own connection |
| `token` | string | — | Your API token. Appended to the endpoint as `?token=` when the connection opens |
| `timeout` | number | `30000` | Default per-operation timeout in milliseconds |
| `transport` | TransportFactory | Built-in `WebSocketTransport` | Replace the built-in socket with your own transport, Puppeteer-style |

Each call to `browser.newPage()` opens its own WebSocket connection, so a script that runs three pages holds three sockets until you call `close()`.

**Python (Coming soon):**

| Option | Type | Default | Description |
| ------ | ---- | ------- | ----------- |
| `browser_ws_endpoint` | `str \| None` | `None` | BrowserQL WebSocket endpoint. Optional only when you supply a custom `transport` that owns its own connection |
| `token` | `str \| None` | `None` | Your API token. Appended to the endpoint as `?token=` when the connection opens |
| `timeout` | `float` | `30000` | Default per-operation timeout in milliseconds |
| `transport` | Custom transport | Built-in WebSocket transport | Replace the built-in socket with your own transport |

Each call to `browser.page()` or `browser.new_page()` opens its own WebSocket connection, so a script that runs three pages holds three sockets until they're closed.

## Runtime notes

Each SDK has one runtime detail worth knowing before you build on it.

**TypeScript:**

**Node.js and browser builds**

The package is isomorphic. Node.js uses the [`ws`](https://www.npmjs.com/package/ws) package, and bundlers targeting the browser (Vite, webpack, esbuild, Rollup) pick the native `WebSocket` build through the `browser` export condition, so no Node built-ins end up in your bundle.

`screenshot()` and `pdf()` resolve to a `Uint8Array` on both platforms. In Node.js the value is a `Buffer`, which is itself a `Uint8Array`. The `path` option writes to disk in Node.js only. In the browser it rejects, so use the returned bytes:

```typescript
// Browser: turn the bytes into something renderable.
const bytes = await page.screenshot({ type: "png" });
const url = URL.createObjectURL(new Blob([bytes], { type: "image/png" }));

// querySelector returns null when the element is absent, so guard before assigning.
const img = document.querySelector("img");
if (img) {
  img.src = url;
}
```

Node.js sends a `User-Agent` of `@browserless.io/bap-ts/<version>` on the WebSocket handshake, so those sessions are identifiable as SDK traffic server-side. Browsers forbid custom handshake headers, so the browser build can't send it. Don't use that header to measure browser-build usage, because it never arrives.

Running BAP in the browser exposes your bundled token to the client and requires the endpoint to accept the origin you connect from. Only do this when your token-delivery policy allows that exposure.

**Python (Coming soon):**

**Async and sync surfaces**

The package ships two API surfaces with matching class and method names: `bap` for asyncio applications, and `bap.sync_api` for synchronous code.

```python
# Synchronous code.

# Asyncio applications: same names, with async with and await.

```

The sync surface isn't a separate transport. It runs the same async implementation on a background event-loop thread and blocks the calling thread until each call resolves. Importing `bap.sync_api` is fine even inside an asyncio application. The restriction is calling its methods from the thread that already owns a running event loop, since blocking that thread would deadlock the loop it's supposed to drive.

## FAQ & Troubleshooting

<details>
<summary>My connection fails immediately</summary>

Check that the endpoint ends in `/bql`. `wss://production-sfo.browserless.io/chromium` is a CDP endpoint and won't speak BrowserQL. Then confirm your token is set, since it's appended when the socket opens and a missing one fails at connect time.

</details>

<details>
<summary>Why did my operation time out after 30 seconds?</summary>

`30000` is the default per-operation timeout, in milliseconds. Raise it globally with `timeout` on `Browserless.connect()`, or per call with the `timeout` option that navigation, selector, and CAPTCHA methods accept. [Error handling](/bap/error-handling#timeouts) covers the third option, `preferences()`, and which one wins when they disagree.

</details>

<details>
<summary>Do I need to call <code>browser.close()</code>?</summary>

TypeScript: yes, always. `close()` closes every page and its WebSocket, and leaving sessions open holds a browser against your account concurrency until it times out.

Python: no, if you use the `with` blocks shown above, since they close the page and browser sockets automatically when the block exits. Call `browser.close()` explicitly only if you're managing the browser without context managers.

</details>

<details>
<summary>(Python) Why does calling a sync method from my running event loop fail?</summary>

Use the async API (`import bap`) for code that runs on a thread that already owns an event loop, such as inside asyncio or Jupyter. Importing `bap.sync_api` is fine anywhere. The sync bridge only rejects being called from the thread that owns a running loop, since blocking that thread would deadlock callbacks and network operations.

</details>

<details>
<summary>(TypeScript) Can I run this in the browser instead of Node.js?</summary>

Yes. The package is isomorphic and bundlers pick the native `WebSocket` build automatically. Two things change: `path` on `screenshot()` and `pdf()` rejects, and your token ends up in the client bundle. See the [runtime notes](#runtime-notes) above.

</details>

## Next steps

---

# Interacting with pages in BAP

URL: https://docs.browserless.io/bap/interacting-with-pages

# Interacting with pages in BAP

BAP's interaction methods run inside the browser, so a click is a real click at real coordinates rather than a dispatched DOM event. This guide covers filling and submitting a form, the wait behavior every interaction method shares, and scrolling to trigger lazy-loaded content.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- The BAP SDK installed and connecting, from the [BAP Quickstart](/bap/getting-started)

## Fill out and submit a form

`type()` enters text character by character with a randomized delay, which is what makes it look human to bot detection. `click()` submits.

**TypeScript:**

```typescript

const TOKEN = "YOUR_API_TOKEN_HERE";

const browser = Browserless.connect({
  browserWSEndpoint: "wss://production-sfo.browserless.io/chromium/bql",
  token: TOKEN,
});

try {
  const page = await browser.newPage();
  await page.goto("https://example.com/login");

  await page.type("#username", "user@example.com");

  // A delay range makes typing look human to bot detection.
  await page.type("#password", "secret", { delay: [100, 200] });

  // Start the wait before the click, or a fast navigation finishes first.
  const navigated = page.waitForNavigation({ waitUntil: "networkIdle" });
  await page.click("#submit");
  await navigated;

  console.log(await page.url());
} finally {
  await browser.close();
}
```

**Python (Coming soon):**

```python

TOKEN = "YOUR_API_TOKEN_HERE"

with bap.Browserless.connect(
    browser_ws_endpoint="wss://production-sfo.browserless.io/chromium/bql",
    token=TOKEN,
) as browser:
    with browser.page() as page:
        page.goto("https://example.com/login")

        page.type("#username", "user@example.com")

        # A delay range makes typing look human to bot detection.
        page.type("#password", "secret", delay=[100, 200])
        page.click("#submit")

        # The sync API can't start the wait first, so wait for an element
        # unique to the destination instead.
        page.wait_for_selector("#dashboard")
        print(page.url())
```

**Response:**

```text
https://example.com/dashboard
```

There is no `fill()`. `type()` is the only entry point for text, because instant value-setting is one of the signals bot detection scores. Pass `delay: [0, 0]` when you're on a trusted site and want the speed instead.

## Other input types

Dropdowns, checkboxes, and hovers each have a method. `select()` takes the option values as varargs, so one value or several both work.

**TypeScript:**

```typescript
// Single or multiple select.
await page.select("#country", "CA");
await page.select("#toppings", "cheese", "basil");

// Checkboxes and radios.
await page.check("#terms");
await page.uncheck("#newsletter");

// Hover, which is how menus that open on mouseover are reached.
await page.hover("#account-menu");
```

**Python (Coming soon):**

```python
# Single or multiple select. Python takes one value or a sequence,
# where TypeScript takes varargs.
page.select_option("#country", "CA")
page.select_option("#toppings", ["cheese", "basil"])

# Checkboxes and radios.
page.check("#terms")
page.uncheck("#newsletter")

# Hover, which is how menus that open on mouseover are reached.
page.hover("#account-menu")
```

## Wait behavior every method shares

Interaction methods take the same four options, and the defaults mean you rarely need explicit waits. The one exception is `select()` / `select_option()`, which takes only a selector and the values, with no options object:

| Option | Default | What it does |
| ------ | ------- | ------------ |
| `wait` | `true` | Wait for the selector to exist in the DOM before acting. Set `false` to fail immediately when it's absent |
| `timeout` | `30000` | How long that wait may take, in milliseconds |
| `scroll` | `true` | Scroll the element into view first, the way a person would have to |
| `visible` | `false` | Act only if the element is actually rendered, not merely present |

`visible` is the one worth reaching for. A selector can match a node that's hidden behind a modal or collapsed in a menu, and clicking it succeeds without doing anything the user would see. `visible: true` turns that into a timeout you can catch instead of a silent no-op.

**TypeScript:**

```typescript
// Fail fast rather than waiting 30 seconds for an element that may not exist.
await page.click(".cookie-accept", { wait: false });

// Only click the button a user could actually see.
await page.click("#checkout", { visible: true, timeout: 10_000 });
```

**Python (Coming soon):**

```python
# Fail fast rather than waiting 30 seconds for an element that may not exist.
page.click(".cookie-accept", wait=False)

# Only click the button a user could actually see.
page.click("#checkout", visible=True, timeout=10000)
```

## Scroll, including for lazy-loaded content

`scroll()` targets a selector or explicit coordinates. Its `throughPage` option walks the whole document in viewport-sized steps and returns to the top, which is how you trigger lazy-loaded images and infinite-scroll content before capturing or extracting.

**TypeScript:**

```typescript
// Scroll to an element, or to a coordinate.
await page.scroll({ selector: "#footer" });
await page.scroll({ x: 0, y: 2000 });

// Trigger everything that loads on scroll, then return to the top.
await page.scroll({ throughPage: true });
await page.screenshot({ path: "full.png", fullPage: true });
```

**Python (Coming soon):**

```python
# Scroll to an element, or to a coordinate.
page.scroll(selector="#footer")
page.scroll(x=0, y=2000)

# Trigger everything that loads on scroll, then return to the top.
page.scroll(through_page=True)
page.screenshot(path="full.png", full_page=True)
```

## Navigate through history

`goBack()`, `goForward()`, and `reload()` drive the browser's own history, so a flow that depends on back-button behavior doesn't need re-navigation. All three take the same `waitUntil` and `timeout` options as `goto()`.

**TypeScript:**

```typescript
await page.goto("https://example.com/results");
await page.click(".result:first-child");

// Back to the results, without re-running the search.
await page.goBack({ waitUntil: "domContentLoaded" });
await page.reload();
```

**Python (Coming soon):**

```python
page.goto("https://example.com/results")
page.click(".result:first-child")

# Back to the results, without re-running the search.
page.go_back(wait_until="domContentLoaded")
page.reload()
```

Neither throws when there's nothing in that direction. `goBack()` and `goForward()` resolve to `null` in TypeScript and return `None` in Python instead.

## What isn't here

BAP has no `page.mouse`, `page.keyboard`, or `page.touchscreen`, because those need a live CDP session and BrowserQL has no equivalent. Arbitrary drag gestures, key chords, and multi-touch are out of reach. When a flow needs them, [hand the session to Puppeteer or Playwright](/bap/session-management/reconnects) for that step and keep the rest on BAP.

## FAQ & Troubleshooting

<details>
<summary>My click succeeded but nothing happened on the page</summary>

The selector probably matched a hidden element, such as a duplicate in a collapsed menu. Add `visible: true` so the call fails instead of silently clicking something invisible, and tighten the selector.

</details>

<details>
<summary>Reading the page after a click gives me the old content</summary>

The click started a navigation that hadn't finished. Wait for an element unique to the destination with `waitForSelector()`, which is the option that works identically in both SDKs. `waitForNavigation()` also works in TypeScript, but it has to be started before the click, as [waiting for things](/bap/waiting) explains.

</details>

<details>
<summary>Why is there no <code>fill()</code>?</summary>

Setting a field's value instantly is a bot-detection signal, so BAP exposes only `type()` with its randomized per-character `delay`. Pass `delay: [0, 0]` when human-like timing doesn't matter.

</details>

<details>
<summary>Images below the fold are missing from my screenshot</summary>

They're lazy-loaded and never entered the viewport. Call `scroll({ throughPage: true })` first, which steps through the document and returns to the top, then capture.

</details>

<details>
<summary>Why does <code>addScriptTag</code> fail with <code>SyntaxError: Invalid or unexpected token</code>?</summary>

Inline `content` must be a single line, because the BrowserQL server rejects multi-line `content`. Host the script and pass `url` instead, or collapse the source to one line first, which only works when it has no newline-sensitive syntax such as `//` comments or multi-line template literals. The Python method is `add_script_tag` and behaves the same way.

</details>

## Next steps

---

# Migrate to BAP

URL: https://docs.browserless.io/bap/migrate

# Migrate to BAP

BAP borrows Puppeteer's method names in TypeScript and Playwright's in Python, but it isn't a drop-in replacement for either: it runs over BrowserQL instead of a live CDP session, so anything that needs direct browser control has no equivalent. This guide covers what changes, what doesn't carry over, and what BAP adds that neither library has.

- An existing Puppeteer or Playwright script
- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- The [BAP Quickstart](/bap/getting-started) if you haven't installed and connected yet

## Should you migrate?

Migrate to BAP when you want BrowserQL's managed stealth, proxies, and CAPTCHA solving without hand-writing GraphQL, and your script doesn't rely on `page.mouse`, `page.keyboard`, frames, or browser contexts. If it does, keep the script as-is and connect it to [Browsers as a Service (BaaS)](/baas/start) over CDP instead.

## Method differences

**TypeScript:**

`Page` borrows Puppeteer's method names, but some shared methods behave differently:

| Method | Puppeteer | BAP |
| ------ | --------- | --- |
| `$eval(selector, fn)` | Runs your function against the matched element and returns its result | Returns the text content of the matched selector. No function argument |
| `$$eval(selector, fn)` | Runs your function against all matched elements | Delegates to `mapSelector` and returns structured `MapSelectorResponse[]` |
| `evaluate(fn, ...args)` | Passes serialized arguments and returns deserialized results | Accepts a string or function, always returns `string \| null`, and passes no arguments |
| `waitForRequest` / `waitForResponse` | Accept a URL string or a predicate function | Accept a URL string or an options object. No predicate functions |
| `scroll()` | Not on `Page`. You use `mouse.wheel()` or `evaluate()` | First-class method with selector and coordinate targeting |

**Python (Coming soon):**

`Page` borrows Playwright's method names, but some shared methods behave differently:

| Method | Playwright | BAP |
| ------ | ---------- | --- |
| `eval_on_selector(selector, expression)` | Runs `expression` against the matched element and returns its result | Returns the text content of the matched selector. No `expression` argument |
| `eval_on_selector_all(selector, expression)` | Runs `expression` against every matched element | Delegates to `map_selector` and returns a list of `MapSelectorResponse` dicts |
| `evaluate(expression, arg)` | Passes a serialized `arg` and returns the deserialized result | Accepts only an expression string, always returns `str \| None`, and takes no `arg` |
| `wait_for_selector(selector, state=...)` | Accepts a `state` predicate (`attached`, `visible`, `hidden`, `detached`) | Accepts `visible`/`hidden` booleans, no `attached`/`detached` states |
| Typing into a field | `fill()` sets the value instantly; `press_sequentially()` types key by key | `type()` is the only entry point, with a per-character `delay` range for human-like typing |

## What has no BAP equivalent

**TypeScript:**

These Puppeteer APIs have no BAP equivalent, because they need a live CDP session:

- Input devices: no `page.keyboard`, `page.mouse`, or `page.touchscreen`
- Frames: no `page.frames()`, `page.mainFrame()`, or frame targeting
- Workers: no `page.workers()`
- Function exposure: no `exposeFunction()`
- Emulation: no `emulate()` or `emulateCPUThrottling()`, though `emulateMediaType()` is supported
- Security and cache: no `setBypassCSP()`, `setCacheEnabled()`, or `setOfflineMode()`
- Coverage, tracing, and accessibility: no `page.coverage`, `page.tracing`, or `page.accessibility`

**Python (Coming soon):**

These Playwright APIs have no BAP equivalent, because they need a live CDP session:

- Browser contexts: no `new_context()`. `Browser.new_page()` opens directly against the BrowserQL endpoint
- Input devices: no `page.keyboard` or `page.mouse`
- Frames: no `page.frames`, `page.main_frame`, or frame targeting
- Function exposure: no `expose_function()`
- Generic interception: no `page.route()`. Use `fulfill()`, `reject()`, `request()`, and `response()` instead, which match filters server-side rather than running a Python callback per request
- Tracing and accessibility: no `page.context.tracing` or accessibility snapshots

### When only one step needs the missing API

Migrating isn't all-or-nothing. Two escape hatches keep the rest of the script on BAP:

- **Hand the session to Puppeteer or Playwright.** [`reconnect()`](/bap/session-management/reconnects) returns a CDP `browserWSEndpoint` for the same live browser, so BAP can handle the stealth, proxying, and CAPTCHA work, then your existing CDP code drives the step that needs `page.mouse` or frames. Nothing is re-run and no state is lost.
- **Send the mutation yourself.** For a BrowserQL capability the SDK doesn't wrap yet, [`page.send()`](/bap/advanced/raw-browserql) runs any BQL document on the current session. This doesn't help with APIs that need direct CDP access, since BrowserQL has no equivalent either.

## What BAP adds

**TypeScript:**

BAP adds methods Puppeteer has no equivalent for, and each one has a guide:

| Methods | Guide |
| ------- | ----- |
| `html()`, `text()`, `markdown()`, `mapSelector()` | [Extracting content](/bap/extracting-content) |
| `reject()`, `fulfill()`, `request()`/`response()` | [Network control](/bap/network-control) |
| `proxy()` | [Stealth & humanization](/bap/bot-detection/stealth) |
| `solve()`, `solveImageCaptcha()` | [Solving CAPTCHAs](/bap/bot-detection/captchas) |
| `reconnect()` | [Reconnecting to sessions](/bap/session-management/reconnects) |
| `liveURL()`, `stopSessionRecording()`, `switchToWindow()` | [Live URLs & recording](/bap/session-management/live-url) |

`check()`/`uncheck()` are checkbox helpers, `preferences()` sets session-wide defaults such as the timeout, and `loadSecret()` fills a credential from a [1Password integration](/baas/features/1password) without exposing it to your script.

**Python (Coming soon):**

BAP adds methods Playwright has no equivalent for, and each one has a guide:

| Methods | Guide |
| ------- | ----- |
| `html()`, `text()`, `markdown()`, `map_selector()` | [Extracting content](/bap/extracting-content) |
| `reject()`, `fulfill()`, `request()`/`response()` | [Network control](/bap/network-control) |
| `proxy()` | [Stealth & humanization](/bap/bot-detection/stealth) |
| `solve()`, `solve_image_captcha()` | [Solving CAPTCHAs](/bap/bot-detection/captchas) |
| `reconnect()` | [Reconnecting to sessions](/bap/session-management/reconnects) |
| `live_url()`, `stop_session_recording()`, `switch_to_window()` | [Live URLs & recording](/bap/session-management/live-url) |

`check()`/`uncheck()` are checkbox helpers, `preferences()` sets session-wide defaults such as the timeout, and `load_secret()` fills a credential from a [1Password integration](/baas/features/1password) without exposing it to your script.

## FAQ & Troubleshooting

<details>
<summary>Can I reuse my existing script unchanged?</summary>

Not without changes. BAP covers the subset of Puppeteer or Playwright that BrowserQL can express, so anything using `page.mouse`, `page.keyboard`, frames, browser contexts, or `page.route()` needs rewriting. If only a step or two needs those APIs, [hand the session to your existing CDP code](#when-only-one-step-needs-the-missing-api) with `reconnect()`. If you'd rather not change the script at all, use [BaaS](/baas/start) over CDP instead.

</details>

<details>
<summary>Why does <code>evaluate()</code> return a string when my function returns an object?</summary>

`evaluate()` always resolves to a string (or `null`/`None`), and it takes no arguments. Serialize inside the page with `JSON.stringify()` and parse the result yourself.

</details>

## Next steps

---

# Network control with BAP

URL: https://docs.browserless.io/bap/network-control

# Network control with BAP

BAP gives you four levers over a session's network traffic: block requests before they leave, query everything the browser captured, wait for a specific request or response, and answer matching requests yourself. This guide shows each one and when to reach for it.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- The BAP SDK installed and connecting, from the [BAP Quickstart](/bap/getting-started)

## Block requests with reject

`reject()` stops matching requests before they're sent. Blocking images, media, and stylesheets is the usual first move for scraping, because pages settle faster and transfer less. Set it up before `goto()`, since rules only apply to requests made after the call.

Match by resource `type`, glob-style `url` patterns, `regex`, or HTTP `method`. The `operator` decides how multiple conditions combine: `or` (the default) rejects a request matching any condition, `and` requires all of them.

**TypeScript:**

```typescript

const TOKEN = "YOUR_API_TOKEN_HERE";

const browser = Browserless.connect({
  browserWSEndpoint: "wss://production-sfo.browserless.io/chromium/bql",
  token: TOKEN,
});

const page = await browser.newPage();

// Rules apply to requests made after this call, so set them before goto().
await page.reject({
  type: ["image", "media", "stylesheet"],
  url: ["*doubleclick.net*", "*google-analytics.com*"],
  operator: "or",
});

await page.goto("https://example.com");
console.log(await page.title());
await browser.close();
```

**Python (Coming soon):**

```python

TOKEN = "YOUR_API_TOKEN_HERE"

with bap.Browserless.connect(
    browser_ws_endpoint="wss://production-sfo.browserless.io/chromium/bql",
    token=TOKEN,
) as browser:
    with browser.page() as page:
        # Rules apply to requests made after this call, so set them before goto().
        page.reject(
            type=["image", "media", "stylesheet"],
            url=["*doubleclick.net*", "*google-analytics.com*"],
            operator="or",
        )

        page.goto("https://example.com")
        print(page.title())
```

**Response:**

```text
Example Domain
```

Pass `enabled: false` to switch rejection off again mid-session.

## Query captured traffic

The browser records the session's traffic, and `request()` and `response()` query that capture with filters: glob `url` patterns, `method`, resource `type`, and for responses `status`. Both default to `wait: true`, which blocks until the first match and returns it. Pass `wait: false` to get everything captured so far instead, which is the mode you want after navigation has finished.

**TypeScript:**

```typescript
await page.goto("https://example.com");

// Everything captured so far; wait: false means "don't block for new matches".
const failures = await page.response({
  status: [404, 500, 502, 503],
  wait: false,
});

for (const res of failures) {
  console.log(res.status, res.url);
}

// The first XHR request to the API, waiting up to 10 seconds for it.
const [apiCall] = await page.request({
  url: ["*api.example.com*"],
  type: ["xhr", "fetch"],
  timeout: 10_000,
});
```

**Python (Coming soon):**

```python
page.goto("https://example.com")

# Everything captured so far; wait=False means "don't block for new matches".
failures = page.response(status=[404, 500, 502, 503], wait=False)

for res in failures:
    print(res["status"], res["url"])

# The first XHR request to the API, waiting up to 10 seconds for it.
api_call = page.request(
    url=["*api.example.com*"],
    type=["xhr", "fetch"],
    timeout=10000,
)[0]
```

Response records carry `url`, `status`, `method`, `type`, `headers`, and `body`. Binary bodies come back base64-encoded with the `base64Encoded` flag set, so check it before decoding.

## Wait for a request or response

When you only care that one call happened, `waitForRequest()` and `waitForResponse()` return a match from the traffic already captured, or block until one arrives. They take a single glob `url` pattern rather than a list, and `waitForResponse()` also matches on `statuses`. Use these to synchronize on an API call a click triggers, instead of sleeping and hoping. Because captured traffic counts, calling them after the click is safe and a fast response isn't missed.

**TypeScript:**

```typescript
// Matches traffic already captured too, so starting the wait
// before or after the click both work.
const settled = page.waitForResponse({
  url: "*api.example.com/cart*",
  statuses: [200],
  timeout: 10_000,
});

await page.click("#add-to-cart");
const response = await settled;
console.log(response.status);
```

**Python (Coming soon):**

```python
page.click("#add-to-cart")

# Matches traffic already captured too, so a fast response isn't missed.
response = page.wait_for_response(
    url="*api.example.com/cart*",
    statuses=[200],
    timeout=10000,
)
print(response["status"])
```

Unlike Puppeteer, these methods don't take predicate functions. The filters are sent to the server as part of the BrowserQL operation, so they must be data. When glob patterns can't express your condition, fetch candidates with `request()` or `response()` and filter client-side.

## Mock responses with fulfill

`fulfill()` answers matching requests yourself instead of letting them reach the network. Match the same way as `reject()`, then supply the `status` (default `200`), `body` or `base64Body` for binary data, `contentType`, and extra `headers`. Like `reject()`, set it up before the navigation that triggers the requests.

**TypeScript:**

```typescript
// Serve a canned API payload so the page renders without the real backend.
await page.fulfill({
  url: ["*api.example.com/products*"],
  status: 200,
  contentType: "application/json",
  body: JSON.stringify([{ id: 1, name: "test product" }]),
});

await page.goto("https://example.com/catalog");
```

**Python (Coming soon):**

```python

# Serve a canned API payload so the page renders without the real backend.
page.fulfill(
    url=["*api.example.com/products*"],
    status=200,
    content_type="application/json",
    body=json.dumps([{"id": 1, "name": "test product"}]),
)

page.goto("https://example.com/catalog")
```

## Set headers and HTTP credentials

`setExtraHTTPHeaders()` attaches headers to every request the page makes from that point on. `authenticate()` answers HTTP authentication challenges (401/407) with the supplied credentials, optionally scoped to one `origin` so credentials don't leak to other hosts.

**TypeScript:**

```typescript
await page.setExtraHTTPHeaders({
  "X-Request-Source": "bap-worker-3",
});

// Scope credentials to one origin so they aren't offered elsewhere.
await page.authenticate("user", "pass", { origin: "https://internal.example.com" });

await page.goto("https://internal.example.com/dashboard");
```

**Python (Coming soon):**

```python
page.set_extra_http_headers({"X-Request-Source": "bap-worker-3"})

# Scope credentials to one origin so they aren't offered elsewhere.
page.authenticate("user", "pass", origin="https://internal.example.com")

page.goto("https://internal.example.com/dashboard")
```

## FAQ & Troubleshooting

<details>
<summary>My <code>reject()</code> rules aren't blocking anything</summary>

Call `reject()` before the navigation whose requests you want blocked. Rules apply to requests made after the call, so setting them after `goto()` only affects later traffic such as XHR polling.

</details>

<details>
<summary><code>request()</code> hangs instead of returning captured traffic</summary>

Both `request()` and `response()` default to `wait: true`, which returns matches from the captured traffic immediately and otherwise blocks until one arrives, timing out after 30 seconds. A hang means nothing captured matches your filters. Loosen them, or pass `wait: false` to return without blocking.

</details>

<details>
<summary>Can I pass a predicate function like Puppeteer's <code>waitForResponse(fn)</code>?</summary>

No. Filters execute on the BrowserQL server, so they're plain data: glob URL patterns, methods, types, and statuses. For conditions globs can't express, pull candidates with `response({ wait: false })` and filter in your own code.

</details>

<details>
<summary>The response <code>body</code> for an image is an unreadable string</summary>

Binary bodies come back base64-encoded, flagged with `base64Encoded: true`, so decode before use rather than treating the value as text. The `bodyEncoding` option on the response filter requires a BQL schema recent enough to support it.

</details>

## Next steps

---

# Page events in BAP

URL: https://docs.browserless.io/bap/page-events

# Page events in BAP

`page.on()` streams console output, network requests, and responses as the page produces them, using the same `on`/`once`/`off` shape as Puppeteer and Playwright. This guide covers the four event types, what each payload carries, and the subscription lifecycle that makes listeners cost something.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- The BAP SDK installed and connecting, from the [BAP Quickstart](/bap/getting-started)

## Listen to events

Register listeners before the navigation that produces the events. Anything the page emits before a listener exists is not replayed.

**TypeScript:**

```typescript

const TOKEN = "YOUR_API_TOKEN_HERE";

const browser = Browserless.connect({
  browserWSEndpoint: "wss://production-sfo.browserless.io/chromium/bql",
  token: TOKEN,
});

try {
  const page = await browser.newPage();

  page.on("console", (msg) => console.log(`[${msg.type}] ${msg.text}`));
  page.on("request", (req) => console.log(`${req.method} ${req.url}`));

  // Response bodies are not fetched by default, since streaming them is expensive.
  page.on("response", (res) => console.log(`${res.status} ${res.url}`));

  // Non-fatal streaming and handshake failures surface here instead of throwing.
  page.on("error", (err) => console.error(err.message));

  await page.goto("https://example.com");
} finally {
  await browser.close();
}
```

**Python (Coming soon):**

```python

TOKEN = "YOUR_API_TOKEN_HERE"

with bap.Browserless.connect(
    browser_ws_endpoint="wss://production-sfo.browserless.io/chromium/bql",
    token=TOKEN,
) as browser:
    with browser.page() as page:
        page.on("console", lambda msg: print(f"[{msg['type']}] {msg['text']}"))
        page.on("request", lambda req: print(f"{req['method']} {req['url']}"))

        # Response bodies are not fetched by default, since streaming them is expensive.
        page.on("response", lambda res: print(f"{res['status']} {res['url']}"))

        # Non-fatal streaming and handshake failures surface here instead of raising.
        page.on("error", lambda err: print(err))

        page.goto("https://example.com")
```

**Response:**

```text
GET https://example.com/
200 https://example.com/
[log] hello from the page
```

## The four events

| Event | Payload | Carries |
| ----- | ------- | ------- |
| `console` | `ConsoleMessage` | `type`, `text`, JSON-encoded `args`, source `location`, and a `timestamp` |
| `request` | `RequestResponse` | `url`, `method`, resource `type`, and `headers` |
| `response` | `ResponseResponse` | `url`, `status`, `method`, `type`, and `headers`. The `body` field is empty on this stream, as explained below |
| `error` | `Error` | Non-fatal streaming and handshake failures that would otherwise be invisible |

The `error` event is the one worth wiring up even when you don't need the others. Failures in the subscription stream itself don't reject any of your pending calls, so without a listener they pass silently.

## Subscriptions open and close with your listeners

Listeners aren't free. Attaching the first `console`, `request`, or `response` listener opens a GraphQL subscription for that event type, and removing the last one closes it. That's why the API has `once()` and `off()`: a listener you forget to remove keeps a stream running for the life of the page.

**TypeScript:**

```typescript
const onRequest = (req) => console.log(req.url);

page.on("request", onRequest);
await page.goto("https://example.com");

// Closes the request subscription, since this was the only listener.
page.off("request", onRequest);

// Fires once, then removes itself.
page.once("console", (msg) => console.log("first message:", msg.text));
```

**Python (Coming soon):**

```python
def on_request(req):
    print(req["url"])

page.on("request", on_request)
page.goto("https://example.com")

# Closes the request subscription, since this was the only listener.
page.off("request", on_request)

# Fires once, then removes itself.
page.once("console", lambda msg: print("first message:", msg["text"]))
```

One more method helps when you're managing listeners in longer-lived code: `removeAllListeners(event)` (Python: `remove_all_listeners(event)`) clears them for one event, or for every event when called with no argument. It's useful in cleanup paths, since clearing the last listener is what closes the underlying stream. Python also exposes `listener_count(event)` to report how many are attached. The TypeScript `Page` doesn't expose a count.

Response bodies are excluded from the `response` stream by default because streaming every body is expensive. When you need bodies, query the captured traffic with [`response()`](/bap/network-control) instead, which lets you filter down to the handful of responses you care about.

## Events versus querying traffic

Both surfaces see network activity, and they suit different jobs:

- **Events** are push-based and live. Use them to log or react as things happen, especially when you don't know in advance which requests will matter.
- **[`request()` and `response()`](/bap/network-control)** are pull-based queries with server-side filters. Use them when you know what you're looking for, need response bodies, or want to check what already happened after a navigation.

## FAQ & Troubleshooting

<details>
<summary>My listener never fires</summary>

It was probably attached after the events happened. Register listeners before `goto()` or before the interaction that triggers the traffic, since nothing is replayed to a late listener.

</details>

<details>
<summary>The <code>response</code> event has no body</summary>

Bodies are omitted by design, because streaming every response body is expensive. Fetch the ones you need with `response()` from the [network control guide](/bap/network-control), filtering by URL or status.

</details>

<details>
<summary>Can I filter events, the way Puppeteer lets me inspect and decide?</summary>

Not on the subscription itself. Filter inside your listener, or use `request()`/`response()` where the filters run server-side and less data crosses the wire.

</details>

<details>
<summary>Is there an event for raw protocol frames?</summary>

Yes, in both SDKs: `page.on('message')` and `page.subscribe()` expose every transport frame, including server pushes. See [raw BrowserQL](/bap/advanced/raw-browserql).

</details>

## Next steps

---

# Python BAP API Reference

URL: https://docs.browserless.io/bap/python/reference

# Python BAP API Reference

Generated async and sync API reference for `bap-py`.

- [Async Browserless](/bap/python/reference/async-browserless)
- [Async Browser](/bap/python/reference/async-browser)
- [Async Page](/bap/python/reference/async-page)
- [Async ElementHandle](/bap/python/reference/async-element-handle)
- [Sync Browserless](/bap/python/reference/sync-browserless)
- [Sync Browser](/bap/python/reference/sync-browser)
- [Sync Page](/bap/python/reference/sync-page)
- [Sync ElementHandle](/bap/python/reference/sync-element-handle)
- [Errors](/bap/python/reference/errors)
- [Enums](/bap/python/reference/enums)
- [Input types](/bap/python/reference/inputs)
- [Response types](/bap/python/reference/responses)
- [Option types](/bap/python/reference/options)
- [Event types](/bap/python/reference/events)

---

# Async Browser

URL: https://docs.browserless.io/bap/python/reference/async-browser

# Async Browser

Owns the pages opened against one Browserless configuration.

## `close`

```python
close() -> None
```

Close every page opened by this browser.

## `get_pages`

```python
get_pages() -> list[Page]
```

Return pages whose transports remain open.

## `new_page`

```python
new_page() -> Page
```

Open a new page with its own BrowserQL WebSocket.

## `page`

```python
page() -> AsyncContextManager[Page]
```

Open a page and close it when the context exits.

```python
async with browser.page() as page:
    await page.goto("https://example.com")
```

---

# Async Browserless

URL: https://docs.browserless.io/bap/python/reference/async-browserless

# Async Browserless

Factory namespace matching the Browserless SDKs.

## `connect`

```python
connect(*, browser_ws_endpoint: str | None = None, token: str | None = None, timeout: float = 30000, transport: TransportFactory | None = None) -> Browser
```

Create a browser configured for a BrowserQL WebSocket endpoint.

---

# Async ElementHandle

URL: https://docs.browserless.io/bap/python/reference/async-element-handle

# Async ElementHandle

A matched element's properties plus selector-scoped actions.

## `child_element_count`

Type: `int | None`

## `class_name`

Type: `str | None`

## `click`

```python
click(**options: Unpack[ClickOptions]) -> ClickResponse
```

## `hover`

```python
hover() -> HoverResponse
```

## `id`

Type: `str | None`

## `inner_html`

Type: `str | None`

## `inner_text`

Type: `str | None`

## `local_name`

Type: `str | None`

## `outer_html`

Type: `str | None`

## `query_selector`

```python
query_selector(selector: str) -> ElementHandle | None
```

## `query_selector_all`

```python
query_selector_all(selector: str) -> list[ElementHandle]
```

## `screenshot`

```python
screenshot(*, path: str | None = None) -> bytes
```

## `select_option`

```python
select_option(value: str | Sequence[str]) -> SelectResponse
```

## `selector`

Type: `str`

## `text_content`

```python
text_content() -> str
```

## `type`

```python
type(text: str, **options: Unpack[TypeOptions]) -> TypeResponse
```

---

# Async Page

URL: https://docs.browserless.io/bap/python/reference/async-page

# Async Page

A BrowserQL page with generated methods and an event lifecycle.

## `add_cookies`

```python
add_cookies(cookies: Sequence[CookieInput]) -> CookieResponse
```

## `add_script_tag`

```python
add_script_tag(*, url: str | UnsetType = UNSET, path: str | Path | UnsetType = UNSET, content: str | UnsetType = UNSET, id: str | UnsetType = UNSET, type: str | UnsetType = UNSET) -> AddScriptTagResponse
```

## `add_style_tag`

```python
add_style_tag(*, url: str | UnsetType = UNSET, path: str | Path | UnsetType = UNSET, content: str | UnsetType = UNSET) -> AddStyleTagResponse
```

## `authenticate`

```python
authenticate(username: str, password: str, *, origin: str | UnsetType = UNSET) -> HTTPResponse | None
```

## `check`

```python
check(selector: str, *, scroll: bool | UnsetType = UNSET, timeout: float | UnsetType = UNSET, visible: bool | UnsetType = UNSET, wait: bool | UnsetType = UNSET) -> ClickResponse
```

## `click`

```python
click(selector: str, *, scroll: bool | UnsetType = UNSET, timeout: float | UnsetType = UNSET, visible: bool | UnsetType = UNSET, wait: bool | UnsetType = UNSET) -> ClickResponse
```

## `close`

```python
close() -> None
```

## `closed`

Type: `bool`

## `content`

```python
content() -> str
```

## `cookies`

```python
cookies() -> list[StandardCookie]
```

## `emit`

```python
emit(event: str, payload: object) -> bool
```

## `emulate_media`

```python
emulate_media(type: MediaType) -> EmulateMediaTypeResponse
```

## `eval_on_selector`

```python
eval_on_selector(selector: str) -> str
```

## `eval_on_selector_all`

```python
eval_on_selector_all(selector: str, *, timeout: float | UnsetType = UNSET, wait: bool | UnsetType = UNSET) -> list[MapSelectorResponse]
```

## `evaluate`

```python
evaluate(expression: str, *, timeout: float | UnsetType = UNSET, url: str | UnsetType = UNSET) -> str | None
```

## `fulfill`

```python
fulfill(*, url: list[str] | UnsetType = UNSET, regex: list[str] | UnsetType = UNSET, method: list[Method] | UnsetType = UNSET, type: list[ResourceType] | UnsetType = UNSET, operator: OperatorType | UnsetType = UNSET, status: int | UnsetType = UNSET, content_type: str | UnsetType = UNSET, body: str | UnsetType = UNSET, base64_body: str | UnsetType = UNSET, headers: list[HeaderInput] | UnsetType = UNSET) -> FulfillResponse
```

## `go_back`

```python
go_back(*, timeout: float | UnsetType = UNSET, wait_until: WaitUntilHistory | UnsetType = UNSET) -> HTTPResponse | None
```

## `go_forward`

```python
go_forward(*, timeout: float | UnsetType = UNSET, wait_until: WaitUntilHistory | UnsetType = UNSET) -> HTTPResponse | None
```

## `goto`

```python
goto(url: str, *, timeout: float | UnsetType = UNSET, wait_until: WaitUntilGoto | UnsetType = UNSET, wait_until_all: list[WaitUntilGoto] | UnsetType = UNSET, referer: str | UnsetType = UNSET, referrer_policy: str | UnsetType = UNSET, remote_response_metadata: bool | UnsetType = UNSET) -> HTTPResponse | None
```

## `hover`

```python
hover(selector: str, *, scroll: bool | UnsetType = UNSET, timeout: float | UnsetType = UNSET, visible: bool | UnsetType = UNSET, wait: bool | UnsetType = UNSET, x: float | UnsetType = UNSET, y: float | UnsetType = UNSET) -> HoverResponse
```

## `html`

```python
html(*, selector: str | UnsetType = UNSET, timeout: float | UnsetType = UNSET, visible: bool | UnsetType = UNSET, clean: CleanInput | UnsetType = UNSET, rewrite: HTMLRewriteInput | UnsetType = UNSET) -> HTMLResponse
```

## `listener_count`

```python
listener_count(event: str) -> int
```

## `live_url`

```python
live_url(*, timeout: float | UnsetType = UNSET, interactable: bool | UnsetType = UNSET, type: LiveURLStreamType | UnsetType = UNSET, quality: int | UnsetType = UNSET, resizable: bool | UnsetType = UNSET, show_browser_interface: bool | UnsetType = UNSET, compressed: bool | UnsetType = UNSET, emulate_components: bool | UnsetType = UNSET) -> LiveURLResponse
```

## `load_secret`

```python
load_secret(ref: str, *, selector: str | UnsetType = UNSET) -> LoadSecretResponse
```

## `map_selector`

```python
map_selector(selector: str, *, timeout: float | UnsetType = UNSET, wait: bool | UnsetType = UNSET) -> list[MapSelectorResponse]
```

## `markdown`

```python
markdown(*, selector: str | UnsetType = UNSET, timeout: float | UnsetType = UNSET, visible: bool | UnsetType = UNSET) -> MarkdownResponse
```

## `off`

```python
off(event: EventName, listener: Listener) -> Self
```

## `on`

```python
on(event: EventName, listener: Listener) -> Self
```

## `once`

```python
once(event: EventName, listener: Listener) -> Self
```

## `pdf`

```python
pdf(*, display_header_footer: bool | UnsetType = UNSET, format: PDFPageFormat | UnsetType = UNSET, footer_template: str | UnsetType = UNSET, generate_document_outline: bool | UnsetType = UNSET, generate_tagged_pdf: bool | UnsetType = UNSET, landscape: bool | UnsetType = UNSET, print_background: bool | UnsetType = UNSET, omit_background: bool | UnsetType = UNSET, scale: float | UnsetType = UNSET, timeout: float | UnsetType = UNSET, wait_for_images: bool | UnsetType = UNSET, wait_for_fonts: bool | UnsetType = UNSET, full_page: bool | UnsetType = UNSET, width: float | str | UnsetType = UNSET, header_template: str | UnsetType = UNSET, height: float | str | UnsetType = UNSET, margin_bottom: float | str | UnsetType = UNSET, margin_left: float | str | UnsetType = UNSET, margin_right: float | str | UnsetType = UNSET, margin_top: float | str | UnsetType = UNSET, page_ranges: str | UnsetType = UNSET, prefer_css_page_size: bool | UnsetType = UNSET, transfer_mode: str | UnsetType = UNSET, path: str | UnsetType = UNSET) -> bytes
```

## `preferences`

```python
preferences(*, timeout: float | UnsetType = UNSET) -> DefaultResponse
```

## `proxy`

```python
proxy(*, network: ProxyNetwork | UnsetType = UNSET, country: CountryCode | UnsetType = UNSET, city: str | UnsetType = UNSET, state: str | UnsetType = UNSET, sticky: bool | UnsetType = UNSET, preset: str | UnsetType = UNSET, server: str | UnsetType = UNSET, method: list[Method] | UnsetType = UNSET, operator: OperatorType | UnsetType = UNSET, type: list[ResourceType] | UnsetType = UNSET, url: list[str] | UnsetType = UNSET) -> ProxyResponse
```

## `query_selector`

```python
query_selector(selector: str, *, timeout: float | UnsetType = UNSET, visible: bool | UnsetType = UNSET) -> ElementHandle | None
```

## `query_selector_all`

```python
query_selector_all(selector: str, *, timeout: float | UnsetType = UNSET, visible: bool | UnsetType = UNSET) -> list[ElementHandle]
```

## `reconnect`

```python
reconnect(*, timeout: float | UnsetType = UNSET) -> ReconnectionResponse
```

## `reject`

```python
reject(*, enabled: bool | UnsetType = UNSET, method: list[Method] | UnsetType = UNSET, operator: OperatorType | UnsetType = UNSET, type: list[ResourceType] | UnsetType = UNSET, url: list[str] | UnsetType = UNSET, regex: list[str] | UnsetType = UNSET) -> RejectResponse
```

## `reload`

```python
reload(*, timeout: float | UnsetType = UNSET, wait_until: WaitUntilHistory | UnsetType = UNSET) -> HTTPResponse | None
```

## `remove_all_listeners`

```python
remove_all_listeners(event: str | None = None) -> Self
```

## `request`

```python
request(*, type: list[ResourceType] | UnsetType = UNSET, method: list[Method] | UnsetType = UNSET, timeout: float | UnsetType = UNSET, url: list[str] | UnsetType = UNSET, wait: bool | UnsetType = UNSET, operator: OperatorType | UnsetType = UNSET) -> list[RequestResponse]
```

## `response`

```python
response(*, status: list[int] | UnsetType = UNSET, method: list[Method] | UnsetType = UNSET, operator: OperatorType | UnsetType = UNSET, timeout: float | UnsetType = UNSET, type: list[ResourceType] | UnsetType = UNSET, url: list[str] | UnsetType = UNSET, wait: bool | UnsetType = UNSET, body_encoding: bool | UnsetType = UNSET) -> list[ResponseResponse]
```

## `screenshot`

```python
screenshot(*, capture_beyond_viewport: bool | UnsetType = UNSET, clip: ScreenshotClip | UnsetType = UNSET, from_surface: bool | UnsetType = UNSET, full_page: bool | UnsetType = UNSET, omit_background: bool | UnsetType = UNSET, optimize_for_speed: bool | UnsetType = UNSET, quality: float | UnsetType = UNSET, selector: str | UnsetType = UNSET, type: ScreenshotType | UnsetType = UNSET, timeout: float | UnsetType = UNSET, wait_for_images: bool | UnsetType = UNSET, path: str | UnsetType = UNSET) -> bytes
```

## `scroll`

```python
scroll(*, selector: str | UnsetType = UNSET, through_page: bool | UnsetType = UNSET, timeout: float | UnsetType = UNSET, visible: bool | UnsetType = UNSET, wait: bool | UnsetType = UNSET, x: float | UnsetType = UNSET, y: float | UnsetType = UNSET) -> ScrollResponse
```

## `select_option`

```python
select_option(selector: str, value: str | Sequence[str]) -> SelectResponse
```

## `send`

```python
send(query: str, *, variables: Mapping[str, Any] | None = None, timeout: float | None = None) -> Any
```

## `set_content`

```python
set_content(html: str, *, timeout: float | UnsetType = UNSET, wait_until: WaitUntilHistory | UnsetType = UNSET, wait_until_all: list[WaitUntilHistory] | UnsetType = UNSET) -> HTTPResponse | None
```

## `set_extra_http_headers`

```python
set_extra_http_headers(headers: Mapping[str, str]) -> HTTPHeadersResponse
```

## `set_java_script_enabled`

```python
set_java_script_enabled(enabled: bool) -> JavaScriptResponse
```

## `set_user_agent`

```python
set_user_agent(user_agent: str) -> UserAgentResponse
```

## `set_viewport_size`

```python
set_viewport_size(*, width: float, height: float, device_scale_factor: float | UnsetType = UNSET, mobile: bool | UnsetType = UNSET, has_touch: bool | UnsetType = UNSET, landscape: bool | UnsetType = UNSET) -> ViewportResponse
```

## `solve`

```python
solve(*, type: CaptchaType | UnsetType = UNSET, timeout: float | UnsetType = UNSET, wait: bool | UnsetType = UNSET) -> CaptchaResponse
```

## `solve_image_captcha`

```python
solve_image_captcha(*, captcha_selector: str, input_selector: str, timeout: float | UnsetType = UNSET) -> CaptchaResponse
```

## `stop_session_recording`

```python
stop_session_recording() -> StopSessionRecordingResponse
```

## `subscribe`

```python
subscribe(listener: Listener) -> Callable[[], None]
```

Listen to every non-handshake raw transport frame.

## `switch_to_window`

```python
switch_to_window(*, url: str | UnsetType = UNSET, title: str | UnsetType = UNSET, newest: bool | UnsetType = UNSET, timeout: float | UnsetType = UNSET, wait_until: WaitUntilGoto | UnsetType = UNSET) -> SwitchWindowResponse
```

## `text`

```python
text(*, timeout: float | UnsetType = UNSET, selector: str | UnsetType = UNSET, visible: bool | UnsetType = UNSET, clean: CleanInput | UnsetType = UNSET) -> TextResponse
```

## `title`

```python
title() -> str
```

## `type`

```python
type(selector: str, text: str, *, delay: list[int] | UnsetType = UNSET, interactable: bool | UnsetType = UNSET, scroll: bool | UnsetType = UNSET, timeout: float | UnsetType = UNSET, visible: bool | UnsetType = UNSET, wait: bool | UnsetType = UNSET) -> TypeResponse
```

## `uncheck`

```python
uncheck(selector: str, *, scroll: bool | UnsetType = UNSET, timeout: float | UnsetType = UNSET, visible: bool | UnsetType = UNSET, wait: bool | UnsetType = UNSET) -> ClickResponse
```

## `url`

```python
url() -> str
```

## `wait_for_event`

```python
wait_for_event(event: str, *, timeout: float | UnsetType = UNSET) -> WaitForEvent
```

Wait for the next event payload, bounded by `timeout` when provided.

## `wait_for_function`

```python
wait_for_function(expression: str, *, polling: float | UnsetType = UNSET, polling_type: WaitForFunctionPolling | UnsetType = UNSET, timeout: float | UnsetType = UNSET, complete_on_promise_resolution: bool | UnsetType = UNSET) -> WaitForFunction
```

## `wait_for_navigation`

```python
wait_for_navigation(*, timeout: float | UnsetType = UNSET, wait_until: WaitUntilGoto | UnsetType = UNSET) -> HTTPResponse
```

## `wait_for_network_idle`

```python
wait_for_network_idle(*, idle_time: float | UnsetType = UNSET, concurrency: int | UnsetType = UNSET, timeout: float | UnsetType = UNSET) -> WaitForTimeoutResponse
```

## `wait_for_request`

```python
wait_for_request(url: str | UnsetType = UNSET, *, method: Method | UnsetType = UNSET, operator: OperatorType | UnsetType = UNSET, timeout: float | UnsetType = UNSET) -> WaitForRequestResponse
```

## `wait_for_response`

```python
wait_for_response(url: str | UnsetType = UNSET, *, statuses: Sequence[int] | UnsetType = UNSET, codes: Sequence[int] | UnsetType = UNSET, operator: OperatorType | UnsetType = UNSET, timeout: float | UnsetType = UNSET) -> WaitForResponseResponse
```

## `wait_for_selector`

```python
wait_for_selector(selector: str, *, timeout: float | UnsetType = UNSET, visible: bool | UnsetType = UNSET, hidden: bool | UnsetType = UNSET) -> WaitForSelectorResponse
```

## `wait_for_timeout`

```python
wait_for_timeout(ms: float) -> None
```

## `watch_event`

```python
watch_event(event: str) -> WaitForEvent
```

Start tracking `event` now so a later `wait_for_event` call can return immediately if that event has already fired.

---

# Enums

URL: https://docs.browserless.io/bap/python/reference/enums

# Enums

## `AttributeMode`

Type: `Literal['ALLOW', 'DENY']`

## `CaptchaType`

Type: `Literal['cloudflare', 'recaptcha', 'recaptchaV3', 'geetest', 'normal', 'numberCaptcha', 'friendlyCaptcha', 'capy', 'textCaptcha', 'amazonWaf', 'datadome', 'akamai', 'lemin', 'mtcaptcha', 'slider', 'audio', 'funcaptcha', 'click', 'yandex', 'math', 'perimeterx']`

## `ConsoleMessageType`

Type: `Literal['log', 'debug', 'info', 'error', 'warning', 'dir', 'dirxml', 'table', 'trace', 'clear', 'startGroup', 'startGroupCollapsed', 'endGroup', 'assert', 'profile', 'profileEnd', 'count', 'timeEnd', 'verbose']`

## `CookiePriority`

Type: `Literal['Low', 'Medium', 'High']`

## `CookieSameSite`

Type: `Literal['Strict', 'Lax', 'None']`

## `CookieSourceScheme`

Type: `Literal['Unset', 'NonSecure', 'Secure']`

## `CountryCode`

Type: `Literal['AD', 'AE', 'AF', 'AG', 'AL', 'AM', 'AO', 'AR', 'AT', 'AU', 'AW', 'AX', 'AZ', 'BA', 'BB', 'BD', 'BE', 'BF', 'BG', 'BH', 'BI', 'BJ', 'BM', 'BN', 'BO', 'BQ', 'BR', 'BS', 'BT', 'BW', 'BY', 'BZ', 'CA', 'CD', 'CG', 'CH', 'CI', 'CL', 'CM', 'CN', 'CO', 'CR', 'CU', 'CV', 'CW', 'CY', 'CZ', 'DE', 'DJ', 'DK', 'DM', 'DO', 'DZ', 'EC', 'EE', 'EG', 'ES', 'ET', 'FI', 'FJ', 'FO', 'FR', 'GA', 'GB', 'GD', 'GE', 'GF', 'GG', 'GH', 'GI', 'GM', 'GN', 'GP', 'GR', 'GT', 'GU', 'GY', 'HK', 'HN', 'HR', 'HT', 'HU', 'ID', 'IE', 'IL', 'IM', 'IN', 'IQ', 'IR', 'IS', 'IT', 'JE', 'JM', 'JO', 'JP', 'KE', 'KG', 'KH', 'KN', 'KR', 'KW', 'KY', 'KZ', 'LA', 'LB', 'LC', 'LI', 'LK', 'LR', 'LS', 'LT', 'LU', 'LV', 'LY', 'MA', 'MC', 'MD', 'ME', 'MG', 'MK', 'ML', 'MM', 'MN', 'MO', 'MQ', 'MR', 'MS', 'MT', 'MU', 'MV', 'MW', 'MX', 'MY', 'MZ', 'NA', 'NC', 'NG', 'NI', 'NL', 'NO', 'NP', 'NZ', 'OM', 'PA', 'PE', 'PF', 'PG', 'PH', 'PK', 'PL', 'PR', 'PS', 'PT', 'PY', 'QA', 'RE', 'RO', 'RS', 'RU', 'RW', 'SC', 'SE', 'SG', 'SI', 'SK', 'SL', 'SM', 'SN', 'SO', 'SR', 'SS', 'ST', 'SV', 'SX', 'SY', 'SZ', 'TC', 'TG', 'TH', 'TJ', 'TN', 'TR', 'TT', 'TW', 'TZ', 'UA', 'UG', 'US', 'UY', 'UZ', 'VC', 'VE', 'VG', 'VI', 'VN', 'WS', 'YT', 'ZA', 'ZM', 'ZW']`

## `LiveURLStreamType`

Type: `Literal['jpeg', 'png']`

## `MediaType`

Type: `Literal['screen', 'print']`

## `Method`

Type: `Literal['CONNECT', 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE']`

## `OperatorType`

Type: `Literal['and', 'or']`

## `PDFPageFormat`

Type: `Literal['a0', 'a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'legal', 'letter', 'ledger', 'tabloid']`

## `ProxyNetwork`

Type: `Literal['residential', 'datacenter']`

## `ResourceType`

Type: `Literal['cspviolationreport', 'document', 'eventsource', 'fetch', 'font', 'image', 'manifest', 'media', 'other', 'ping', 'prefetch', 'preflight', 'script', 'signedexchange', 'stylesheet', 'texttrack', 'websocket', 'xhr']`

## `ScreenshotType`

Type: `Literal['jpeg', 'png', 'webp']`

## `WaitForFunctionPolling`

Type: `Literal['raf', 'mutation']`

## `WaitUntilGoto`

Type: `Literal['commit', 'load', 'domContentLoaded', 'firstMeaningfulPaint', 'firstContentfulPaint', 'interactiveTime', 'networkIdle']`

## `WaitUntilHistory`

Type: `Literal['commit', 'domContentLoaded', 'load', 'networkIdle']`

---

# Errors

URL: https://docs.browserless.io/bap/python/reference/errors

# Errors

## `BAPError`

Base class for errors produced by the SDK.

## `BrowserQLError`

The GraphQL operation returned one or more errors.

## `ConnectionError`

The BAP transport could not connect or disconnected.

## `SchemaMismatchError`

The generated schema metadata and runtime call site disagree.

## `TimeoutError`

A BAP operation exceeded its transport deadline.

## `ValueError`

A public SDK argument failed client-side validation.

---

# Event types

URL: https://docs.browserless.io/bap/python/reference/events

# Event types

## `EventName`

Type: `Literal['console', 'request', 'response', 'error', 'message']`

## `Listener`

Type: `Callable[[Any], Any]`

## `PageEventMap`

| Field | Type |
| ----- | ---- |
| `console` | `ConsoleMessage` |
| `error` | `Exception` |
| `message` | `str` |
| `request` | `RequestResponse` |
| `response` | `ResponseResponse` |

## `PageEventsMixin`

### `off`

```python
off(event: EventName, listener: Listener) -> Self
```

### `on`

```python
on(event: EventName, listener: Listener) -> Self
```

### `once`

```python
once(event: EventName, listener: Listener) -> Self
```

---

# Input types

URL: https://docs.browserless.io/bap/python/reference/inputs

# Input types

## `CleanInput`

| Field | Type |
| ----- | ---- |
| `attributes` | `NotRequired[list[str]]` |
| `mode` | `NotRequired[AttributeMode]` |
| `regexes` | `NotRequired[list[str]]` |
| `removeAttributes` | `NotRequired[bool]` |
| `removeNonTextNodes` | `NotRequired[bool]` |
| `removeRegex` | `NotRequired[bool]` |
| `selectors` | `NotRequired[list[str]]` |

## `CookieInput`

| Field | Type |
| ----- | ---- |
| `domain` | `NotRequired[str]` |
| `expires` | `NotRequired[float]` |
| `httpOnly` | `NotRequired[bool]` |
| `name` | `Required[str]` |
| `partitionKey` | `NotRequired[CookiePartitionKeyInput]` |
| `path` | `NotRequired[str]` |
| `priority` | `NotRequired[CookiePriority]` |
| `sameSite` | `NotRequired[CookieSameSite]` |
| `secure` | `NotRequired[bool]` |
| `sourceScheme` | `NotRequired[CookieSourceScheme]` |
| `url` | `NotRequired[str]` |
| `value` | `Required[str]` |

## `CookiePartitionKeyInput`

| Field | Type |
| ----- | ---- |
| `hasCrossSiteAncestor` | `NotRequired[bool]` |
| `sourceOrigin` | `Required[str]` |

## `HTMLResourcePathInput`

| Field | Type |
| ----- | ---- |
| `path` | `Required[str]` |
| `url` | `Required[str]` |

## `HTMLRewriteInput`

| Field | Type |
| ----- | ---- |
| `baseURL` | `Required[str]` |
| `resources` | `Required[list[HTMLResourcePathInput]]` |

## `HeaderInput`

| Field | Type |
| ----- | ---- |
| `name` | `Required[str]` |
| `value` | `Required[str]` |

## `RequestInput`

| Field | Type |
| ----- | ---- |
| `method` | `NotRequired[Method]` |
| `url` | `NotRequired[str]` |

## `ResponseInput`

| Field | Type |
| ----- | ---- |
| `codes` | `NotRequired[list[int]]` |
| `statuses` | `NotRequired[list[int]]` |
| `url` | `NotRequired[str]` |

## `ScreenshotClip`

| Field | Type |
| ----- | ---- |
| `height` | `NotRequired[float]` |
| `scale` | `NotRequired[float]` |
| `width` | `NotRequired[float]` |
| `x` | `NotRequired[float]` |
| `y` | `NotRequired[float]` |

---

# Option types

URL: https://docs.browserless.io/bap/python/reference/options

# Option types

## `AddScriptTagOptions`

| Field | Type |
| ----- | ---- |
| `content` | `NotRequired[str]` |
| `id` | `NotRequired[str]` |
| `path` | `NotRequired[str]` |
| `type` | `NotRequired[str]` |
| `url` | `NotRequired[str]` |

## `AddStyleTagOptions`

| Field | Type |
| ----- | ---- |
| `content` | `NotRequired[str]` |
| `path` | `NotRequired[str]` |
| `url` | `NotRequired[str]` |

## `AuthenticateOptions`

| Field | Type |
| ----- | ---- |
| `origin` | `NotRequired[str]` |

## `CheckboxOptions`

| Field | Type |
| ----- | ---- |
| `scroll` | `NotRequired[bool]` |
| `timeout` | `NotRequired[float]` |
| `visible` | `NotRequired[bool]` |
| `wait` | `NotRequired[bool]` |

## `ClickOptions`

| Field | Type |
| ----- | ---- |
| `scroll` | `NotRequired[bool]` |
| `timeout` | `NotRequired[float]` |
| `visible` | `NotRequired[bool]` |
| `wait` | `NotRequired[bool]` |

## `ConnectOptions`

| Field | Type |
| ----- | ---- |
| `browser_ws_endpoint` | `NotRequired[str]` |
| `timeout` | `NotRequired[float]` |
| `token` | `NotRequired[str]` |
| `transport` | `NotRequired[object]` |

## `EvaluateOptions`

| Field | Type |
| ----- | ---- |
| `timeout` | `NotRequired[float]` |
| `url` | `NotRequired[str]` |

## `FulfillOptions`

| Field | Type |
| ----- | ---- |
| `base64_body` | `NotRequired[str]` |
| `body` | `NotRequired[str]` |
| `content_type` | `NotRequired[str]` |
| `headers` | `NotRequired[list[HeaderInput]]` |
| `method` | `NotRequired[list[Method]]` |
| `operator` | `NotRequired[OperatorType]` |
| `regex` | `NotRequired[list[str]]` |
| `status` | `NotRequired[int]` |
| `type` | `NotRequired[list[ResourceType]]` |
| `url` | `NotRequired[list[str]]` |

## `GotoOptions`

| Field | Type |
| ----- | ---- |
| `referer` | `NotRequired[str]` |
| `referrer_policy` | `NotRequired[str]` |
| `remote_response_metadata` | `NotRequired[bool]` |
| `timeout` | `NotRequired[float]` |
| `wait_until` | `NotRequired[WaitUntilGoto]` |
| `wait_until_all` | `NotRequired[list[WaitUntilGoto]]` |

## `HTMLOptions`

| Field | Type |
| ----- | ---- |
| `clean` | `NotRequired[CleanInput]` |
| `rewrite` | `NotRequired[HTMLRewriteInput]` |
| `selector` | `NotRequired[str]` |
| `timeout` | `NotRequired[float]` |
| `visible` | `NotRequired[bool]` |

## `HoverOptions`

| Field | Type |
| ----- | ---- |
| `scroll` | `NotRequired[bool]` |
| `timeout` | `NotRequired[float]` |
| `visible` | `NotRequired[bool]` |
| `wait` | `NotRequired[bool]` |
| `x` | `NotRequired[float]` |
| `y` | `NotRequired[float]` |

## `JavaScriptEnabledOptions`

| Field | Type |
| ----- | ---- |
| `enabled` | `NotRequired[bool]` |

## `LiveURLOptions`

| Field | Type |
| ----- | ---- |
| `compressed` | `NotRequired[bool]` |
| `emulate_components` | `NotRequired[bool]` |
| `interactable` | `NotRequired[bool]` |
| `quality` | `NotRequired[int]` |
| `resizable` | `NotRequired[bool]` |
| `show_browser_interface` | `NotRequired[bool]` |
| `timeout` | `NotRequired[float]` |
| `type` | `NotRequired[LiveURLStreamType]` |

## `LoadSecretOptions`

| Field | Type |
| ----- | ---- |
| `selector` | `NotRequired[str]` |

## `MapSelectorOptions`

| Field | Type |
| ----- | ---- |
| `timeout` | `NotRequired[float]` |
| `wait` | `NotRequired[bool]` |

## `MarkdownOptions`

| Field | Type |
| ----- | ---- |
| `selector` | `NotRequired[str]` |
| `timeout` | `NotRequired[float]` |
| `visible` | `NotRequired[bool]` |

## `NavigationOptions`

| Field | Type |
| ----- | ---- |
| `timeout` | `NotRequired[float]` |
| `wait_until` | `NotRequired[WaitUntilHistory]` |

## `PDFOptions`

| Field | Type |
| ----- | ---- |
| `display_header_footer` | `NotRequired[bool]` |
| `footer_template` | `NotRequired[str]` |
| `format` | `NotRequired[PDFPageFormat]` |
| `full_page` | `NotRequired[bool]` |
| `generate_document_outline` | `NotRequired[bool]` |
| `generate_tagged_pdf` | `NotRequired[bool]` |
| `header_template` | `NotRequired[str]` |
| `height` | `NotRequired[float \| str]` |
| `landscape` | `NotRequired[bool]` |
| `margin_bottom` | `NotRequired[float \| str]` |
| `margin_left` | `NotRequired[float \| str]` |
| `margin_right` | `NotRequired[float \| str]` |
| `margin_top` | `NotRequired[float \| str]` |
| `omit_background` | `NotRequired[bool]` |
| `page_ranges` | `NotRequired[str]` |
| `path` | `NotRequired[str]` |
| `prefer_css_page_size` | `NotRequired[bool]` |
| `print_background` | `NotRequired[bool]` |
| `scale` | `NotRequired[float]` |
| `timeout` | `NotRequired[float]` |
| `transfer_mode` | `NotRequired[str]` |
| `wait_for_fonts` | `NotRequired[bool]` |
| `wait_for_images` | `NotRequired[bool]` |
| `width` | `NotRequired[float \| str]` |

## `PreferencesOptions`

| Field | Type |
| ----- | ---- |
| `timeout` | `NotRequired[float]` |

## `ProxyOptions`

| Field | Type |
| ----- | ---- |
| `city` | `NotRequired[str]` |
| `country` | `NotRequired[CountryCode]` |
| `method` | `NotRequired[list[Method]]` |
| `network` | `NotRequired[ProxyNetwork]` |
| `operator` | `NotRequired[OperatorType]` |
| `preset` | `NotRequired[str]` |
| `server` | `NotRequired[str]` |
| `state` | `NotRequired[str]` |
| `sticky` | `NotRequired[bool]` |
| `type` | `NotRequired[list[ResourceType]]` |
| `url` | `NotRequired[list[str]]` |

## `QuerySelectorOptions`

| Field | Type |
| ----- | ---- |
| `timeout` | `NotRequired[float]` |
| `visible` | `NotRequired[bool]` |

## `ReconnectOptions`

| Field | Type |
| ----- | ---- |
| `timeout` | `NotRequired[float]` |

## `RejectOptions`

| Field | Type |
| ----- | ---- |
| `enabled` | `NotRequired[bool]` |
| `method` | `NotRequired[list[Method]]` |
| `operator` | `NotRequired[OperatorType]` |
| `regex` | `NotRequired[list[str]]` |
| `type` | `NotRequired[list[ResourceType]]` |
| `url` | `NotRequired[list[str]]` |

## `RequestFilterOptions`

| Field | Type |
| ----- | ---- |
| `method` | `NotRequired[list[Method]]` |
| `operator` | `NotRequired[OperatorType]` |
| `timeout` | `NotRequired[float]` |
| `type` | `NotRequired[list[ResourceType]]` |
| `url` | `NotRequired[list[str]]` |
| `wait` | `NotRequired[bool]` |

## `ResponseFilterOptions`

| Field | Type |
| ----- | ---- |
| `body_encoding` | `NotRequired[bool]` |
| `method` | `NotRequired[list[Method]]` |
| `operator` | `NotRequired[OperatorType]` |
| `status` | `NotRequired[list[int]]` |
| `timeout` | `NotRequired[float]` |
| `type` | `NotRequired[list[ResourceType]]` |
| `url` | `NotRequired[list[str]]` |
| `wait` | `NotRequired[bool]` |

## `ScreenshotOptions`

| Field | Type |
| ----- | ---- |
| `capture_beyond_viewport` | `NotRequired[bool]` |
| `clip` | `NotRequired[ScreenshotClip]` |
| `from_surface` | `NotRequired[bool]` |
| `full_page` | `NotRequired[bool]` |
| `omit_background` | `NotRequired[bool]` |
| `optimize_for_speed` | `NotRequired[bool]` |
| `path` | `NotRequired[str]` |
| `quality` | `NotRequired[float]` |
| `selector` | `NotRequired[str]` |
| `timeout` | `NotRequired[float]` |
| `type` | `NotRequired[ScreenshotType]` |
| `wait_for_images` | `NotRequired[bool]` |

## `ScrollOptions`

| Field | Type |
| ----- | ---- |
| `selector` | `NotRequired[str]` |
| `through_page` | `NotRequired[bool]` |
| `timeout` | `NotRequired[float]` |
| `visible` | `NotRequired[bool]` |
| `wait` | `NotRequired[bool]` |
| `x` | `NotRequired[float]` |
| `y` | `NotRequired[float]` |

## `SelectOptions`

| Field | Type |
| ----- | ---- |
| `scroll` | `NotRequired[bool]` |
| `timeout` | `NotRequired[float]` |
| `visible` | `NotRequired[bool]` |
| `wait` | `NotRequired[bool]` |

## `SendOptions`

| Field | Type |
| ----- | ---- |
| `timeout` | `NotRequired[float]` |
| `variables` | `NotRequired[dict[str, object]]` |

## `SetContentOptions`

| Field | Type |
| ----- | ---- |
| `timeout` | `NotRequired[float]` |
| `wait_until` | `NotRequired[WaitUntilHistory]` |
| `wait_until_all` | `NotRequired[list[WaitUntilHistory]]` |

## `SetCookieOptions`

| Field | Type |
| ----- | ---- |
| `cookies` | `NotRequired[list[CookieInput]]` |

## `SetExtraHTTPHeadersOptions`

| Field | Type |
| ----- | ---- |
| `headers` | `NotRequired[list[HeaderInput]]` |

## `SolveImageCaptchaOptions`

| Field | Type |
| ----- | ---- |
| `captcha_selector` | `Required[str]` |
| `input_selector` | `Required[str]` |
| `timeout` | `NotRequired[float]` |

## `SolveOptions`

| Field | Type |
| ----- | ---- |
| `timeout` | `NotRequired[float]` |
| `type` | `NotRequired[CaptchaType]` |
| `wait` | `NotRequired[bool]` |

## `SwitchToWindowOptions`

| Field | Type |
| ----- | ---- |
| `newest` | `NotRequired[bool]` |
| `timeout` | `NotRequired[float]` |
| `title` | `NotRequired[str]` |
| `url` | `NotRequired[str]` |
| `wait_until` | `NotRequired[WaitUntilGoto]` |

## `TextOptions`

| Field | Type |
| ----- | ---- |
| `clean` | `NotRequired[CleanInput]` |
| `selector` | `NotRequired[str]` |
| `timeout` | `NotRequired[float]` |
| `visible` | `NotRequired[bool]` |

## `TypeOptions`

| Field | Type |
| ----- | ---- |
| `delay` | `NotRequired[list[int]]` |
| `interactable` | `NotRequired[bool]` |
| `scroll` | `NotRequired[bool]` |
| `timeout` | `NotRequired[float]` |
| `visible` | `NotRequired[bool]` |
| `wait` | `NotRequired[bool]` |

## `ViewportOptions`

| Field | Type |
| ----- | ---- |
| `device_scale_factor` | `NotRequired[float]` |
| `has_touch` | `NotRequired[bool]` |
| `height` | `Required[float]` |
| `landscape` | `NotRequired[bool]` |
| `mobile` | `NotRequired[bool]` |
| `width` | `Required[float]` |

## `WaitForEventOptions`

| Field | Type |
| ----- | ---- |
| `timeout` | `NotRequired[float]` |

## `WaitForFunctionOptions`

| Field | Type |
| ----- | ---- |
| `complete_on_promise_resolution` | `NotRequired[bool]` |
| `polling` | `NotRequired[float]` |
| `polling_type` | `NotRequired[WaitForFunctionPolling]` |
| `timeout` | `NotRequired[float]` |

## `WaitForNavigationOptions`

| Field | Type |
| ----- | ---- |
| `timeout` | `NotRequired[float]` |
| `wait_until` | `NotRequired[WaitUntilGoto]` |

## `WaitForNetworkIdleOptions`

| Field | Type |
| ----- | ---- |
| `concurrency` | `NotRequired[int]` |
| `idle_time` | `NotRequired[float]` |
| `timeout` | `NotRequired[float]` |

## `WaitForRequestOptions`

| Field | Type |
| ----- | ---- |
| `method` | `NotRequired[Method]` |
| `operator` | `NotRequired[OperatorType]` |
| `timeout` | `NotRequired[float]` |
| `url` | `NotRequired[str]` |

## `WaitForResponseOptions`

| Field | Type |
| ----- | ---- |
| `codes` | `NotRequired[list[int]]` |
| `operator` | `NotRequired[OperatorType]` |
| `statuses` | `NotRequired[list[int]]` |
| `timeout` | `NotRequired[float]` |
| `url` | `NotRequired[str]` |

## `WaitForSelectorOptions`

| Field | Type |
| ----- | ---- |
| `hidden` | `NotRequired[bool]` |
| `timeout` | `NotRequired[float]` |
| `visible` | `NotRequired[bool]` |

---

# Response types

URL: https://docs.browserless.io/bap/python/reference/responses

# Response types

## `AddScriptTagResponse`

| Field | Type |
| ----- | ---- |
| `time` | `NotRequired[float \| None]` |

## `AddStyleTagResponse`

| Field | Type |
| ----- | ---- |
| `time` | `NotRequired[float \| None]` |

## `Attribute`

| Field | Type |
| ----- | ---- |
| `name` | `NotRequired[str \| None]` |
| `value` | `NotRequired[str \| None]` |

## `CaptchaResponse`

| Field | Type |
| ----- | ---- |
| `error` | `NotRequired[str \| None]` |
| `found` | `NotRequired[bool \| None]` |
| `solved` | `NotRequired[bool \| None]` |
| `time` | `NotRequired[float \| None]` |
| `token` | `NotRequired[str \| None]` |

## `ClickResponse`

| Field | Type |
| ----- | ---- |
| `selector` | `NotRequired[str \| None]` |
| `time` | `NotRequired[float \| None]` |
| `x` | `NotRequired[float \| None]` |
| `y` | `NotRequired[float \| None]` |

## `ConsoleMessage`

| Field | Type |
| ----- | ---- |
| `args` | `NotRequired[list[str] \| None]` |
| `location` | `NotRequired[ConsoleMessageLocation \| None]` |
| `text` | `NotRequired[str \| None]` |
| `timestamp` | `NotRequired[float \| None]` |
| `type` | `NotRequired[ConsoleMessageType \| None]` |

## `ConsoleMessageLocation`

| Field | Type |
| ----- | ---- |
| `columnNumber` | `NotRequired[int \| None]` |
| `lineNumber` | `NotRequired[int \| None]` |
| `url` | `NotRequired[str \| None]` |

## `CookieResponse`

| Field | Type |
| ----- | ---- |
| `cookies` | `NotRequired[list[StandardCookie] \| None]` |
| `time` | `NotRequired[float \| None]` |

## `DefaultResponse`

| Field | Type |
| ----- | ---- |
| `timeout` | `NotRequired[float \| None]` |

## `EmulateMediaTypeResponse`

| Field | Type |
| ----- | ---- |
| `time` | `NotRequired[float \| None]` |
| `type` | `NotRequired[str \| None]` |

## `EvaluateResponse`

| Field | Type |
| ----- | ---- |
| `time` | `NotRequired[float \| None]` |
| `value` | `NotRequired[str \| None]` |

## `FulfillResponse`

| Field | Type |
| ----- | ---- |
| `time` | `NotRequired[float \| None]` |

## `HTMLResponse`

| Field | Type |
| ----- | ---- |
| `html` | `NotRequired[str \| None]` |
| `time` | `NotRequired[float \| None]` |

## `HTTPHeaders`

| Field | Type |
| ----- | ---- |
| `name` | `NotRequired[str \| None]` |
| `value` | `NotRequired[str \| None]` |

## `HTTPHeadersResponse`

| Field | Type |
| ----- | ---- |
| `headers` | `NotRequired[list[HTTPHeaders] \| None]` |
| `time` | `NotRequired[float \| None]` |

## `HTTPResponse`

| Field | Type |
| ----- | ---- |
| `ip` | `NotRequired[str \| None]` |
| `port` | `NotRequired[int \| None]` |
| `status` | `NotRequired[int \| None]` |
| `text` | `NotRequired[str \| None]` |
| `time` | `NotRequired[float \| None]` |
| `url` | `NotRequired[str \| None]` |

## `HoverResponse`

| Field | Type |
| ----- | ---- |
| `selector` | `NotRequired[str \| None]` |
| `time` | `NotRequired[float \| None]` |
| `x` | `NotRequired[float \| None]` |
| `y` | `NotRequired[float \| None]` |

## `JavaScriptResponse`

| Field | Type |
| ----- | ---- |
| `enabled` | `NotRequired[bool \| None]` |
| `time` | `NotRequired[float \| None]` |

## `LiveURLResponse`

| Field | Type |
| ----- | ---- |
| `liveURL` | `NotRequired[str \| None]` |
| `liveURLId` | `NotRequired[str \| None]` |

## `LoadSecretResponse`

| Field | Type |
| ----- | ---- |
| `error` | `NotRequired[str \| None]` |
| `message` | `NotRequired[str \| None]` |
| `ok` | `NotRequired[bool \| None]` |
| `time` | `NotRequired[float \| None]` |

## `MapSelectorResponse`

| Field | Type |
| ----- | ---- |
| `innerHTML` | `NotRequired[str \| None]` |
| `innerText` | `NotRequired[str \| None]` |
| `id` | `NotRequired[str \| None]` |
| `class` | `NotRequired[list[str] \| None]` |

## `MarkdownResponse`

| Field | Type |
| ----- | ---- |
| `markdown` | `NotRequired[str \| None]` |
| `time` | `NotRequired[float \| None]` |

## `PDFResponse`

| Field | Type |
| ----- | ---- |
| `base64` | `NotRequired[str \| None]` |
| `size` | `NotRequired[float \| None]` |
| `time` | `NotRequired[float \| None]` |

## `ProxyResponse`

| Field | Type |
| ----- | ---- |
| `time` | `NotRequired[float \| None]` |

## `QuerySelectorResponse`

| Field | Type |
| ----- | ---- |
| `childElementCount` | `NotRequired[float \| None]` |
| `className` | `NotRequired[str \| None]` |
| `id` | `NotRequired[str \| None]` |
| `innerHTML` | `NotRequired[str \| None]` |
| `innerText` | `NotRequired[str \| None]` |
| `localName` | `NotRequired[str \| None]` |
| `outerHTML` | `NotRequired[str \| None]` |

## `ReconnectionResponse`

| Field | Type |
| ----- | ---- |
| `browserQLEndpoint` | `NotRequired[str \| None]` |
| `browserWSEndpoint` | `NotRequired[str \| None]` |
| `devtoolsFrontendUrl` | `NotRequired[str \| None]` |
| `webSocketDebuggerUrl` | `NotRequired[str \| None]` |

## `RejectResponse`

| Field | Type |
| ----- | ---- |
| `enabled` | `NotRequired[bool \| None]` |
| `time` | `NotRequired[float \| None]` |

## `RequestHeader`

| Field | Type |
| ----- | ---- |
| `name` | `NotRequired[str \| None]` |
| `value` | `NotRequired[str \| None]` |

## `RequestResponse`

| Field | Type |
| ----- | ---- |
| `headers` | `NotRequired[list[RequestHeader] \| None]` |
| `method` | `NotRequired[Method \| None]` |
| `type` | `NotRequired[ResourceType \| None]` |
| `url` | `NotRequired[str \| None]` |

## `ResponseResponse`

| Field | Type |
| ----- | ---- |
| `base64Encoded` | `NotRequired[bool \| None]` |
| `body` | `NotRequired[str \| None]` |
| `headers` | `NotRequired[list[RequestHeader] \| None]` |
| `method` | `NotRequired[Method \| None]` |
| `status` | `NotRequired[int \| None]` |
| `type` | `NotRequired[ResourceType \| None]` |
| `url` | `NotRequired[str \| None]` |

## `ScreenshotResponse`

| Field | Type |
| ----- | ---- |
| `base64` | `NotRequired[str \| None]` |
| `format` | `NotRequired[str \| None]` |
| `time` | `NotRequired[float \| None]` |

## `ScrollResponse`

| Field | Type |
| ----- | ---- |
| `selector` | `NotRequired[str \| None]` |
| `time` | `NotRequired[float \| None]` |
| `x` | `NotRequired[float \| None]` |
| `y` | `NotRequired[float \| None]` |

## `SelectResponse`

| Field | Type |
| ----- | ---- |
| `selector` | `NotRequired[str \| None]` |
| `time` | `NotRequired[float \| None]` |
| `value` | `NotRequired[str \| list[str] \| None]` |

## `StandardCookie`

| Field | Type |
| ----- | ---- |
| `domain` | `NotRequired[str \| None]` |
| `expires` | `NotRequired[float \| None]` |
| `httpOnly` | `NotRequired[bool \| None]` |
| `name` | `NotRequired[str \| None]` |
| `path` | `NotRequired[str \| None]` |
| `sameSite` | `NotRequired[CookieSameSite \| None]` |
| `secure` | `NotRequired[bool \| None]` |
| `url` | `NotRequired[str \| None]` |
| `value` | `NotRequired[str \| None]` |

## `StopSessionRecordingResponse`

| Field | Type |
| ----- | ---- |
| `error` | `NotRequired[str \| None]` |
| `success` | `NotRequired[bool \| None]` |

## `SwitchWindowResponse`

| Field | Type |
| ----- | ---- |
| `error` | `NotRequired[str \| None]` |
| `navigation` | `NotRequired[HTTPResponse \| None]` |
| `success` | `NotRequired[bool \| None]` |
| `title` | `NotRequired[str \| None]` |
| `url` | `NotRequired[str \| None]` |

## `TextResponse`

| Field | Type |
| ----- | ---- |
| `text` | `NotRequired[str \| None]` |
| `time` | `NotRequired[float \| None]` |

## `TitleResponse`

| Field | Type |
| ----- | ---- |
| `title` | `NotRequired[str \| None]` |

## `TypeResponse`

| Field | Type |
| ----- | ---- |
| `selector` | `NotRequired[str \| None]` |
| `text` | `NotRequired[str \| None]` |
| `time` | `NotRequired[float \| None]` |
| `x` | `NotRequired[float \| None]` |
| `y` | `NotRequired[float \| None]` |

## `URLResponse`

| Field | Type |
| ----- | ---- |
| `url` | `NotRequired[str \| None]` |

## `UserAgentResponse`

| Field | Type |
| ----- | ---- |
| `time` | `NotRequired[float \| None]` |
| `userAgent` | `NotRequired[str \| None]` |

## `ViewportResponse`

| Field | Type |
| ----- | ---- |
| `deviceScaleFactor` | `NotRequired[float \| None]` |
| `hasTouch` | `NotRequired[bool \| None]` |
| `height` | `NotRequired[float \| None]` |
| `landscape` | `NotRequired[bool \| None]` |
| `mobile` | `NotRequired[bool \| None]` |
| `time` | `NotRequired[float \| None]` |
| `width` | `NotRequired[float \| None]` |

## `WaitForEvent`

| Field | Type |
| ----- | ---- |
| `time` | `NotRequired[float \| None]` |

## `WaitForFunction`

| Field | Type |
| ----- | ---- |
| `time` | `NotRequired[float \| None]` |

## `WaitForRequestResponse`

| Field | Type |
| ----- | ---- |
| `time` | `NotRequired[float \| None]` |
| `url` | `NotRequired[str \| None]` |

## `WaitForResponseResponse`

| Field | Type |
| ----- | ---- |
| `status` | `NotRequired[int \| None]` |
| `time` | `NotRequired[float \| None]` |
| `url` | `NotRequired[str \| None]` |

## `WaitForSelectorResponse`

| Field | Type |
| ----- | ---- |
| `height` | `NotRequired[float \| None]` |
| `selector` | `NotRequired[str \| None]` |
| `time` | `NotRequired[float \| None]` |
| `width` | `NotRequired[float \| None]` |
| `x` | `NotRequired[float \| None]` |
| `y` | `NotRequired[float \| None]` |

## `WaitForTimeoutResponse`

| Field | Type |
| ----- | ---- |
| `time` | `NotRequired[float \| None]` |

---

# Sync Browser

URL: https://docs.browserless.io/bap/python/reference/sync-browser

# Sync Browser

## `close`

```python
close() -> None
```

## `get_pages`

```python
get_pages() -> list[Page]
```

## `new_page`

```python
new_page() -> Page
```

## `page`

```python
page() -> ContextManager[Page]
```

Open a page and close it when the context exits.

```python
with browser.page() as page:
    page.goto("https://example.com")
```

## `pages`

```python
pages() -> list[Page]
```

---

# Sync Browserless

URL: https://docs.browserless.io/bap/python/reference/sync-browserless

# Sync Browserless

## `connect`

```python
connect(*, browser_ws_endpoint: str | None = None, token: str | None = None, timeout: float = 30000, transport: Any = None) -> Browser
```

---

# Sync ElementHandle

URL: https://docs.browserless.io/bap/python/reference/sync-element-handle

# Sync ElementHandle

## `child_element_count`

Type: `int | None`

## `class_name`

Type: `str | None`

## `click`

```python
click(**options: Unpack[ClickOptions]) -> ClickResponse
```

## `hover`

```python
hover() -> HoverResponse
```

## `id`

Type: `str | None`

## `inner_html`

Type: `str | None`

## `inner_text`

Type: `str | None`

## `local_name`

Type: `str | None`

## `outer_html`

Type: `str | None`

## `query_selector`

```python
query_selector(selector: str) -> ElementHandle | None
```

## `query_selector_all`

```python
query_selector_all(selector: str) -> list[ElementHandle]
```

## `screenshot`

```python
screenshot(*, path: str | None = None) -> bytes
```

## `select_option`

```python
select_option(value: str | Sequence[str]) -> SelectResponse
```

## `selector`

Type: `str`

## `text_content`

```python
text_content() -> str
```

## `type`

```python
type(text: str, **options: Unpack[TypeOptions]) -> TypeResponse
```

---

# Sync Page

URL: https://docs.browserless.io/bap/python/reference/sync-page

# Sync Page

## `add_cookies`

```python
add_cookies(cookies: Sequence[CookieInput]) -> CookieResponse
```

## `add_script_tag`

```python
add_script_tag(*, url: str | UnsetType = UNSET, path: str | Path | UnsetType = UNSET, content: str | UnsetType = UNSET, id: str | UnsetType = UNSET, type: str | UnsetType = UNSET) -> AddScriptTagResponse
```

## `add_style_tag`

```python
add_style_tag(*, url: str | UnsetType = UNSET, path: str | Path | UnsetType = UNSET, content: str | UnsetType = UNSET) -> AddStyleTagResponse
```

## `authenticate`

```python
authenticate(username: str, password: str, *, origin: str | UnsetType = UNSET) -> HTTPResponse | None
```

## `check`

```python
check(selector: str, *, scroll: bool | UnsetType = UNSET, timeout: float | UnsetType = UNSET, visible: bool | UnsetType = UNSET, wait: bool | UnsetType = UNSET) -> ClickResponse
```

## `click`

```python
click(selector: str, *, scroll: bool | UnsetType = UNSET, timeout: float | UnsetType = UNSET, visible: bool | UnsetType = UNSET, wait: bool | UnsetType = UNSET) -> ClickResponse
```

## `close`

```python
close() -> None
```

## `closed`

Type: `bool`

## `content`

```python
content() -> str
```

## `cookies`

```python
cookies() -> list[StandardCookie]
```

## `emit`

```python
emit(event: str, payload: object) -> bool
```

## `emulate_media`

```python
emulate_media(type: MediaType) -> EmulateMediaTypeResponse
```

## `eval_on_selector`

```python
eval_on_selector(selector: str) -> str
```

## `eval_on_selector_all`

```python
eval_on_selector_all(selector: str, *, timeout: float | UnsetType = UNSET, wait: bool | UnsetType = UNSET) -> list[MapSelectorResponse]
```

## `evaluate`

```python
evaluate(expression: str, *, timeout: float | UnsetType = UNSET, url: str | UnsetType = UNSET) -> str | None
```

## `fulfill`

```python
fulfill(*, url: list[str] | UnsetType = UNSET, regex: list[str] | UnsetType = UNSET, method: list[Method] | UnsetType = UNSET, type: list[ResourceType] | UnsetType = UNSET, operator: OperatorType | UnsetType = UNSET, status: int | UnsetType = UNSET, content_type: str | UnsetType = UNSET, body: str | UnsetType = UNSET, base64_body: str | UnsetType = UNSET, headers: list[HeaderInput] | UnsetType = UNSET) -> FulfillResponse
```

## `go_back`

```python
go_back(*, timeout: float | UnsetType = UNSET, wait_until: WaitUntilHistory | UnsetType = UNSET) -> HTTPResponse | None
```

## `go_forward`

```python
go_forward(*, timeout: float | UnsetType = UNSET, wait_until: WaitUntilHistory | UnsetType = UNSET) -> HTTPResponse | None
```

## `goto`

```python
goto(url: str, *, timeout: float | UnsetType = UNSET, wait_until: WaitUntilGoto | UnsetType = UNSET, wait_until_all: list[WaitUntilGoto] | UnsetType = UNSET, referer: str | UnsetType = UNSET, referrer_policy: str | UnsetType = UNSET, remote_response_metadata: bool | UnsetType = UNSET) -> HTTPResponse | None
```

## `hover`

```python
hover(selector: str, *, scroll: bool | UnsetType = UNSET, timeout: float | UnsetType = UNSET, visible: bool | UnsetType = UNSET, wait: bool | UnsetType = UNSET, x: float | UnsetType = UNSET, y: float | UnsetType = UNSET) -> HoverResponse
```

## `html`

```python
html(*, selector: str | UnsetType = UNSET, timeout: float | UnsetType = UNSET, visible: bool | UnsetType = UNSET, clean: CleanInput | UnsetType = UNSET, rewrite: HTMLRewriteInput | UnsetType = UNSET) -> HTMLResponse
```

## `listener_count`

```python
listener_count(event: str) -> int
```

Return the number of listeners registered for `event`.

## `live_url`

```python
live_url(*, timeout: float | UnsetType = UNSET, interactable: bool | UnsetType = UNSET, type: LiveURLStreamType | UnsetType = UNSET, quality: int | UnsetType = UNSET, resizable: bool | UnsetType = UNSET, show_browser_interface: bool | UnsetType = UNSET, compressed: bool | UnsetType = UNSET, emulate_components: bool | UnsetType = UNSET) -> LiveURLResponse
```

## `load_secret`

```python
load_secret(ref: str, *, selector: str | UnsetType = UNSET) -> LoadSecretResponse
```

## `map_selector`

```python
map_selector(selector: str, *, timeout: float | UnsetType = UNSET, wait: bool | UnsetType = UNSET) -> list[MapSelectorResponse]
```

## `markdown`

```python
markdown(*, selector: str | UnsetType = UNSET, timeout: float | UnsetType = UNSET, visible: bool | UnsetType = UNSET) -> MarkdownResponse
```

## `off`

```python
off(event: str, callback: Callback) -> Self
```

## `on`

```python
on(event: str, callback: Callback) -> Self
```

## `once`

```python
once(event: str, callback: Callback) -> Self
```

## `pdf`

```python
pdf(*, display_header_footer: bool | UnsetType = UNSET, format: PDFPageFormat | UnsetType = UNSET, footer_template: str | UnsetType = UNSET, generate_document_outline: bool | UnsetType = UNSET, generate_tagged_pdf: bool | UnsetType = UNSET, landscape: bool | UnsetType = UNSET, print_background: bool | UnsetType = UNSET, omit_background: bool | UnsetType = UNSET, scale: float | UnsetType = UNSET, timeout: float | UnsetType = UNSET, wait_for_images: bool | UnsetType = UNSET, wait_for_fonts: bool | UnsetType = UNSET, full_page: bool | UnsetType = UNSET, width: float | str | UnsetType = UNSET, header_template: str | UnsetType = UNSET, height: float | str | UnsetType = UNSET, margin_bottom: float | str | UnsetType = UNSET, margin_left: float | str | UnsetType = UNSET, margin_right: float | str | UnsetType = UNSET, margin_top: float | str | UnsetType = UNSET, page_ranges: str | UnsetType = UNSET, prefer_css_page_size: bool | UnsetType = UNSET, transfer_mode: str | UnsetType = UNSET, path: str | UnsetType = UNSET) -> bytes
```

## `preferences`

```python
preferences(*, timeout: float | UnsetType = UNSET) -> DefaultResponse
```

## `proxy`

```python
proxy(*, network: ProxyNetwork | UnsetType = UNSET, country: CountryCode | UnsetType = UNSET, city: str | UnsetType = UNSET, state: str | UnsetType = UNSET, sticky: bool | UnsetType = UNSET, preset: str | UnsetType = UNSET, server: str | UnsetType = UNSET, method: list[Method] | UnsetType = UNSET, operator: OperatorType | UnsetType = UNSET, type: list[ResourceType] | UnsetType = UNSET, url: list[str] | UnsetType = UNSET) -> ProxyResponse
```

## `query_selector`

```python
query_selector(selector: str, *, timeout: float | UnsetType = UNSET, visible: bool | UnsetType = UNSET) -> ElementHandle | None
```

## `query_selector_all`

```python
query_selector_all(selector: str, *, timeout: float | UnsetType = UNSET, visible: bool | UnsetType = UNSET) -> list[ElementHandle]
```

## `reconnect`

```python
reconnect(*, timeout: float | UnsetType = UNSET) -> ReconnectionResponse
```

## `reject`

```python
reject(*, enabled: bool | UnsetType = UNSET, method: list[Method] | UnsetType = UNSET, operator: OperatorType | UnsetType = UNSET, type: list[ResourceType] | UnsetType = UNSET, url: list[str] | UnsetType = UNSET, regex: list[str] | UnsetType = UNSET) -> RejectResponse
```

## `reload`

```python
reload(*, timeout: float | UnsetType = UNSET, wait_until: WaitUntilHistory | UnsetType = UNSET) -> HTTPResponse | None
```

## `remove_all_listeners`

```python
remove_all_listeners(event: str | None = None) -> Self
```

## `request`

```python
request(*, type: list[ResourceType] | UnsetType = UNSET, method: list[Method] | UnsetType = UNSET, timeout: float | UnsetType = UNSET, url: list[str] | UnsetType = UNSET, wait: bool | UnsetType = UNSET, operator: OperatorType | UnsetType = UNSET) -> list[RequestResponse]
```

## `response`

```python
response(*, status: list[int] | UnsetType = UNSET, method: list[Method] | UnsetType = UNSET, operator: OperatorType | UnsetType = UNSET, timeout: float | UnsetType = UNSET, type: list[ResourceType] | UnsetType = UNSET, url: list[str] | UnsetType = UNSET, wait: bool | UnsetType = UNSET, body_encoding: bool | UnsetType = UNSET) -> list[ResponseResponse]
```

## `screenshot`

```python
screenshot(*, capture_beyond_viewport: bool | UnsetType = UNSET, clip: ScreenshotClip | UnsetType = UNSET, from_surface: bool | UnsetType = UNSET, full_page: bool | UnsetType = UNSET, omit_background: bool | UnsetType = UNSET, optimize_for_speed: bool | UnsetType = UNSET, quality: float | UnsetType = UNSET, selector: str | UnsetType = UNSET, type: ScreenshotType | UnsetType = UNSET, timeout: float | UnsetType = UNSET, wait_for_images: bool | UnsetType = UNSET, path: str | UnsetType = UNSET) -> bytes
```

## `scroll`

```python
scroll(*, selector: str | UnsetType = UNSET, through_page: bool | UnsetType = UNSET, timeout: float | UnsetType = UNSET, visible: bool | UnsetType = UNSET, wait: bool | UnsetType = UNSET, x: float | UnsetType = UNSET, y: float | UnsetType = UNSET) -> ScrollResponse
```

## `select_option`

```python
select_option(selector: str, value: str | Sequence[str]) -> SelectResponse
```

## `send`

```python
send(query: str, *, variables: Mapping[str, Any] | None = None, timeout: float | None = None) -> Any
```

## `set_content`

```python
set_content(html: str, *, timeout: float | UnsetType = UNSET, wait_until: WaitUntilHistory | UnsetType = UNSET, wait_until_all: list[WaitUntilHistory] | UnsetType = UNSET) -> HTTPResponse | None
```

## `set_extra_http_headers`

```python
set_extra_http_headers(headers: Mapping[str, str]) -> HTTPHeadersResponse
```

## `set_java_script_enabled`

```python
set_java_script_enabled(enabled: bool) -> JavaScriptResponse
```

## `set_user_agent`

```python
set_user_agent(user_agent: str) -> UserAgentResponse
```

## `set_viewport_size`

```python
set_viewport_size(*, width: float, height: float, device_scale_factor: float | UnsetType = UNSET, mobile: bool | UnsetType = UNSET, has_touch: bool | UnsetType = UNSET, landscape: bool | UnsetType = UNSET) -> ViewportResponse
```

## `solve`

```python
solve(*, type: CaptchaType | UnsetType = UNSET, timeout: float | UnsetType = UNSET, wait: bool | UnsetType = UNSET) -> CaptchaResponse
```

## `solve_image_captcha`

```python
solve_image_captcha(*, captcha_selector: str, input_selector: str, timeout: float | UnsetType = UNSET) -> CaptchaResponse
```

## `stop_session_recording`

```python
stop_session_recording() -> StopSessionRecordingResponse
```

## `subscribe`

```python
subscribe(listener: Callback) -> Callable[[], None]
```

## `switch_to_window`

```python
switch_to_window(*, url: str | UnsetType = UNSET, title: str | UnsetType = UNSET, newest: bool | UnsetType = UNSET, timeout: float | UnsetType = UNSET, wait_until: WaitUntilGoto | UnsetType = UNSET) -> SwitchWindowResponse
```

## `text`

```python
text(*, timeout: float | UnsetType = UNSET, selector: str | UnsetType = UNSET, visible: bool | UnsetType = UNSET, clean: CleanInput | UnsetType = UNSET) -> TextResponse
```

## `title`

```python
title() -> str
```

## `type`

```python
type(selector: str, text: str, *, delay: list[int] | UnsetType = UNSET, interactable: bool | UnsetType = UNSET, scroll: bool | UnsetType = UNSET, timeout: float | UnsetType = UNSET, visible: bool | UnsetType = UNSET, wait: bool | UnsetType = UNSET) -> TypeResponse
```

## `uncheck`

```python
uncheck(selector: str, *, scroll: bool | UnsetType = UNSET, timeout: float | UnsetType = UNSET, visible: bool | UnsetType = UNSET, wait: bool | UnsetType = UNSET) -> ClickResponse
```

## `url`

```python
url() -> str
```

## `wait_for_event`

```python
wait_for_event(event: str, *, timeout: float | UnsetType = UNSET) -> WaitForEvent
```

Wait for the next event payload, bounded by `timeout` when provided.

## `wait_for_function`

```python
wait_for_function(expression: str, *, polling: float | UnsetType = UNSET, polling_type: WaitForFunctionPolling | UnsetType = UNSET, timeout: float | UnsetType = UNSET, complete_on_promise_resolution: bool | UnsetType = UNSET) -> WaitForFunction
```

## `wait_for_navigation`

```python
wait_for_navigation(*, timeout: float | UnsetType = UNSET, wait_until: WaitUntilGoto | UnsetType = UNSET) -> HTTPResponse
```

## `wait_for_network_idle`

```python
wait_for_network_idle(*, idle_time: float | UnsetType = UNSET, concurrency: int | UnsetType = UNSET, timeout: float | UnsetType = UNSET) -> WaitForTimeoutResponse
```

## `wait_for_request`

```python
wait_for_request(url: str | UnsetType = UNSET, *, method: Method | UnsetType = UNSET, operator: OperatorType | UnsetType = UNSET, timeout: float | UnsetType = UNSET) -> WaitForRequestResponse
```

## `wait_for_response`

```python
wait_for_response(url: str | UnsetType = UNSET, *, statuses: Sequence[int] | UnsetType = UNSET, codes: Sequence[int] | UnsetType = UNSET, operator: OperatorType | UnsetType = UNSET, timeout: float | UnsetType = UNSET) -> WaitForResponseResponse
```

## `wait_for_selector`

```python
wait_for_selector(selector: str, *, timeout: float | UnsetType = UNSET, visible: bool | UnsetType = UNSET, hidden: bool | UnsetType = UNSET) -> WaitForSelectorResponse
```

## `wait_for_timeout`

```python
wait_for_timeout(ms: float) -> None
```

## `watch_event`

```python
watch_event(event: str) -> WaitForEvent
```

Start tracking `event` now so a later `wait_for_event` call can return immediately if that event has already fired.

---

# Screenshots and PDFs with BAP

URL: https://docs.browserless.io/bap/screenshots-and-pdfs

# Screenshots and PDFs with BAP

`screenshot()` and `pdf()` both render the current page and hand back bytes. This guide covers capturing a full page or a single element, choosing a format, and the wait options that decide whether images and fonts have loaded before the capture happens.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- The BAP SDK installed and connecting, from the [BAP Quickstart](/bap/getting-started)

## Capture a screenshot

Both methods return the image bytes. The `path` option additionally writes to disk, which works in Node.js and Python but rejects in the browser build, where you use the returned bytes instead.

**TypeScript:**

```typescript

const TOKEN = "YOUR_API_TOKEN_HERE";

const browser = Browserless.connect({
  browserWSEndpoint: "wss://production-sfo.browserless.io/chromium/bql",
  token: TOKEN,
});

try {
  const page = await browser.newPage();
  await page.goto("https://example.com");

  // Returns a Uint8Array. `path` also writes to disk, and is Node.js only.
  await page.screenshot({ path: "screenshot.png" });

  // Full page, in a smaller format.
  await page.screenshot({
    path: "full.webp",
    type: "webp",
    fullPage: true,
    quality: 80,
  });
} finally {
  await browser.close();
}
```

**Python (Coming soon):**

```python

TOKEN = "YOUR_API_TOKEN_HERE"

with bap.Browserless.connect(
    browser_ws_endpoint="wss://production-sfo.browserless.io/chromium/bql",
    token=TOKEN,
) as browser:
    with browser.page() as page:
        page.goto("https://example.com")

        # Returns bytes. `path` also writes to disk.
        page.screenshot(path="screenshot.png")

        # Full page, in a smaller format.
        page.screenshot(
            path="full.webp",
            type="webp",
            full_page=True,
            quality=80,
        )
```

### Screenshot options

| Option | Default | What it does |
| ------ | ------- | ------------ |
| `type` | `png` | Output format. `quality` applies to everything except PNG |
| `fullPage` | `false` | Capture the entire scroll height instead of the viewport |
| `selector` | — | Capture one element instead of the page |
| `clip` | — | Capture a specific region, given `x`, `y`, `width`, and `height` |
| `quality` | — | Compression quality from 0 to 100. Ignored for PNG |
| `omitBackground` | `false` | Drop the default white background so transparency survives |
| `waitForImages` | `false` | Wait for every image to load first, which is what you want on image-heavy pages |
| `captureBeyondViewport` | varies | Defaults to `false` with a `clip`, `true` otherwise |

Set the viewport before capturing when the layout is responsive, since the screenshot reflects whatever width the page was rendered at:

**TypeScript:**

```typescript
await page.setViewport({ width: 1920, height: 1080 });
await page.goto("https://example.com");

// One element, not the whole page.
await page.screenshot({ selector: "#pricing-table", path: "pricing.png" });
```

**Python (Coming soon):**

```python
page.set_viewport_size(width=1920, height=1080)
page.goto("https://example.com")

# One element, not the whole page.
page.screenshot(selector="#pricing-table", path="pricing.png")
```

## Generate a PDF

`pdf()` renders through Chrome's print pipeline, so it honors print stylesheets rather than the screen layout. That's why `printBackground` exists: browsers drop background graphics when printing, and a page that looks right on screen comes out white without it.

**TypeScript:**

```typescript
await page.goto("https://example.com/invoice");

await page.pdf({
  path: "invoice.pdf",
  format: "a4",
  printBackground: true,
  marginTop: "1in",
  waitForFonts: true,
});
```

**Python (Coming soon):**

```python
page.goto("https://example.com/invoice")

page.pdf(
    path="invoice.pdf",
    format="a4",
    print_background=True,
    margin_top="1in",
    wait_for_fonts=True,
)
```

### PDF options worth knowing

| Option | Default | What it does |
| ------ | ------- | ------------ |
| `format` | — | Page size such as `a4` or `letter`. Use `width`/`height` for a custom size |
| `printBackground` | `false` | Include background graphics, which most page designs need |
| `landscape` | `false` | Paper orientation |
| `fullPage` | `false` | Size the page height to the full document so everything lands on one page |
| `marginTop`, `marginRight`, `marginBottom`, `marginLeft` | ~1cm | Margins, in inches or any CSS unit |
| `pageRanges` | — | Print a subset, such as `"1-5, 8"` |
| `displayHeaderFooter` + `headerTemplate` / `footerTemplate` | `false` | Inject HTML headers and footers |
| `preferCSSPageSize` | `false` | Let the page's own CSS `@page` size win over `format` |
| `waitForFonts`, `waitForImages` | `false` | Wait for fonts or images before rendering |
| `generateTaggedPDF` | varies | Emit a tagged, accessible PDF |

To render the screen layout rather than the print stylesheet, switch the media type first:

**TypeScript:**

```typescript
// Render what the screen shows, ignoring print CSS.
await page.emulateMediaType("screen");
await page.pdf({ path: "screen.pdf", printBackground: true });
```

**Python (Coming soon):**

```python
# Render what the screen shows, ignoring print CSS.
page.emulate_media(type="screen")
page.pdf(path="screen.pdf", print_background=True)
```

## FAQ & Troubleshooting

<details>
<summary>My screenshot is missing images that appear in a browser</summary>

The capture happened before the images finished loading. Pass `waitForImages: true`, or wait for a specific element with `waitForSelector()` first. Lazy-loaded images below the fold also need `scroll({ throughPage: true })` to trigger their loading.

</details>

<details>
<summary>The PDF came out white with no styling</summary>

Set `printBackground: true`. Browsers omit background colors and images when printing, so the default output drops most of a page's design. If the print stylesheet itself is the problem, call `emulateMediaType("screen")` before `pdf()`.

</details>

<details>
<summary>Why does <code>quality</code> have no effect?</summary>

`quality` doesn't apply to PNG, which is lossless. Switch `type` to `jpeg` or `webp` to use it.

</details>

<details>
<summary>(Browser build) <code>path</code> rejects instead of saving the file</summary>

Writing to disk is a Node.js and Python capability. In the browser build, take the returned `Uint8Array` and turn it into a `Blob` for display or download. See the [Quickstart's runtime notes](/bap/getting-started#runtime-notes).

</details>

## Next steps

---

# Loading secrets in BAP

URL: https://docs.browserless.io/bap/secrets

# Loading secrets in BAP

`loadSecret()` fills a credential straight from 1Password into a login form. The value is resolved in the browser at the moment it's typed, so it never appears in your code, your query, or the response. This guide covers the setup it needs and the session lockdown it deliberately triggers.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- The BAP SDK installed and connecting, from the [BAP Quickstart](/bap/getting-started)
- A [1Password service account](/baas/features/1password) configured in your dashboard, with the target site in its allowed-domain list

## Connect with an integration

The session has to know which 1Password service account to resolve against, and that's a connection-time decision: append `integrationId` to the endpoint. A `loadSecret()` call on a session without one fails.

**TypeScript:**

```typescript

const TOKEN = "YOUR_API_TOKEN_HERE";
const INTEGRATION_ID = "YOUR_SERVICE_ACCOUNT_ID";

const browser = Browserless.connect({
  // The integration is bound to the session, not to the call.
  browserWSEndpoint: `wss://production-sfo.browserless.io/chromium/bql?integrationId=${INTEGRATION_ID}`,
  token: TOKEN,
});
```

**Python (Coming soon):**

```python

TOKEN = "YOUR_API_TOKEN_HERE"
INTEGRATION_ID = "YOUR_SERVICE_ACCOUNT_ID"

# The integration is bound to the session, not to the call.
endpoint = (
    "wss://production-sfo.browserless.io/chromium/bql"
    f"?integrationId={INTEGRATION_ID}"
)

with bap.Browserless.connect(browser_ws_endpoint=endpoint, token=TOKEN) as browser:
    ...
```

## Fill a credential

Pass the 1Password `op://` reference and the input to fill. With no `selector`, the currently focused element receives the value, which suits flows where you've just tabbed or clicked into the field.

**TypeScript:**

```typescript
try {
  const page = await browser.newPage();
  await page.goto("https://example.com/login");

  // The username is not a secret, so type it normally.
  await page.type("#username", "name@example.com");

  // The password is resolved in the browser and never reaches this script.
  const result = await page.loadSecret("op://Vault/ExampleLogin/password", {
    selector: "#password",
  });

  // Bail out rather than submitting an empty password field.
  if (!result.ok) {
    throw new Error(`${result.error}: ${result.message}`);
  }

  const navigated = page.waitForNavigation({ waitUntil: "networkIdle" });
  await page.click("#submit");
  await navigated;
} finally {
  await browser.close();
}
```

**Python (Coming soon):**

```python
with browser.page() as page:
    page.goto("https://example.com/login")

    # The username is not a secret, so type it normally.
    page.type("#username", "name@example.com")

    # The password is resolved in the browser and never reaches this script.
    result = page.load_secret(
        "op://Vault/ExampleLogin/password",
        selector="#password",
    )

    # Bail out rather than submitting an empty password field.
    if not result["ok"]:
        raise RuntimeError(f"{result['error']}: {result['message']}")

    page.click("#submit")
    page.wait_for_selector("#dashboard")
```

The response carries `ok`, plus an `error` code and human-readable `message` when it's false. Codes include `DomainNotAllowed`, `SelectorNotFound`, `NoFocusedElement`, `TargetNotFillable`, and `CredentialNotResolved`, which between them tell you whether the problem is your integration's allowed domains, your selector, or the reference itself.

## The session locks down after the first secret

This is the part worth reading before you build on it. Once any secret has been filled, the session refuses everything that could read the credential back out, for the rest of its life:

- **Captures:** screenshots, PDFs, screencasts, Live URLs, and session recording
- **Content readback:** `evaluate()`, `html()`, `text()`, `querySelector()`, `querySelectorAll()`, and `cookies()`

That's deliberate. A filled password is present in the DOM, so a screenshot or an `html()` call would hand back the very value the feature exists to protect. The consequence for your script is that **anything you need to read or capture has to happen before the `loadSecret()` call**, and afterward you're limited to interaction and navigation.

Reconnecting doesn't lift this. [`reconnect()`](/bap/session-management/reconnects) returns you to the same browser session, and the lockdown lasts for that session's life, so the restrictions follow you across the reconnect. When a run needs both a `loadSecret()` login and captures afterward, use a separate session for the capture work.

**TypeScript:**

```typescript
// Wrong order: this html() call rejects, because a secret was already filled.
await page.loadSecret("op://Vault/ExampleLogin/password", { selector: "#password" });
await page.click("#submit");
const { html } = await page.html();

// Right order: capture what you need first, then authenticate.
const { html: before } = await page.html();
await page.loadSecret("op://Vault/ExampleLogin/password", { selector: "#password" });
await page.click("#submit");
```

**Python (Coming soon):**

```python
# Wrong order: this html() call fails, because a secret was already filled.
page.load_secret("op://Vault/ExampleLogin/password", selector="#password")
page.click("#submit")
html = page.html()["html"]

# Right order: capture what you need first, then authenticate.
before = page.html()["html"]
page.load_secret("op://Vault/ExampleLogin/password", selector="#password")
page.click("#submit")
```

## When to use this instead of cookies

`loadSecret()` and [cookie injection](/bap/session-state) both avoid putting a password in your code, and they suit different situations.

Reach for `loadSecret()` when the site requires a real login each run, when a human rotates the credential in 1Password and you want automation to pick that up without a deploy, or when your threat model says the password must never exist in your process memory.

Reach for cookies when you can log in once and reuse the session, since you skip the login entirely and keep full capture and readback for the whole run.

## FAQ & Troubleshooting

<details>
<summary>My screenshot fails after logging in with <code>loadSecret()</code></summary>

That's the intended lockdown, not a bug. Filling a secret disables captures and content readback for the rest of the session, because a screenshot of a filled password field would defeat the purpose. Capture before the call, or do the capture work in a separate session. Reconnecting won't help, since it returns to the same session and inherits the same restrictions.

</details>

<details>
<summary>The call returns <code>DomainNotAllowed</code></summary>

The page's origin isn't in the service account's allowed-domain list. Add it in the dashboard under the 1Password service account, and check you're on the origin you expect after any redirects.

</details>

<details>
<summary>The call returns <code>NoFocusedElement</code></summary>

You omitted `selector` and nothing had focus. Either pass the input's selector, or click into the field first so the focused element is the one you want filled.

</details>

<details>
<summary>Can I read the secret's value in my script?</summary>

No, by design. The value is resolved in the browser and appears in neither the query nor the response, and the readback methods that could expose it are disabled once it's filled. If you need the value itself, fetch it from 1Password directly in your own code rather than through BAP.

</details>

## Next steps

---

# Live URLs and session recording in BAP

URL: https://docs.browserless.io/bap/session-management/live-url

# Live URLs and session recording in BAP

A BAP session doesn't have to run blind. `liveURL()` returns a link that streams the browser to anyone you send it to, optionally letting them click and type, and session replay records everything for the dashboard. This guide covers both, plus switching windows when a flow opens more than one.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- The BAP SDK installed and connecting, from the [BAP Quickstart](/bap/getting-started)

## Stream the session with a live URL

`liveURL()` returns a shareable link to a live stream of the browser. The two big use cases are watching your own automation while you debug it, and human-in-the-loop flows where a person completes the one step automation can't, such as entering a one-time passcode.

**TypeScript:**

```typescript

const TOKEN = "YOUR_API_TOKEN_HERE";

const browser = Browserless.connect({
  browserWSEndpoint: "wss://production-sfo.browserless.io/chromium/bql",
  token: TOKEN,
});

try {
  const page = await browser.newPage();
  await page.goto("https://example.com/login");

  // interactable lets the viewer click and type, not just watch.
  const { liveURL } = await page.liveURL({ interactable: true });
  console.log(`Hand off to a human: ${liveURL}`);

  // Block until the human's part is done, then continue the automation.
  await page.waitForSelector("#dashboard", { timeout: 120_000 });
} finally {
  // Close even when the human never finishes, or the session keeps billing.
  await browser.close();
}
```

**Python (Coming soon):**

```python

TOKEN = "YOUR_API_TOKEN_HERE"

with bap.Browserless.connect(
    browser_ws_endpoint="wss://production-sfo.browserless.io/chromium/bql",
    token=TOKEN,
) as browser:
    with browser.page() as page:
        page.goto("https://example.com/login")

        # interactable lets the viewer click and type, not just watch.
        live = page.live_url(interactable=True)
        print(f"Hand off to a human: {live['liveURL']}")

        # Block until the human's part is done, then continue the automation.
        page.wait_for_selector("#dashboard", timeout=120000)
```

**Response:**

```text
Hand off to a human: https://production-sfo.browserless.io/live/?i=a1b2c3d4...
```

The URL needs no token or account, so the person you hand it to doesn't need Browserless credentials. It streams exactly one session and dies with it.

> Treat a live URL as a bearer secret. With `interactable: true`, anyone holding the URL controls the browser session until it ends, including any logged-in state inside it. Send it over a trusted private channel only, and keep it out of logs, tickets, and shared messages.

### Tuning the stream

| Option | Default | What it does |
| ------ | ------- | ------------ |
| `interactable` | `true` | Forward the viewer's clicks and keystrokes into the browser. Set `false` for watch-only streams |
| `type` | `jpeg` | Stream codec. `jpeg` uses less bandwidth; `png` looks better and costs more |
| `quality` | `70` | JPEG quality from 1 to 100. Ignored for `png` |
| `resizable` | `true` | Resize the browser to match the viewer's screen. Set `false` to keep your viewport and letterbox the stream instead, which you want when a later screenshot depends on the viewport |
| `showBrowserInterface` | `false` | Show a navigation bar and tabs around the stream |
| `emulateComponents` | `true` | Emulate `<select>` dropdowns and the virtual keyboard for touch devices. Set `false` to suppress the virtual keyboard |
| `timeout` | — | Sets link validity and the post-disconnect keep-alive window, capped to the time the session has left; when it expires the viewer sees a session-closed prompt |

If no session time remains, `liveURL()` rejects the request with `The browser session has expired.`

The response also carries `liveURLId`, a stable identifier for the stream that's useful for correlating with other APIs. When using BrowserQL directly, select `timeout` in the `liveURL` response to read the link lifetime actually applied in milliseconds. BAP SDK versions whose generated `LiveURLResponse` includes only `liveURL` and `liveURLId` do not expose this field. The generated [`LiveURLOptions` reference](/bap/typescript/reference/interfaces/LiveURLOptions) stays current with the schema, including the `compressed` bandwidth toggle.

## Record the session for replay

Session replay captures DOM events, console logs, and network activity for the dashboard's Session Replay viewer. Recording is a connection decision, not a method call: add `replay=true` to the endpoint you connect to, and it starts as soon as the session does.

**TypeScript:**

```typescript
const browser = Browserless.connect({
  // Recording starts automatically because of replay=true.
  browserWSEndpoint: "wss://production-sfo.browserless.io/chromium/bql?replay=true",
  token: TOKEN,
});

try {
  const page = await browser.newPage();
  await page.goto("https://example.com");

  // Optional: stop early and upload now instead of at session end.
  const { success, error } = await page.stopSessionRecording();
  console.log(success ? "uploaded" : `failed: ${error}`);
} finally {
  // Close once the work is done; an open session keeps billing after the stop.
  await browser.close();
}
```

**Python (Coming soon):**

```python
with bap.Browserless.connect(
    # Recording starts automatically because of replay=true.
    browser_ws_endpoint="wss://production-sfo.browserless.io/chromium/bql?replay=true",
    token=TOKEN,
) as browser:
    with browser.page() as page:
        page.goto("https://example.com")

        # Optional: stop early and upload now instead of at session end.
        result = page.stop_session_recording()
        print("uploaded" if result["success"] else f"failed: {result['error']}")
```

Recordings upload when the session ends or when `stopSessionRecording()` succeeds, whichever comes first, and appear in the [Session Replay section of your dashboard](https://www.browserless.io/account/session-replay). Stopping early is worth doing on long sessions where only the first part matters, since everything after the stop isn't captured.

## Switch between windows

When a click opens a new tab or window, your page methods keep talking to the original. `switchToWindow()` retargets the session at another window, matched by `url` or `title` pattern, or just the `newest` one.

**TypeScript:**

```typescript
await page.click("#open-checkout");

// The click spawned a tab; follow it.
await page.switchToWindow({ newest: true });
console.log(await page.url());

// Or match the window you want explicitly.
await page.switchToWindow({ url: "*checkout.example.com*" });
```

**Python (Coming soon):**

```python
page.click("#open-checkout")

# The click spawned a tab; follow it.
page.switch_to_window(newest=True)
print(page.url())

# Or match the window you want explicitly.
page.switch_to_window(url="*checkout.example.com*")
```

Matching waits up to `timeout` (default `5000` milliseconds) for the window to exist, so calling right after the click that spawns it is safe.

## FAQ & Troubleshooting

<details>
<summary>The live URL shows a closed-session message</summary>

The session ended: your script called `close()`, the operation finished, or the session hit its timeout. A live URL streams one session and can't outlive it. For handoff flows, keep the script blocked on something like `waitForSelector()` while the human works.

</details>

<details>
<summary>Viewers can see the stream but can't click anything</summary>

The stream was created with `interactable: false`, or the element they're clicking needs the virtual keyboard you disabled with `emulateComponents: false`. Create a new live URL with interaction on; the option can't be flipped on an existing stream.

</details>

<details>
<summary>My replay never showed up in the dashboard</summary>

Recording only happens when the connection URL included `replay=true`. Confirm the parameter made it onto the endpoint, and if you called `stopSessionRecording()`, check its `success` and `error` fields, since a failed stop means nothing was uploaded.

</details>

<details>
<summary>Is a live URL the same as session replay?</summary>

No. `liveURL()` streams the session while it runs and leaves nothing behind. Replay records the session for viewing afterward in the dashboard. They're independent, and one session can use both.

</details>

## Next steps

---

# Reconnecting to sessions in BAP

URL: https://docs.browserless.io/bap/session-management/reconnects

# Reconnecting to sessions in BAP

`reconnect()` keeps the browser alive after your connection closes and hands you the endpoints to come back to it. That's how a session survives a client restart, moves between processes, or gets handed from BAP to Puppeteer mid-run.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- The BAP SDK installed and connecting, from the [BAP Quickstart](/bap/getting-started)

## How reconnects work

Normally the browser shuts down when your page's WebSocket closes. Calling `reconnect()` changes that: after you disconnect, the browser stays up for the `timeout` you passed (default `30000` milliseconds) waiting for a new connection. The call returns the URLs a future connection needs. Nothing about your current connection changes, so you keep working on the page and disconnect whenever you're ready.

**TypeScript:**

```typescript

const TOKEN = "YOUR_API_TOKEN_HERE";

const browser = Browserless.connect({
  browserWSEndpoint: "wss://production-sfo.browserless.io/chromium/bql",
  token: TOKEN,
});

try {
  const page = await browser.newPage();
  await page.goto("https://example.com/dashboard");

  // After close(), the browser now survives for 60 seconds waiting for us.
  const session = await page.reconnect({ timeout: 60_000 });
  console.log(session.browserQLEndpoint);
} finally {
  // Safe either way: after a successful reconnect() the keep-alive window
  // outlives this close; before one, closing prevents a leaked session.
  await browser.close();
}
```

**Python (Coming soon):**

```python

TOKEN = "YOUR_API_TOKEN_HERE"

with bap.Browserless.connect(
    browser_ws_endpoint="wss://production-sfo.browserless.io/chromium/bql",
    token=TOKEN,
) as browser:
    with browser.page() as page:
        page.goto("https://example.com/dashboard")

        # After the block exits, the browser survives for 60 seconds waiting for us.
        session = page.reconnect(timeout=60000)
        print(session["browserQLEndpoint"])
```

**Response:**

```text
https://production-sfo.browserless.io/chromium/bql/e4f5a6b7-c8d9-4012-a3b4-c5d6e7f8a9b0
```

## The endpoints you get back

The response carries four URLs, each for a different way back in. None of them include your token, so append it when you connect.

| Field | Reconnects | Use it with |
| ----- | ---------- | ----------- |
| `browserQLEndpoint` | The BrowserQL session | BAP and raw BQL queries |
| `browserWSEndpoint` | The browser over CDP | Puppeteer or Playwright `connect()` |
| `devtoolsFrontendUrl` | Chrome DevTools remotely | A regular browser, for inspection |
| `webSocketDebuggerUrl` | One page over CDP | Libraries that attach to a page instead of a browser |

## Continue the session from BAP

`browserQLEndpoint` comes back as an `https://` URL for HTTP-based BQL queries. BAP talks BrowserQL over WebSocket, so swap the scheme to `wss://` and connect a fresh browser to it:

**TypeScript:**

```typescript
// The returned endpoint is https://; BAP connects over WebSocket.
const endpoint = session.browserQLEndpoint!.replace(/^https/, "wss");

const resumed = Browserless.connect({
  browserWSEndpoint: endpoint,
  token: TOKEN,
});

try {
  const samePage = await resumed.newPage();

  // Still on the dashboard: this is the same browser, same state.
  console.log(await samePage.url());
} finally {
  await resumed.close();
}
```

**Python (Coming soon):**

```python
# The returned endpoint is https://; BAP connects over WebSocket.
endpoint = session["browserQLEndpoint"].replace("https", "wss", 1)

with bap.Browserless.connect(
    browser_ws_endpoint=endpoint,
    token=TOKEN,
) as resumed:
    with resumed.page() as same_page:
        # Still on the dashboard: this is the same browser, same state.
        print(same_page.url())
```

**Response:**

```text
https://example.com/dashboard
```

Cookies, storage, and the open page all persist, because it's the same browser process that never shut down.

## Hand the session to Puppeteer or Playwright

`browserWSEndpoint` is a CDP URL, which means a session that BAP set up, solved a CAPTCHA in, or navigated through a login can be finished by CDP tooling:

```javascript

const TOKEN = "YOUR_API_TOKEN_HERE";

// The endpoint BAP's reconnect() returned, plus your token.
const browser = await puppeteer.connect({
  browserWSEndpoint: `${session.browserWSEndpoint}?token=${TOKEN}`,
});

const [page] = await browser.pages();
console.log(await page.title());

// Terminate the session when done; a browser left to wait out its
// reconnect timeout still bills and holds a concurrency slot.
await browser.close();
```

This is the escape hatch for the methods BAP deliberately doesn't have, such as arbitrary `evaluate()` with argument passing. Do the protected part in BAP, then reconnect into Puppeteer for the rest.

## Timeout limits and billing

## FAQ & Troubleshooting

<details>
<summary>My reconnect endpoint stopped working after a minute</summary>

The `timeout` you passed is an idle grace period measured from disconnect. Once it elapses with no new connection, the browser terminates and the endpoint dies. Call `reconnect()` again on each new connection to keep the chain going, within your plan's maximum session duration.

</details>

<details>
<summary>I get an authorization error connecting to the returned endpoint</summary>

Returned endpoints never include token information. Pass your token the same way as a normal connection: the `token` option in BAP, or `?token=` appended to the URL for Puppeteer and Playwright.

</details>

<details>
<summary>Does the session keep billing while nobody is connected?</summary>

Yes. A browser waiting out its reconnect timeout occupies a concurrency slot like any connected session, and each reconnection is billed as a new connection. Close sessions you're done with instead of letting the timeout expire.

</details>

<details>
<summary>Can I raise the timeout above my plan's session limit?</summary>

No. A `timeout` beyond the plan maximum fails immediately with `Reconnect timeout (Xms) exceeds the maximum allowed limit (Yms).`, and reconnecting never extends the absolute deadline measured from when the browser started.

</details>

## Next steps

---

# Cookies and page setup in BAP

URL: https://docs.browserless.io/bap/session-state

# Cookies and page setup in BAP

Cookie injection is how you skip a login: authenticate once, keep the cookies, and start later sessions already signed in. This guide covers reading and injecting cookies, plus the other session-level setup methods: user agent, direct HTML, injected scripts and styles, and disabling JavaScript.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- The BAP SDK installed and connecting, from the [BAP Quickstart](/bap/getting-started)

## Read cookies

`cookies()` returns the current cookies as structured records. Save these after a successful login and you have a reusable credential for future sessions.

**TypeScript:**

```typescript

const TOKEN = "YOUR_API_TOKEN_HERE";

const browser = Browserless.connect({
  browserWSEndpoint: "wss://production-sfo.browserless.io/chromium/bql",
  token: TOKEN,
});

try {
  const page = await browser.newPage();
  await page.goto("https://example.com/login");

  await page.type("#user", "name@example.com");
  await page.type("#pass", "secret");

  // Start the wait before the click, or a fast navigation finishes first.
  const navigated = page.waitForNavigation({ waitUntil: "networkIdle" });
  await page.click("#submit");
  await navigated;

  // Persist these and later sessions can skip the login entirely.
  const cookies = await page.cookies();
  console.log(cookies.length);
} finally {
  await browser.close();
}
```

**Python (Coming soon):**

```python

TOKEN = "YOUR_API_TOKEN_HERE"

with bap.Browserless.connect(
    browser_ws_endpoint="wss://production-sfo.browserless.io/chromium/bql",
    token=TOKEN,
) as browser:
    with browser.page() as page:
        page.goto("https://example.com/login")

        page.type("#user", "name@example.com")
        page.type("#pass", "secret")
        page.click("#submit")
        page.wait_for_selector("#dashboard")

        # Persist these and later sessions can skip the login entirely.
        cookies = page.cookies()
        print(len(cookies))
```

**Response:**

```text
4
```

## Inject cookies to skip the login

Set cookies before navigating, so the very first request carries them. Each cookie needs at least a `name` and `value`, plus a `domain` or `url` so the browser knows where it applies.

The two SDKs differ here by inheritance: TypeScript follows Puppeteer's `setCookie(...cookies)` varargs, while Python follows Playwright's `add_cookies([...])` with a single sequence.

**TypeScript:**

```typescript
const page = await browser.newPage();

// Before goto(), so the first request is already authenticated.
await page.setCookie(
  { name: "session", value: SAVED_SESSION_VALUE, domain: "example.com" },
  { name: "csrf", value: SAVED_CSRF_VALUE, domain: "example.com" },
);

await page.goto("https://example.com/dashboard");
console.log(await page.url());
```

**Python (Coming soon):**

```python
# The context manager closes the page and its socket on exit.
with browser.page() as page:
    # Before goto(), so the first request is already authenticated.
    page.add_cookies([
        {"name": "session", "value": SAVED_SESSION_VALUE, "domain": "example.com"},
        {"name": "csrf", "value": SAVED_CSRF_VALUE, "domain": "example.com"},
    ])

    page.goto("https://example.com/dashboard")
    print(page.url())
```

**Response**, landing on the dashboard rather than being bounced to the login:

```text
https://example.com/dashboard
```

Cookies also accept `path`, `expires`, `httpOnly`, `secure`, `sameSite`, and `priority`. Copy those through from what `cookies()` returned rather than guessing, since a session cookie recreated without its original `secure` or `sameSite` flags may be rejected or ignored.

When the credential is a password rather than a cookie, and you'd rather it never touch your script, see [loading secrets](/bap/secrets) instead.

## Set the user agent

`setUserAgent()` replaces the string the browser reports. On stealth sessions, leave it alone unless you have a specific reason: Browserless manages a coherent fingerprint, and a hand-set user agent that disagrees with the rest of it is easier to detect than the default.

**TypeScript:**

```typescript
await page.setUserAgent("MyCrawler/1.0 (+https://example.com/bot)");
await page.goto("https://example.com");
```

**Python (Coming soon):**

```python
page.set_user_agent("MyCrawler/1.0 (+https://example.com/bot)")
page.goto("https://example.com")
```

Identifying yourself honestly is the right move on sites you own or whose crawl policy you're respecting. For sites that block automation, [stealth and humanization](/bap/bot-detection/stealth) is the relevant guide.

## Load HTML directly

`setContent()` puts your own markup in the page instead of navigating. It's how you render a template to a PDF or screenshot without hosting it anywhere first.

**TypeScript:**

```typescript
await page.setContent("<h1>Invoice 1042</h1>Paid in full.");
await page.pdf({ path: "invoice.pdf", format: "a4", printBackground: true });
```

**Python (Coming soon):**

```python
page.set_content("<h1>Invoice 1042</h1>Paid in full.")
page.pdf(path="invoice.pdf", format="a4", print_background=True)
```

Relative URLs in that markup have no origin to resolve against, so reference images, styles, and scripts absolutely, or inline them.

## Inject scripts and styles

`addScriptTag()` and `addStyleTag()` add a `<script>` or `<style>` to the current page, from a `url`, a local `path`, or inline `content`. Injected styles are the practical way to hide cookie banners or sticky headers before a screenshot.

**TypeScript:**

```typescript
// Hide chrome that would otherwise cover the screenshot.
await page.addStyleTag({ content: ".cookie-banner, .sticky-nav { display: none !important; }" });

// Load a helper library into the page.
await page.addScriptTag({ url: "https://cdn.example.com/helper.js" });
```

**Python (Coming soon):**

```python
# Hide chrome that would otherwise cover the screenshot.
page.add_style_tag(content=".cookie-banner, .sticky-nav { display: none !important; }")

# Load a helper library into the page.
page.add_script_tag(url="https://cdn.example.com/helper.js")
```

Inline `content` must be a single line, because the BrowserQL server rejects multi-line `content`. Host the script and pass `url` instead, or collapse the source to one line, which only works when it has no newline-sensitive syntax such as `//` comments or multi-line template literals.

## Disable JavaScript

`setJavaScriptEnabled(false)` loads pages without running their scripts. Server-rendered content arrives faster and cheaper, and ad and tracking scripts never execute. Anything client-rendered will be missing, so this only suits sites whose content is in the initial HTML.

**TypeScript:**

```typescript
await page.setJavaScriptEnabled(false);
await page.goto("https://example.com/article");

const { text } = await page.text({ selector: "article" });
```

**Python (Coming soon):**

```python
page.set_java_script_enabled(False)
page.goto("https://example.com/article")

text = page.text(selector="article")["text"]
```

## FAQ & Troubleshooting

<details>
<summary>My injected cookies didn't log me in</summary>

Three usual causes: they were set after `goto()` rather than before, the `domain` doesn't match the site (including the leading-dot and subdomain rules), or flags like `secure` and `sameSite` were dropped when you recreated the cookie. Copy every field through from what `cookies()` returned.

</details>

<details>
<summary>Is storing session cookies safe?</summary>

They're live credentials, so treat them like passwords: keep them in your secret store rather than in source, and expect them to expire. For password-based logins where you'd rather the credential never reach your code, use [`loadSecret()`](/bap/secrets).

</details>

<details>
<summary>Images and CSS are missing after <code>setContent()</code></summary>

Relative URLs have no origin to resolve against when the markup didn't come from a navigation. Use absolute URLs, or inline the assets as data URIs.

</details>

<details>
<summary>Should I set a custom user agent on stealth sessions?</summary>

Usually not. Browserless manages a coherent fingerprint on the stealth endpoint, and a user agent that contradicts the rest of it stands out more than the default. Set one when you're identifying your crawler to a site that expects it.

</details>

## Next steps

---

# BAP TypeScript SDK Reference

URL: https://docs.browserless.io/bap/typescript/reference

# BAP TypeScript SDK Reference

## Classes

| Class | Description |
| ------ | ------ |
| [Browser](classes/Browser) | Manages BAP pages connected to a Browserless BrowserQL endpoint. |
| [Browserless](classes/Browserless) | Entry point for creating BAP browser sessions backed by Browserless BrowserQL. |
| [BrowserQLError](classes/BrowserQLError) | Raised when the BrowserQL server returns GraphQL errors. |
| [ConnectionError](classes/ConnectionError) | Raised when the WebSocket connection fails, drops, or is unavailable. |
| [ElementHandle](classes/ElementHandle) | A handle to a matched element. |
| [EventEmitter](classes/EventEmitter) | Minimal typed event emitter backing the puppeteer-style Page event API. |
| [Page](classes/Page) | - |
| [TimeoutError](classes/TimeoutError) | Raised when an operation exceeds its timeout. |
| [Transport](classes/Transport) | - |
| [WebSocketTransport](classes/WebSocketTransport) | The built-in [ConnectionTransport](interfaces/ConnectionTransport): a WebSocket via `#platform` (native in the browser, `ws` in Node so the User-Agent header can be sent). This is what the library uses whenever no custom `transport` is supplied — connecting to a WebSocket URL goes through [WebSocketTransport.create](classes/WebSocketTransport.mdx#create), which negotiates the SUBPROTOCOL subprotocol. |

## Functions

| Function | Description |
| ------ | ------ |
| [buildOperation](functions/buildOperation) | Builds a parameterized GraphQL mutation document and its accompanying variables payload. Argument values are passed as GraphQL variables rather than interpolated into the query string, which lets the server coerce and validate them by type (including enums and input objects) and keeps the operation document constant across calls. |

## Interfaces

| Interface | Description |
| ------ | ------ |
| [AddScriptTagOptions](interfaces/AddScriptTagOptions) | - |
| [AddScriptTagResponse](interfaces/AddScriptTagResponse) | Response returned after injecting a script tag |
| [AddStyleTagOptions](interfaces/AddStyleTagOptions) | - |
| [AddStyleTagResponse](interfaces/AddStyleTagResponse) | Response returned after injecting a style tag |
| [Attribute](interfaces/Attribute) | - |
| [AuthenticateOptions](interfaces/AuthenticateOptions) | - |
| [CaptchaResponse](interfaces/CaptchaResponse) | Response returned after a captcha has been solved |
| [CheckboxOptions](interfaces/CheckboxOptions) | - |
| [CleanInput](interfaces/CleanInput) | Options for cleaning up the DOM prior to exporting its content. Many options are available, and this query can destructively remove non-text DOM nodes, DOM attributes, and gratuitous whitespace characters. Since these operations are destructive in their nature it's recommended to run them at the very end of your query in order to preserve page functionality |
| [ClickOptions](interfaces/ClickOptions) | - |
| [ClickResponse](interfaces/ClickResponse) | Response returned after having clicked on an element |
| [ConnectionTransport](interfaces/ConnectionTransport) | A user-suppliable transport, modeled on puppeteer's `ConnectionTransport`. Pass one via [ConnectOptions.transport](interfaces/ConnectOptions) to send BrowserQL frames over your own channel instead of the built-in WebSocket. The frames it relays are graphql-transport-ws envelopes (SUBPROTOCOL) — the transport is just the pipe, [Transport](classes/Transport) does the framing on top. |
| [ConnectOptions](interfaces/ConnectOptions) | - |
| [ConsoleMessage](interfaces/ConsoleMessage) | A console message emitted by the page. Mirrors puppeteer's ConsoleMessage, sourced from CDP Runtime.consoleAPICalled and Runtime.exceptionThrown. |
| [ConsoleMessageLocation](interfaces/ConsoleMessageLocation) | The source location a console message originated from. |
| [CookieInput](interfaces/CookieInput) | The cookie to be sent to the page |
| [CookiePartitionKeyInput](interfaces/CookiePartitionKeyInput) | - |
| [CookieResponse](interfaces/CookieResponse) | The response returned after setting or getting cookies |
| [DefaultResponse](interfaces/DefaultResponse) | Default response for all methods |
| [EmulateMediaTypeOptions](interfaces/EmulateMediaTypeOptions) | - |
| [EmulateMediaTypeResponse](interfaces/EmulateMediaTypeResponse) | Response returned after emulating a CSS media type |
| [EvaluateOptions](interfaces/EvaluateOptions) | - |
| [EvaluateResponse](interfaces/EvaluateResponse) | Response returned after evaluating a script |
| [FulfillOptions](interfaces/FulfillOptions) | - |
| [FulfillResponse](interfaces/FulfillResponse) | Response returned after registering a request-fulfillment interceptor |
| [GotoOptions](interfaces/GotoOptions) | - |
| [HeaderInput](interfaces/HeaderInput) | A HTTP header input type |
| [HoverOptions](interfaces/HoverOptions) | - |
| [HoverResponse](interfaces/HoverResponse) | Response returned after having hovered over an element |
| [HTMLOptions](interfaces/HTMLOptions) | - |
| [HTMLResourcePathInput](interfaces/HTMLResourcePathInput) | A resource URL and its replacement archive path when rewriting HTML for export. |
| [HTMLResponse](interfaces/HTMLResponse) | HTML content of a page |
| [HTMLRewriteInput](interfaces/HTMLRewriteInput) | Options for rewriting page resource URLs before returning HTML. |
| [HTTPHeaders](interfaces/HTTPHeaders) | An object representing the header's name and underlying value |
| [HTTPHeadersResponse](interfaces/HTTPHeadersResponse) | The response returned after setting HTTP headers |
| [HTTPResponse](interfaces/HTTPResponse) | Response returned after a navigation event |
| [JavaScriptEnabledOptions](interfaces/JavaScriptEnabledOptions) | - |
| [JavaScriptResponse](interfaces/JavaScriptResponse) | The response returned after enabling or disabling JavaScript on the page |
| [LiveURLOptions](interfaces/LiveURLOptions) | - |
| [LiveURLResponse](interfaces/LiveURLResponse) | The response from the Live-URL query |
| [LoadSecretOptions](interfaces/LoadSecretOptions) | - |
| [LoadSecretResponse](interfaces/LoadSecretResponse) | Response returned after attempting to fill a credential resolved from a 1Password integration. The resolved value is never included in the response. |
| [MapSelectorOptions](interfaces/MapSelectorOptions) | - |
| [MapSelectorResponse](interfaces/MapSelectorResponse) | Response returned from a Map Selector |
| [MarkdownOptions](interfaces/MarkdownOptions) | - |
| [MarkdownResponse](interfaces/MarkdownResponse) | Response returned after converting the page content to Markdown |
| [NavigationOptions](interfaces/NavigationOptions) | - |
| [PageEventMap](interfaces/PageEventMap) | Maps each Page event name to the payload delivered to its listeners. Backed by a GraphQL subscription that is opened lazily on first listen. |
| [PDFOptions](interfaces/PDFOptions) | - |
| [PDFResponse](interfaces/PDFResponse) | The response returned after generating a PDF |
| [PreferencesOptions](interfaces/PreferencesOptions) | - |
| [ProxyOptions](interfaces/ProxyOptions) | - |
| [ProxyResponse](interfaces/ProxyResponse) | Response returned after setting up the proxy patterns |
| [QuerySelectorOptions](interfaces/QuerySelectorOptions) | - |
| [QuerySelectorResponse](interfaces/QuerySelectorResponse) | The response returned after querying the page for a selector |
| [ReconnectionResponse](interfaces/ReconnectionResponse) | The response received after attempting to reconnect a BrowserQL session |
| [ReconnectOptions](interfaces/ReconnectOptions) | - |
| [RejectOptions](interfaces/RejectOptions) | - |
| [RejectResponse](interfaces/RejectResponse) | The response parameters for the reject mutation |
| [RequestFilterOptions](interfaces/RequestFilterOptions) | - |
| [RequestHeader](interfaces/RequestHeader) | An object representing the header's name and underlying value |
| [RequestInput](interfaces/RequestInput) | The specific request to perform a conditional action on |
| [RequestResponse](interfaces/RequestResponse) | Response returned from the request API |
| [ResponseFilterOptions](interfaces/ResponseFilterOptions) | - |
| [ResponseInput](interfaces/ResponseInput) | The specific response to perform a conditional action on |
| [ResponseResponse](interfaces/ResponseResponse) | Response returned from the response API |
| [ScreenshotClip](interfaces/ScreenshotClip) | The clipping to be applied to the screenshot |
| [ScreenshotOptions](interfaces/ScreenshotOptions) | - |
| [ScreenshotResponse](interfaces/ScreenshotResponse) | The response returned after generating a Screenshot |
| [ScrollOptions](interfaces/ScrollOptions) | - |
| [ScrollResponse](interfaces/ScrollResponse) | Response returned after having scrolling inside the page |
| [SelectOptions](interfaces/SelectOptions) | - |
| [SelectResponse](interfaces/SelectResponse) | The response returned after selecting a value from a dropdown or multiple select element |
| [SendOptions](interfaces/SendOptions) | - |
| [SetContentOptions](interfaces/SetContentOptions) | - |
| [SetCookieOptions](interfaces/SetCookieOptions) | - |
| [SetExtraHTTPHeadersOptions](interfaces/SetExtraHTTPHeadersOptions) | - |
| [SolveImageCaptchaOptions](interfaces/SolveImageCaptchaOptions) | - |
| [SolveOptions](interfaces/SolveOptions) | - |
| [StandardCookie](interfaces/StandardCookie) | A standard cookie |
| [StopSessionRecordingResponse](interfaces/StopSessionRecordingResponse) | The response received after stopping session recording |
| [SubscriptionSpec](interfaces/SubscriptionSpec) | - |
| [SwitchToWindowOptions](interfaces/SwitchToWindowOptions) | - |
| [SwitchWindowResponse](interfaces/SwitchWindowResponse) | Response from window switching operation |
| [TextOptions](interfaces/TextOptions) | - |
| [TextResponse](interfaces/TextResponse) | Text content of a page |
| [TitleResponse](interfaces/TitleResponse) | Response returned after the page's title has been set or get |
| [TypeOptions](interfaces/TypeOptions) | - |
| [TypeResponse](interfaces/TypeResponse) | Response returned after having typed into an element |
| [URLResponse](interfaces/URLResponse) | Response returned after the URL of the page has been set or get |
| [UserAgentResponse](interfaces/UserAgentResponse) | The response returned after setting the User-Agent |
| [ViewportOptions](interfaces/ViewportOptions) | - |
| [ViewportResponse](interfaces/ViewportResponse) | Response returned after setting the viewport |
| [WaitForEvent](interfaces/WaitForEvent) | Response returned after having waited for an event to fire on the page |
| [WaitForEventOptions](interfaces/WaitForEventOptions) | - |
| [WaitForFunction](interfaces/WaitForFunction) | Response returned after having waited for a JavaScript predicate to become truthy |
| [WaitForFunctionOptions](interfaces/WaitForFunctionOptions) | - |
| [WaitForNavigationOptions](interfaces/WaitForNavigationOptions) | - |
| [WaitForNetworkIdleOptions](interfaces/WaitForNetworkIdleOptions) | - |
| [WaitForRequestOptions](interfaces/WaitForRequestOptions) | - |
| [WaitForRequestResponse](interfaces/WaitForRequestResponse) | Response returned after a particular network request has been sent |
| [WaitForResponseOptions](interfaces/WaitForResponseOptions) | - |
| [WaitForResponseResponse](interfaces/WaitForResponseResponse) | Response returned after a particular network response has been received |
| [WaitForSelectorOptions](interfaces/WaitForSelectorOptions) | - |
| [WaitForSelectorResponse](interfaces/WaitForSelectorResponse) | Response returned after a particular selector has been found in the DOM |
| [WaitForTimeoutResponse](interfaces/WaitForTimeoutResponse) | Response returned after having waited for a selector to appear in the DOM |

## References

### default

Renames and re-exports [Browserless](classes/Browserless)

## Type Aliases

| Type Alias | Description |
| ------ | ------ |
| [AttributeMode](type-aliases/AttributeMode) | Controls how the attributes field is interpreted in cleaning operations |
| [CaptchaType](type-aliases/CaptchaType) | The different types of captchas that can be solved |
| [ConsoleMessageType](type-aliases/ConsoleMessageType) | The kind of console message, mirroring the CDP Runtime.consoleAPICalled type (and puppeteer's ConsoleMessageType). Uncaught exceptions are reported as `error`. |
| [CookiePriority](type-aliases/CookiePriority) | - |
| [CookieSameSite](type-aliases/CookieSameSite) | The values a cookie's SameSite attribute can hold |
| [CookieSourceScheme](type-aliases/CookieSourceScheme) | - |
| [CountryCode](type-aliases/CountryCode) | The two-letter ISO code for the specified country |
| [Listener](type-aliases/Listener) | - |
| [LiveURLStreamType](type-aliases/LiveURLStreamType) | The binary-type of the stream |
| [MediaType](type-aliases/MediaType) | The CSS media type to emulate for the page |
| [MessageListener](type-aliases/MessageListener) | A raw-frame listener registered via [Page.on](classes/Page.mdx#on)/[Page.subscribe](classes/Page.mdx#subscribe). |
| [Method](type-aliases/Method) | The various HTTP-based methods to wait for |
| [OperatorType](type-aliases/OperatorType) | Logical operators for APIs that have conditions |
| [PageEvents](type-aliases/PageEvents) | Page events surfaced through the puppeteer-style emitter. The schema-derived [PageEventMap](interfaces/PageEventMap) entries (`console`/`request`/`response`) each open a GraphQL subscription lazily; `error` is emitted if opening or streaming one fails (it never throws, matching puppeteer's non-fatal emitter errors). `message` carries every raw frame the transport receives — including server-pushed frames with no matching operation (e.g. data over a custom [ConnectionTransport](interfaces/ConnectionTransport)); it is not subscription-backed. |
| [PDFPageFormat](type-aliases/PDFPageFormat) | The values a pdf's Size attribute can hold |
| [ProxyNetwork](type-aliases/ProxyNetwork) | The Browserless proxy network to route matching requests through. |
| [ResourceType](type-aliases/ResourceType) | The type of resource for a given network or HTTP request |
| [ScreenshotType](type-aliases/ScreenshotType) | The different types of screenshot formats |
| [TransportFactory](type-aliases/TransportFactory) | Builds a [ConnectionTransport](interfaces/ConnectionTransport) for a given endpoint URL. Supply one via [ConnectOptions.transport](interfaces/ConnectOptions) to override the default WebSocket transport. The returned promise must resolve only once the connection is open — the library begins sending frames as soon as it resolves. This is a factory rather than a single instance because [Browser.newPage](classes/Browser.mdx#newpage) invokes it per page and [Transport](classes/Transport) assigns `onmessage`/`onclose`/`onerror` on the returned transport each time — so every page must get a fresh transport instance. Reusing one instance across pages would overwrite the earlier pages' handlers and cross-wire their frames, unless the transport itself multiplexes. |
| [WaitForFunctionPolling](type-aliases/WaitForFunctionPolling) | Browser-driven polling modes for waitForFunction |
| [WaitUntilGoto](type-aliases/WaitUntilGoto) | Options for when to consider the page has loaded and proceed with further execution |
| [WaitUntilHistory](type-aliases/WaitUntilHistory) | The different stages of the browser's loading process |

## Variables

| Variable | Description |
| ------ | ------ |
| [ARG\_TYPES](variables/ARG_TYPES) | - |
| [RESPONSE\_FIELDS](variables/RESPONSE_FIELDS) | - |
| [SUBSCRIPTIONS](variables/SUBSCRIPTIONS) | - |

---

# Browser

URL: https://docs.browserless.io/bap/typescript/reference/classes/Browser

# Browser

Manages BAP pages connected to a Browserless BrowserQL endpoint.

## Methods

### close()

```ts
close(): Promise<void>;
```

Closes every open page session.

#### Returns

`Promise`\<`void`\>

***

### getPages()

```ts
getPages(): Page[];
```

Returns open pages created by this Browser instance.

#### Returns

[`Page`](Page)[]

***

### newPage()

```ts
newPage(): Promise;
```

Opens a new BrowserQL-backed page session.

#### Returns

`Promise`\<[`Page`](Page)\>

---

# Browserless

URL: https://docs.browserless.io/bap/typescript/reference/classes/Browserless

# Browserless

Entry point for creating BAP browser sessions backed by Browserless
BrowserQL.

## Methods

### connect()

```ts
static connect(options): Browser;
```

Creates a Browser instance configured for the BrowserQL WebSocket endpoint.

The returned Browser opens BrowserQL-backed pages with [Browser.newPage](Browser.mdx#newpage).

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `options` | [`ConnectOptions`](../interfaces/ConnectOptions) |

#### Returns

[`Browser`](Browser)

---

# BrowserQLError

URL: https://docs.browserless.io/bap/typescript/reference/classes/BrowserQLError

# BrowserQLError

Raised when the BrowserQL server returns GraphQL errors.

## Extends

- `Error`

## Constructors

### Constructor

```ts
new BrowserQLError(errors): BrowserQLError;
```

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `errors` | `object`[] |

#### Returns

`BrowserQLError`

#### Overrides

```ts
Error.constructor
```

## Methods

### captureStackTrace()

```ts
static captureStackTrace(targetObject, constructorOpt?): void;
```

Creates a `.stack` property on `targetObject`, which when accessed returns
a string representing the location in the code at which
`Error.captureStackTrace()` was called.

```js
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack;  // Similar to `new Error().stack`
```

The first line of the trace will be prefixed with
`${myObject.name}: ${myObject.message}`.

The optional `constructorOpt` argument accepts a function. If given, all frames
above `constructorOpt`, including `constructorOpt`, will be omitted from the
generated stack trace.

The `constructorOpt` argument is useful for hiding implementation
details of error generation from the user. For instance:

```js
function a() {
  b();
}

function b() {
  c();
}

function c() {
  // Create an error without stack trace to avoid calculating the stack trace twice.
  const { stackTraceLimit } = Error;
  Error.stackTraceLimit = 0;
  const error = new Error();
  Error.stackTraceLimit = stackTraceLimit;

  // Capture the stack trace above function b
  Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
  throw error;
}

a();
```

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `targetObject` | `object` |
| `constructorOpt?` | `Function` |

#### Returns

`void`

#### Inherited from

```ts
Error.captureStackTrace
```

***

### prepareStackTrace()

```ts
static prepareStackTrace(err, stackTraces): any;
```

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `err` | `Error` |
| `stackTraces` | `CallSite`[] |

#### Returns

`any`

#### See

https://v8.dev/docs/stack-trace-api#customizing-stack-traces

#### Inherited from

```ts
Error.prepareStackTrace
```

## Properties

| Property | Modifier | Type | Default value | Description | Inherited from |
| ------ | ------ | ------ | ------ | ------ | ------ |
| <a id="property-cause"></a> `cause?` | `public` | `unknown` | `undefined` | - | `Error.cause` |
| <a id="property-code"></a> `code` | `readonly` | `ErrorCode` | `'BQL'` | - | - |
| <a id="property-errors"></a> `errors` | `readonly` | `object`[] | `undefined` | - | - |
| <a id="property-message"></a> `message` | `public` | `string` | `undefined` | - | `Error.message` |
| <a id="property-name"></a> `name` | `public` | `string` | `undefined` | - | `Error.name` |
| <a id="property-stack"></a> `stack?` | `public` | `string` | `undefined` | - | `Error.stack` |
| <a id="property-stacktracelimit"></a> `stackTraceLimit` | `static` | `number` | `undefined` | The `Error.stackTraceLimit` property specifies the number of stack frames collected by a stack trace (whether generated by `new Error().stack` or `Error.captureStackTrace(obj)`). The default value is `10` but may be set to any valid JavaScript number. Changes will affect any stack trace captured _after_ the value has been changed. If set to a non-number value, or set to a negative number, stack traces will not capture any frames. | `Error.stackTraceLimit` |

---

# ConnectionError

URL: https://docs.browserless.io/bap/typescript/reference/classes/ConnectionError

# ConnectionError

Raised when the WebSocket connection fails, drops, or is unavailable.

## Extends

- `Error`

## Constructors

### Constructor

```ts
new ConnectionError(message): ConnectionError;
```

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `message` | `string` |

#### Returns

`ConnectionError`

#### Overrides

```ts
Error.constructor
```

## Methods

### captureStackTrace()

```ts
static captureStackTrace(targetObject, constructorOpt?): void;
```

Creates a `.stack` property on `targetObject`, which when accessed returns
a string representing the location in the code at which
`Error.captureStackTrace()` was called.

```js
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack;  // Similar to `new Error().stack`
```

The first line of the trace will be prefixed with
`${myObject.name}: ${myObject.message}`.

The optional `constructorOpt` argument accepts a function. If given, all frames
above `constructorOpt`, including `constructorOpt`, will be omitted from the
generated stack trace.

The `constructorOpt` argument is useful for hiding implementation
details of error generation from the user. For instance:

```js
function a() {
  b();
}

function b() {
  c();
}

function c() {
  // Create an error without stack trace to avoid calculating the stack trace twice.
  const { stackTraceLimit } = Error;
  Error.stackTraceLimit = 0;
  const error = new Error();
  Error.stackTraceLimit = stackTraceLimit;

  // Capture the stack trace above function b
  Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
  throw error;
}

a();
```

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `targetObject` | `object` |
| `constructorOpt?` | `Function` |

#### Returns

`void`

#### Inherited from

```ts
Error.captureStackTrace
```

***

### prepareStackTrace()

```ts
static prepareStackTrace(err, stackTraces): any;
```

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `err` | `Error` |
| `stackTraces` | `CallSite`[] |

#### Returns

`any`

#### See

https://v8.dev/docs/stack-trace-api#customizing-stack-traces

#### Inherited from

```ts
Error.prepareStackTrace
```

## Properties

| Property | Modifier | Type | Default value | Description | Inherited from |
| ------ | ------ | ------ | ------ | ------ | ------ |
| <a id="property-cause"></a> `cause?` | `public` | `unknown` | `undefined` | - | `Error.cause` |
| <a id="property-code"></a> `code` | `readonly` | `ErrorCode` | `'CONNECTION'` | - | - |
| <a id="property-message"></a> `message` | `public` | `string` | `undefined` | - | `Error.message` |
| <a id="property-name"></a> `name` | `public` | `string` | `undefined` | - | `Error.name` |
| <a id="property-stack"></a> `stack?` | `public` | `string` | `undefined` | - | `Error.stack` |
| <a id="property-stacktracelimit"></a> `stackTraceLimit` | `static` | `number` | `undefined` | The `Error.stackTraceLimit` property specifies the number of stack frames collected by a stack trace (whether generated by `new Error().stack` or `Error.captureStackTrace(obj)`). The default value is `10` but may be set to any valid JavaScript number. Changes will affect any stack trace captured _after_ the value has been changed. If set to a non-number value, or set to a negative number, stack traces will not capture any frames. | `Error.stackTraceLimit` |

---

# ElementHandle

URL: https://docs.browserless.io/bap/typescript/reference/classes/ElementHandle

# ElementHandle

A handle to a matched element.

Note: unlike puppeteer's live `ElementHandle`, the property getters
(`innerText`, `outerHTML`, …) return a **snapshot** captured when the handle
was created via `page.$()`. They do not re-query the DOM, so they won't
reflect later mutations. The action methods (`click`, `type`, `hover`,
`select`) and the scoped query methods (`$`, `$$`) always operate against the
live page.

## Accessors

### childElementCount

#### Get Signature

```ts
get childElementCount(): number | null;
```

The number of direct child elements.

##### Returns

`number` \| `null`

***

### className

#### Get Signature

```ts
get className(): string | null;
```

The element class attribute, when present.

##### Returns

`string` \| `null`

***

### id

#### Get Signature

```ts
get id(): string | null;
```

The element id, when present.

##### Returns

`string` \| `null`

***

### innerHTML

#### Get Signature

```ts
get innerHTML(): string | null;
```

The element's inner HTML, when returned by BrowserQL.

##### Returns

`string` \| `null`

***

### innerText

#### Get Signature

```ts
get innerText(): string | null;
```

The element's visible text, when returned by BrowserQL.

##### Returns

`string` \| `null`

***

### localName

#### Get Signature

```ts
get localName(): string | null;
```

The element local tag name.

##### Returns

`string` \| `null`

***

### outerHTML

#### Get Signature

```ts
get outerHTML(): string | null;
```

The element's outer HTML, when returned by BrowserQL.

##### Returns

`string` \| `null`

***

### selector

#### Get Signature

```ts
get selector(): string;
```

The selector used to create this handle.

##### Returns

`string`

## Methods

### $()

```ts
$(selector): Promise;
```

Query for the first descendant matching `selector`, scoped to this element.

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `selector` | `string` |

#### Returns

`Promise`\<`ElementHandle` \| `null`\>

***

### $$()

```ts
$$(selector): Promise;
```

Query for all descendants matching `selector`, scoped to this element.

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `selector` | `string` |

#### Returns

`Promise`\<`ElementHandle`[]\>

***

### click()

```ts
click(options?): Promise;
```

Clicks the element using its original selector.

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `options?` | [`ClickOptions`](../interfaces/ClickOptions) |

#### Returns

`Promise`\<[`ClickResponse`](../interfaces/ClickResponse)\>

***

### hover()

```ts
hover(): Promise;
```

Hovers the element using its original selector.

#### Returns

`Promise`\<[`HoverResponse`](../interfaces/HoverResponse)\>

***

### screenshot()

```ts
screenshot(options?): Promise>;
```

Captures a screenshot of the element.

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `options?` | \{ `path?`: `string`; \} |
| `options.path?` | `string` |

#### Returns

`Promise`\<`Uint8Array`\<`ArrayBufferLike`\>\>

***

### select()

```ts
select(...values): Promise;
```

Select option(s) on this element (it must be a `<select>`).

#### Parameters

| Parameter | Type |
| ------ | ------ |
| ...`values` | `string`[] |

#### Returns

`Promise`\<[`SelectResponse`](../interfaces/SelectResponse)\>

***

### textContent()

```ts
textContent(): Promise<string>;
```

Reads the element's current text content.

#### Returns

`Promise`\<`string`\>

***

### type()

```ts
type(text, options?): Promise;
```

Types text into the element using its original selector.

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `text` | `string` |
| `options?` | [`TypeOptions`](../interfaces/TypeOptions) |

#### Returns

`Promise`\<[`TypeResponse`](../interfaces/TypeResponse)\>

---

# EventEmitter\<Events\>

URL: https://docs.browserless.io/bap/typescript/reference/classes/EventEmitter

# EventEmitter\

Minimal typed event emitter backing the puppeteer-style Page event API.

The optional `onCountChange` hook fires whenever an event's listener count
changes (adds, removes, `once` auto-removal, `removeAllListeners`). Page uses
it to lazily open a subscription on the 0→1 transition and close it on 1→0.

Listeners are stored in a `Set`, so registering the *same* function twice for
an event is deduped to a single registration (one `emit` call, one `off` to
remove) — unlike Node's `EventEmitter`, which keeps duplicates. This is
intentional: it keeps the 0→1/1→0 count transitions that drive lazy
subscriptions unambiguous. Wrap in distinct closures if you need duplicates.

## Type Parameters

| Type Parameter |
| ------ |
| `Events` |

## Constructors

### Constructor

```ts
new EventEmitter(onCountChange?): EventEmitter;
```

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `onCountChange?` | (`event`, `count`) => `void` |

#### Returns

`EventEmitter`\<`Events`\>

## Methods

### emit()

```ts
emit(event, payload): boolean;
```

Invoke every listener for `event`. Returns whether any listener ran.

#### Type Parameters

| Type Parameter |
| ------ |
| `K` *extends* `string` \| `number` \| `symbol` |

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `event` | `K` |
| `payload` | `Events`\[`K`\] |

#### Returns

`boolean`

***

### listenerCount()

```ts
listenerCount(event): number;
```

#### Type Parameters

| Type Parameter |
| ------ |
| `K` *extends* `string` \| `number` \| `symbol` |

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `event` | `K` |

#### Returns

`number`

***

### off()

```ts
off(event, listener): this;
```

#### Type Parameters

| Type Parameter |
| ------ |
| `K` *extends* `string` \| `number` \| `symbol` |

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `event` | `K` |
| `listener` | [`Listener`](../type-aliases/Listener)\<`Events`\[`K`\]\> |

#### Returns

`this`

***

### on()

```ts
on(event, listener): this;
```

#### Type Parameters

| Type Parameter |
| ------ |
| `K` *extends* `string` \| `number` \| `symbol` |

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `event` | `K` |
| `listener` | [`Listener`](../type-aliases/Listener)\<`Events`\[`K`\]\> |

#### Returns

`this`

***

### once()

```ts
once(event, listener): this;
```

#### Type Parameters

| Type Parameter |
| ------ |
| `K` *extends* `string` \| `number` \| `symbol` |

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `event` | `K` |
| `listener` | [`Listener`](../type-aliases/Listener)\<`Events`\[`K`\]\> |

#### Returns

`this`

***

### removeAllListeners()

```ts
removeAllListeners(event?): this;
```

#### Type Parameters

| Type Parameter |
| ------ |
| `K` *extends* `string` \| `number` \| `symbol` |

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `event?` | `K` |

#### Returns

`this`

---

# Page

URL: https://docs.browserless.io/bap/typescript/reference/classes/Page

# Page

## Extends

- `PageGenerated`

## Accessors

### closed

#### Get Signature

```ts
get closed(): boolean;
```

##### Returns

`boolean`

## Constructors

### Constructor

```ts
new Page(transport): Page;
```

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `transport` | [`Transport`](Transport) |

#### Returns

`Page`

#### Overrides

```ts
PageGenerated.constructor
```

## Methods

### $()

```ts
$(selector, options?): Promise;
```

Passes through certain properties of the browsers' own `document.querySelector` API

#### Parameters

| Parameter | Type | Description |
| ------ | ------ | ------ |
| `selector` | `string` | - |
| `options?` | [`QuerySelectorOptions`](../interfaces/QuerySelectorOptions) | - |

#### Returns

`Promise`\<[`ElementHandle`](ElementHandle) \| `null`\>

#### Inherited from

```ts
PageGenerated.$
```

***

### $$()

```ts
$$(selector, options?): Promise;
```

Passes through certain properties of the browsers' own `document.querySelectorAll` API

#### Parameters

| Parameter | Type | Description |
| ------ | ------ | ------ |
| `selector` | `string` | - |
| `options?` | [`QuerySelectorOptions`](../interfaces/QuerySelectorOptions) | - |

#### Returns

`Promise`\<[`ElementHandle`](ElementHandle)[]\>

#### Inherited from

```ts
PageGenerated.$$
```

***

### $$eval()

```ts
$$eval(selector, options?): Promise;
```

Specify a selector that returns multiple nodes in a document (similar to `document.querySelectorAll`), or JavaScript that returns a NodeList, and this API will respond with details about those DOM nodes. Similar to how "map" works in most functional programming languages and libraries. Useful for mapping over repetitive data in sites and pages like product listings or search results. This will automatically wait for the selector to be present on the page, and is configurable with the "wait" and "timeout" options.

For getting arbitrary DOM attributes back you can specify them via the `attribute(name: "data-custom-attribute")` property. This will return an object with `name` and `value` properties.

You may also continuously map further nested items as well, for instance this query might get all books on a page, and then a nested `mapSelector` call might list all sellers of that book, or shipping speeds. Hierarchy of data is preserved to pass through the hierarchical data modeled inside the DOM.

This API will always return a list of results back regardless if one or more items are found, or `null` if none are found.

Using aliases can also give the returned JSON more meaning and better model the data returned by this powerful API.

Browser Automation Protocol divergence: unlike puppeteer's `$$eval(selector, pageFunction)`, this delegates to `mapSelector` and returns structured `MapSelectorResponse[]`, not a page-function result.

#### Parameters

| Parameter | Type | Description |
| ------ | ------ | ------ |
| `selector` | `string` | A `document.querySelectorAll` compatible string, or JavaScript that returns a DOM NodeList. Examples include: |
| `options?` | [`MapSelectorOptions`](../interfaces/MapSelectorOptions) | - |

#### Returns

`Promise`\<[`MapSelectorResponse`](../interfaces/MapSelectorResponse)[]\>

#### Inherited from

```ts
PageGenerated.$$eval
```

***

### $eval()

```ts
$eval(selector): Promise<string>;
```

Returns the text content on the given page or by selector when specified

Browser Automation Protocol divergence: unlike puppeteer's `$eval(selector, pageFunction)`, this returns the matched element's text content and takes no page-function argument.

#### Parameters

| Parameter | Type | Description |
| ------ | ------ | ------ |
| `selector` | `string` | The DOM selector of the given element you want to return the text of |

#### Returns

`Promise`\<`string`\>

#### Inherited from

```ts
PageGenerated.$eval
```

***

### addScriptTag()

```ts
addScriptTag(options?): Promise;
```

Reads Puppeteer's client-only `path` option before sending the BrowserQL
mutation. Exactly one content source is required.

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `options?` | [`AddScriptTagOptions`](../interfaces/AddScriptTagOptions) |

#### Returns

`Promise`\<[`AddScriptTagResponse`](../interfaces/AddScriptTagResponse)\>

***

### addStyleTag()

```ts
addStyleTag(options?): Promise;
```

Reads Puppeteer's client-only `path` option before sending the BrowserQL
mutation. Exactly one content source is required.

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `options?` | [`AddStyleTagOptions`](../interfaces/AddStyleTagOptions) |

#### Returns

`Promise`\<[`AddStyleTagResponse`](../interfaces/AddStyleTagResponse)\>

***

### authenticate()

```ts
authenticate(
   username,
   password,
options?): Promise;
```

Provides credentials for HTTP authentication (RFC 7235). When set, any HTTP
authentication challenge (401/407) will be answered with the supplied
credentials automatically. If an `origin` is provided, only challenges whose
origin matches will use these credentials; otherwise every challenge is
answered (Puppeteer-style).

#### Parameters

| Parameter | Type | Description |
| ------ | ------ | ------ |
| `username` | `string` | The username to authenticate with |
| `password` | `string` | The password to authenticate with |
| `options?` | [`AuthenticateOptions`](../interfaces/AuthenticateOptions) | - |

#### Returns

`Promise`\<[`HTTPResponse`](../interfaces/HTTPResponse) \| `null`\>

#### Inherited from

```ts
PageGenerated.authenticate
```

***

### check()

```ts
check(selector, options?): Promise;
```

Sets or un-sets the value of a checkbox on the page

#### Parameters

| Parameter | Type | Description |
| ------ | ------ | ------ |
| `selector` | `string` | The CSS selector of the element on the page you want to check/uncheck |
| `options?` | [`CheckboxOptions`](../interfaces/CheckboxOptions) | - |

#### Returns

`Promise`\<[`ClickResponse`](../interfaces/ClickResponse)\>

#### Inherited from

```ts
PageGenerated.check
```

***

### click()

```ts
click(selector, options?): Promise;
```

Waits for the element to be visible, scrolls to it, then clicks on it with native events

#### Parameters

| Parameter | Type | Description |
| ------ | ------ | ------ |
| `selector` | `string` | A query-selector compatible string, JavaScript that returns an HTML Node, OR a Browserless-deep query. Examples include: |
| `options?` | [`ClickOptions`](../interfaces/ClickOptions) | - |

#### Returns

`Promise`\<[`ClickResponse`](../interfaces/ClickResponse)\>

#### Inherited from

```ts
PageGenerated.click
```

***

### close()

```ts
close(): Promise<void>;
```

#### Returns

`Promise`\<`void`\>

***

### content()

```ts
content(): Promise<string>;
```

Returns the HTML content of the page or selector when specified. This API can also "clean" HTML markup returned by specifying a "clean" argument with numerous options. Features of the "clean" argument include removal of non-text nodes, removal of DOM attributes, as well as removal of excessive whitespace and newlines. Using "clean" can save nearly 1,000 times the payload size. Useful for LLM's and other scenarios

#### Returns

`Promise`\<`string`\>

#### Inherited from

```ts
PageGenerated.content
```

***

### cookies()

```ts
cookies(): Promise;
```

Sets and gets cookies on the page

#### Returns

`Promise`\<[`StandardCookie`](../interfaces/StandardCookie)[]\>

#### Inherited from

```ts
PageGenerated.cookies
```

***

### emit()

```ts
emit(event, payload): boolean;
```

Emit an event to its listeners (primarily internal).

#### Type Parameters

| Type Parameter |
| ------ |
| `K` *extends* `"error"` \| `"message"` \| keyof PageEventMap |

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `event` | `K` |
| `payload` | [`PageEvents`](../type-aliases/PageEvents)\[`K`\] |

#### Returns

`boolean`

***

### emulateMediaType()

```ts
emulateMediaType(type): Promise;
```

Emulates the given CSS media type for the page, mirroring the behavior of
printing ("print") or on-screen rendering ("screen"). Useful before a `pdf`
or `screenshot` to control how the page renders.

#### Parameters

| Parameter | Type | Description |
| ------ | ------ | ------ |
| `type` | [`MediaType`](../type-aliases/MediaType) | The CSS media type to emulate |

#### Returns

`Promise`\<[`EmulateMediaTypeResponse`](../interfaces/EmulateMediaTypeResponse)\>

#### Inherited from

```ts
PageGenerated.emulateMediaType
```

***

### evaluate()

```ts
evaluate(content, options?): Promise<string | null>;
```

Serializes a function argument and unwraps the scalar result value.

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `content` | `string` \| ((...`args`) => `unknown`) |
| `options?` | [`EvaluateOptions`](../interfaces/EvaluateOptions) |

#### Returns

`Promise`\<`string` \| `null`\>

***

### fulfill()

```ts
fulfill(options?): Promise;
```

Fulfills matching requests with a canned response instead of letting them hit
the network, mirroring the request-interceptor mocking used by the REST APIs.
Matching follows the same glob-style URL/method/type conditions as `reject`.

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `options?` | [`FulfillOptions`](../interfaces/FulfillOptions) |

#### Returns

`Promise`\<[`FulfillResponse`](../interfaces/FulfillResponse)\>

#### Inherited from

```ts
PageGenerated.fulfill
```

***

### goBack()

```ts
goBack(options?): Promise;
```

Goes back in browser history, optionally accepting waitUntil and timeout arguments. Returns null if no back is possible

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `options?` | [`NavigationOptions`](../interfaces/NavigationOptions) |

#### Returns

`Promise`\<[`HTTPResponse`](../interfaces/HTTPResponse) \| `null`\>

#### Inherited from

```ts
PageGenerated.goBack
```

***

### goForward()

```ts
goForward(options?): Promise;
```

Goes forward in browser history, optionally accepting waitUntil and timeout arguments. Returns null if no forward is possible

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `options?` | [`NavigationOptions`](../interfaces/NavigationOptions) |

#### Returns

`Promise`\<[`HTTPResponse`](../interfaces/HTTPResponse) \| `null`\>

#### Inherited from

```ts
PageGenerated.goForward
```

***

### goto()

```ts
goto(url, options?): Promise;
```

Opt-in remote response metadata keeps new clients compatible with older BQL schemas.

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `url` | `string` |
| `options?` | [`GotoOptions`](../interfaces/GotoOptions) |

#### Returns

`Promise`\<[`HTTPResponse`](../interfaces/HTTPResponse) \| `null`\>

#### Overrides

```ts
PageGenerated.goto
```

***

### hover()

```ts
hover(selector, options?): Promise;
```

Waits for the element to be visible, scrolls to it, then hover on it with native events

#### Parameters

| Parameter | Type | Description |
| ------ | ------ | ------ |
| `selector` | `string` | The CSS selector of the element on the page you want to hover on |
| `options?` | [`HoverOptions`](../interfaces/HoverOptions) | - |

#### Returns

`Promise`\<[`HoverResponse`](../interfaces/HoverResponse)\>

#### Inherited from

```ts
PageGenerated.hover
```

***

### html()

```ts
html(options?): Promise;
```

Returns the HTML content of the page or selector when specified. This API can also "clean" HTML markup returned by specifying a "clean" argument with numerous options. Features of the "clean" argument include removal of non-text nodes, removal of DOM attributes, as well as removal of excessive whitespace and newlines. Using "clean" can save nearly 1,000 times the payload size. Useful for LLM's and other scenarios

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `options?` | [`HTMLOptions`](../interfaces/HTMLOptions) |

#### Returns

`Promise`\<[`HTMLResponse`](../interfaces/HTMLResponse)\>

#### Inherited from

```ts
PageGenerated.html
```

***

### liveURL()

```ts
liveURL(options?): Promise;
```

Returns a fully-qualified, user-shareable live-URL for streaming the web-browser to an end-user, optionally interactive.

**Set 30 second timer for interaction**

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `options?` | [`LiveURLOptions`](../interfaces/LiveURLOptions) |

#### Returns

`Promise`\<[`LiveURLResponse`](../interfaces/LiveURLResponse)\>

#### Inherited from

```ts
PageGenerated.liveURL
```

***

### loadSecret()

```ts
loadSecret(ref, options?): Promise;
```

Fills a credential resolved just-in-time from a configured 1Password
integration into a target input. The resolved value never appears in your
query or in the response. The session must be started with an integration
(the `integrationId` connection parameter or session field), and the page's
origin must be in the integration's allowed-domain list. Once any secret has
been filled, capture and page-content readback are disabled for the rest of the
session — screenshots, PDFs, screencasts, Live URLs, session recording, and the
content-return mutations (evaluate, html, text, querySelector, querySelectorAll,
cookies).

#### Parameters

| Parameter | Type | Description |
| ------ | ------ | ------ |
| `ref` | `string` | The 1Password op:// reference (e.g. op://Vault/Item/password), or an alias |
| `options?` | `Omit`\<[`LoadSecretOptions`](../interfaces/LoadSecretOptions), `"ref"`\> | - |

#### Returns

`Promise`\<[`LoadSecretResponse`](../interfaces/LoadSecretResponse)\>

#### Inherited from

```ts
PageGenerated.loadSecret
```

***

### mapSelector()

```ts
mapSelector(selector, options?): Promise;
```

Specify a selector that returns multiple nodes in a document (similar to `document.querySelectorAll`), or JavaScript that returns a NodeList, and this API will respond with details about those DOM nodes. Similar to how "map" works in most functional programming languages and libraries. Useful for mapping over repetitive data in sites and pages like product listings or search results. This will automatically wait for the selector to be present on the page, and is configurable with the "wait" and "timeout" options.

For getting arbitrary DOM attributes back you can specify them via the `attribute(name: "data-custom-attribute")` property. This will return an object with `name` and `value` properties.

You may also continuously map further nested items as well, for instance this query might get all books on a page, and then a nested `mapSelector` call might list all sellers of that book, or shipping speeds. Hierarchy of data is preserved to pass through the hierarchical data modeled inside the DOM.

This API will always return a list of results back regardless if one or more items are found, or `null` if none are found.

Using aliases can also give the returned JSON more meaning and better model the data returned by this powerful API.

#### Parameters

| Parameter | Type | Description |
| ------ | ------ | ------ |
| `selector` | `string` | A `document.querySelectorAll` compatible string, or JavaScript that returns a DOM NodeList. Examples include: |
| `options?` | [`MapSelectorOptions`](../interfaces/MapSelectorOptions) | - |

#### Returns

`Promise`\<[`MapSelectorResponse`](../interfaces/MapSelectorResponse)[]\>

#### Inherited from

```ts
PageGenerated.mapSelector
```

***

### markdown()

```ts
markdown(options?): Promise;
```

Returns the page's content converted to Markdown. The HTML is fetched from
the page (optionally scoped to a selector) and converted server-side, with
script/style/noscript/iframe nodes stripped.

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `options?` | [`MarkdownOptions`](../interfaces/MarkdownOptions) |

#### Returns

`Promise`\<[`MarkdownResponse`](../interfaces/MarkdownResponse)\>

#### Inherited from

```ts
PageGenerated.markdown
```

***

### off()

```ts
off(event, listener): this;
```

Remove a listener. Removing the last listener for a subscription-backed
event closes the underlying stream.

#### Type Parameters

| Type Parameter |
| ------ |
| `K` *extends* `"error"` \| `"message"` \| keyof PageEventMap |

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `event` | `K` |
| `listener` | [`Listener`](../type-aliases/Listener)\<[`PageEvents`](../type-aliases/PageEvents)\[`K`\]\> |

#### Returns

`this`

***

### on()

```ts
on(event, listener): this;
```

Register a listener for a page event. Attaching the first listener for a
subscription-backed event (`console`, `request`, `response`) lazily opens
the underlying stream; see [PageEvents](../type-aliases/PageEvents).

#### Type Parameters

| Type Parameter |
| ------ |
| `K` *extends* `"error"` \| `"message"` \| keyof PageEventMap |

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `event` | `K` |
| `listener` | [`Listener`](../type-aliases/Listener)\<[`PageEvents`](../type-aliases/PageEvents)\[`K`\]\> |

#### Returns

`this`

***

### once()

```ts
once(event, listener): this;
```

Register a one-shot listener, auto-removed after it fires once.

#### Type Parameters

| Type Parameter |
| ------ |
| `K` *extends* `"error"` \| `"message"` \| keyof PageEventMap |

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `event` | `K` |
| `listener` | [`Listener`](../type-aliases/Listener)\<[`PageEvents`](../type-aliases/PageEvents)\[`K`\]\> |

#### Returns

`this`

***

### pdf()

```ts
pdf(options?): Promise>;
```

Generates a PDF of the page with the print CSS media type

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `options?` | [`PDFOptions`](../interfaces/PDFOptions) |

#### Returns

`Promise`\<`Uint8Array`\<`ArrayBufferLike`\>\>

#### Inherited from

```ts
PageGenerated.pdf
```

***

### preferences()

```ts
preferences(options?): Promise;
```

Sets configuration for the entirety of the session, replacing defaults like the 30 second timeout default

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `options?` | [`PreferencesOptions`](../interfaces/PreferencesOptions) |

#### Returns

`Promise`\<[`DefaultResponse`](../interfaces/DefaultResponse)\>

#### Inherited from

```ts
PageGenerated.preferences
```

***

### proxy()

```ts
proxy(options?): Promise;
```

Proxies requests, by a specified set of conditions, through either the Browserless residential proxy or through an external proxy. Only requests that match these conditions are proxied and the rest are sent from the instance's own IP address.

Use the "server" argument to specify an external proxy for Browserless to use for requests. For requests with authentication, the username and password should be included in the URL using Basic Authentication. See the examples below for more information on how to format those URLs.

**Using the browserless proxy for all requests and proxy through Brazil**

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `options?` | [`ProxyOptions`](../interfaces/ProxyOptions) |

#### Returns

`Promise`\<[`ProxyResponse`](../interfaces/ProxyResponse)\>

#### Inherited from

```ts
PageGenerated.proxy
```

***

### reconnect()

```ts
reconnect(options?): Promise;
```

Returns a payload with reconnection information in order to reconnect back to the same browser session

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `options?` | [`ReconnectOptions`](../interfaces/ReconnectOptions) |

#### Returns

`Promise`\<[`ReconnectionResponse`](../interfaces/ReconnectionResponse)\>

#### Inherited from

```ts
PageGenerated.reconnect
```

***

### reject()

```ts
reject(options?): Promise;
```

Rejects requests by a specified URL pattern, method, or type and operator. You may supply a single pattern, or a list (array) of them. This mutation, by default, will reject any requests that match *any* pattern, which we call an "or" operator. To reject requests where conditions must all match, specify an "and" operator in the mutation. Note that this only has an effect when the query is executing, so scripts that return quickly will likely see assets loading in the editor as these rejections only happen when mutations are executing.
___
**Rejecting images or media**

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `options?` | [`RejectOptions`](../interfaces/RejectOptions) |

#### Returns

`Promise`\<[`RejectResponse`](../interfaces/RejectResponse)\>

#### Inherited from

```ts
PageGenerated.reject
```

***

### reload()

```ts
reload(options?): Promise;
```

Reloads the given page with an optional waitUntil parameter and timeout parameter

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `options?` | [`NavigationOptions`](../interfaces/NavigationOptions) |

#### Returns

`Promise`\<[`HTTPResponse`](../interfaces/HTTPResponse) \| `null`\>

#### Inherited from

```ts
PageGenerated.reload
```

***

### removeAllListeners()

```ts
removeAllListeners(event?): this;
```

Remove all listeners for an event, or for every event when omitted.

#### Type Parameters

| Type Parameter |
| ------ |
| `K` *extends* `"error"` \| `"message"` \| keyof PageEventMap |

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `event?` | `K` |

#### Returns

`this`

***

### request()

```ts
request(options?): Promise;
```

Returns request information made by the Browser with optional filters via arguments. You may filter the returned results by a glob-like URL-pattern, the method of the request or the type of request. Applying an operator to this will then change the behavior by either "and"ing the filters together or "or"ing them. This API will automatically wait for the request to be made if none is immediately found which you can turn off by disabling the "wait" option.

**Getting all "Document" requests**

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `options?` | [`RequestFilterOptions`](../interfaces/RequestFilterOptions) |

#### Returns

`Promise`\<[`RequestResponse`](../interfaces/RequestResponse)[]\>

#### Inherited from

```ts
PageGenerated.request
```

***

### response()

```ts
response(options?): Promise;
```

Opt-in body-encoding flag keeps new clients compatible with older BQL schemas.

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `options?` | [`ResponseFilterOptions`](../interfaces/ResponseFilterOptions) |

#### Returns

`Promise`\<[`ResponseResponse`](../interfaces/ResponseResponse)[]\>

#### Overrides

```ts
PageGenerated.response
```

***

### screenshot()

```ts
screenshot(options?): Promise>;
```

Screenshots the page or a specific selector

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `options?` | [`ScreenshotOptions`](../interfaces/ScreenshotOptions) |

#### Returns

`Promise`\<`Uint8Array`\<`ArrayBufferLike`\>\>

#### Inherited from

```ts
PageGenerated.screenshot
```

***

### scroll()

```ts
scroll(options?): Promise;
```

Waits for a selector, then scrolls to it on the page or an x,y coordinate in pixels

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `options?` | [`ScrollOptions`](../interfaces/ScrollOptions) |

#### Returns

`Promise`\<[`ScrollResponse`](../interfaces/ScrollResponse)\>

#### Inherited from

```ts
PageGenerated.scroll
```

***

### select()

```ts
select(selector, ...values): Promise;
```

Varargs collapse to a single value or an array (puppeteer `select`).

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `selector` | `string` |
| ...`values` | `string`[] |

#### Returns

`Promise`\<[`SelectResponse`](../interfaces/SelectResponse)\>

***

### send()

```ts
send(query, options?): Promise;
```

#### Type Parameters

| Type Parameter | Default type |
| ------ | ------ |
| `T` | `unknown` |

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `query` | `string` |
| `options?` | [`SendOptions`](../interfaces/SendOptions) |

#### Returns

`Promise`\<`T`\>

***

### setContent()

```ts
setContent(html, options?): Promise;
```

Sets the given HTML content on the page with an optional waitUntil parameter

#### Parameters

| Parameter | Type | Description |
| ------ | ------ | ------ |
| `html` | `string` | When present, sets the content of page to the value passed, then returns the pages content |
| `options?` | [`SetContentOptions`](../interfaces/SetContentOptions) | - |

#### Returns

`Promise`\<[`HTTPResponse`](../interfaces/HTTPResponse) \| `null`\>

#### Inherited from

```ts
PageGenerated.setContent
```

***

### setCookie()

```ts
setCookie(...cookies): Promise;
```

Varargs of cookie inputs.

#### Parameters

| Parameter | Type |
| ------ | ------ |
| ...`cookies` | [`CookieInput`](../interfaces/CookieInput)[] |

#### Returns

`Promise`\<[`CookieResponse`](../interfaces/CookieResponse)\>

***

### setExtraHTTPHeaders()

```ts
setExtraHTTPHeaders(headers): Promise;
```

Accepts a `Record` and reshapes it into the GraphQL `[HeaderInput]`.

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `headers` | `Record`\<`string`, `string`\> |

#### Returns

`Promise`\<[`HTTPHeadersResponse`](../interfaces/HTTPHeadersResponse)\>

***

### setJavaScriptEnabled()

```ts
setJavaScriptEnabled(enabled): Promise;
```

Sets and gets JavaScript execution on the page

**Note: changing this value won't affect scripts that have already been run. It will take full effect on the next navigation.**

#### Parameters

| Parameter | Type | Description |
| ------ | ------ | ------ |
| `enabled` | `boolean` | Whether or not to enable JavaScript on the page |

#### Returns

`Promise`\<[`JavaScriptResponse`](../interfaces/JavaScriptResponse)\>

#### Inherited from

```ts
PageGenerated.setJavaScriptEnabled
```

***

### setUserAgent()

```ts
setUserAgent(userAgent): Promise;
```

Sets the User-Agent string for the browser session

#### Parameters

| Parameter | Type | Description |
| ------ | ------ | ------ |
| `userAgent` | `string` | The User-Agent string to set for the browser session |

#### Returns

`Promise`\<[`UserAgentResponse`](../interfaces/UserAgentResponse)\>

#### Inherited from

```ts
PageGenerated.setUserAgent
```

***

### setViewport()

```ts
setViewport(options?): Promise;
```

Sets the viewport dimensions for the browser session

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `options?` | [`ViewportOptions`](../interfaces/ViewportOptions) |

#### Returns

`Promise`\<[`ViewportResponse`](../interfaces/ViewportResponse)\>

#### Inherited from

```ts
PageGenerated.setViewport
```

***

### solve()

```ts
solve(options?): Promise;
```

Solves a captcha or other challenge. Can auto-detect the CAPTCHA type or solve a specific type.
Uses the same detection logic as BaaS to automatically identify the CAPTCHA type
and routes to the appropriate solver. Optionally accepts a specific type to solve.

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `options?` | [`SolveOptions`](../interfaces/SolveOptions) |

#### Returns

`Promise`\<[`CaptchaResponse`](../interfaces/CaptchaResponse)\>

#### Inherited from

```ts
PageGenerated.solve
```

***

### solveImageCaptcha()

```ts
solveImageCaptcha(options?): Promise;
```

Solves an image captcha using caller-provided selectors. Unlike solve, which auto-detects known captcha types, this mutation works on any site by accepting custom captchaSelector and inputSelector arguments. It detects the captcha image, solves it, and fills the result into the specified input field.

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `options?` | [`SolveImageCaptchaOptions`](../interfaces/SolveImageCaptchaOptions) |

#### Returns

`Promise`\<[`CaptchaResponse`](../interfaces/CaptchaResponse)\>

#### Inherited from

```ts
PageGenerated.solveImageCaptcha
```

***

### stopSessionRecording()

```ts
stopSessionRecording(): Promise;
```

Stops the current session recording and processes the replay data, similar to closing the context. This mutation will stop RRWeb recording and upload the collected events.

#### Returns

`Promise`\<[`StopSessionRecordingResponse`](../interfaces/StopSessionRecordingResponse)\>

#### Inherited from

```ts
PageGenerated.stopSessionRecording
```

***

### subscribe()

```ts
subscribe(listener): () => void;
```

Sugar for [Page.on](#on)('message', …) that returns an unsubscribe fn.

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `listener` | [`MessageListener`](../type-aliases/MessageListener) |

#### Returns

() => `void`

***

### switchToWindow()

```ts
switchToWindow(options?): Promise;
```

Switches context to a popup window matching specified criteria

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `options?` | [`SwitchToWindowOptions`](../interfaces/SwitchToWindowOptions) |

#### Returns

`Promise`\<[`SwitchWindowResponse`](../interfaces/SwitchWindowResponse)\>

#### Inherited from

```ts
PageGenerated.switchToWindow
```

***

### text()

```ts
text(options?): Promise;
```

Returns the text content on the given page or by selector when specified

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `options?` | [`TextOptions`](../interfaces/TextOptions) |

#### Returns

`Promise`\<[`TextResponse`](../interfaces/TextResponse)\>

#### Inherited from

```ts
PageGenerated.text
```

***

### title()

```ts
title(): Promise<string>;
```

Returns the title of the page that the browser is currently at

#### Returns

`Promise`\<`string`\>

#### Inherited from

```ts
PageGenerated.title
```

***

### type()

```ts
type(
   selector,
   text,
options?): Promise;
```

Types text into an element by scrolling to it, clicking it, then emitting key events for every character. Include a `\n` (or `\r`) in `text` to send an Enter/Return key press — useful for submitting search boxes or forms without a separate click. BrowserQL has no standalone key-press mutation, so non-Enter keys cannot be simulated; use `click` for non-keyboard submit interactions (for example, a submit button).

#### Parameters

| Parameter | Type | Description |
| ------ | ------ | ------ |
| `selector` | `string` | The CSS selector of the element on the page you want to type text into |
| `text` | `string` | The text content you want to type into the element. A `\n` or `\r` character is sent as an Enter key press. |
| `options?` | [`TypeOptions`](../interfaces/TypeOptions) | - |

#### Returns

`Promise`\<[`TypeResponse`](../interfaces/TypeResponse)\>

#### Inherited from

```ts
PageGenerated.type
```

***

### uncheck()

```ts
uncheck(selector, options?): Promise;
```

Sets or un-sets the value of a checkbox on the page

#### Parameters

| Parameter | Type | Description |
| ------ | ------ | ------ |
| `selector` | `string` | The CSS selector of the element on the page you want to check/uncheck |
| `options?` | [`CheckboxOptions`](../interfaces/CheckboxOptions) | - |

#### Returns

`Promise`\<[`ClickResponse`](../interfaces/ClickResponse)\>

#### Inherited from

```ts
PageGenerated.uncheck
```

***

### url()

```ts
url(): Promise<string>;
```

Returns the URL of the page that the browser is currently at

#### Returns

`Promise`\<`string`\>

#### Inherited from

```ts
PageGenerated.url
```

***

### waitForEvent()

```ts
waitForEvent(event, options?): Promise;
```

Leaves enough transport time for BrowserQL to return its own timeout
result instead of racing the client timer at the same millisecond.

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `event` | `string` |
| `options?` | [`WaitForEventOptions`](../interfaces/WaitForEventOptions) |

#### Returns

`Promise`\<[`WaitForEvent`](../interfaces/WaitForEvent)\>

#### Overrides

```ts
PageGenerated.waitForEvent
```

***

### waitForFunction()

```ts
waitForFunction(fn, options?): Promise;
```

Preserves the REST compatibility mode that considers an async function
complete once its returned promise settles, without changing the default
BrowserQL predicate semantics for other callers.

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `fn` | `string` |
| `options?` | [`WaitForFunctionOptions`](../interfaces/WaitForFunctionOptions) |

#### Returns

`Promise`\<[`WaitForFunction`](../interfaces/WaitForFunction)\>

***

### waitForNavigation()

```ts
waitForNavigation(options?): Promise;
```

Waits for a navigation even to fire, useful for clicking an element and waiting for a page load of some

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `options?` | [`WaitForNavigationOptions`](../interfaces/WaitForNavigationOptions) |

#### Returns

`Promise`\<[`HTTPResponse`](../interfaces/HTTPResponse)\>

#### Inherited from

```ts
PageGenerated.waitForNavigation
```

***

### waitForNetworkIdle()

```ts
waitForNetworkIdle(options?): Promise;
```

Wait for the page to have no more than the configured number of in-flight network requests for a period of time.

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `options?` | [`WaitForNetworkIdleOptions`](../interfaces/WaitForNetworkIdleOptions) |

#### Returns

`Promise`\<[`WaitForTimeoutResponse`](../interfaces/WaitForTimeoutResponse)\>

#### Inherited from

```ts
PageGenerated.waitForNetworkIdle
```

***

### waitForRequest()

```ts
waitForRequest(urlOrOptions?): Promise;
```

Accepts a bare URL string or an options object.

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `urlOrOptions?` | \| `string` \| [`WaitForRequestOptions`](../interfaces/WaitForRequestOptions) |

#### Returns

`Promise`\<[`WaitForRequestResponse`](../interfaces/WaitForRequestResponse)\>

***

### waitForResponse()

```ts
waitForResponse(urlOrOptions?): Promise;
```

Accepts a bare URL string or an options object.

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `urlOrOptions?` | \| `string` \| [`WaitForResponseOptions`](../interfaces/WaitForResponseOptions) |

#### Returns

`Promise`\<[`WaitForResponseResponse`](../interfaces/WaitForResponseResponse)\>

***

### waitForSelector()

```ts
waitForSelector(selector, options?): Promise;
```

Waits for a given selector to be present in the DOM, with optional visibility

#### Parameters

| Parameter | Type | Description |
| ------ | ------ | ------ |
| `selector` | `string` | The selector to wait for until present in the DOM |
| `options?` | [`WaitForSelectorOptions`](../interfaces/WaitForSelectorOptions) | - |

#### Returns

`Promise`\<[`WaitForSelectorResponse`](../interfaces/WaitForSelectorResponse)\>

#### Inherited from

```ts
PageGenerated.waitForSelector
```

***

### waitForTimeout()

```ts
waitForTimeout(ms): Promise<void>;
```

Milliseconds → the GraphQL `time` arg, with a transport timeout cushion.

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `ms` | `number` |

#### Returns

`Promise`\<`void`\>

***

### watchEvent()

```ts
watchEvent(event): Promise;
```

Starts tracking a named document/window event in the current document and
before scripts execute in future documents. A later waitForEvent call returns
immediately when the tracked event has already fired.

#### Parameters

| Parameter | Type | Description |
| ------ | ------ | ------ |
| `event` | `string` | The name of the event to track, eg: "load" or a custom event name |

#### Returns

`Promise`\<[`WaitForEvent`](../interfaces/WaitForEvent)\>

#### Inherited from

```ts
PageGenerated.watchEvent
```

---

# TimeoutError

URL: https://docs.browserless.io/bap/typescript/reference/classes/TimeoutError

# TimeoutError

Raised when an operation exceeds its timeout.

## Extends

- `Error`

## Constructors

### Constructor

```ts
new TimeoutError(message, context?): TimeoutError;
```

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `message` | `string` |
| `context?` | \{ `operation?`: `string`; `timeoutMs?`: `number`; \} |
| `context.operation?` | `string` |
| `context.timeoutMs?` | `number` |

#### Returns

`TimeoutError`

#### Overrides

```ts
Error.constructor
```

## Methods

### captureStackTrace()

```ts
static captureStackTrace(targetObject, constructorOpt?): void;
```

Creates a `.stack` property on `targetObject`, which when accessed returns
a string representing the location in the code at which
`Error.captureStackTrace()` was called.

```js
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack;  // Similar to `new Error().stack`
```

The first line of the trace will be prefixed with
`${myObject.name}: ${myObject.message}`.

The optional `constructorOpt` argument accepts a function. If given, all frames
above `constructorOpt`, including `constructorOpt`, will be omitted from the
generated stack trace.

The `constructorOpt` argument is useful for hiding implementation
details of error generation from the user. For instance:

```js
function a() {
  b();
}

function b() {
  c();
}

function c() {
  // Create an error without stack trace to avoid calculating the stack trace twice.
  const { stackTraceLimit } = Error;
  Error.stackTraceLimit = 0;
  const error = new Error();
  Error.stackTraceLimit = stackTraceLimit;

  // Capture the stack trace above function b
  Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
  throw error;
}

a();
```

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `targetObject` | `object` |
| `constructorOpt?` | `Function` |

#### Returns

`void`

#### Inherited from

```ts
Error.captureStackTrace
```

***

### prepareStackTrace()

```ts
static prepareStackTrace(err, stackTraces): any;
```

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `err` | `Error` |
| `stackTraces` | `CallSite`[] |

#### Returns

`any`

#### See

https://v8.dev/docs/stack-trace-api#customizing-stack-traces

#### Inherited from

```ts
Error.prepareStackTrace
```

## Properties

| Property | Modifier | Type | Default value | Description | Inherited from |
| ------ | ------ | ------ | ------ | ------ | ------ |
| <a id="property-cause"></a> `cause?` | `public` | `unknown` | `undefined` | - | `Error.cause` |
| <a id="property-code"></a> `code` | `readonly` | `ErrorCode` | `'TIMEOUT'` | - | - |
| <a id="property-message"></a> `message` | `public` | `string` | `undefined` | - | `Error.message` |
| <a id="property-name"></a> `name` | `public` | `string` | `undefined` | - | `Error.name` |
| <a id="property-operation"></a> `operation?` | `readonly` | `string` | `undefined` | The operation that timed out (truncated GraphQL query), when known. | - |
| <a id="property-stack"></a> `stack?` | `public` | `string` | `undefined` | - | `Error.stack` |
| <a id="property-timeoutms"></a> `timeoutMs?` | `readonly` | `number` | `undefined` | The timeout that was exceeded, in milliseconds, when known. | - |
| <a id="property-stacktracelimit"></a> `stackTraceLimit` | `static` | `number` | `undefined` | The `Error.stackTraceLimit` property specifies the number of stack frames collected by a stack trace (whether generated by `new Error().stack` or `Error.captureStackTrace(obj)`). The default value is `10` but may be set to any valid JavaScript number. Changes will affect any stack trace captured _after_ the value has been changed. If set to a non-number value, or set to a negative number, stack traces will not capture any frames. | `Error.stackTraceLimit` |

---

# Transport

URL: https://docs.browserless.io/bap/typescript/reference/classes/Transport

# Transport

## Accessors

### closed

#### Get Signature

```ts
get closed(): boolean;
```

##### Returns

`boolean`

## Constructors

### Constructor

```ts
new Transport(
   url,
   defaultTimeout?,
   transportFactory?): Transport;
```

#### Parameters

| Parameter | Type | Default value |
| ------ | ------ | ------ |
| `url` | `string` \| `undefined` | `undefined` |
| `defaultTimeout` | `number` | `30000` |
| `transportFactory` | [`TransportFactory`](../type-aliases/TransportFactory) | `defaultTransportFactory` |

#### Returns

`Transport`

## Methods

### close()

```ts
close(): void;
```

#### Returns

`void`

***

### connect()

```ts
connect(): Promise<void>;
```

Build the transport (via the factory) and send the graphql-transport-ws
`connection_init`. Idempotent: repeated calls return the same in-flight
promise. Resolves once the transport is wired and `connection_init` is
sent; the server's `connection_ack` is consumed asynchronously. Frames ride
a single ordered channel, so operations sent right after `connect()`
resolves still arrive after `connection_init` server-side.

#### Returns

`Promise`\<`void`\>

***

### onMessage()

```ts
onMessage(listener): () => void;
```

Subscribe to every raw frame the transport receives — in addition to the
per-operation routing, and including frames with no matching operation
(e.g. server-pushed data over a custom [ConnectionTransport](../interfaces/ConnectionTransport)). The
`connection_ack` handshake frame is consumed internally and not delivered.
Returns an unsubscribe function.

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `listener` | (`message`) => `void` |

#### Returns

() => `void`

***

### operationTimeout()

```ts
operationTimeout(timeout?, cushion?): number;
```

Resolve a per-operation timeout against the transport default.

#### Parameters

| Parameter | Type | Default value |
| ------ | ------ | ------ |
| `timeout?` | `number` | `undefined` |
| `cushion?` | `number` | `0` |

#### Returns

`number`

***

### send()

```ts
send(
   query,
   variables?,
timeout?): Promise;
```

Run a query or mutation and resolve with its `data` object. The operation
emits a single `next` (whose `data` is captured) followed by `complete`
(which resolves); a pre-execution `error` frame or GraphQL `errors` reject
with [BrowserQLError](BrowserQLError).

#### Type Parameters

| Type Parameter |
| ------ |
| `T` |

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `query` | `string` |
| `variables?` | `Record`\<`string`, `unknown`\> |
| `timeout?` | `number` |

#### Returns

`Promise`\<`T`\>

***

### subscribe()

```ts
subscribe(
   query,
   variables,
handlers): Promise<() => void>;
```

Open a subscription. Returns an unsubscribe function that completes just
this stream. Streamed values and errors are delivered to `handlers`.

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `query` | `string` |
| `variables` | `Record`\<`string`, `unknown`\> |
| `handlers` | `Subscriber` |

#### Returns

`Promise`\<() => `void`\>

---

# WebSocketTransport

URL: https://docs.browserless.io/bap/typescript/reference/classes/WebSocketTransport

# WebSocketTransport

The built-in [ConnectionTransport](../interfaces/ConnectionTransport): a WebSocket via `#platform` (native
in the browser, `ws` in Node so the User-Agent header can be sent). This is
what the library uses whenever no custom `transport` is supplied — connecting
to a WebSocket URL goes through [WebSocketTransport.create](#create), which
negotiates the SUBPROTOCOL subprotocol.

## Implements

- [`ConnectionTransport`](../interfaces/ConnectionTransport)

## Methods

### close()

```ts
close(): void;
```

#### Returns

`void`

#### Implementation of

[`ConnectionTransport`](../interfaces/ConnectionTransport).[`close`](../interfaces/ConnectionTransport.mdx#close)

***

### send()

```ts
send(message): void;
```

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `message` | `string` |

#### Returns

`void`

#### Implementation of

[`ConnectionTransport`](../interfaces/ConnectionTransport).[`send`](../interfaces/ConnectionTransport.mdx#send)

***

### create()

```ts
static create(
   url,
   headers?,
protocols?): Promise;
```

Opens a WebSocket to `url` and resolves once it is connected.

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `url` | `string` |
| `headers?` | `Record`\<`string`, `string`\> |
| `protocols?` | `string` \| `string`[] |

#### Returns

`Promise`\<`WebSocketTransport`\>

## Properties

| Property | Type |
| ------ | ------ |
| <a id="property-onclose"></a> `onclose?` | (`event?`) => `void` |
| <a id="property-onerror"></a> `onerror?` | (`error`) => `void` |
| <a id="property-onmessage"></a> `onmessage?` | (`message`) => `void` |

---

# buildOperation()

URL: https://docs.browserless.io/bap/typescript/reference/functions/buildOperation

# buildOperation()

```ts
function buildOperation(
   name,
   args,
   fields,
   argTypes?,
   operation?): object;
```

Builds a parameterized GraphQL mutation document and its accompanying
variables payload. Argument values are passed as GraphQL variables rather
than interpolated into the query string, which lets the server coerce and
validate them by type (including enums and input objects) and keeps the
operation document constant across calls.

## Parameters

| Parameter | Type | Default value | Description |
| ------ | ------ | ------ | ------ |
| `name` | `string` | `undefined` | The mutation field name, eg `goto`. |
| `args` | `Record`\<`string`, `unknown`\> \| `null` | `undefined` | Argument values keyed by argument name. `undefined` values are dropped. Pass `null` when the mutation takes no args. |
| `fields` | `string` | `undefined` | The selection set for the mutation's response. |
| `argTypes` | `Record`\<`string`, `string`\> | `{}` | Map of argument name → GraphQL type (eg `String!`), used to declare the operation's variables. Sourced from `ARG_TYPES`. |
| `operation` | `"mutation"` \| `"subscription"` | `'mutation'` | The GraphQL operation type. Defaults to `mutation`; pass `subscription` to build a streaming operation. |

## Returns

`object`

| Name | Type |
| ------ | ------ |
| `query` | `string` |
| `variables` | `Record`\<`string`, `unknown`\> |

---

# AddScriptTagOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/AddScriptTagOptions

# AddScriptTagOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-content"></a> `content?` | `string` | The raw JavaScript content of the script to add. One of "url" or "content" is required. |
| <a id="property-id"></a> `id?` | `string` | The id attribute to set on the injected script element |
| <a id="property-path"></a> `path?` | `string` | Read JavaScript from this local file before sending the mutation. Node.js only. |
| <a id="property-type"></a> `type?` | `string` | The type attribute to set on the injected script element, eg: "module" |
| <a id="property-url"></a> `url?` | `string` | The URL of the script to add. One of "url" or "content" is required. |

---

# AddScriptTagResponse

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/AddScriptTagResponse

# AddScriptTagResponse

Response returned after injecting a script tag

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-time"></a> `time` | `number` \| `null` | The period of time elapsed, in milliseconds |

---

# AddStyleTagOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/AddStyleTagOptions

# AddStyleTagOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-content"></a> `content?` | `string` | The raw CSS content of the stylesheet to add. One of "url" or "content" is required. |
| <a id="property-path"></a> `path?` | `string` | Read CSS from this local file before sending the mutation. Node.js only. |
| <a id="property-url"></a> `url?` | `string` | The URL of the stylesheet to add. One of "url" or "content" is required. |

---

# AddStyleTagResponse

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/AddStyleTagResponse

# AddStyleTagResponse

Response returned after injecting a style tag

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-time"></a> `time` | `number` \| `null` | The period of time elapsed, in milliseconds |

---

# Attribute

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/Attribute

# Attribute

## Properties

| Property | Type |
| ------ | ------ |
| <a id="property-name"></a> `name` | `string` \| `null` |
| <a id="property-value"></a> `value` | `string` \| `null` |

---

# AuthenticateOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/AuthenticateOptions

# AuthenticateOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-origin"></a> `origin?` | `string` | The origin of the requests needing authentication, specified as a full HTTP origin in `protocol://host(:port)` format (e.g. `https://example.com`). If not provided, every request needing authentication will use the provided credentials. |

---

# CaptchaResponse

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/CaptchaResponse

# CaptchaResponse

Response returned after a captcha has been solved

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-error"></a> `error` | `string` \| `null` | Error message if something went wrong, providing guidance on how to fix the issue |
| <a id="property-found"></a> `found` | `boolean` \| `null` | If a captcha was found or not |
| <a id="property-solved"></a> `solved` | `boolean` \| `null` | If a captcha was found, whether or not it was solved |
| <a id="property-time"></a> `time` | `number` \| `null` | The total time it took to find, and solve, the captcha |
| <a id="property-token"></a> `token` | `string` \| `null` | The solved token of the response, if any is provided |

---

# CheckboxOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/CheckboxOptions

# CheckboxOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-scroll"></a> `scroll?` | `boolean` | Whether or not to scroll to the element prior to clicking, defaults to true **Default** `true` |
| <a id="property-timeout"></a> `timeout?` | `number` | How long to wait for the element to appear before timing out on the handler, overriding any defaults. Default timeout is 30 seconds, or 30000. |
| <a id="property-visible"></a> `visible?` | `boolean` | Whether or not to check/uncheck the element only if it's visible **Default** `false` |
| <a id="property-wait"></a> `wait?` | `boolean` | Whether or not to wait for the element to present in the DOM **Default** `true` |

---

# CleanInput

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/CleanInput

# CleanInput

Options for cleaning up the DOM prior to exporting its content. Many options are available, and this query can destructively remove non-text DOM nodes, DOM attributes, and gratuitous whitespace characters. Since these operations are destructive in their nature it's recommended to run them at the very end of your query in order to preserve page functionality

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-attributes"></a> `attributes?` | `string`[] | A list of attributes to remove from all DOM nodes. When this isn't specified, and `removeAttributes` is true, all attributes on all DOM nodes are removed. `removeNonTextNodes` must be set to `true` for this to take effect |
| <a id="property-mode"></a> `mode?` | [`AttributeMode`](../type-aliases/AttributeMode) | Controls how the `attributes` field is interpreted. When set to "allow", the `attributes` field specifies which attributes to keep. When set to "deny" (default), the `attributes` field specifies which attributes to remove. Defaults to "deny" for backward compatibility. **Default** `DENY` |
| <a id="property-regexes"></a> `regexes?` | `string`[] | When `removeRegex` is set to "true" this list of regex items, without the beginning and ending `/`, are removed from the page. These are each run in order and replaced with a single space character to preserve some of their contents **Default** `[" +", "\n", "\r", "\t", "<\\!--.*?-->"]` |
| <a id="property-removeattributes"></a> `removeAttributes?` | `boolean` | When true (default is false) this will remove all attributes on all DOM nodes. Useful for "cleaning" up all HTML markup but preserving the structure overall. You can specify specific attributes to remove with `attributes` argument **Default** `false` |
| <a id="property-removenontextnodes"></a> `removeNonTextNodes?` | `boolean` | When true (default is true) this will remove non-textual nodes from the DOM like scripts, links, video, canvas, etc. You may override this by specifying a `selectors` argument for DOM selectors to remove. **Default** `true` |
| <a id="property-removeregex"></a> `removeRegex?` | `boolean` | Removes any characters in the HTML by a regex pattern and arn in order. By default this is true and removes newlines, returns, tabs, multi-spaces and HTML comments in that order. You may supply your own regex by using the `regexes` argument **Default** `true` |
| <a id="property-selectors"></a> `selectors?` | `string`[] | A list of selectors to remove from the page when `removeNonTextNodes` is set to true (`true` by default). |

---

# ClickOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/ClickOptions

# ClickOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-scroll"></a> `scroll?` | `boolean` | Whether or not to scroll to the element prior to clicking, defaults to true **Default** `true` |
| <a id="property-timeout"></a> `timeout?` | `number` | How long to wait for the element to appear before timing out on the click handler, overriding any defaults. Default timeout is 30 seconds, or 30000. |
| <a id="property-visible"></a> `visible?` | `boolean` | Whether or not to click the element only if it's visible **Default** `false` |
| <a id="property-wait"></a> `wait?` | `boolean` | Whether or not to wait for the element to present in the DOM **Default** `true` |

---

# ClickResponse

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/ClickResponse

# ClickResponse

Response returned after having clicked on an element

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-selector"></a> `selector` | `string` \| `null` | The selector text if specified |
| <a id="property-time"></a> `time` | `number` \| `null` | The amount of time, in milliseconds, elapsed since the start of clicking to completion |
| <a id="property-x"></a> `x` | `number` \| `null` | The X coordinate of the click, in pixels, on the page |
| <a id="property-y"></a> `y` | `number` \| `null` | The Y coordinate of the click, in pixels, on the page |

---

# ConnectionTransport

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/ConnectionTransport

# ConnectionTransport

A user-suppliable transport, modeled on puppeteer's `ConnectionTransport`.
Pass one via [ConnectOptions.transport](ConnectOptions) to send BrowserQL frames over
your own channel instead of the built-in WebSocket. The frames it relays are
graphql-transport-ws envelopes (SUBPROTOCOL) — the transport is just
the pipe, [Transport](../classes/Transport) does the framing on top.

The library assigns `onmessage`/`onclose`/`onerror` after the transport is
created; the transport must invoke `onmessage` for every frame it receives,
`onclose` when the connection ends, and `onerror` on a transport error.
`send` transmits one text frame; `close` tears the connection down.

## Methods

### close()

```ts
close(): void;
```

#### Returns

`void`

***

### send()

```ts
send(message): void;
```

#### Parameters

| Parameter | Type |
| ------ | ------ |
| `message` | `string` |

#### Returns

`void`

## Properties

| Property | Type |
| ------ | ------ |
| <a id="property-onclose"></a> `onclose?` | (`event?`) => `void` |
| <a id="property-onerror"></a> `onerror?` | (`error`) => `void` |
| <a id="property-onmessage"></a> `onmessage?` | (`message`) => `void` |

---

# ConnectOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/ConnectOptions

# ConnectOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-browserwsendpoint"></a> `browserWSEndpoint?` | `string` | The BrowserQL WebSocket endpoint to connect to. Optional when a custom `transport` is supplied, since the transport owns the connection. |
| <a id="property-timeout"></a> `timeout?` | `number` | - |
| <a id="property-token"></a> `token?` | `string` | - |
| <a id="property-transport"></a> `transport?` | [`TransportFactory`](../type-aliases/TransportFactory) | Override the built-in WebSocket with your own transport (puppeteer-style). Called once per Browser.newPage with the endpoint URL; must resolve a ConnectionTransport that is already open. See [TransportFactory](../type-aliases/TransportFactory). |

---

# ConsoleMessage

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/ConsoleMessage

# ConsoleMessage

A console message emitted by the page. Mirrors puppeteer's ConsoleMessage,
sourced from CDP Runtime.consoleAPICalled and Runtime.exceptionThrown.

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-args"></a> `args` | `string`[] \| `null` | Each console argument, JSON-encoded and in order (equivalent to msg.args()). |
| <a id="property-location"></a> `location` | [`ConsoleMessageLocation`](ConsoleMessageLocation) \| `null` | The source location the message originated from. |
| <a id="property-text"></a> `text` | `string` \| `null` | The joined, human-readable message text (equivalent to puppeteer's msg.text()). |
| <a id="property-timestamp"></a> `timestamp` | `number` \| `null` | Wall-clock milliseconds since the Unix epoch when the message was emitted. |
| <a id="property-type"></a> `type` | [`ConsoleMessageType`](../type-aliases/ConsoleMessageType) \| `null` | The kind of console message. |

---

# ConsoleMessageLocation

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/ConsoleMessageLocation

# ConsoleMessageLocation

The source location a console message originated from.

## Properties

| Property | Type |
| ------ | ------ |
| <a id="property-columnnumber"></a> `columnNumber` | `number` \| `null` |
| <a id="property-linenumber"></a> `lineNumber` | `number` \| `null` |
| <a id="property-url"></a> `url` | `string` \| `null` |

---

# CookieInput

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/CookieInput

# CookieInput

The cookie to be sent to the page

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-domain"></a> `domain?` | `string` | Cookie domain |
| <a id="property-expires"></a> `expires?` | `number` | Cookie expiration date, session cookie if not set |
| <a id="property-httponly"></a> `httpOnly?` | `boolean` | True if cookie is http-only |
| <a id="property-name"></a> `name` | `string` | Cookie name |
| <a id="property-partitionkey"></a> `partitionKey?` | [`CookiePartitionKeyInput`](CookiePartitionKeyInput) | - |
| <a id="property-path"></a> `path?` | `string` | Cookie path |
| <a id="property-priority"></a> `priority?` | [`CookiePriority`](../type-aliases/CookiePriority) | - |
| <a id="property-samesite"></a> `sameSite?` | [`CookieSameSite`](../type-aliases/CookieSameSite) | Cookie SameSite type |
| <a id="property-secure"></a> `secure?` | `boolean` | True if cookie is secure |
| <a id="property-sourcescheme"></a> `sourceScheme?` | [`CookieSourceScheme`](../type-aliases/CookieSourceScheme) | - |
| <a id="property-url"></a> `url?` | `string` | The request-URI to associate with the setting of the cookie. This value can affect the default domain, path, source port, and source scheme values of the created cookie |
| <a id="property-value"></a> `value` | `string` | Cookie value |

---

# CookiePartitionKeyInput

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/CookiePartitionKeyInput

# CookiePartitionKeyInput

## Properties

| Property | Type |
| ------ | ------ |
| <a id="property-hascrosssiteancestor"></a> `hasCrossSiteAncestor?` | `boolean` |
| <a id="property-sourceorigin"></a> `sourceOrigin` | `string` |

---

# CookieResponse

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/CookieResponse

# CookieResponse

The response returned after setting or getting cookies

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-cookies"></a> `cookies` | [`StandardCookie`](StandardCookie)[] \| `null` | A standard cookie object with the values of the set cookies |
| <a id="property-time"></a> `time` | `number` \| `null` | The time it took to set and return the cookies |

---

# DefaultResponse

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/DefaultResponse

# DefaultResponse

Default response for all methods

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-timeout"></a> `timeout` | `number` \| `null` | The default timeout for all methods. Default timeout is 30 seconds, or 30000. |

---

# EmulateMediaTypeOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/EmulateMediaTypeOptions

# EmulateMediaTypeOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-type"></a> `type` | [`MediaType`](../type-aliases/MediaType) | The CSS media type to emulate |

---

# EmulateMediaTypeResponse

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/EmulateMediaTypeResponse

# EmulateMediaTypeResponse

Response returned after emulating a CSS media type

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-time"></a> `time` | `number` \| `null` | The period of time elapsed, in milliseconds |
| <a id="property-type"></a> `type` | `string` \| `null` | The media type that was emulated |

---

# EvaluateOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/EvaluateOptions

# EvaluateOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-timeout"></a> `timeout?` | `number` | A timeout to wait for the script to finish evaluating, overriding any defaults. Useful for async scripts that may be longer running. Default timeout is 30 seconds, or 30000. |
| <a id="property-url"></a> `url?` | `string` | The URL of the script you'd like to evaluate. This code gets wrapped in an async function so you can use `return` at the end as well as `await` and other async concepts. You can return any stringified value from this function |

---

# EvaluateResponse

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/EvaluateResponse

# EvaluateResponse

Response returned after evaluating a script

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-time"></a> `time` | `number` \| `null` | The time it took for the evaluate call to happen |
| <a id="property-value"></a> `value` | `string` \| `null` | The returned value of the script, if any |

---

# FulfillOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/FulfillOptions

# FulfillOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-base64body"></a> `base64Body?` | `string` | A base64-encoded response body. Use this instead of body for binary data. |
| <a id="property-body"></a> `body?` | `string` | The response body to respond with |
| <a id="property-contenttype"></a> `contentType?` | `string` | The Content-Type header to respond with |
| <a id="property-headers"></a> `headers?` | [`HeaderInput`](HeaderInput)[] | Additional response headers to respond with |
| <a id="property-method"></a> `method?` | [`Method`](../type-aliases/Method)[] | The HTTP Method(s) of the requests to fulfill |
| <a id="property-operator"></a> `operator?` | [`OperatorType`](../type-aliases/OperatorType) | Whether to "or" the conditions together (any match fulfills) or "and" them (all must match) **Default** `or` |
| <a id="property-regex"></a> `regex?` | `string`[] | JavaScript regular-expression pattern(s) to match request URLs |
| <a id="property-status"></a> `status?` | `number` | The HTTP status code to respond with. Defaults to 200 **Default** `200` |
| <a id="property-type"></a> `type?` | [`ResourceType`](../type-aliases/ResourceType)[] | The resource type(s) of the requests to fulfill |
| <a id="property-url"></a> `url?` | `string`[] | The glob-style URL pattern(s) to fulfill requests for |

---

# FulfillResponse

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/FulfillResponse

# FulfillResponse

Response returned after registering a request-fulfillment interceptor

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-time"></a> `time` | `number` \| `null` | The period of time elapsed, in milliseconds |

---

# GotoOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/GotoOptions

# GotoOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-referer"></a> `referer?` | `string` | The Referer header to send with the navigation request |
| <a id="property-referrerpolicy"></a> `referrerPolicy?` | `string` | The referrer policy to use for the navigation request |
| <a id="property-remoteresponsemetadata"></a> `remoteResponseMetadata?` | `boolean` | Request remote IP/port fields. Enable only when the target BQL schema supports them. |
| <a id="property-timeout"></a> `timeout?` | `number` | The maximum amount of time, in milliseconds, to wait for the page to load, overriding any defaults. Default timeout is 30 seconds, or 30000. |
| <a id="property-waituntil"></a> `waitUntil?` | [`WaitUntilGoto`](../type-aliases/WaitUntilGoto) | When to consider the page fully-loaded and proceed with further execution **Default** `load` |
| <a id="property-waituntilall"></a> `waitUntilAll?` | [`WaitUntilGoto`](../type-aliases/WaitUntilGoto)[] | Wait for every listed lifecycle event. This preserves Puppeteer waitUntil array semantics. |

---

# HeaderInput

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/HeaderInput

# HeaderInput

A HTTP header input type

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-name"></a> `name` | `string` | The name of the HTTP header |
| <a id="property-value"></a> `value` | `string` | The value of the HTTP header |

---

# HoverOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/HoverOptions

# HoverOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-scroll"></a> `scroll?` | `boolean` | Whether or not to scroll to the element, defaults to true **Default** `true` |
| <a id="property-timeout"></a> `timeout?` | `number` | How long to wait for the element to appear before timing out, overriding any defaults. Default timeout is 30 seconds, or 30000. |
| <a id="property-visible"></a> `visible?` | `boolean` | Whether or not to hover on the element only if it's visible **Default** `false` |
| <a id="property-wait"></a> `wait?` | `boolean` | Whether or not to wait for the element to present in the DOM **Default** `true` |
| <a id="property-x"></a> `x?` | `number` | The X coordinate, in pixels, to hover on the page |
| <a id="property-y"></a> `y?` | `number` | The Y coordinate, in pixels, to hover on the page |

---

# HoverResponse

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/HoverResponse

# HoverResponse

Response returned after having hovered over an element

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-selector"></a> `selector` | `string` \| `null` | The selector text |
| <a id="property-time"></a> `time` | `number` \| `null` | The amount of time, in milliseconds, elapsed since the start to completion |
| <a id="property-x"></a> `x` | `number` \| `null` | The X coordinate in pixels, on the page |
| <a id="property-y"></a> `y` | `number` \| `null` | The Y coordinate in pixels, on the page |

---

# HTMLOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/HTMLOptions

# HTMLOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-clean"></a> `clean?` | [`CleanInput`](CleanInput) | Specifies conditions for "cleaning" HTML, useful for minimizing the amount of markup returned for cases like LLMs and more. See nested options for parameters. |
| <a id="property-rewrite"></a> `rewrite?` | [`HTMLRewriteInput`](HTMLRewriteInput) | Rewrites captured resource URLs to archive-relative paths before returning HTML. |
| <a id="property-selector"></a> `selector?` | `string` | The DOM selector of the given element you want to return the HTML of |
| <a id="property-timeout"></a> `timeout?` | `number` | The maximum amount of time, in milliseconds, to wait for the selector to appear, overriding any defaults. Default timeout is 30 seconds, or 30000. |
| <a id="property-visible"></a> `visible?` | `boolean` | Whether or not to return the HTML content of the element only if it's visible **Default** `false` |

---

# HTMLResourcePathInput

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/HTMLResourcePathInput

# HTMLResourcePathInput

A resource URL and its replacement archive path when rewriting HTML for export.

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-path"></a> `path` | `string` | The relative path used for the resource in the exported archive. |
| <a id="property-url"></a> `url` | `string` | The absolute resource URL found in the page. |

---

# HTMLResponse

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/HTMLResponse

# HTMLResponse

HTML content of a page

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-html"></a> `html` | `string` \| `null` | The content of the page's HTML |
| <a id="property-time"></a> `time` | `number` \| `null` | The amount of time, in milliseconds, elapsed since the start of content retrieval to completion |

---

# HTMLRewriteInput

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/HTMLRewriteInput

# HTMLRewriteInput

Options for rewriting page resource URLs before returning HTML.

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-baseurl"></a> `baseURL` | `string` | The base URL used to resolve relative resource URLs. |
| <a id="property-resources"></a> `resources` | [`HTMLResourcePathInput`](HTMLResourcePathInput)[] | The resources to rewrite from absolute URLs to archive-relative paths. |

---

# HTTPHeaders

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/HTTPHeaders

# HTTPHeaders

An object representing the header's name and underlying value

## Properties

| Property | Type |
| ------ | ------ |
| <a id="property-name"></a> `name` | `string` \| `null` |
| <a id="property-value"></a> `value` | `string` \| `null` |

---

# HTTPHeadersResponse

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/HTTPHeadersResponse

# HTTPHeadersResponse

The response returned after setting HTTP headers

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-headers"></a> `headers` | [`HTTPHeaders`](HTTPHeaders)[] \| `null` | The HTTP headers that were set |
| <a id="property-time"></a> `time` | `number` \| `null` | The time it took to set the HTTP headers |

---

# HTTPResponse

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/HTTPResponse

# HTTPResponse

Response returned after a navigation event

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-ip"></a> `ip` | `string` \| `null` | The remote IP address that served the initial page-load response |
| <a id="property-port"></a> `port` | `number` \| `null` | The remote port that served the initial page-load response |
| <a id="property-status"></a> `status` | `number` \| `null` | The status code response of the initial page-load |
| <a id="property-text"></a> `text` | `string` \| `null` | The status text of the response from the initial page-load. Generally 'ok' |
| <a id="property-time"></a> `time` | `number` \| `null` | The amount of time, in milliseconds, elapsed since the start of navigation to completion |
| <a id="property-url"></a> `url` | `string` \| `null` | The final URL of the page after any potential redirects or URL rewrites |

---

# JavaScriptEnabledOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/JavaScriptEnabledOptions

# JavaScriptEnabledOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-enabled"></a> `enabled?` | `boolean` | Whether or not to enable JavaScript on the page |

---

# JavaScriptResponse

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/JavaScriptResponse

# JavaScriptResponse

The response returned after enabling or disabling JavaScript on the page

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-enabled"></a> `enabled` | `boolean` \| `null` | Whether or not JavaScript is enabled on the page |
| <a id="property-time"></a> `time` | `number` \| `null` | The time it took to perform this action |

---

# LiveURLOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/LiveURLOptions

# LiveURLOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-compressed"></a> `compressed?` | `boolean` | Whether to compress each frame of the streamed image data. When `true` (default), frames are compressed to reduce bandwidth usage. Set to `false` to disable compression for lower latency at the cost of increased bandwidth usage. **Default** `true` |
| <a id="property-emulatecomponents"></a> `emulateComponents?` | `boolean` | Whether to emulate HTML components such as `<select>` dropdowns and the virtual keyboard. Setting this to `false` will force the virtual keyboard to not appear on the end-user's screen when interacting with input fields. Defaults to `true`. **Default** `true` |
| <a id="property-interactable"></a> `interactable?` | `boolean` | Whether the session is interactable or not. Set to "false" to not allow click and mouse events to be forwarded through to the end-user **Default** `true` |
| <a id="property-quality"></a> `quality?` | `number` | The quality of the stream, represented as number from 1 - 100. Only used when "type" is "jpeg". Default is 70. **Default** `70` |
| <a id="property-resizable"></a> `resizable?` | `boolean` | Whether or not to resize the underlying browser to match the end-user's screen size. When `false` the underlying browser will retain it's current viewport, and the end users's screen wil maintain the appropriate aspect ratio. **Default** `true` |
| <a id="property-showbrowserinterface"></a> `showBrowserInterface?` | `boolean` | Whether to show the browser interface and tabs in the live URL. When `false` (default), only the browser content is shown. When `true`, navigation bar and tabs are displayed. **Default** `false` |
| <a id="property-timeout"></a> `timeout?` | `number` | The maximum time allowed for the browser to remain alive. Once the time is reached, the end-user will receive a prompt that the session has closed |
| <a id="property-type"></a> `type?` | [`LiveURLStreamType`](../type-aliases/LiveURLStreamType) | The binary-type of the streamed imaged. "jpeg" will consumer lower bandwidth and useful low bandwidth networks and devices. "png" is a much higher quality but will consume considerably more bandwidth. Use "jpeg" when setting a custom quality **Default** `jpeg` |

---

# LiveURLResponse

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/LiveURLResponse

# LiveURLResponse

The response from the Live-URL query

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-liveurl"></a> `liveURL` | `string` \| `null` | The fully-qualified URL to share with the end-user to allow them to live-stream the browser |
| <a id="property-liveurlid"></a> `liveURLId` | `string` \| `null` | A unique ID representing the session. Useful for storing and associations in other APIs |

---

# LoadSecretOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/LoadSecretOptions

# LoadSecretOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-selector"></a> `selector?` | `string` | The CSS selector of the input to fill. If omitted, the currently focused element is used. |

---

# LoadSecretResponse

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/LoadSecretResponse

# LoadSecretResponse

Response returned after attempting to fill a credential resolved from a
1Password integration. The resolved value is never included in the response.

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-error"></a> `error` | `string` \| `null` | Error code when ok is false: DomainNotAllowed, SelectorNotFound, NoFocusedElement, TargetNotFillable, CredentialNotResolved, CredentialIntegrationExpired, VaultUnreachable, or AuditWriteFailed. |
| <a id="property-message"></a> `message` | `string` \| `null` | Human-readable explanation when ok is false. |
| <a id="property-ok"></a> `ok` | `boolean` | Whether the credential was resolved and filled into the target element. |
| <a id="property-time"></a> `time` | `number` \| `null` | The amount of time, in milliseconds, the operation took. |

---

# MapSelectorOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/MapSelectorOptions

# MapSelectorOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-timeout"></a> `timeout?` | `number` | How long to wait for the element to appear before timing out, overriding any defaults. Default timeout is 30 seconds, or 30000. |
| <a id="property-wait"></a> `wait?` | `boolean` | Whether or not to wait for the selectors to present in the DOM **Default** `true` |

---

# MapSelectorResponse

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/MapSelectorResponse

# MapSelectorResponse

Response returned from a Map Selector

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-class"></a> `class` | `string`[] \| `null` | The class attribute's value, if any, of the node represented as an array of strings |
| <a id="property-id"></a> `id` | `string` \| `null` | The ID attribute's value, if any, of the node |
| <a id="property-innerhtml"></a> `innerHTML` | `string` \| `null` | The innerHTML of the selected DOM Node |
| <a id="property-innertext"></a> `innerText` | `string` \| `null` | The innerText of the selected DOM Node, eg, the raw textual content |

---

# MarkdownOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/MarkdownOptions

# MarkdownOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-selector"></a> `selector?` | `string` | The DOM selector to scope the Markdown conversion to. Defaults to the whole document. |
| <a id="property-timeout"></a> `timeout?` | `number` | The maximum amount of time, in milliseconds, to wait for the selector to appear, overriding any defaults. Default timeout is 30 seconds, or 30000. |
| <a id="property-visible"></a> `visible?` | `boolean` | Whether or not to convert the element only if it's visible **Default** `false` |

---

# MarkdownResponse

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/MarkdownResponse

# MarkdownResponse

Response returned after converting the page content to Markdown

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-markdown"></a> `markdown` | `string` \| `null` | The page content converted to Markdown |
| <a id="property-time"></a> `time` | `number` \| `null` | The period of time elapsed, in milliseconds |

---

# NavigationOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/NavigationOptions

# NavigationOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-timeout"></a> `timeout?` | `number` | The maximum amount of time, in milliseconds, to wait for the page to load, overriding any defaults. Default timeout is 30 seconds, or 30000. |
| <a id="property-waituntil"></a> `waitUntil?` | [`WaitUntilHistory`](../type-aliases/WaitUntilHistory) | When to consider the page fully-loaded and proceed with further execution **Default** `load` |

---

# PageEventMap

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/PageEventMap

# PageEventMap

Maps each Page event name to the payload delivered to its listeners.
Backed by a GraphQL subscription that is opened lazily on first listen.

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-console"></a> `console` | [`ConsoleMessage`](ConsoleMessage) | Streams console output as the page emits it. Mirrors page.on('console'). |
| <a id="property-request"></a> `request` | [`RequestResponse`](RequestResponse) | Streams each network request as it is issued. Mirrors page.on('request'). The optional filters mirror the request() mutation. |
| <a id="property-response"></a> `response` | [`ResponseResponse`](ResponseResponse) | Streams each network response as it is received. Mirrors page.on('response'). The response body is NOT fetched by default (streaming many bodies is expensive); request the `body` field in your selection set to trigger a per-event fetch. |

---

# PDFOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/PDFOptions

# PDFOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-displayheaderfooter"></a> `displayHeaderFooter?` | `boolean` | Display header and footer. Defaults to false |
| <a id="property-footertemplate"></a> `footerTemplate?` | `string` | HTML template for the print footer. Should use the same format as the `headerTemplate`. |
| <a id="property-format"></a> `format?` | [`PDFPageFormat`](../type-aliases/PDFPageFormat) | The page format to use for the PDF |
| <a id="property-fullpage"></a> `fullPage?` | `boolean` | When true, sizes the PDF page height to the full scroll height of the document so the entire page renders on a single page. Mutually exclusive with "format" and "height". Default: False |
| <a id="property-generatedocumentoutline"></a> `generateDocumentOutline?` | `boolean` | Whether or not to embed the document outline into the PDF |
| <a id="property-generatetaggedpdf"></a> `generateTaggedPDF?` | `boolean` | Whether or not to generate tagged (accessible) PDF. Defaults to embedded choice |
| <a id="property-headertemplate"></a> `headerTemplate?` | `string` | HTML template for the print header. Should be valid HTML markup with following classes used to inject printing values into them: - `date`: formatted print date - `title`: document title - `url`: document location - `pageNumber`: current page number - `totalPages`: total pages in the document For example, `` would generate span containing the title |
| <a id="property-height"></a> `height?` | `string` \| `number` | Height in inches or CSS unit. Defaults to 11 inches |
| <a id="property-landscape"></a> `landscape?` | `boolean` | Paper orientation. Defaults to false |
| <a id="property-marginbottom"></a> `marginBottom?` | `string` \| `number` | Bottom margin in inches or CSS unit. Defaults to 1cm (~0.4 inches). |
| <a id="property-marginleft"></a> `marginLeft?` | `string` \| `number` | Left margin in inches or CSS unit. Defaults to 1cm (~0.4 inches). |
| <a id="property-marginright"></a> `marginRight?` | `string` \| `number` | Right margin in inches or CSS unit. Defaults to 1cm (~0.4 inches). |
| <a id="property-margintop"></a> `marginTop?` | `string` \| `number` | Top margin in inches or CSS unit. Defaults to 1cm (~0.4 inches). |
| <a id="property-omitbackground"></a> `omitBackground?` | `boolean` | Hides the default white background and allows the PDF to preserve transparency. Defaults to false. |
| <a id="property-pageranges"></a> `pageRanges?` | `string` | Paper ranges to print, one based, e.g., '1-5, 8, 11-13'. Pages are printed in the document order, not in the order specified, and no more than once. Defaults to empty string, which implies the entire document is printed. The page numbers are quietly capped to actual page count of the document, and ranges beyond the end of the document are ignored. If this results in no pages to print, an error is reported. It is an error to specify a range with start greater than end |
| <a id="property-path"></a> `path?` | `string` | - |
| <a id="property-prefercsspagesize"></a> `preferCSSPageSize?` | `boolean` | Whether or not to prefer page size as defined by css. Defaults to false, in which case the content will be scaled to fit the paper size |
| <a id="property-printbackground"></a> `printBackground?` | `boolean` | Print background graphics. Defaults to false |
| <a id="property-scale"></a> `scale?` | `number` | Scale of the webpage rendering. Defaults to 1 |
| <a id="property-timeout"></a> `timeout?` | `number` | The maximum amount of time, in milliseconds, to wait for the PDF to be generated. Default timeout is 30 seconds, or 30000. |
| <a id="property-transfermode"></a> `transferMode?` | `string` | Return as stream (PrintToPDFRequestTransferMode enum) |
| <a id="property-waitforfonts"></a> `waitForFonts?` | `boolean` | When true, waits for document fonts to finish loading before generating the PDF. Default: False |
| <a id="property-waitforimages"></a> `waitForImages?` | `boolean` | When true, waits for all images on the page to load before generating the PDF. Default: False |
| <a id="property-width"></a> `width?` | `string` \| `number` | Width in inches or CSS unit. Defaults to 8.5 inches |

---

# PDFResponse

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/PDFResponse

# PDFResponse

The response returned after generating a PDF

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-base64"></a> `base64` | `string` \| `null` | Base64 encoded PDF content |
| <a id="property-size"></a> `size` | `number` \| `null` | The size of the resulting PDF in bytes |
| <a id="property-time"></a> `time` | `number` \| `null` | The time it took to generate the PDF |

---

# PreferencesOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/PreferencesOptions

# PreferencesOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-timeout"></a> `timeout?` | `number` | Sets a default timeout for all methods, including 'goto', 'type', 'wait', etc. Default timeout is 30 seconds, or 30000. **Default** `30000` |

---

# ProxyOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/ProxyOptions

# ProxyOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-city"></a> `city?` | `string` | The city you wish to proxy through. Any spaces should be removed and all casing lowercase. For instance, "New York City" should be "newyorkcity" |
| <a id="property-country"></a> `country?` | [`CountryCode`](../type-aliases/CountryCode) | The country you wish to proxy through. Only allowed when using the browserless.io proxy and no `server` argument. |
| <a id="property-method"></a> `method?` | [`Method`](../type-aliases/Method)[] | The Method(s) of the request you'd like to proxy |
| <a id="property-network"></a> `network?` | [`ProxyNetwork`](../type-aliases/ProxyNetwork) | The Browserless proxy network to route matching requests through: `residential` (default) or `datacenter`. Only valid for the browserless.io proxy network (not with `server`). **Default** `residential` |
| <a id="property-operator"></a> `operator?` | [`OperatorType`](../type-aliases/OperatorType) | Whether to "or" conditions together, meaning any condition that matches will be proxied, or "and" them together meaning every condition must match to proxy the request. **Default** `or` |
| <a id="property-preset"></a> `preset?` | `string` | A preset code for website-specific proxy configurations. Use this with known presets like "px_gov01" for government websites. Only applies to the `residential` network; the `datacenter` pool ignores it |
| <a id="property-server"></a> `server?` | `string` | An external proxy to use for these requests matching the specified patterns set in the other arguments. When this is set then `country`, `city`, `state` and `sticky` options will throw errors as these are only valid for the browserless.io proxy network |
| <a id="property-state"></a> `state?` | `string` | The state or provence you wish to proxy through. Any spaces should be removed and all casing lowercase. For instance, "Rhode Island" would be "rhodeisland" and "New Brunswick" would be "newbrunswick" |
| <a id="property-sticky"></a> `sticky?` | `boolean` | Whether or not you want the same IP to be used for subsequent requests matching the pattern |
| <a id="property-type"></a> `type?` | [`ResourceType`](../type-aliases/ResourceType)[] | The content-type of the request you'd like to proxy requests to |
| <a id="property-url"></a> `url?` | `string`[] | A glob-style URL pattern to match requests, and if matched, are proxied through |

---

# ProxyResponse

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/ProxyResponse

# ProxyResponse

Response returned after setting up the proxy patterns

## Properties

| Property | Type |
| ------ | ------ |
| <a id="property-time"></a> `time` | `number` \| `null` |

---

# QuerySelectorOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/QuerySelectorOptions

# QuerySelectorOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-timeout"></a> `timeout?` | `number` | - |
| <a id="property-visible"></a> `visible?` | `boolean` | **Default** `false` |

---

# QuerySelectorResponse

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/QuerySelectorResponse

# QuerySelectorResponse

The response returned after querying the page for a selector

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-childelementcount"></a> `childElementCount` | `number` \| `null` | The Element.childElementCount read-only property returns the number of child elements of this element |
| <a id="property-classname"></a> `className` | `string` \| `null` | The className property of the Element interface gets and sets the value of the class attribute of the specified element |
| <a id="property-id"></a> `id` | `string` \| `null` | The id property of the Element interface represents the element's identifier, reflecting the id global attribute |
| <a id="property-innerhtml"></a> `innerHTML` | `string` \| `null` | The Element property innerHTML gets the HTML or XML markup contained within the element |
| <a id="property-innertext"></a> `innerText` | `string` \| `null` | The Element property innerText gets the text contained within the element |
| <a id="property-localname"></a> `localName` | `string` \| `null` | The Element.localName read-only property returns the local part of the qualified name of an element |
| <a id="property-outerhtml"></a> `outerHTML` | `string` \| `null` | The outerHTML attribute of the Element DOM interface gets the serialized HTML fragment describing the element including its descendants. It can also be set to replace the element with nodes parsed from the given string |

---

# ReconnectionResponse

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/ReconnectionResponse

# ReconnectionResponse

The response received after attempting to reconnect a BrowserQL session

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-browserqlendpoint"></a> `browserQLEndpoint` | `string` \| `null` | The fully-qualified URL to reconnect future BrowserQL sessions, eg: https://chrome.browserless.io/bql/$id. Please note that token information is not returned by this API and might be required |
| <a id="property-browserwsendpoint"></a> `browserWSEndpoint` | `string` \| `null` | The fully-qualified URL of the browserWSEndpoint which can be used with other libraries like playwright or puppeteer. Please note that token information is not returned by this API and might be required |
| <a id="property-devtoolsfrontendurl"></a> `devtoolsFrontendUrl` | `string` \| `null` | The fully-qualified URL of the devtools resources for loading Chrome's developer tools remotely |
| <a id="property-websocketdebuggerurl"></a> `webSocketDebuggerUrl` | `string` \| `null` | The underlying page's webSocketDebuggerUrl, useful for hooking libraries that operate on a page and not a browser object |

---

# ReconnectOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/ReconnectOptions

# ReconnectOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-timeout"></a> `timeout?` | `number` | The amount of time, in milliseconds, to leave the browser open without a connection before it is manually terminated. Defaults to 30 seconds **Default** `30000` |

---

# RejectOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/RejectOptions

# RejectOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-enabled"></a> `enabled?` | `boolean` | Whether or not to enable request rejections **Default** `true` |
| <a id="property-method"></a> `method?` | [`Method`](../type-aliases/Method)[] | The Method of the request you'd like to reject |
| <a id="property-operator"></a> `operator?` | [`OperatorType`](../type-aliases/OperatorType) | Whether to "or" conditions together, meaning any condition that matches will be rejected, or "and" them together meaning every condition must match to reject the request. **Default** `or` |
| <a id="property-regex"></a> `regex?` | `string`[] | The JavaScript regular-expression pattern(s) you'd like to reject requests by URL |
| <a id="property-type"></a> `type?` | [`ResourceType`](../type-aliases/ResourceType)[] | The type of resource you'd like to reject request to |
| <a id="property-url"></a> `url?` | `string`[] | The glob-style URL pattern you'd like to reject requests to |

---

# RejectResponse

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/RejectResponse

# RejectResponse

The response parameters for the reject mutation

## Properties

| Property | Type |
| ------ | ------ |
| <a id="property-enabled"></a> `enabled` | `boolean` \| `null` |
| <a id="property-time"></a> `time` | `number` \| `null` |

---

# RequestFilterOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/RequestFilterOptions

# RequestFilterOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-method"></a> `method?` | [`Method`](../type-aliases/Method)[] | The method of the request to return results for |
| <a id="property-operator"></a> `operator?` | [`OperatorType`](../type-aliases/OperatorType) | When applying arguments like URL or method, this operator will either "and" them together, or "or" them together. Default is "or" **Default** `or` |
| <a id="property-timeout"></a> `timeout?` | `number` | How long to wait for the request(s) to be made before timing out, overriding any defaults. Default timeout is 30 seconds, or 30000. "wait" parameter must also be "true". |
| <a id="property-type"></a> `type?` | [`ResourceType`](../type-aliases/ResourceType)[] | The type content-type of the request to match against |
| <a id="property-url"></a> `url?` | `string`[] | The pattern of the request URL to wait for, using glob-style pattern-matching |
| <a id="property-wait"></a> `wait?` | `boolean` | Whether or not to wait for the request to be made. When set to `true`, generally only one request is returned since this API will wait for the first request that matches any patterns to be returned **Default** `true` |

---

# RequestHeader

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/RequestHeader

# RequestHeader

An object representing the header's name and underlying value

## Properties

| Property | Type |
| ------ | ------ |
| <a id="property-name"></a> `name` | `string` \| `null` |
| <a id="property-value"></a> `value` | `string` \| `null` |

---

# RequestInput

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/RequestInput

# RequestInput

The specific request to perform a conditional action on

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-method"></a> `method?` | [`Method`](../type-aliases/Method) | The HTTP Method of the request |
| <a id="property-url"></a> `url?` | `string` | The pattern of the request URL to wait for, using glob-style pattern-matching |

---

# RequestResponse

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/RequestResponse

# RequestResponse

Response returned from the request API

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-headers"></a> `headers` | [`RequestHeader`](RequestHeader)[] \| `null` | The headers of the request |
| <a id="property-method"></a> `method` | [`Method`](../type-aliases/Method) \| `null` | The HTTP method of the request |
| <a id="property-type"></a> `type` | [`ResourceType`](../type-aliases/ResourceType) \| `null` | The type of request that was made |
| <a id="property-url"></a> `url` | `string` \| `null` | The final URL of the request |

---

# ResponseFilterOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/ResponseFilterOptions

# ResponseFilterOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-bodyencoding"></a> `bodyEncoding?` | `boolean` | Request the `base64Encoded` body flag. Enable only when the target BQL schema supports it. |
| <a id="property-method"></a> `method?` | [`Method`](../type-aliases/Method)[] | The method of the response to return results for |
| <a id="property-operator"></a> `operator?` | [`OperatorType`](../type-aliases/OperatorType) | When applying arguments like URL or method, this operator will either "and" them together, or "or" them together. Default is "or" **Default** `or` |
| <a id="property-status"></a> `status?` | `number`[] | The status codes response to return results for |
| <a id="property-timeout"></a> `timeout?` | `number` | How long to wait for the response(s) to be made before timing out, overriding any defaults. Default timeout is 30 seconds, or 30000. "wait" parameter must also be "true". |
| <a id="property-type"></a> `type?` | [`ResourceType`](../type-aliases/ResourceType)[] | The type content-type of the response to match against |
| <a id="property-url"></a> `url?` | `string`[] | The pattern of the response URL to wait for, using glob-style pattern-matching |
| <a id="property-wait"></a> `wait?` | `boolean` | Whether or not to wait for the response to be received. When set to `true`, generally only one response is returned since this API will wait for the first response that matches any patterns to be returned **Default** `true` |

---

# ResponseInput

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/ResponseInput

# ResponseInput

The specific response to perform a conditional action on

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-codes"></a> ~~`codes?`~~ | `number`[] | The HTTP Response code(s) of the URL to wait for. Can be a single HTTP code or a list of desired codes **Deprecated** Use `statuses` field instead as it is more consistent in BrowserQL. |
| <a id="property-statuses"></a> `statuses?` | `number`[] | The HTTP Response code(s) of the URL to wait for. Can be a single HTTP code or a list of desired codes |
| <a id="property-url"></a> `url?` | `string` | The pattern of the response URL to wait for, using glob-style pattern-matching |

---

# ResponseResponse

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/ResponseResponse

# ResponseResponse

Response returned from the response API

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-base64encoded"></a> `base64Encoded` | `boolean` | Whether the response body is base64 encoded. |
| <a id="property-body"></a> `body` | `string` \| `null` | The body of the response, represented as a string when possible or base64 for binary-type requests like images |
| <a id="property-headers"></a> `headers` | [`RequestHeader`](RequestHeader)[] \| `null` | The response headers returned |
| <a id="property-method"></a> `method` | [`Method`](../type-aliases/Method) \| `null` | The HTTP method of the request that facilitated the response |
| <a id="property-status"></a> `status` | `number` \| `null` | The HTTP status code of the response |
| <a id="property-type"></a> `type` | [`ResourceType`](../type-aliases/ResourceType) \| `null` | The type of response that was received |
| <a id="property-url"></a> `url` | `string` \| `null` | The final URL of the response |

---

# ScreenshotClip

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/ScreenshotClip

# ScreenshotClip

The clipping to be applied to the screenshot

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-height"></a> `height?` | `number` | The height of the clip, in pixels |
| <a id="property-scale"></a> `scale?` | `number` | The scale factor of the clip |
| <a id="property-width"></a> `width?` | `number` | The width of the clip, in pixels |
| <a id="property-x"></a> `x?` | `number` | The x coordinate to start clipping, in pixels |
| <a id="property-y"></a> `y?` | `number` | The y coordinate to start clipping, in pixels |

---

# ScreenshotOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/ScreenshotOptions

# ScreenshotOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-capturebeyondviewport"></a> `captureBeyondViewport?` | `boolean` | Capture the screenshot beyond the viewport. Default: False if there is no clip. True otherwise |
| <a id="property-clip"></a> `clip?` | [`ScreenshotClip`](ScreenshotClip) | Specifies the region of the page/element to clip |
| <a id="property-fromsurface"></a> `fromSurface?` | `boolean` | Capture the screenshot from the surface, rather than the view. Default: True |
| <a id="property-fullpage"></a> `fullPage?` | `boolean` | When True, takes a screenshot of the full page. Default: False |
| <a id="property-omitbackground"></a> `omitBackground?` | `boolean` | Hides default white background and allows capturing screenshots with transparency. Default: False |
| <a id="property-optimizeforspeed"></a> `optimizeForSpeed?` | `boolean` | Optimize image encoding for speed, not for resulting size. Default: False |
| <a id="property-path"></a> `path?` | `string` | - |
| <a id="property-quality"></a> `quality?` | `number` | Quality of the image, between 0-100. Not applicable to png images. |
| <a id="property-selector"></a> `selector?` | `string` | The CSS selector of the element on the page you want to screenshot |
| <a id="property-timeout"></a> `timeout?` | `number` | The maximum amount of time, in milliseconds, to wait for the screenshot to be taken. Default timeout is 30 seconds, or 30000. |
| <a id="property-type"></a> `type?` | [`ScreenshotType`](../type-aliases/ScreenshotType) | The final format of the screenshot |
| <a id="property-waitforimages"></a> `waitForImages?` | `boolean` | When true, waits for all images on the page to load before taking the screenshot. Default: False |

---

# ScreenshotResponse

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/ScreenshotResponse

# ScreenshotResponse

The response returned after generating a Screenshot

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-base64"></a> `base64` | `string` \| `null` | The base64 encoded image of the screenshot |
| <a id="property-format"></a> `format` | `string` \| `null` | The format of the screenshot |
| <a id="property-time"></a> `time` | `number` \| `null` | The time it took to take the screenshot |

---

# ScrollOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/ScrollOptions

# ScrollOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-selector"></a> `selector?` | `string` | The DOM selector of the element on the page you want to scroll to |
| <a id="property-throughpage"></a> `throughPage?` | `boolean` | Scrolls through the entire document in viewport-sized steps to trigger lazy-loaded content, then returns to the top. Cannot be combined with selector, x, or y. **Default** `false` |
| <a id="property-timeout"></a> `timeout?` | `number` | How long to wait for the element to appear before timing out, overriding any defaults. Default timeout is 30 seconds, or 30000. |
| <a id="property-visible"></a> `visible?` | `boolean` | Whether or not to scroll to the element only if it's visible **Default** `false` |
| <a id="property-wait"></a> `wait?` | `boolean` | Whether or not to wait for the element, then scroll to it **Default** `true` |
| <a id="property-x"></a> `x?` | `number` | The X coordinate, in pixels, to scroll to |
| <a id="property-y"></a> `y?` | `number` | The Y coordinate, in pixels, to scroll to |

---

# ScrollResponse

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/ScrollResponse

# ScrollResponse

Response returned after having scrolling inside the page

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-selector"></a> `selector` | `string` \| `null` | The CSS selector of the element on the page you want to scroll to |
| <a id="property-time"></a> `time` | `number` \| `null` | The amount of time, in milliseconds, elapsed since the start of scrolling to completion |
| <a id="property-x"></a> `x` | `number` \| `null` | The X coordinate, in pixels, to scroll to |
| <a id="property-y"></a> `y` | `number` \| `null` | The Y coordinate, in pixels, to scroll to |

---

# SelectOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/SelectOptions

# SelectOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-scroll"></a> `scroll?` | `boolean` | Whether or not to scroll to the select element prior to selecting, defaults to true **Default** `true` |
| <a id="property-timeout"></a> `timeout?` | `number` | How long to wait for the element to appear before timing out on the handler, overriding any defaults. Default timeout is 30 seconds, or 30000. |
| <a id="property-visible"></a> `visible?` | `boolean` | Whether or not to select the element only if it's visible **Default** `false` |
| <a id="property-wait"></a> `wait?` | `boolean` | Whether or not to wait for the select to present in the DOM **Default** `true` |

---

# SelectResponse

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/SelectResponse

# SelectResponse

The response returned after selecting a value from a dropdown or multiple select element

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-selector"></a> `selector` | `string` \| `null` | The selector of the element you selected from |
| <a id="property-time"></a> `time` | `number` \| `null` | The amount of time, in milliseconds, elapsed since the start of selecting to completion |
| <a id="property-value"></a> `value` | `string` \| `string`[] \| `null` | The value or values you selected from the select element |

---

# SendOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/SendOptions

# SendOptions

## Properties

| Property | Type |
| ------ | ------ |
| <a id="property-timeout"></a> `timeout?` | `number` |
| <a id="property-variables"></a> `variables?` | `Record`\<`string`, `unknown`\> |

---

# SetContentOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/SetContentOptions

# SetContentOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-timeout"></a> `timeout?` | `number` | The maximum amount of time, in milliseconds, to wait for the content to load, overriding any defaults. Default timeout is 30 seconds, or 30000. |
| <a id="property-waituntil"></a> `waitUntil?` | [`WaitUntilHistory`](../type-aliases/WaitUntilHistory) | When to consider the page fully-loaded and proceed with further execution, used in conjunction with the value parameter |
| <a id="property-waituntilall"></a> `waitUntilAll?` | [`WaitUntilHistory`](../type-aliases/WaitUntilHistory)[] | Wait for every listed lifecycle event. Use this instead of waitUntil when preserving an ordered REST array contract. |

---

# SetCookieOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/SetCookieOptions

# SetCookieOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-cookies"></a> `cookies?` | [`CookieInput`](CookieInput)[] | The cookies to set on the page |

---

# SetExtraHTTPHeadersOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/SetExtraHTTPHeadersOptions

# SetExtraHTTPHeadersOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-headers"></a> `headers?` | [`HeaderInput`](HeaderInput)[] | The HTTP headers to set on the page |

---

# SolveImageCaptchaOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/SolveImageCaptchaOptions

# SolveImageCaptchaOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-captchaselector"></a> `captchaSelector` | `string` | The CSS selector for the captcha image element on the page |
| <a id="property-inputselector"></a> `inputSelector` | `string` | The CSS selector for the input field where the solved captcha value should be filled |
| <a id="property-timeout"></a> `timeout?` | `number` | A time, in milliseconds, to wait for the captcha to appear and be solved. Default timeout is 30 seconds, or 30000. |

---

# SolveOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/SolveOptions

# SolveOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-timeout"></a> `timeout?` | `number` | A time, in milliseconds, to wait for a captcha to appear. Only valid when wait = true. If a captcha is found then this timer doesn't have an effect on timing-out the solve Default timeout is 30 seconds, or 30000. |
| <a id="property-type"></a> `type?` | [`CaptchaType`](../type-aliases/CaptchaType) | An enum of the type of captcha to look for and solve. If not provided, auto-detection will be used. |
| <a id="property-wait"></a> `wait?` | `boolean` | Whether or not to wait for the Captcha to be present on the page **Default** `true` |

---

# StandardCookie

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/StandardCookie

# StandardCookie

A standard cookie

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-domain"></a> `domain` | `string` \| `null` | Cookie domain |
| <a id="property-expires"></a> `expires` | `number` \| `null` | Cookie expiration date, session cookie if not set |
| <a id="property-httponly"></a> `httpOnly` | `boolean` \| `null` | True if cookie is http-only |
| <a id="property-name"></a> `name` | `string` | Cookie name |
| <a id="property-path"></a> `path` | `string` \| `null` | Cookie path |
| <a id="property-samesite"></a> `sameSite` | [`CookieSameSite`](../type-aliases/CookieSameSite) \| `null` | Cookie SameSite type |
| <a id="property-secure"></a> `secure` | `boolean` \| `null` | True if cookie is secure |
| <a id="property-url"></a> `url` | `string` \| `null` | The request-URI to associate with the setting of the cookie. This value can affect the default domain, path, source port, and source scheme values of the created cookie |
| <a id="property-value"></a> `value` | `string` | Cookie value |

---

# StopSessionRecordingResponse

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/StopSessionRecordingResponse

# StopSessionRecordingResponse

The response received after stopping session recording

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-error"></a> `error` | `string` \| `null` | Error message if the operation failed |
| <a id="property-success"></a> `success` | `boolean` \| `null` | Whether the recording was successfully stopped and uploaded |

---

# SubscriptionSpec

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/SubscriptionSpec

# SubscriptionSpec

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-argtypes"></a> `argTypes` | `Record`\<`string`, `string`\> | Argument name → GraphQL type, for parameterized documents. |
| <a id="property-fields"></a> `fields` | `string` | GraphQL selection set requested for each streamed value. |
| <a id="property-responsetype"></a> `responseType` | `string` | TS name of the payload type emitted to listeners. |

---

# SwitchToWindowOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/SwitchToWindowOptions

# SwitchToWindowOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-newest"></a> `newest?` | `boolean` | Whether to target most recently opened window **Default** `false` |
| <a id="property-timeout"></a> `timeout?` | `number` | Maximum time to wait for matching window (in milliseconds) **Default** `5000` |
| <a id="property-title"></a> `title?` | `string` | Window title pattern to match |
| <a id="property-url"></a> `url?` | `string` | URL pattern to match against window URLs (supports wildcards) |
| <a id="property-waituntil"></a> `waitUntil?` | [`WaitUntilGoto`](../type-aliases/WaitUntilGoto) | When to consider the page fully-loaded and proceed with further execution |

---

# SwitchWindowResponse

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/SwitchWindowResponse

# SwitchWindowResponse

Response from window switching operation

## Properties

| Property | Type |
| ------ | ------ |
| <a id="property-error"></a> `error` | `string` \| `null` |
| <a id="property-navigation"></a> `navigation` | [`HTTPResponse`](HTTPResponse) \| `null` |
| <a id="property-success"></a> `success` | `boolean` |
| <a id="property-title"></a> `title` | `string` \| `null` |
| <a id="property-url"></a> `url` | `string` \| `null` |

---

# TextOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/TextOptions

# TextOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-clean"></a> `clean?` | [`CleanInput`](CleanInput) | Specifies conditions for "cleaning" the returned text, useful for minimizing the amount of markup returned for cases like LLMs and more. See nested options for parameters. |
| <a id="property-selector"></a> `selector?` | `string` | The DOM selector of the given element you want to return the text of **Default** `"html"` |
| <a id="property-timeout"></a> `timeout?` | `number` | The maximum amount of time, in milliseconds, to wait for the selector to appear, overriding any defaults. Default timeout is 30 seconds, or 30000. |
| <a id="property-visible"></a> `visible?` | `boolean` | Whether or not to return the text content of the element only if it's visible **Default** `false` |

---

# TextResponse

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/TextResponse

# TextResponse

Text content of a page

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-text"></a> `text` | `string` \| `null` | The textual content of the page |
| <a id="property-time"></a> `time` | `number` \| `null` | The amount of time, in milliseconds, elapsed since the start of text retrieval to completion |

---

# TitleResponse

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/TitleResponse

# TitleResponse

Response returned after the page's title has been set or get

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-title"></a> `title` | `string` \| `null` | The title of the current page |

---

# TypeOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/TypeOptions

# TypeOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-delay"></a> `delay?` | `number`[] | The amount of delay between keystrokes in milliseconds. Values are used as a range and chosen at random **Default** `[50, 200]` |
| <a id="property-interactable"></a> `interactable?` | `boolean` | Whether or not to check if element can be interacted with by hovering over it and seeing if the element is available at that x and y position **Default** `true` |
| <a id="property-scroll"></a> `scroll?` | `boolean` | Whether or not to scroll to the element prior to typing, defaults to true **Default** `true` |
| <a id="property-timeout"></a> `timeout?` | `number` | How long to wait for the element to appear before timing out, overriding any defaults. Default timeout is 30 seconds, or 30000. |
| <a id="property-visible"></a> `visible?` | `boolean` | Whether or not to type into the element only if it's visible **Default** `false` |
| <a id="property-wait"></a> `wait?` | `boolean` | Whether or not to wait for the element to present in the DOM **Default** `true` |

---

# TypeResponse

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/TypeResponse

# TypeResponse

Response returned after having typed into an element

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-selector"></a> `selector` | `string` \| `null` | The selector of the element you typed into |
| <a id="property-text"></a> `text` | `string` \| `null` | The textual content that was typed |
| <a id="property-time"></a> `time` | `number` \| `null` | The amount of time, in milliseconds, elapsed since the start of typing to completion |
| <a id="property-x"></a> `x` | `number` \| `null` | The X coordinate of the element, in pixels, on the page |
| <a id="property-y"></a> `y` | `number` \| `null` | The Y coordinate of the element, in pixels, on the page |

---

# URLResponse

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/URLResponse

# URLResponse

Response returned after the URL of the page has been set or get

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-url"></a> `url` | `string` \| `null` | The URL of the current page |

---

# UserAgentResponse

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/UserAgentResponse

# UserAgentResponse

The response returned after setting the User-Agent

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-time"></a> `time` | `number` \| `null` | The time it took to set the User-Agent |
| <a id="property-useragent"></a> `userAgent` | `string` \| `null` | The User-Agent string that was set |

---

# ViewportOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/ViewportOptions

# ViewportOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-devicescalefactor"></a> `deviceScaleFactor?` | `number` | The device scale factor, defaults to 1 **Default** `1` |
| <a id="property-hastouch"></a> `hasTouch?` | `boolean` | Whether the viewport supports touch events, defaults to false **Default** `false` |
| <a id="property-height"></a> `height` | `number` | The height of the viewport in pixels |
| <a id="property-landscape"></a> `landscape?` | `boolean` | Whether the viewport is in landscape orientation, defaults to false **Default** `false` |
| <a id="property-mobile"></a> `mobile?` | `boolean` | Whether to simulate a mobile device, defaults to false **Default** `false` |
| <a id="property-width"></a> `width` | `number` | The width of the viewport in pixels |

---

# ViewportResponse

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/ViewportResponse

# ViewportResponse

Response returned after setting the viewport

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-devicescalefactor"></a> `deviceScaleFactor` | `number` \| `null` | The device scale factor |
| <a id="property-hastouch"></a> `hasTouch` | `boolean` \| `null` | Whether the viewport supports touch events |
| <a id="property-height"></a> `height` | `number` \| `null` | The height of the viewport in pixels |
| <a id="property-landscape"></a> `landscape` | `boolean` \| `null` | Whether the viewport is in landscape orientation |
| <a id="property-mobile"></a> `mobile` | `boolean` \| `null` | Whether the device is in mobile mode |
| <a id="property-time"></a> `time` | `number` \| `null` | The time it took to set the viewport |
| <a id="property-width"></a> `width` | `number` \| `null` | The width of the viewport in pixels |

---

# WaitForEvent

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/WaitForEvent

# WaitForEvent

Response returned after having waited for an event to fire on the page

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-time"></a> `time` | `number` \| `null` | The period of time elapsed, in milliseconds, waited for |

---

# WaitForEventOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/WaitForEventOptions

# WaitForEventOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-timeout"></a> `timeout?` | `number` | The maximum amount of time, in milliseconds, to wait for the event to fire, overriding any defaults. Default timeout is 30 seconds, or 30000. |

---

# WaitForFunction

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/WaitForFunction

# WaitForFunction

Response returned after having waited for a JavaScript predicate to become truthy

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-time"></a> `time` | `number` \| `null` | The period of time elapsed, in milliseconds, waited for |

---

# WaitForFunctionOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/WaitForFunctionOptions

# WaitForFunctionOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-completeonpromiseresolution"></a> `completeOnPromiseResolution?` | `boolean` | Resolve after an async predicate promise settles, regardless of its return value. |
| <a id="property-polling"></a> `polling?` | `number` | The interval, in milliseconds, at which the predicate is re-evaluated. Defaults to 100ms |
| <a id="property-pollingtype"></a> `pollingType?` | [`WaitForFunctionPolling`](../type-aliases/WaitForFunctionPolling) | A browser-driven polling mode. Use this instead of polling for raf or mutation behavior. |
| <a id="property-timeout"></a> `timeout?` | `number` | The maximum amount of time, in milliseconds, to wait for the predicate to become truthy, overriding any defaults. Default timeout is 30 seconds, or 30000. |

---

# WaitForNavigationOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/WaitForNavigationOptions

# WaitForNavigationOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-timeout"></a> `timeout?` | `number` | The maximum amount of time, in milliseconds, to wait for the page to load, overriding any defaults. Default timeout is 30 seconds, or 30000. |
| <a id="property-waituntil"></a> `waitUntil?` | [`WaitUntilGoto`](../type-aliases/WaitUntilGoto) | When to consider the page fully-loaded and proceed with further execution **Default** `load` |

---

# WaitForNetworkIdleOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/WaitForNetworkIdleOptions

# WaitForNetworkIdleOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-concurrency"></a> `concurrency?` | `number` | The maximum number of in-flight requests allowed while considering the network idle. **Default** `0` |
| <a id="property-idletime"></a> `idleTime?` | `number` | The number of milliseconds the network must remain idle. **Default** `500` |
| <a id="property-timeout"></a> `timeout?` | `number` | The maximum amount of time, in milliseconds, to wait for network idle. |

---

# WaitForRequestOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/WaitForRequestOptions

# WaitForRequestOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-method"></a> `method?` | [`Method`](../type-aliases/Method) | The method of the request to wait for |
| <a id="property-operator"></a> `operator?` | [`OperatorType`](../type-aliases/OperatorType) | When applying arguments like URL or method, this operator will either "and" them together, or "or" them together. Default is "or" **Default** `or` |
| <a id="property-timeout"></a> `timeout?` | `number` | How long to wait for the request to be made before timing out, overriding any defaults. Default timeout is 30 seconds, or 30000. |
| <a id="property-url"></a> `url?` | `string` | The pattern of the request URL to wait for, using glob-style pattern-matching |

---

# WaitForRequestResponse

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/WaitForRequestResponse

# WaitForRequestResponse

Response returned after a particular network request has been sent

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-time"></a> `time` | `number` \| `null` | The period of time elapsed, in milliseconds, waited for |
| <a id="property-url"></a> `url` | `string` \| `null` | The URL parameter used to match the response with |

---

# WaitForResponseOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/WaitForResponseOptions

# WaitForResponseOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-codes"></a> ~~`codes?`~~ | `number`[] | The HTTP Response code(s) of the URL to wait for. Can be a single HTTP code or a list of desired codes **Deprecated** Use `statuses` field instead as it is more consistent in BrowserQL. |
| <a id="property-operator"></a> `operator?` | [`OperatorType`](../type-aliases/OperatorType) | When applying arguments like URL or statuses, this operator will either "and" them together, or "or" them together. Default is "or" **Default** `or` |
| <a id="property-statuses"></a> `statuses?` | `number`[] | The HTTP Response code(s) of the URL to wait for. Can be a single HTTP code or a list of desired codes |
| <a id="property-timeout"></a> `timeout?` | `number` | - |
| <a id="property-url"></a> `url?` | `string` | The pattern of the response URL to wait for, using glob-style pattern-matching |

---

# WaitForResponseResponse

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/WaitForResponseResponse

# WaitForResponseResponse

Response returned after a particular network response has been received

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-status"></a> `status` | `number` \| `null` | The status code response of the response |
| <a id="property-time"></a> `time` | `number` \| `null` | The period of time elapsed, in milliseconds, waited for |
| <a id="property-url"></a> `url` | `string` \| `null` | The URL parameter used to match the response with |

---

# WaitForSelectorOptions

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/WaitForSelectorOptions

# WaitForSelectorOptions

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-hidden"></a> `hidden?` | `boolean` | Wait for the selector to become hidden, i.e. removed from the DOM or with no visible bounding box. Mutually exclusive with "visible". **Default** `false` |
| <a id="property-timeout"></a> `timeout?` | `number` | When waiting for a selector applies a timeout to wait for in milliseconds, overriding any defaults. Default timeout is 30 seconds, or 30000. |
| <a id="property-visible"></a> `visible?` | `boolean` | Whether or not to consider the element as present only if it's visible **Default** `false` |

---

# WaitForSelectorResponse

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/WaitForSelectorResponse

# WaitForSelectorResponse

Response returned after a particular selector has been found in the DOM

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-height"></a> `height` | `number` \| `null` | The height, in pixels, of the element |
| <a id="property-selector"></a> `selector` | `string` \| `null` | The selector waited for |
| <a id="property-time"></a> `time` | `number` \| `null` | The period of time elapsed, in milliseconds, waited for |
| <a id="property-width"></a> `width` | `number` \| `null` | The width, in pixels, of the element |
| <a id="property-x"></a> `x` | `number` \| `null` | The position, in pixels, left of the viewport |
| <a id="property-y"></a> `y` | `number` \| `null` | The position, in pixels, top of the viewport |

---

# WaitForTimeoutResponse

URL: https://docs.browserless.io/bap/typescript/reference/interfaces/WaitForTimeoutResponse

# WaitForTimeoutResponse

Response returned after having waited for a selector to appear in the DOM

## Properties

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="property-time"></a> `time` | `number` \| `null` | The period of time elapsed, in milliseconds, waited for |

---

# AttributeMode

URL: https://docs.browserless.io/bap/typescript/reference/type-aliases/AttributeMode

# AttributeMode

```ts
type AttributeMode = "ALLOW" | "DENY";
```

Controls how the attributes field is interpreted in cleaning operations

---

# CaptchaType

URL: https://docs.browserless.io/bap/typescript/reference/type-aliases/CaptchaType

# CaptchaType

```ts
type CaptchaType =
  | "cloudflare"
  | "recaptcha"
  | "recaptchaV3"
  | "geetest"
  | "normal"
  | "numberCaptcha"
  | "friendlyCaptcha"
  | "capy"
  | "textCaptcha"
  | "amazonWaf"
  | "datadome"
  | "akamai"
  | "lemin"
  | "mtcaptcha"
  | "slider"
  | "audio"
  | "funcaptcha"
  | "click"
  | "yandex"
  | "math"
  | "perimeterx";
```

The different types of captchas that can be solved

---

# ConsoleMessageType

URL: https://docs.browserless.io/bap/typescript/reference/type-aliases/ConsoleMessageType

# ConsoleMessageType

```ts
type ConsoleMessageType =
  | "log"
  | "debug"
  | "info"
  | "error"
  | "warning"
  | "dir"
  | "dirxml"
  | "table"
  | "trace"
  | "clear"
  | "startGroup"
  | "startGroupCollapsed"
  | "endGroup"
  | "assert"
  | "profile"
  | "profileEnd"
  | "count"
  | "timeEnd"
  | "verbose";
```

The kind of console message, mirroring the CDP Runtime.consoleAPICalled type
(and puppeteer's ConsoleMessageType). Uncaught exceptions are reported as `error`.

---

# CookiePriority

URL: https://docs.browserless.io/bap/typescript/reference/type-aliases/CookiePriority

# CookiePriority

```ts
type CookiePriority = "Low" | "Medium" | "High";
```

---

# CookieSameSite

URL: https://docs.browserless.io/bap/typescript/reference/type-aliases/CookieSameSite

# CookieSameSite

```ts
type CookieSameSite = "Strict" | "Lax" | "None";
```

The values a cookie's SameSite attribute can hold

---

# CookieSourceScheme

URL: https://docs.browserless.io/bap/typescript/reference/type-aliases/CookieSourceScheme

# CookieSourceScheme

```ts
type CookieSourceScheme = "Unset" | "NonSecure" | "Secure";
```

---

# CountryCode

URL: https://docs.browserless.io/bap/typescript/reference/type-aliases/CountryCode

# CountryCode

```ts
type CountryCode =
  | "AD"
  | "AE"
  | "AF"
  | "AG"
  | "AL"
  | "AM"
  | "AO"
  | "AR"
  | "AT"
  | "AU"
  | "AW"
  | "AX"
  | "AZ"
  | "BA"
  | "BB"
  | "BD"
  | "BE"
  | "BF"
  | "BG"
  | "BH"
  | "BI"
  | "BJ"
  | "BM"
  | "BN"
  | "BO"
  | "BQ"
  | "BR"
  | "BS"
  | "BT"
  | "BW"
  | "BY"
  | "BZ"
  | "CA"
  | "CD"
  | "CG"
  | "CH"
  | "CI"
  | "CL"
  | "CM"
  | "CN"
  | "CO"
  | "CR"
  | "CU"
  | "CV"
  | "CW"
  | "CY"
  | "CZ"
  | "DE"
  | "DJ"
  | "DK"
  | "DM"
  | "DO"
  | "DZ"
  | "EC"
  | "EE"
  | "EG"
  | "ES"
  | "ET"
  | "FI"
  | "FJ"
  | "FO"
  | "FR"
  | "GA"
  | "GB"
  | "GD"
  | "GE"
  | "GF"
  | "GG"
  | "GH"
  | "GI"
  | "GM"
  | "GN"
  | "GP"
  | "GR"
  | "GT"
  | "GU"
  | "GY"
  | "HK"
  | "HN"
  | "HR"
  | "HT"
  | "HU"
  | "ID"
  | "IE"
  | "IL"
  | "IM"
  | "IN"
  | "IQ"
  | "IR"
  | "IS"
  | "IT"
  | "JE"
  | "JM"
  | "JO"
  | "JP"
  | "KE"
  | "KG"
  | "KH"
  | "KN"
  | "KR"
  | "KW"
  | "KY"
  | "KZ"
  | "LA"
  | "LB"
  | "LC"
  | "LI"
  | "LK"
  | "LR"
  | "LS"
  | "LT"
  | "LU"
  | "LV"
  | "LY"
  | "MA"
  | "MC"
  | "MD"
  | "ME"
  | "MG"
  | "MK"
  | "ML"
  | "MM"
  | "MN"
  | "MO"
  | "MQ"
  | "MR"
  | "MS"
  | "MT"
  | "MU"
  | "MV"
  | "MW"
  | "MX"
  | "MY"
  | "MZ"
  | "NA"
  | "NC"
  | "NG"
  | "NI"
  | "NL"
  | "NO"
  | "NP"
  | "NZ"
  | "OM"
  | "PA"
  | "PE"
  | "PF"
  | "PG"
  | "PH"
  | "PK"
  | "PL"
  | "PR"
  | "PS"
  | "PT"
  | "PY"
  | "QA"
  | "RE"
  | "RO"
  | "RS"
  | "RU"
  | "RW"
  | "SC"
  | "SE"
  | "SG"
  | "SI"
  | "SK"
  | "SL"
  | "SM"
  | "SN"
  | "SO"
  | "SR"
  | "SS"
  | "ST"
  | "SV"
  | "SX"
  | "SY"
  | "SZ"
  | "TC"
  | "TG"
  | "TH"
  | "TJ"
  | "TN"
  | "TR"
  | "TT"
  | "TW"
  | "TZ"
  | "UA"
  | "UG"
  | "US"
  | "UY"
  | "UZ"
  | "VC"
  | "VE"
  | "VG"
  | "VI"
  | "VN"
  | "WS"
  | "YT"
  | "ZA"
  | "ZM"
  | "ZW";
```

The two-letter ISO code for the specified country

---

# Listener\<T\>

URL: https://docs.browserless.io/bap/typescript/reference/type-aliases/Listener

# Listener\

```ts
type Listener = (payload: T) => void;
```

## Type Parameters

| Type Parameter |
| ------ |
| `T` |

## Parameters

| Parameter | Type |
| ------ | ------ |
| `payload` | `T` |

## Returns

`void`

---

# LiveURLStreamType

URL: https://docs.browserless.io/bap/typescript/reference/type-aliases/LiveURLStreamType

# LiveURLStreamType

```ts
type LiveURLStreamType = "jpeg" | "png";
```

The binary-type of the stream

---

# MediaType

URL: https://docs.browserless.io/bap/typescript/reference/type-aliases/MediaType

# MediaType

```ts
type MediaType = "screen" | "print";
```

The CSS media type to emulate for the page

---

# MessageListener

URL: https://docs.browserless.io/bap/typescript/reference/type-aliases/MessageListener

# MessageListener

```ts
type MessageListener = (message: string) => void;
```

A raw-frame listener registered via [Page.on](../classes/Page.mdx#on)/[Page.subscribe](../classes/Page.mdx#subscribe).

## Parameters

| Parameter | Type |
| ------ | ------ |
| `message` | `string` |

## Returns

`void`

---

# Method

URL: https://docs.browserless.io/bap/typescript/reference/type-aliases/Method

# Method

```ts
type Method =
  | "CONNECT"
  | "DELETE"
  | "GET"
  | "HEAD"
  | "OPTIONS"
  | "PATCH"
  | "POST"
  | "PUT"
  | "TRACE";
```

The various HTTP-based methods to wait for

---

# OperatorType

URL: https://docs.browserless.io/bap/typescript/reference/type-aliases/OperatorType

# OperatorType

```ts
type OperatorType = "and" | "or";
```

Logical operators for APIs that have conditions

---

# PageEvents

URL: https://docs.browserless.io/bap/typescript/reference/type-aliases/PageEvents

# PageEvents

```ts
type PageEvents = PageEventMap & object;
```

Page events surfaced through the puppeteer-style emitter. The schema-derived
[PageEventMap](../interfaces/PageEventMap) entries (`console`/`request`/`response`) each open a
GraphQL subscription lazily; `error` is emitted if opening or streaming one
fails (it never throws, matching puppeteer's non-fatal emitter errors).
`message` carries every raw frame the transport receives — including
server-pushed frames with no matching operation (e.g. data over a custom
[ConnectionTransport](../interfaces/ConnectionTransport)); it is not subscription-backed.

## Type Declaration

| Name | Type |
| ------ | ------ |
| `error` | `Error` |
| `message` | `string` |

---

# PDFPageFormat

URL: https://docs.browserless.io/bap/typescript/reference/type-aliases/PDFPageFormat

# PDFPageFormat

```ts
type PDFPageFormat =
  | "a0"
  | "a1"
  | "a2"
  | "a3"
  | "a4"
  | "a5"
  | "a6"
  | "legal"
  | "letter"
  | "ledger"
  | "tabloid";
```

The values a pdf's Size attribute can hold

---

# ProxyNetwork

URL: https://docs.browserless.io/bap/typescript/reference/type-aliases/ProxyNetwork

# ProxyNetwork

```ts
type ProxyNetwork = "residential" | "datacenter";
```

The Browserless proxy network to route matching requests through.

---

# ResourceType

URL: https://docs.browserless.io/bap/typescript/reference/type-aliases/ResourceType

# ResourceType

```ts
type ResourceType =
  | "cspviolationreport"
  | "document"
  | "eventsource"
  | "fetch"
  | "font"
  | "image"
  | "manifest"
  | "media"
  | "other"
  | "ping"
  | "prefetch"
  | "preflight"
  | "script"
  | "signedexchange"
  | "stylesheet"
  | "texttrack"
  | "websocket"
  | "xhr";
```

The type of resource for a given network or HTTP request

---

# ScreenshotType

URL: https://docs.browserless.io/bap/typescript/reference/type-aliases/ScreenshotType

# ScreenshotType

```ts
type ScreenshotType = "jpeg" | "png" | "webp";
```

The different types of screenshot formats

---

# TransportFactory

URL: https://docs.browserless.io/bap/typescript/reference/type-aliases/TransportFactory

# TransportFactory

```ts
type TransportFactory = (url?: string) =>
  | ConnectionTransport
  | Promise;
```

Builds a [ConnectionTransport](../interfaces/ConnectionTransport) for a given endpoint URL. Supply one via
[ConnectOptions.transport](../interfaces/ConnectOptions) to override the default WebSocket transport.
The returned promise must resolve only once the connection is open — the
library begins sending frames as soon as it resolves. This is a factory
rather than a single instance because [Browser.newPage](../classes/Browser.mdx#newpage) invokes it per
page and [Transport](../classes/Transport) assigns `onmessage`/`onclose`/`onerror` on the
returned transport each time — so every page must get a fresh transport
instance. Reusing one instance across pages would overwrite the earlier
pages' handlers and cross-wire their frames, unless the transport itself
multiplexes.

## Parameters

| Parameter | Type |
| ------ | ------ |
| `url?` | `string` |

## Returns

  \| [`ConnectionTransport`](../interfaces/ConnectionTransport)
  \| `Promise`\<[`ConnectionTransport`](../interfaces/ConnectionTransport)\>

---

# WaitForFunctionPolling

URL: https://docs.browserless.io/bap/typescript/reference/type-aliases/WaitForFunctionPolling

# WaitForFunctionPolling

```ts
type WaitForFunctionPolling = "raf" | "mutation";
```

Browser-driven polling modes for waitForFunction

---

# WaitUntilGoto

URL: https://docs.browserless.io/bap/typescript/reference/type-aliases/WaitUntilGoto

# WaitUntilGoto

```ts
type WaitUntilGoto =
  | "commit"
  | "load"
  | "domContentLoaded"
  | "firstMeaningfulPaint"
  | "firstContentfulPaint"
  | "interactiveTime"
  | "networkIdle";
```

Options for when to consider the page has loaded and proceed with further execution

---

# WaitUntilHistory

URL: https://docs.browserless.io/bap/typescript/reference/type-aliases/WaitUntilHistory

# WaitUntilHistory

```ts
type WaitUntilHistory = "commit" | "domContentLoaded" | "load" | "networkIdle";
```

The different stages of the browser's loading process

---

# ARG\_TYPES

URL: https://docs.browserless.io/bap/typescript/reference/variables/ARG_TYPES

# ARG\_TYPES

```ts
const ARG_TYPES: Record<string, Record<string, string>>;
```

---

# RESPONSE\_FIELDS

URL: https://docs.browserless.io/bap/typescript/reference/variables/RESPONSE_FIELDS

# RESPONSE\_FIELDS

```ts
const RESPONSE_FIELDS: Record<string, string>;
```

---

# SUBSCRIPTIONS

URL: https://docs.browserless.io/bap/typescript/reference/variables/SUBSCRIPTIONS

# SUBSCRIPTIONS

```ts
const SUBSCRIPTIONS: Record<string, SubscriptionSpec>;
```

---

# Waiting for things in BAP

URL: https://docs.browserless.io/bap/waiting

# Waiting for things in BAP

Most timeouts in automation come from waiting for the wrong thing. BAP has five explicit waits plus several implicit ones, and picking correctly is usually the difference between a flaky script and a reliable one. This guide covers each wait, what it actually observes, and which waits you don't need to write.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- The BAP SDK installed and connecting, from the [BAP Quickstart](/bap/getting-started)

## Pick a wait

| You're waiting for | Use |
| ------------------ | --- |
| An element to appear, or disappear | `waitForSelector()` |
| A navigation you just triggered to finish | `waitForNavigation()` |
| The page to stop making requests | `waitForNetworkIdle()` |
| Some JavaScript condition to become true | `waitForFunction()` |
| A specific request or response | [`waitForRequest()` / `waitForResponse()`](/bap/network-control) |
| A page event to fire once | `waitForEvent()` |
| A fixed amount of time | `waitForTimeout()`, and almost never |

Every wait accepts a `timeout` that overrides the 30-second default for that call. When one fires, you get a `TimeoutError`, covered in [error handling](/bap/error-handling).

## Waits you don't need to write

Interaction and extraction methods already wait for their selector. `click()`, `type()`, `text()`, `html()`, and `mapSelector()` all default to `wait: true`, which means an explicit `waitForSelector()` before them is redundant:

**TypeScript:**

```typescript
// Redundant: click() already waits for the selector.
await page.waitForSelector("#submit");
await page.click("#submit");

// Enough on its own.
await page.click("#submit");
```

**Python (Coming soon):**

```python
# Redundant: click() already waits for the selector.
page.wait_for_selector("#submit")
page.click("#submit")

# Enough on its own.
page.click("#submit")
```

Reach for an explicit `waitForSelector()` when you need to wait for something you aren't about to act on, such as a loading spinner disappearing or a result count rendering before you extract.

## Wait for an element

`waitForSelector()` takes `visible` and `hidden` booleans rather than Playwright's `state` enum. `hidden` is the one worth knowing: it's how you wait for a spinner or overlay to go away.

**TypeScript:**

```typescript
// Present in the DOM.
await page.waitForSelector(".results");

// Present and actually rendered.
await page.waitForSelector(".results", { visible: true });

// Gone, which is how you wait out a loading overlay.
await page.waitForSelector(".spinner", { hidden: true, timeout: 15_000 });
```

**Python (Coming soon):**

```python
# Present in the DOM.
page.wait_for_selector(".results")

# Present and actually rendered.
page.wait_for_selector(".results", visible=True)

# Gone, which is how you wait out a loading overlay.
page.wait_for_selector(".spinner", hidden=True, timeout=15000)
```

## Wait for navigation

`goto()` waits for the page to load on its own. `waitForNavigation()` is for navigations *you* trigger some other way, usually a click, and it must be in flight before the navigation completes or you'll wait for one that already happened. In TypeScript that means starting the wait without awaiting it, then awaiting after the click. The Python sync API can't express that ordering, so wait for an element unique to the destination instead, which sidesteps the race in both languages.

Both take `waitUntil`, which decides what "loaded" means: `commit`, `load`, `domContentLoaded`, `firstContentfulPaint`, `firstMeaningfulPaint`, `interactiveTime`, or `networkIdle`. `domContentLoaded` returns soonest and is enough when your target is in the initial HTML; `networkIdle` waits longest and suits pages that assemble themselves with XHR.

**TypeScript:**

```typescript
// Start the wait first, so a fast navigation can't finish before it begins.
const navigated = page.waitForNavigation({ waitUntil: "networkIdle" });
await page.click("#login");
await navigated;

console.log(await page.url());

// Cheaper when the content you want is in the first response.
await page.goto("https://example.com", { waitUntil: "domContentLoaded" });
```

**Python (Coming soon):**

```python
page.click("#login")

# The sync API blocks, so the wait can't be started before the click.
# Waiting for an element unique to the destination avoids the race entirely.
page.wait_for_selector("#dashboard")
print(page.url())

# Cheaper when the content you want is in the first response.
page.goto("https://example.com", wait_until="domContentLoaded")
```

## Wait for the network to settle

`waitForNetworkIdle()` returns once the page stops making requests. Two options shape it: `idleTime` (default `500` milliseconds) is how long quiet counts as idle, and `concurrency` (default `0`) is how many in-flight requests you'll tolerate while still calling it idle.

Raise `concurrency` on pages that keep a long-poll or analytics beacon open forever, since with the default those pages never reach idle and you wait out the full timeout instead.

**TypeScript:**

```typescript
// Strict: no requests in flight for half a second.
await page.waitForNetworkIdle();

// Tolerant: ignore up to two long-lived connections.
await page.waitForNetworkIdle({ concurrency: 2, idleTime: 1000 });
```

**Python (Coming soon):**

```python
# Strict: no requests in flight for half a second.
page.wait_for_network_idle()

# Tolerant: ignore up to two long-lived connections.
page.wait_for_network_idle(concurrency=2, idle_time=1000)
```

## Wait for a JavaScript condition

`waitForFunction()` evaluates an expression in the page until it returns something truthy. It's the escape hatch for conditions no selector expresses, such as a global that a third-party script sets, or a computed count.

Polling defaults to every `100` milliseconds. `pollingType` swaps that for browser-driven modes: `raf` re-evaluates each animation frame, and `mutation` re-evaluates on DOM changes, which is cheaper than fast interval polling on a busy page.

**TypeScript:**

```typescript
// Wait for a third-party script to signal readiness.
await page.waitForFunction("window.appReady === true");

// Re-evaluate on DOM changes instead of on a timer.
await page.waitForFunction(
  "document.querySelectorAll('.row').length > 20",
  { pollingType: "mutation", timeout: 20_000 },
);
```

**Python (Coming soon):**

```python
# Wait for a third-party script to signal readiness.
page.wait_for_function("window.appReady === true")

# Re-evaluate on DOM changes instead of on a timer.
page.wait_for_function(
    "document.querySelectorAll('.row').length > 20",
    polling_type="mutation",
    timeout=20000,
)
```

Pass `completeOnPromiseResolution: true` when your expression returns a promise and you want the wait to end once it settles, whatever it resolves to.

## Wait for an event

`waitForEvent()` resolves the next time a page event fires, which is the one-shot counterpart to the listeners in [page events](/bap/page-events). `watchEvent()` starts watching without blocking, so you can trigger the action first and collect the event afterward.

**TypeScript:**

```typescript
// Block until the next console message.
const event = await page.waitForEvent("console", { timeout: 10_000 });
```

**Python (Coming soon):**

```python
# Block until the next console message.
event = page.wait_for_event("console", timeout=10000)
```

## Why not a fixed sleep

`waitForTimeout()` exists and takes a duration in milliseconds, but it's the wrong tool nearly every time: too short and the script is flaky, too long and every run pays for the worst case. Worse, it holds a billed session open doing nothing. Wait on the condition you actually care about instead. The honest use for a fixed sleep is pacing something deliberately, such as spacing interactions on a site that rate-limits by timing.

## FAQ & Troubleshooting

<details>
<summary>My <code>waitForNetworkIdle()</code> always times out</summary>

The page keeps a connection open, usually analytics, a long-poll, or a websocket, so in-flight requests never reach zero. Raise `concurrency` to tolerate those, or wait for a selector that proves your content arrived instead.

</details>

<details>
<summary><code>waitForNavigation()</code> resolved immediately, or timed out for no reason</summary>

It has to be waiting before the navigation completes. If you `await` the click first and then call it, a fast navigation is already done and you're waiting for the next one. In TypeScript, start the wait without awaiting, click, then await it. In Python's sync API, where that ordering isn't expressible, wait for an element unique to the destination instead.

</details>

<details>
<summary>Should I use <code>waitForSelector()</code> before every click?</summary>

No. `click()`, `type()`, and the extraction methods default to `wait: true` and already wait for their selector. Add an explicit wait only for things you aren't about to act on.

</details>

<details>
<summary><code>waitForFunction()</code> never resolves even though the condition is true in DevTools</summary>

The expression runs in the page, so it can't see variables from your script's scope, and it must return a value rather than only having a side effect. Check that it's an expression, not a statement, and that the global you're testing exists on `window` at that point.

</details>

## Next steps

---

# export

URL: https://docs.browserless.io/bql-schema/operations/directives/export

The @export directive allows you to export a field value as a variable
that can be used in subsequent mutations within the same request.

Example:
```
mutation {
  page: goto(url: "https://example.com") {
    url @export(as: "currentUrl")
  }
  text: type(selector: "input", text: "${currentUrl}") {
    selector
  }
}
```

```graphql
directive @export(
  as: String!
) on FIELD
```

### Arguments

#### [export.as](#as)[String!](/bql-schema/types/scalars/string) 
The variable name to export this field value as. Can be referenced
in subsequent mutations using $&#x007B;variableName&#x007D; template syntax.

---

# include

URL: https://docs.browserless.io/bql-schema/operations/directives/include

Directs the executor to include this field or fragment only when the `if` argument is true.

```graphql
directive @include(
  if: Boolean!
) on 
  | FIELD
  | FRAGMENT_SPREAD
  | INLINE_FRAGMENT
```

### Arguments

#### [include.if](#if)[Boolean!](/bql-schema/types/scalars/boolean) 
Included when true.

---

# skip

URL: https://docs.browserless.io/bql-schema/operations/directives/skip

Directs the executor to skip this field or fragment when the `if` argument is true.

```graphql
directive @skip(
  if: Boolean!
) on 
  | FIELD
  | FRAGMENT_SPREAD
  | INLINE_FRAGMENT
```

### Arguments

#### [skip.if](#if)[Boolean!](/bql-schema/types/scalars/boolean) 
Skipped when true.

---

# addScriptTag

URL: https://docs.browserless.io/bql-schema/operations/mutations/add-script-tag

Injects a `<script>` tag into the page, either from a URL or from raw
content. When a URL is given the mutation resolves once the script has
loaded.

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation InjectScript {
  goto(url: "https://example.com") {
    status
  }
  addScriptTag(url: "https://code.jquery.com/jquery-3.7.1.min.js") {
    time
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
addScriptTag(
  url: String
  content: String
  id: String
  type: String
): AddScriptTagResponse
```

### Arguments

#### [addScriptTag.url](#url)[String](/bql-schema/types/scalars/string) 
The URL of the script to add. One of "url" or "content" is required.

#### [addScriptTag.content](#content)[String](/bql-schema/types/scalars/string) 
The raw JavaScript content of the script to add. One of "url" or "content" is required.

#### [addScriptTag.id](#id)[String](/bql-schema/types/scalars/string) 
The id attribute to set on the injected script element

#### [addScriptTag.type](#type)[String](/bql-schema/types/scalars/string) 
The type attribute to set on the injected script element, eg: "module"

### Type

#### [AddScriptTagResponse](/bql-schema/types/objects/add-script-tag-response)  
Response returned after injecting a script tag

---

# addStyleTag

URL: https://docs.browserless.io/bql-schema/operations/mutations/add-style-tag

Injects a stylesheet into the page, either as a `<link>` from a URL or as a
`<style>` element from raw content. When a URL is given the mutation resolves
once the stylesheet has loaded.

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation InjectStyle {
  goto(url: "https://example.com") {
    status
  }
  addStyleTag(content: "body { background: #000; }") {
    time
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
addStyleTag(
  url: String
  content: String
): AddStyleTagResponse
```

### Arguments

#### [addStyleTag.url](#url)[String](/bql-schema/types/scalars/string) 
The URL of the stylesheet to add. One of "url" or "content" is required.

#### [addStyleTag.content](#content)[String](/bql-schema/types/scalars/string) 
The raw CSS content of the stylesheet to add. One of "url" or "content" is required.

### Type

#### [AddStyleTagResponse](/bql-schema/types/objects/add-style-tag-response)  
Response returned after injecting a style tag

---

# authenticate

URL: https://docs.browserless.io/bql-schema/operations/mutations/authenticate

Provides credentials for HTTP authentication (RFC 7235). When set, any HTTP
authentication challenge (401/407) will be answered with the supplied
credentials automatically. If an `origin` is provided, only challenges whose
origin matches will use these credentials; otherwise every challenge is
answered (Puppeteer-style).

&#x002A;&#x002A;Usage Example — authenticate all requests:&#x002A;&#x002A;
```graphql
mutation AuthenticateAndNavigate {
  authenticate(username: "user", password: "pass") {
    time
  }

  goto(url: "https://httpbin.org/basic-auth/user/pass") {
    status
    url
  }
}
```

&#x002A;&#x002A;Usage Example — scope credentials to a specific origin:&#x002A;&#x002A;
```graphql
mutation AuthenticateProxy {
  authenticate(
    username: "proxyUser"
    password: "proxyPass"
    origin: "http://my-proxy.example.com:8080"
  ) {
    time
  }

  goto(url: "https://example.com") {
    status
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
authenticate(
  username: String!
  password: String!
  origin: String
): HTTPResponse
```

### Arguments

#### [authenticate.username](#username)[String!](/bql-schema/types/scalars/string) 
The username to authenticate with

#### [authenticate.password](#password)[String!](/bql-schema/types/scalars/string) 
The password to authenticate with

#### [authenticate.origin](#origin)[String](/bql-schema/types/scalars/string) 
The origin of the requests needing authentication, specified as a full
HTTP origin in `protocol://host(:port)` format (e.g.
`https://example.com`). If not provided, every request needing
authentication will use the provided credentials.

### Type

#### [HTTPResponse](/bql-schema/types/objects/httpresponse)  
Response returned after a navigation event

---

# back

URL: https://docs.browserless.io/bql-schema/operations/mutations/back

Goes back in browser history, optionally accepting waitUntil and timeout arguments. Returns null if no back is possible

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation GoBack {
  firstNav: goto(url: "https://example.com") {
    time
  }

  secondNav: goto(url: "https://browserless.com") {
    time
  }

  back(waitUntil: domContentLoaded) {
    status
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
back(
  timeout: Float
  waitUntil: WaitUntilHistory = load
): HTTPResponse
```

### Arguments

#### [back.timeout](#timeout)[Float](/bql-schema/types/scalars/float) 
The maximum amount of time, in milliseconds, to wait for the page to load, overriding any defaults. Default timeout is 30 seconds, or 30000.

#### [back.waitUntil](#wait-until)[WaitUntilHistory](/bql-schema/types/enums/wait-until-history) 
When to consider the page fully-loaded and proceed with further execution

### Type

#### [HTTPResponse](/bql-schema/types/objects/httpresponse)  
Response returned after a navigation event

---

# checkbox

URL: https://docs.browserless.io/bql-schema/operations/mutations/checkbox

Sets or un-sets the value of a checkbox on the page

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation ClickCheckbox {
  goto(url: "https://example.com") {
    status
  }

  checkbox(
    checked: true
    selector: "input[type='checkbox']"
  ) {
    time
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
checkbox(
  checked: Boolean!
  selector: String!
  scroll: Boolean = true
  timeout: Float
  visible: Boolean = false
  wait: Boolean = true
): ClickResponse
```

### Arguments

#### [checkbox.checked](#checked)[Boolean!](/bql-schema/types/scalars/boolean) 
Whether or not the input should be checked

#### [checkbox.selector](#selector)[String!](/bql-schema/types/scalars/string) 
The CSS selector of the element on the page you want to check/uncheck

#### [checkbox.scroll](#scroll)[Boolean](/bql-schema/types/scalars/boolean) 
Whether or not to scroll to the element prior to clicking, defaults to true

#### [checkbox.timeout](#timeout)[Float](/bql-schema/types/scalars/float) 
How long to wait for the element to appear before timing out on the handler, overriding any defaults. Default timeout is 30 seconds, or 30000.

#### [checkbox.visible](#visible)[Boolean](/bql-schema/types/scalars/boolean) 
Whether or not to check/uncheck the element only if it's visible

#### [checkbox.wait](#wait)[Boolean](/bql-schema/types/scalars/boolean) 
Whether or not to wait for the element to present in the DOM

### Type

#### [ClickResponse](/bql-schema/types/objects/click-response)  
Response returned after having clicked on an element

---

# click

URL: https://docs.browserless.io/bql-schema/operations/mutations/click

Waits for the element to be visible, scrolls to it, then clicks on it with native events

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation ClickButton {
  goto(url: "https://example.com") {
    status
  }

  click(selector: "a") {
    time
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
click(
  selector: String
  x: Float
  y: Float
  scroll: Boolean = true
  timeout: Float
  visible: Boolean = false
  wait: Boolean = true
): ClickResponse
```

### Arguments

#### [click.selector](#selector)[String](/bql-schema/types/scalars/string) 
A query-selector compatible string, JavaScript that returns an HTML Node, OR a Browserless-deep query. Examples include:
- A simple `<button />` Element:
  `selector: "button"`

- A JavaScript snippet that returns a button element
  `selector: "document.querySelector('button')"`

- A Browserless Deep query. These queries must start with a "&#x003C;" character.
  Deep queries will traverse all iframes, shadow-doms (open or closed), and more.
  Basic deep query:
  `selector: "< button"`

Advanced deep query features:
- URL Pattern Matching: Target elements within specific iframes using glob patterns
  `selector: "< https://example.com/* button.active"`
  `selector: "< *google.com/recaptcha* #recaptcha-anchor"`

- Complex Selectors: Combine multiple attributes, classes, and IDs
  `selector: "< input#username.login-form[type='text'][data-test-id*='user']"`

- Attribute Matching: Use various attribute matching operators
  - Contains (&#x002A;): `[data-test*="partial"]`
  - Starts with (^): `[class^="prefix-"]`
  - Ends with ($): `[id$="-suffix"]`
  - Exact match: `[type="submit"]`
  - Presence only: `[required]`

- Class and ID Combinations:
  `selector: "< button#submit.primary.large[data-action='save']"`

For reCAPTCHA interactions, you can use deep selectors to target elements within the reCAPTCHA iframe:
`selector: "< *google.com/recaptcha* #recaptcha-anchor"`

Note: Deep selectors support standard CSS selector syntax while maintaining security by disallowing potentially dangerous characters and patterns.

Omit selector to click by coordinate instead — pass x and y (see below).

#### [click.x](#x)[Float](/bql-schema/types/scalars/float) 
Vision fallback: click at a raw screenshot-pixel coordinate instead of a selector. Read the target's pixel off a viewport `screenshot` and pass it here; the server maps screenshot pixels to the page (dividing by the device pixel ratio). Provide both x and y, or use selector instead — not both. Coordinates are viewport-relative, so take a non-fullPage screenshot.

#### [click.y](#y)[Float](/bql-schema/types/scalars/float) 
See `x`. The vertical screenshot-pixel coordinate to click.

#### [click.scroll](#scroll)[Boolean](/bql-schema/types/scalars/boolean) 
Whether or not to scroll to the element prior to clicking, defaults to true

#### [click.timeout](#timeout)[Float](/bql-schema/types/scalars/float) 
How long to wait for the element to appear before timing out on the click handler, overriding any defaults. Default timeout is 30 seconds, or 30000.

#### [click.visible](#visible)[Boolean](/bql-schema/types/scalars/boolean) 
Whether or not to click the element only if it's visible

#### [click.wait](#wait)[Boolean](/bql-schema/types/scalars/boolean) 
Whether or not to wait for the element to present in the DOM

### Type

#### [ClickResponse](/bql-schema/types/objects/click-response)  
Response returned after having clicked on an element

---

# closeLiveURL

URL: https://docs.browserless.io/bql-schema/operations/mutations/close-live-url

Closes a Live URL created by this browser session.

```graphql
closeLiveURL(
  liveURLId: String!
): CloseLiveURLResponse!
```

### Arguments

#### [closeLiveURL.liveURLId](#live-urlid)[String!](/bql-schema/types/scalars/string) 
The ID of the Live URL to close

### Type

#### [CloseLiveURLResponse](/bql-schema/types/objects/close-live-urlresponse)  
The response returned after closing a Live URL

---

# content

URL: https://docs.browserless.io/bql-schema/operations/mutations/content

Sets the given HTML content on the page with an optional waitUntil parameter

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation SetContent {
  content(html: "<h1>Hello, World!</h1>") {
    status
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
content(
  html: String!
  timeout: Float
  waitUntil: WaitUntilHistory
  waitUntilAll: [WaitUntilHistory!]
): HTTPResponse
```

### Arguments

#### [content.html](#html)[String!](/bql-schema/types/scalars/string) 
When present, sets the content of page to the value passed, then returns the pages content

#### [content.timeout](#timeout)[Float](/bql-schema/types/scalars/float) 
The maximum amount of time, in milliseconds, to wait for the content to load, overriding any defaults. Default timeout is 30 seconds, or 30000.

#### [content.waitUntil](#wait-until)[WaitUntilHistory](/bql-schema/types/enums/wait-until-history) 
When to consider the page fully-loaded and proceed with further execution, used in conjunction with the value parameter

#### [content.waitUntilAll](#wait-until-all)[[WaitUntilHistory!]](/bql-schema/types/enums/wait-until-history) 
Wait for every listed lifecycle event. Use this instead of waitUntil when preserving an ordered REST array contract.

### Type

#### [HTTPResponse](/bql-schema/types/objects/httpresponse)  
Response returned after a navigation event

---

# cookies

URL: https://docs.browserless.io/bql-schema/operations/mutations/cookies

Sets and gets cookies on the page

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation SetCookies {
  # Get the cookies on the page
  getCookies: cookies {
    cookies {
      name
      value
    }
  }

  # Set a cookie on the page
  setCookies: cookies(cookies: [
      {
        name: "my-cookie"
        value: "my-value"
        url: "https://example.com"
      }
  ]) {
    cookies {
      name
      value
    }
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
cookies(
  cookies: [CookieInput]
): CookieResponse
```

### Arguments

#### [cookies.cookies](#cookies)[[CookieInput]](/bql-schema/types/inputs/cookie-input) 
The cookies to set on the page

### Type

#### [CookieResponse](/bql-schema/types/objects/cookie-response)  
The response returned after setting or getting cookies

---

# emulateMediaType

URL: https://docs.browserless.io/bql-schema/operations/mutations/emulate-media-type

Emulates the given CSS media type for the page, mirroring the behavior of
printing ("print") or on-screen rendering ("screen"). Useful before a `pdf`
or `screenshot` to control how the page renders.

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation EmulatePrint {
  goto(url: "https://example.com") {
    status
  }
  emulateMediaType(type: print) {
    time
  }
  pdf {
    base64
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
emulateMediaType(
  type: MediaType!
): EmulateMediaTypeResponse
```

### Arguments

#### [emulateMediaType.type](#type)[MediaType!](/bql-schema/types/enums/media-type) 
The CSS media type to emulate

### Type

#### [EmulateMediaTypeResponse](/bql-schema/types/objects/emulate-media-type-response)  
Response returned after emulating a CSS media type

---

# evaluate

URL: https://docs.browserless.io/bql-schema/operations/mutations/evaluate

Evaluates JavaScript client-side, via raw content or a URL to some JavaScript code, in the browser's page environment

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation EvaluateScript {
  byContent: evaluate(content: "2 + 2") {
    value
  }

  byUrl: evaluate(url: "https://example.com/script.js") {
    value
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
evaluate(
  content: String
  timeout: Float
  url: String
): EvaluateResponse
```

### Arguments

#### [evaluate.content](#content)[String](/bql-schema/types/scalars/string) 
The raw script you'd like to evaluate. This code gets wrapped in an async function so you can use `return` at the end as well as `await` and other async concepts. You can return any stringified value from this function

#### [evaluate.timeout](#timeout)[Float](/bql-schema/types/scalars/float) 
A timeout to wait for the script to finish evaluating, overriding any defaults. Useful for async scripts that may be longer running. Default timeout is 30 seconds, or 30000.

#### [evaluate.url](#url)[String](/bql-schema/types/scalars/string) 
The URL of the script you'd like to evaluate. This code gets wrapped in an async function so you can use `return` at the end as well as `await` and other async
concepts. You can return any stringified value from this function

### Type

#### [EvaluateResponse](/bql-schema/types/objects/evaluate-response)  
Response returned after evaluating a script

---

# forward

URL: https://docs.browserless.io/bql-schema/operations/mutations/forward

Goes forward in browser history, optionally accepting waitUntil and timeout arguments. Returns null if no forward is possible

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation GoForward {
  firstNav: goto(url: "https://example.com", waitUntil: load) {
    time
  }

  secondNav: goto(url: "https://browserless.io", waitUntil: load) {
    time
  }

  back(waitUntil: domContentLoaded) {
    status
  }

  forward(waitUntil: domContentLoaded) {
    status
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
forward(
  timeout: Float
  waitUntil: WaitUntilHistory = load
): HTTPResponse
```

### Arguments

#### [forward.timeout](#timeout)[Float](/bql-schema/types/scalars/float) 
The maximum amount of time, in milliseconds, to wait for the page to load, overriding any defaults. Default timeout is 30 seconds, or 30000.

#### [forward.waitUntil](#wait-until)[WaitUntilHistory](/bql-schema/types/enums/wait-until-history) 
When to consider the page fully-loaded and proceed with further execution

### Type

#### [HTTPResponse](/bql-schema/types/objects/httpresponse)  
Response returned after a navigation event

---

# fulfill

URL: https://docs.browserless.io/bql-schema/operations/mutations/fulfill

Fulfills matching requests with a canned response instead of letting them hit
the network, mirroring the request-interceptor mocking used by the REST APIs.
Matching follows the same glob-style URL/method/type conditions as `reject`.

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation MockApi {
  fulfill(
    url: ["*/api/user"]
    status: 200
    contentType: "application/json"
    body: "{\\"name\\":\\"test\\"}"
  ) {
    time
  }
  goto(url: "https://example.com") {
    status
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
fulfill(
  url: [String]
  regex: [String]
  method: [Method]
  type: [ResourceType]
  operator: OperatorTypes = or
  status: Int = 200
  contentType: String
  body: String
  base64Body: String
  headers: [HeaderInput]
): FulfillResponse
```

### Arguments

#### [fulfill.url](#url)[[String]](/bql-schema/types/scalars/string) 
The glob-style URL pattern(s) to fulfill requests for

#### [fulfill.regex](#regex)[[String]](/bql-schema/types/scalars/string) 
JavaScript regular-expression pattern(s) to match request URLs

#### [fulfill.method](#method)[[Method]](/bql-schema/types/enums/method) 
The HTTP Method(s) of the requests to fulfill

#### [fulfill.type](#type)[[ResourceType]](/bql-schema/types/enums/resource-type) 
The resource type(s) of the requests to fulfill

#### [fulfill.operator](#operator)[OperatorTypes](/bql-schema/types/enums/operator-types) 
Whether to "or" the conditions together (any match fulfills) or "and" them (all must match)

#### [fulfill.status](#status)[Int](/bql-schema/types/scalars/int) 
The HTTP status code to respond with. Defaults to 200

#### [fulfill.contentType](#content-type)[String](/bql-schema/types/scalars/string) 
The Content-Type header to respond with

#### [fulfill.body](#body)[String](/bql-schema/types/scalars/string) 
The response body to respond with

#### [fulfill.base64Body](#base-64-body)[String](/bql-schema/types/scalars/string) 
A base64-encoded response body. Use this instead of body for binary data.

#### [fulfill.headers](#headers)[[HeaderInput]](/bql-schema/types/inputs/header-input) 
Additional response headers to respond with

### Type

#### [FulfillResponse](/bql-schema/types/objects/fulfill-response)  
Response returned after registering a request-fulfillment interceptor

---

# goto

URL: https://docs.browserless.io/bql-schema/operations/mutations/goto

Navigates to a URL with an optional waitUntil parameter and timeout parameter

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation NavigateToPage {
  goto(url: "https://example.com") {
    status
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
goto(
  url: String!
  timeout: Float
  waitUntil: WaitUntilGoto = load
  waitUntilAll: [WaitUntilGoto!]
  referer: String
  referrerPolicy: String
): HTTPResponse
```

### Arguments

#### [goto.url](#url)[String!](/bql-schema/types/scalars/string) 
The fully-qualified URL of the page you'd like to navigate to

#### [goto.timeout](#timeout)[Float](/bql-schema/types/scalars/float) 
The maximum amount of time, in milliseconds, to wait for the page to load, overriding any defaults. Default timeout is 30 seconds, or 30000.

#### [goto.waitUntil](#wait-until)[WaitUntilGoto](/bql-schema/types/enums/wait-until-goto) 
When to consider the page fully-loaded and proceed with further execution

#### [goto.waitUntilAll](#wait-until-all)[[WaitUntilGoto!]](/bql-schema/types/enums/wait-until-goto) 
Wait for every listed lifecycle event. This preserves Puppeteer waitUntil array semantics.

#### [goto.referer](#referer)[String](/bql-schema/types/scalars/string) 
The Referer header to send with the navigation request

#### [goto.referrerPolicy](#referrer-policy)[String](/bql-schema/types/scalars/string) 
The referrer policy to use for the navigation request

### Type

#### [HTTPResponse](/bql-schema/types/objects/httpresponse)  
Response returned after a navigation event

---

# hover

URL: https://docs.browserless.io/bql-schema/operations/mutations/hover

Waits for the element to be visible, scrolls to it, then hover on it with native events

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation HoverElement {
  goto(url: "https://example.com") {
    status
  }

  hover(selector: "a") {
    time
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
hover(
  scroll: Boolean = true
  selector: String
  timeout: Float
  visible: Boolean = false
  wait: Boolean = true
  x: Float
  y: Float
): HoverResponse
```

### Arguments

#### [hover.scroll](#scroll)[Boolean](/bql-schema/types/scalars/boolean) 
Whether or not to scroll to the element, defaults to true

#### [hover.selector](#selector)[String](/bql-schema/types/scalars/string) 
The CSS selector of the element on the page you want to hover on

#### [hover.timeout](#timeout)[Float](/bql-schema/types/scalars/float) 
How long to wait for the element to appear before timing out, overriding any defaults. Default timeout is 30 seconds, or 30000.

#### [hover.visible](#visible)[Boolean](/bql-schema/types/scalars/boolean) 
Whether or not to hover on the element only if it's visible

#### [hover.wait](#wait)[Boolean](/bql-schema/types/scalars/boolean) 
Whether or not to wait for the element to present in the DOM

#### [hover.x](#x)[Float](/bql-schema/types/scalars/float) 
The X coordinate, in pixels, to hover on the page

#### [hover.y](#y)[Float](/bql-schema/types/scalars/float) 
The Y coordinate, in pixels, to hover on the page

### Type

#### [HoverResponse](/bql-schema/types/objects/hover-response)  
Response returned after having hovered over an element

---

# html

URL: https://docs.browserless.io/bql-schema/operations/mutations/html

Returns the HTML content of the page or selector when specified. This API can also "clean" HTML markup returned by specifying a "clean" argument with numerous options. Features of the "clean" argument include removal of non-text nodes, removal of DOM attributes, as well as removal of excessive whitespace and newlines. Using "clean" can save nearly 1,000 times the payload size. Useful for LLM's and other scenarios

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation ExtractHTML {
  goto(url: "https://example.com") {
    status
  }
  html(selector: "h1") {
    html
  }
}
```

Remove non-text DOM nodes and all Node attributes, but preserve the DOM tree
```graphql
mutation ExtractHTML {
  goto(url: "https://example.com") {
    status
  }

  html(clean: {
    removeAttributes: true
    removeNonTextNodes: true
  }) {
    html
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
html(
  selector: String
  timeout: Float
  visible: Boolean = false
  clean: CleanInput
  rewrite: HTMLRewriteInput
): HTMLResponse
```

### Arguments

#### [html.selector](#selector)[String](/bql-schema/types/scalars/string) 
The DOM selector of the given element you want to return the HTML of

#### [html.timeout](#timeout)[Float](/bql-schema/types/scalars/float) 
The maximum amount of time, in milliseconds, to wait for the selector to appear, overriding any defaults. Default timeout is 30 seconds, or 30000.

#### [html.visible](#visible)[Boolean](/bql-schema/types/scalars/boolean) 
Whether or not to return the HTML content of the element only if it's visible

#### [html.clean](#clean)[CleanInput](/bql-schema/types/inputs/clean-input) 
Specifies conditions for "cleaning" HTML, useful for minimizing the amount of markup returned for cases like LLMs and more. See nested options for parameters.

#### [html.rewrite](#rewrite)[HTMLRewriteInput](/bql-schema/types/inputs/htmlrewrite-input) 
Rewrites captured resource URLs to archive-relative paths before returning HTML.

### Type

#### [HTMLResponse](/bql-schema/types/objects/htmlresponse)  
HTML content of a page

---

# if

URL: https://docs.browserless.io/bql-schema/operations/mutations/if

Triggers a nested branch of work when a given condition is `true`. Does not wait for these items and is a point-in-time check. Use the wait method if you're wanting to await certain behaviors to be present

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation ConditionalRequest {
  goto(url: "https://example.com") {
    status
  }

  # Will only trigger the screenshot if the h1 is present
  if(selector: "h1") {
    screenshot {
      base64
    }
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
if(
  operator: OperatorTypes = or
  request: RequestInput
  response: ResponseInput
  selector: String
  visible: Boolean = false
): Mutation
```

### Arguments

#### [if.operator](#operator)[OperatorTypes](/bql-schema/types/enums/operator-types) 
When applying arguments like URL or method, this operator will either "and" them together, or "or" them together. Default is "or"

#### [if.request](#request)[RequestInput](/bql-schema/types/inputs/request-input) 
Triggers the nested conditions if a request has been made with the following conditions

#### [if.response](#response)[ResponseInput](/bql-schema/types/inputs/response-input) 
Triggers the nested conditions if a response has been received with the following conditions

#### [if.selector](#selector)[String](/bql-schema/types/scalars/string) 
Triggers the subsequent conditions if the selector is immediately present

#### [if.visible](#visible)[Boolean](/bql-schema/types/scalars/boolean) 
When using selectors in conditionals this options sets whether their or not to consider if they're visible to the viewport

### Type

#### [Mutation](/bql-schema/types/objects/mutation)  
Below is a list of all available API options for the service. Click on an API for options, examples, and even argument details.

For more comprehensive examples and recipes, [see our documentation site here](https://docs.browserless.io/).

---

# ifnot

URL: https://docs.browserless.io/bql-schema/operations/mutations/ifnot

Triggers a nested branch of work when a given condition is `false`. This method does not wait for these items and is a point-in-time check. Use the wait method if you're wanting to await certain behaviors to be present

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation ConditionalRequestNot {
  goto(url: "https://example.com") {
    status
  }

  # Will only trigger the screenshot if the h2 is not present
  ifnot(selector: "h2") {
    screenshot {
      base64
    }
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
ifnot(
  operator: OperatorTypes = or
  request: RequestInput
  response: ResponseInput
  selector: String
): Mutation
```

### Arguments

#### [ifnot.operator](#operator)[OperatorTypes](/bql-schema/types/enums/operator-types) 
When applying arguments like URL or method, this operator will either "and" them together, or "or" them together. Default is "or"

#### [ifnot.request](#request)[RequestInput](/bql-schema/types/inputs/request-input) 
Triggers the nested conditions if a request has been made with the following conditions

#### [ifnot.response](#response)[ResponseInput](/bql-schema/types/inputs/response-input) 
Triggers the nested conditions if a response has been received with the following conditions

#### [ifnot.selector](#selector)[String](/bql-schema/types/scalars/string) 
Triggers the subsequent conditions if the selector is immediately present

### Type

#### [Mutation](/bql-schema/types/objects/mutation)  
Below is a list of all available API options for the service. Click on an API for options, examples, and even argument details.

For more comprehensive examples and recipes, [see our documentation site here](https://docs.browserless.io/).

---

# javaScriptEnabled

URL: https://docs.browserless.io/bql-schema/operations/mutations/java-script-enabled

Sets and gets JavaScript execution on the page

&#x002A;&#x002A;Note: changing this value won't affect scripts that have already been run. It will take full effect on the next navigation.&#x002A;&#x002A;

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation EnableJavaScript {
  status: javaScriptEnabled {
    enabled
  }

  disable: javaScriptEnabled(enabled: false) {
    enabled
  }

  enable: javaScriptEnabled(enabled: true) {
    enabled
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
javaScriptEnabled(
  enabled: Boolean
): JavaScriptResponse
```

### Arguments

#### [javaScriptEnabled.enabled](#enabled)[Boolean](/bql-schema/types/scalars/boolean) 
Whether or not to enable JavaScript on the page

### Type

#### [JavaScriptResponse](/bql-schema/types/objects/java-script-response)  
The response returned after enabling or disabling JavaScript on the page

---

# liveURL

URL: https://docs.browserless.io/bql-schema/operations/mutations/live-url

Returns a fully-qualified, user-shareable live-URL for streaming the web-browser to an end-user, optionally interactive.

&#x002A;&#x002A;Set 30 second timer for interaction&#x002A;&#x002A;
```graphql
mutation CreateLiveURL {
  goto(url: "https://example.com") {
    status
  }
  liveURL {
    liveURL
  }
}
```

&#x002A;&#x002A;Maintain the current browsers viewport and don't resize it&#x002A;&#x002A;
```graphql
mutation CreateLiveURL {
  goto(url: "https://example.com") {
    status
  }
  liveURL(resizable: false) {
    liveURL
  }
}
```

&#x002A;&#x002A;Don't allow interaction at all&#x002A;&#x002A;
```graphql
mutation CreateLiveURL {
  goto(url: "https://example.com") {
    status
  }
  liveURL(interactable: false) {
    liveURL
  }
}
```

&#x002A;&#x002A;Low quality for better bandwidth&#x002A;&#x002A;
```graphql
mutation CreateLiveURL {
  goto(url: "https://example.com") {
    status
  }
  liveURL(quality: 20 type: jpeg) {
    liveURL
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
liveURL(
  captureScale: Int = 1
  timeout: Float
  interactable: Boolean = true
  type: LiveURLStreamType = jpeg
  quality: Int = 70
  resizable: Boolean = true
  showBrowserInterface: Boolean = false
  compressed: Boolean = true
  emulateComponents: Boolean = true
  instructions: String
): LiveURLResponse
```

### Arguments

#### [liveURL.captureScale](#capture-scale)[Int](/bql-schema/types/scalars/int) 
Encoded backing pixels per CSS pixel. Must be 1 or 2. Defaults to 1; 2 increases stream resolution without changing the page's CSS viewport.

#### [liveURL.timeout](#timeout)[Float](/bql-schema/types/scalars/float) 
The maximum time allowed for the browser to remain alive. Once the time is reached, the end-user will receive a prompt that the session has closed. Capped to the session's remaining time; the applied value is returned as `timeout`.

#### [liveURL.interactable](#interactable)[Boolean](/bql-schema/types/scalars/boolean) 
Whether the session is interactable or not. Set to "false" to not allow click and mouse events to be forwarded through to the end-user

#### [liveURL.type](#type)[LiveURLStreamType](/bql-schema/types/enums/live-urlstream-type) 
The binary-type of the streamed imaged. "jpeg" will consumer lower bandwidth and useful low bandwidth networks and devices. "png" is a much higher quality but will consume considerably more bandwidth.

Use "jpeg" when setting a custom quality

#### [liveURL.quality](#quality)[Int](/bql-schema/types/scalars/int) 
The quality of the stream, represented as number from 1 - 100. Only used when "type" is "jpeg". Default is 70.

#### [liveURL.resizable](#resizable)[Boolean](/bql-schema/types/scalars/boolean) 
Whether or not to resize the underlying browser to match the end-user's screen size. When `false` the underlying browser will retain it's current viewport, and the end users's screen wil maintain the appropriate aspect ratio.

#### [liveURL.showBrowserInterface](#show-browser-interface)[Boolean](/bql-schema/types/scalars/boolean) 
Whether to show the browser interface and tabs in the live URL. When `false` (default), only the browser content is shown. When `true`, navigation bar and tabs are displayed.

#### [liveURL.compressed](#compressed)[Boolean](/bql-schema/types/scalars/boolean) 
Whether to compress each frame of the streamed image data. When `true` (default), frames are compressed to reduce bandwidth usage. Set to `false` to disable compression for lower latency at the cost of increased bandwidth usage.

#### [liveURL.emulateComponents](#emulate-components)[Boolean](/bql-schema/types/scalars/boolean) 
Whether to emulate HTML components such as `<select>` dropdowns and the virtual keyboard. Setting this to `false` will force the virtual keyboard to not appear on the end-user's screen when interacting with input fields. Defaults to `true`.

#### [liveURL.instructions](#instructions)[String](/bql-schema/types/scalars/string) 
Plain-text task shown to the live viewer. Whitespace is trimmed; text over 2048 characters is truncated.

### Type

#### [LiveURLResponse](/bql-schema/types/objects/live-urlresponse)  
The response from the Live-URL query

---

# loadSecret

URL: https://docs.browserless.io/bql-schema/operations/mutations/load-secret

Fills a credential resolved just-in-time from a configured 1Password
integration into a target input. The resolved value never appears in your
query or in the response. The session must be started with an integration
(the `integrationId` connection parameter or session field), and the page's
origin must be in the integration's allowed-domain list. Once any secret has
been filled, capture and page-content readback are disabled for the rest of the
session — screenshots, PDFs, screencasts, Live URLs, session recording, and the
content-return mutations (evaluate, html, text, querySelector, querySelectorAll,
cookies).

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation Login {
  goto(url: "https://example.com/login") {
    status
  }

  username: loadSecret(
    ref: "op://Vault/Item/username"
    selector: "#email"
  ) {
    ok
    error
  }

  password: loadSecret(
    ref: "op://Vault/Item/password"
    selector: "#password"
  ) {
    ok
    error
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
loadSecret(
  ref: String!
  selector: String
): LoadSecretResponse
```

### Arguments

#### [loadSecret.ref](#ref)[String!](/bql-schema/types/scalars/string) 
The 1Password op:// reference (e.g. op://Vault/Item/password), or an alias
declared in the session's credentials map.

Also accepts a mail://&#x003C;vault&#x003E;/&#x003C;item&#x003E;/otp reference to resolve the newest
emailed one-time code server-side (the value is never surfaced). The named
1Password Login item holds the inbox's own IMAP credentials: `username`
(inbox address) and `password` (an app-password), with optional `host`/`port`
fields overriding the imap.gmail.com:993 default for other IMAP providers.
Options: `?wait=` (ms, default 45000, clamped 5000-120000), `?from=` (sender
substring filter), `?pattern=` (custom extraction regex). Only a message that
arrives AFTER the call is accepted, so trigger the send and then call
loadSecret promptly, and open the session with `?timeout=` above the wait
budget. Returns OtpTimeout if no fresh code arrives in the window.

#### [loadSecret.selector](#selector)[String](/bql-schema/types/scalars/string) 
The CSS selector of the input to fill. If omitted, the currently focused
element is used.

### Type

#### [LoadSecretResponse](/bql-schema/types/objects/load-secret-response)  
Response returned after attempting to fill a credential resolved from a
1Password integration. The resolved value is never included in the response.

---

# mapSelector

URL: https://docs.browserless.io/bql-schema/operations/mutations/map-selector

Specify a selector that returns multiple nodes in a document (similar to `document.querySelectorAll`), or JavaScript that returns a NodeList, and this API will respond with details about those DOM nodes. Similar to how "map" works in most functional programming languages and libraries. Useful for mapping over repetitive data in sites and pages like product listings or search results. This will automatically wait for the selector to be present on the page, and is configurable with the "wait" and "timeout" options.

For getting arbitrary DOM attributes back you can specify them via the `attribute(name: "data-custom-attribute")` property. This will return an object with `name` and `value` properties.

You may also continuously map further nested items as well, for instance this query might get all books on a page, and then a nested `mapSelector` call might list all sellers of that book, or shipping speeds. Hierarchy of data is preserved to pass through the hierarchical data modeled inside the DOM.

This API will always return a list of results back regardless if one or more items are found, or `null` if none are found.

Using aliases can also give the returned JSON more meaning and better model the data returned by this powerful API.

&#x002A;&#x002A;Simple Example&#x002A;&#x002A;
```graphql
mutation GetHNLinks {
  goto(url: "https://news.ycombinator.com") {
    status
  }

  # Get all the top links HTML
  mapSelector(selector: ".athing") {
    innerHTML
  }
}
```

&#x002A;&#x002A;Nested Example With Aliases&#x002A;&#x002A;
```graphql
mutation GetHNLinksWithMetaData {
  goto(url: "https://news.ycombinator.com") {
    status
  }

  # Get all textual content
  posts: mapSelector(selector: ".athing") {
    postName: innerText

    # Get the author(s)
    authors: mapSelector(selector: ".author") {
      authorName: innerText
    }

    # Get the post score
    score: mapSelector(selector: ".score") {
      score: innerText
    }
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
mapSelector(
  selector: String!
  timeout: Float
  wait: Boolean = true
): [MapSelectorResponse]
```

### Arguments

#### [mapSelector.selector](#selector)[String!](/bql-schema/types/scalars/string) 
A `document.querySelectorAll` compatible string, or JavaScript that returns a DOM NodeList. Examples include:
- A list of `<button />` Elements:
  `selector: "button"`

- A JavaScript snippet that returns a button element
  `selector: "document.querySelectorAll('button')"`

#### [mapSelector.timeout](#timeout)[Float](/bql-schema/types/scalars/float) 
How long to wait for the element to appear before timing out, overriding any defaults. Default timeout is 30 seconds, or 30000.

#### [mapSelector.wait](#wait)[Boolean](/bql-schema/types/scalars/boolean) 
Whether or not to wait for the selectors to present in the DOM

### Type

#### [MapSelectorResponse](/bql-schema/types/objects/map-selector-response)  
Response returned from a Map Selector

---

# markdown

URL: https://docs.browserless.io/bql-schema/operations/mutations/markdown

Returns the page's content converted to Markdown. The HTML is fetched from
the page (optionally scoped to a selector) and converted server-side, with
script/style/noscript/iframe nodes stripped.

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation Markdownify {
  goto(url: "https://example.com") {
    status
  }
  markdown {
    markdown
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
markdown(
  selector: String
  timeout: Float
  visible: Boolean = false
): MarkdownResponse
```

### Arguments

#### [markdown.selector](#selector)[String](/bql-schema/types/scalars/string) 
The DOM selector to scope the Markdown conversion to. Defaults to the whole document.

#### [markdown.timeout](#timeout)[Float](/bql-schema/types/scalars/float) 
The maximum amount of time, in milliseconds, to wait for the selector to appear, overriding any defaults. Default timeout is 30 seconds, or 30000.

#### [markdown.visible](#visible)[Boolean](/bql-schema/types/scalars/boolean) 
Whether or not to convert the element only if it's visible

### Type

#### [MarkdownResponse](/bql-schema/types/objects/markdown-response)  
Response returned after converting the page content to Markdown

---

# pdf

URL: https://docs.browserless.io/bql-schema/operations/mutations/pdf

Generates a PDF of the page with the print CSS media type

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation GeneratePDF {
  goto(url: "https://example.com", waitUntil: firstMeaningfulPaint) {
    status
  }

  simple: pdf {
    base64
  }

  customArgs: pdf(
    format: a5
    displayHeaderFooter: true
    headerTemplate: "Hello World"
  ) {
    base64
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
pdf(
  displayHeaderFooter: Boolean
  format: PDFPageFormat
  footerTemplate: String
  generateDocumentOutline: Boolean
  generateTaggedPDF: Boolean
  landscape: Boolean
  printBackground: Boolean
  omitBackground: Boolean
  scale: Float
  timeout: Float
  waitForImages: Boolean
  waitForFonts: Boolean
  fullPage: Boolean
  width: FloatOrString
  headerTemplate: String
  height: FloatOrString
  marginBottom: FloatOrString
  marginLeft: FloatOrString
  marginRight: FloatOrString
  marginTop: FloatOrString
  pageRanges: String
  preferCSSPageSize: Boolean
  transferMode: String
): PDFResponse
```

### Arguments

#### [pdf.displayHeaderFooter](#display-header-footer)[Boolean](/bql-schema/types/scalars/boolean) 
Display header and footer. Defaults to false

#### [pdf.format](#format)[PDFPageFormat](/bql-schema/types/enums/pdfpage-format) 
The page format to use for the PDF

#### [pdf.footerTemplate](#footer-template)[String](/bql-schema/types/scalars/string) 
HTML template for the print footer. Should use the same format as the `headerTemplate`.

#### [pdf.generateDocumentOutline](#generate-document-outline)[Boolean](/bql-schema/types/scalars/boolean) 
Whether or not to embed the document outline into the PDF

#### [pdf.generateTaggedPDF](#generate-tagged-pdf)[Boolean](/bql-schema/types/scalars/boolean) 
Whether or not to generate tagged (accessible) PDF. Defaults to embedded choice

#### [pdf.landscape](#landscape)[Boolean](/bql-schema/types/scalars/boolean) 
Paper orientation. Defaults to false

#### [pdf.printBackground](#print-background)[Boolean](/bql-schema/types/scalars/boolean) 
Print background graphics. Defaults to false

#### [pdf.omitBackground](#omit-background)[Boolean](/bql-schema/types/scalars/boolean) 
Hides the default white background and allows the PDF to preserve transparency. Defaults to false.

#### [pdf.scale](#scale)[Float](/bql-schema/types/scalars/float) 
Scale of the webpage rendering. Defaults to 1

#### [pdf.timeout](#timeout)[Float](/bql-schema/types/scalars/float) 
The maximum amount of time, in milliseconds, to wait for the PDF to be generated. Default timeout is 30 seconds, or 30000.

#### [pdf.waitForImages](#wait-for-images)[Boolean](/bql-schema/types/scalars/boolean) 
When true, waits for all images on the page to load before generating the PDF.
Default: False

#### [pdf.waitForFonts](#wait-for-fonts)[Boolean](/bql-schema/types/scalars/boolean) 
When true, waits for document fonts to finish loading before generating the PDF.
Default: False

#### [pdf.fullPage](#full-page)[Boolean](/bql-schema/types/scalars/boolean) 
When true, sizes the PDF page height to the full scroll height of the
document so the entire page renders on a single page. Mutually exclusive
with "format" and "height". Default: False

#### [pdf.width](#width)[FloatOrString](/bql-schema/types/scalars/float-or-string) 
Width in inches or CSS unit. Defaults to 8.5 inches

#### [pdf.headerTemplate](#header-template)[String](/bql-schema/types/scalars/string) 
HTML template for the print header. Should be valid HTML markup with following
classes used to inject printing values into them:
- `date`: formatted print date
- `title`: document title
- `url`: document location
- `pageNumber`: current page number
- `totalPages`: total pages in the document

For example, `` would generate span containing the title

#### [pdf.height](#height)[FloatOrString](/bql-schema/types/scalars/float-or-string) 
Height in inches or CSS unit. Defaults to 11 inches

#### [pdf.marginBottom](#margin-bottom)[FloatOrString](/bql-schema/types/scalars/float-or-string) 
Bottom margin in inches or CSS unit. Defaults to 1cm (~0.4 inches).

#### [pdf.marginLeft](#margin-left)[FloatOrString](/bql-schema/types/scalars/float-or-string) 
Left margin in inches or CSS unit. Defaults to 1cm (~0.4 inches).

#### [pdf.marginRight](#margin-right)[FloatOrString](/bql-schema/types/scalars/float-or-string) 
Right margin in inches or CSS unit. Defaults to 1cm (~0.4 inches).

#### [pdf.marginTop](#margin-top)[FloatOrString](/bql-schema/types/scalars/float-or-string) 
Top margin in inches or CSS unit. Defaults to 1cm (~0.4 inches).

#### [pdf.pageRanges](#page-ranges)[String](/bql-schema/types/scalars/string) 
Paper ranges to print, one based, e.g., '1-5, 8, 11-13'. Pages are
printed in the document order, not in the order specified, and no
more than once. Defaults to empty string, which implies the entire document is printed.
The page numbers are quietly capped to actual page count of the
document, and ranges beyond the end of the document are ignored.
If this results in no pages to print, an error is reported.
It is an error to specify a range with start greater than end

#### [pdf.preferCSSPageSize](#prefer-csspage-size)[Boolean](/bql-schema/types/scalars/boolean) 
Whether or not to prefer page size as defined by css. Defaults to false,
in which case the content will be scaled to fit the paper size

#### [pdf.transferMode](#transfer-mode)[String](/bql-schema/types/scalars/string) 
Return as stream (PrintToPDFRequestTransferMode enum)

### Type

#### [PDFResponse](/bql-schema/types/objects/pdfresponse)  
The response returned after generating a PDF

---

# preferences

URL: https://docs.browserless.io/bql-schema/operations/mutations/preferences

Sets configuration for the entirety of the session, replacing defaults like the 30 second timeout default

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation SetPreferences {
  preferences(timeout: 10000) {
    timeout
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
preferences(
  timeout: Float = 30000
): DefaultResponse
```

### Arguments

#### [preferences.timeout](#timeout)[Float](/bql-schema/types/scalars/float) 
Sets a default timeout for all methods, including 'goto', 'type', 'wait', etc. Default timeout is 30 seconds, or 30000.

### Type

#### [DefaultResponse](/bql-schema/types/objects/default-response)  
Default response for all methods

---

# proxy

URL: https://docs.browserless.io/bql-schema/operations/mutations/proxy

Proxies requests, by a specified set of conditions, through either the Browserless residential proxy or through an external proxy. Only requests that match these conditions are proxied and the rest are sent from the instance's own IP address.

Use the "server" argument to specify an external proxy for Browserless to use for requests. For requests with authentication, the username and password should be included in the URL using Basic Authentication. See the examples below for more information on how to format those URLs.

&#x002A;&#x002A;Using the browserless proxy for all requests and proxy through Brazil&#x002A;&#x002A;
```graphql
proxy(
  url: ["*"]
  country: BR
) {
  time
}
```
&#x002A;&#x002A;Using an external proxy for all requests&#x002A;&#x002A;
```graphql
proxy(
  url: ["*"]
  server: "http://username:password@my-proxy.com:12321"
) {
  time
}
```
&#x002A;&#x002A;Using the Browserless proxy for only document requests and proxying through France&#x002A;&#x002A;
```graphql
proxy(
  url: ["*"]
  type: document
  country: FR
) {
  time
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
proxy(
  network: ProxyNetwork = residential
  country: CountryType
  city: String
  state: String
  sticky: Boolean
  preset: String
  server: String
  method: [Method]
  operator: OperatorTypes = or
  type: [ResourceType]
  url: [String]
): ProxyResponse
```

### Arguments

#### [proxy.network](#network)[ProxyNetwork](/bql-schema/types/enums/proxy-network) 
The Browserless proxy network to route matching requests through: `residential` (default) or `datacenter`. Only valid for the browserless.io proxy network (not with `server`).

#### [proxy.country](#country)[CountryType](/bql-schema/types/enums/country-type) 
The country you wish to proxy through. Only allowed when using the browserless.io proxy and no `server` argument.

#### [proxy.city](#city)[String](/bql-schema/types/scalars/string) 
The city you wish to proxy through. Any spaces should be removed and all casing lowercase. For instance, "New York City" should be "newyorkcity"

#### [proxy.state](#state)[String](/bql-schema/types/scalars/string) 
The state or provence you wish to proxy through. Any spaces should be removed and all casing lowercase. For instance, "Rhode Island" would be "rhodeisland" and "New Brunswick" would be "newbrunswick"

#### [proxy.sticky](#sticky)[Boolean](/bql-schema/types/scalars/boolean) 
Whether the same exit IP is held for subsequent requests matching the pattern. Sticky is ON by default for stealth and BQL sessions (Akamai and DataDome tie their sensor/cookie to the egress IP, so a mid-session IP change re-triggers the block); pass `sticky: false` to use a rotating exit IP instead.

#### [proxy.preset](#preset)[String](/bql-schema/types/scalars/string) 
A preset code for website-specific proxy configurations. Use this with known presets like "px&#x005F;gov01" for government websites. Only applies to the `residential` network; the `datacenter` pool ignores it

#### [proxy.server](#server)[String](/bql-schema/types/scalars/string) 
An external proxy to use for these requests matching the specified patterns set in the other arguments. When this is set then `country`, `city`, `state` and `sticky` options will throw errors as these are only valid for the browserless.io proxy network

#### [proxy.method](#method)[[Method]](/bql-schema/types/enums/method) 
The Method(s) of the request you'd like to proxy

#### [proxy.operator](#operator)[OperatorTypes](/bql-schema/types/enums/operator-types) 
Whether to "or" conditions together, meaning any condition that matches will be proxied, or "and" them together meaning every condition must match to proxy the request.

#### [proxy.type](#type)[[ResourceType]](/bql-schema/types/enums/resource-type) 
The content-type of the request you'd like to proxy requests to

#### [proxy.url](#url)[[String]](/bql-schema/types/scalars/string) 
A glob-style URL pattern to match requests, and if matched, are proxied through

### Type

#### [ProxyResponse](/bql-schema/types/objects/proxy-response)  
Response returned after setting up the proxy patterns

---

# querySelector

URL: https://docs.browserless.io/bql-schema/operations/mutations/query-selector

Passes through certain properties of the browsers' own `document.querySelector` API

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation FindElement {
  goto(url: "https://example.com") {
    status
  }

  query: querySelector(selector: "h1") {
    innerHTML
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
querySelector(
  selector: String!
  timeout: Float
  visible: Boolean = false
): QuerySelectorResponse
```

### Arguments

#### [querySelector.selector](#selector)[String!](/bql-schema/types/scalars/string) 

#### [querySelector.timeout](#timeout)[Float](/bql-schema/types/scalars/float) 

#### [querySelector.visible](#visible)[Boolean](/bql-schema/types/scalars/boolean) 

### Type

#### [QuerySelectorResponse](/bql-schema/types/objects/query-selector-response)  
The response returned after querying the page for a selector

---

# querySelectorAll

URL: https://docs.browserless.io/bql-schema/operations/mutations/query-selector-all

Passes through certain properties of the browsers' own `document.querySelectorAll` API

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation FindElements {
  goto(url: "https://example.com") {
    status
  }

  query: querySelectorAll(selector: "h1") {
    innerHTML
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
querySelectorAll(
  selector: String!
  timeout: Float
  visible: Boolean = false
): [QuerySelectorResponse]
```

### Arguments

#### [querySelectorAll.selector](#selector)[String!](/bql-schema/types/scalars/string) 

#### [querySelectorAll.timeout](#timeout)[Float](/bql-schema/types/scalars/float) 

#### [querySelectorAll.visible](#visible)[Boolean](/bql-schema/types/scalars/boolean) 

### Type

#### [QuerySelectorResponse](/bql-schema/types/objects/query-selector-response)  
The response returned after querying the page for a selector

---

# reconnect

URL: https://docs.browserless.io/bql-schema/operations/mutations/reconnect

Returns a payload with reconnection information in order to reconnect back to the same browser session

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation ReconnectSession {
  goto(url: "https://example.com") {
    status
  }

  reconnect(timeout: 30000) {
    browserQLEndpoint
    browserWSEndpoint
    devtoolsFrontendUrl
    webSocketDebuggerUrl
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
reconnect(
  timeout: Float = 30000
): ReconnectionResponse
```

### Arguments

#### [reconnect.timeout](#timeout)[Float](/bql-schema/types/scalars/float) 
The amount of time, in milliseconds, to leave the browser open without a connection before it is manually terminated. Defaults to 30 seconds

### Type

#### [ReconnectionResponse](/bql-schema/types/objects/reconnection-response)  
The response received after attempting to reconnect a BrowserQL session

---

# reject

URL: https://docs.browserless.io/bql-schema/operations/mutations/reject

Rejects requests by a specified URL pattern, method, or type and operator. You may supply a single pattern, or a list (array) of them. This mutation, by default, will reject any requests that match &#x002A;any&#x002A; pattern, which we call an "or" operator. To reject requests where conditions must all match, specify an "and" operator in the mutation. Note that this only has an effect when the query is executing, so scripts that return quickly will likely see assets loading in the editor as these rejections only happen when mutations are executing.
&#x005F;&#x005F;&#x005F;
&#x002A;&#x002A;Rejecting images or media&#x002A;&#x002A;
```graphql
mutation RejectImages {
  reject(type: [image, media]) {
    enabled
    time
  }
  goto(url: "https://cnn.com"
  waitUntil: firstContentfulPaint) {
    status
    time
  }
}
```
&#x005F;&#x005F;&#x005F;
&#x002A;&#x002A;Rejecting media when coming from google.com domain&#x002A;&#x002A;
```graphql
mutation RejectRequests {
  reject(
    operator: and
    type: image
    url: "*google.com*"
  ) {
    enabled
    time
  }
  goto(url: "https://cnn.com"
  waitUntil: firstContentfulPaint) {
    status
    time
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
reject(
  enabled: Boolean = true
  method: [Method]
  operator: OperatorTypes = or
  type: [ResourceType]
  url: [String]
  regex: [String]
): RejectResponse
```

### Arguments

#### [reject.enabled](#enabled)[Boolean](/bql-schema/types/scalars/boolean) 
Whether or not to enable request rejections

#### [reject.method](#method)[[Method]](/bql-schema/types/enums/method) 
The Method of the request you'd like to reject

#### [reject.operator](#operator)[OperatorTypes](/bql-schema/types/enums/operator-types) 
Whether to "or" conditions together, meaning any condition that matches will be rejected, or "and" them together meaning every condition must match to reject the request.

#### [reject.type](#type)[[ResourceType]](/bql-schema/types/enums/resource-type) 
The type of resource you'd like to reject request to

#### [reject.url](#url)[[String]](/bql-schema/types/scalars/string) 
The glob-style URL pattern you'd like to reject requests to

#### [reject.regex](#regex)[[String]](/bql-schema/types/scalars/string) 
The JavaScript regular-expression pattern(s) you'd like to reject requests by URL

### Type

#### [RejectResponse](/bql-schema/types/objects/reject-response)  
The response parameters for the reject mutation

---

# reload

URL: https://docs.browserless.io/bql-schema/operations/mutations/reload

Reloads the given page with an optional waitUntil parameter and timeout parameter

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation ReloadPage {
  goto(url: "https://example.com") {
    status
  }
  reload(timeout: 10000) {
    status
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
reload(
  timeout: Float
  waitUntil: WaitUntilHistory = load
): HTTPResponse
```

### Arguments

#### [reload.timeout](#timeout)[Float](/bql-schema/types/scalars/float) 
The maximum amount of time, in milliseconds, to wait for the page to load, overriding any defaults. Default timeout is 30 seconds, or 30000.

#### [reload.waitUntil](#wait-until)[WaitUntilHistory](/bql-schema/types/enums/wait-until-history) 
When to consider the page fully-loaded and proceed with further execution

### Type

#### [HTTPResponse](/bql-schema/types/objects/httpresponse)  
Response returned after a navigation event

---

# request

URL: https://docs.browserless.io/bql-schema/operations/mutations/request

Returns request information made by the Browser with optional filters via arguments. You may filter the returned results by a glob-like URL-pattern, the method of the request or the type of request. Applying an operator to this will then change the behavior by either "and"ing the filters together or "or"ing them. This API will automatically wait for the request to be made if none is immediately found which you can turn off by disabling the "wait" option.

&#x002A;&#x002A;Getting all "Document" requests&#x002A;&#x002A;
```graphql
  mutation DocumentRequests {
    goto(url: "https://example.com/", waitUntil: load) {
      status
    }
    request(type:document) {
      url
      type
      method
      headers {
        name
        value
      }
    }
  }
```

&#x002A;&#x002A;Load all "GET" AJAX Requests&#x002A;&#x002A;
```graphql
  mutation AJAXGetCalls {
    goto(url: "https://msn.com/", waitUntil: load) {
      status
    }
    request(type: xhr, method: GET, operator: and) {
      url
      type
      method
      headers {
        name
        value
      }
    }
  }
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
request(
  type: [ResourceType]
  method: [Method]
  timeout: Float
  url: [String]
  wait: Boolean = true
  operator: OperatorTypes = or
): [RequestResponse]
```

### Arguments

#### [request.type](#type)[[ResourceType]](/bql-schema/types/enums/resource-type) 
The type content-type of the request to match against

#### [request.method](#method)[[Method]](/bql-schema/types/enums/method) 
The method of the request to return results for

#### [request.timeout](#timeout)[Float](/bql-schema/types/scalars/float) 
How long to wait for the request(s) to be made before timing out, overriding any defaults. Default timeout is 30 seconds, or 30000. "wait" parameter must also be "true".

#### [request.url](#url)[[String]](/bql-schema/types/scalars/string) 
The pattern of the request URL to wait for, using glob-style pattern-matching

#### [request.wait](#wait)[Boolean](/bql-schema/types/scalars/boolean) 
Whether or not to wait for the request to be made. When set to `true`, generally only one request is returned since this API will wait for the first request that matches any patterns to be returned

#### [request.operator](#operator)[OperatorTypes](/bql-schema/types/enums/operator-types) 
When applying arguments like URL or method, this operator will either "and" them together, or "or" them together. Default is "or"

### Type

#### [RequestResponse](/bql-schema/types/objects/request-response)  
Response returned from the request API

---

# response

URL: https://docs.browserless.io/bql-schema/operations/mutations/response

Returns response information, filtered by the provided arguments, made by the browser. You may optionally filter the returned results by a glob-like URL-pattern, the Method of the response or the Type of response. Applying an operator to this will then change the behavior by either "and"ing the filters together, or "or"ing them. This API will automatically wait for the response to be made if none is immediately found which you can turn off by disabling the "wait" option.

&#x002A;&#x002A;Getting all "Document" responses&#x002A;&#x002A;
```graphql
  mutation DocumentResponses{
    goto(url: "https://example.com/", waitUntil: load) {
      status
    }
    response(type:document) {
      url
      body
      headers {
        name
        value
      }
    }
  }
```

&#x002A;&#x002A;Load all "GET" AJAX Responses&#x002A;&#x002A;
```graphql
  mutation AJAXGetCalls {
    goto(url: "https://msn.com/", waitUntil: load) {
      status
    }
    response(type: xhr, method: GET, operator: and) {
      url
      type
      method
      body
      headers {
        name
        value
      }
    }
  }
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
response(
  status: [Int]
  method: [Method]
  operator: OperatorTypes = or
  timeout: Float
  type: [ResourceType]
  url: [String]
  wait: Boolean = true
): [ResponseResponse]
```

### Arguments

#### [response.status](#status)[[Int]](/bql-schema/types/scalars/int) 
The status codes response to return results for

#### [response.method](#method)[[Method]](/bql-schema/types/enums/method) 
The method of the response to return results for

#### [response.operator](#operator)[OperatorTypes](/bql-schema/types/enums/operator-types) 
When applying arguments like URL or method, this operator will either "and" them together, or "or" them together. Default is "or"

#### [response.timeout](#timeout)[Float](/bql-schema/types/scalars/float) 
How long to wait for the response(s) to be made before timing out, overriding any defaults. Default timeout is 30 seconds, or 30000. "wait" parameter must also be "true".

#### [response.type](#type)[[ResourceType]](/bql-schema/types/enums/resource-type) 
The type content-type of the response to match against

#### [response.url](#url)[[String]](/bql-schema/types/scalars/string) 
The pattern of the response URL to wait for, using glob-style pattern-matching

#### [response.wait](#wait)[Boolean](/bql-schema/types/scalars/boolean) 
Whether or not to wait for the response to be received. When set to `true`, generally only one response is returned since this API will wait for the first response that matches any patterns to be returned

### Type

#### [ResponseResponse](/bql-schema/types/objects/response-response)  
Response returned from the response API

---

# saveSecret

URL: https://docs.browserless.io/bql-schema/operations/mutations/save-secret

Creates a Login item in a vault visible to the session's 1Password
integration. Returns only an op:// reference; the password is never echoed.

```graphql
saveSecret(
  vault: String!
  title: String!
  username: String!
  password: String!
  website: String
): SaveSecretResponse
```

### Arguments

#### [saveSecret.vault](#vault)[String!](/bql-schema/types/scalars/string) 
Exact title of the destination vault.

#### [saveSecret.title](#title)[String!](/bql-schema/types/scalars/string) 
Title for the new Login item.

#### [saveSecret.username](#username)[String!](/bql-schema/types/scalars/string) 
Username stored in the Login item.

#### [saveSecret.password](#password)[String!](/bql-schema/types/scalars/string) 
Password stored as a concealed field.

#### [saveSecret.website](#website)[String](/bql-schema/types/scalars/string) 
Optional website metadata stored with the Login item.

### Type

#### [SaveSecretResponse](/bql-schema/types/objects/save-secret-response)  
Response returned after attempting to create a Login item in a 1Password
integration. The supplied password is never included in the response.

---

# screenshot

URL: https://docs.browserless.io/bql-schema/operations/mutations/screenshot

Screenshots the page or a specific selector

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation TakeScreenshot {
  goto(url: "https://example.com") {
    status
  }

  screenshot {
    base64
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
screenshot(
  captureBeyondViewport: Boolean
  clip: ScreenshotClip
  fromSurface: Boolean
  fullPage: Boolean
  omitBackground: Boolean
  optimizeForSpeed: Boolean
  quality: Float
  selector: String
  type: ScreenshotType
  timeout: Float
  waitForImages: Boolean
): ScreenshotResponse
```

### Arguments

#### [screenshot.captureBeyondViewport](#capture-beyond-viewport)[Boolean](/bql-schema/types/scalars/boolean) 
Capture the screenshot beyond the viewport.
Default: False if there is no clip. True otherwise

#### [screenshot.clip](#clip)[ScreenshotClip](/bql-schema/types/inputs/screenshot-clip) 
Specifies the region of the page/element to clip

#### [screenshot.fromSurface](#from-surface)[Boolean](/bql-schema/types/scalars/boolean) 
Capture the screenshot from the surface, rather than the view.
Default: True

#### [screenshot.fullPage](#full-page)[Boolean](/bql-schema/types/scalars/boolean) 
When True, takes a screenshot of the full page.
Default: False

#### [screenshot.omitBackground](#omit-background)[Boolean](/bql-schema/types/scalars/boolean) 
Hides default white background and allows capturing screenshots with transparency.
Default: False

#### [screenshot.optimizeForSpeed](#optimize-for-speed)[Boolean](/bql-schema/types/scalars/boolean) 
Optimize image encoding for speed, not for resulting size.
Default: False

#### [screenshot.quality](#quality)[Float](/bql-schema/types/scalars/float) 
Quality of the image, between 0-100. Not applicable to png images.

#### [screenshot.selector](#selector)[String](/bql-schema/types/scalars/string) 
The CSS selector of the element on the page you want to screenshot

#### [screenshot.type](#type)[ScreenshotType](/bql-schema/types/enums/screenshot-type) 
The final format of the screenshot

#### [screenshot.timeout](#timeout)[Float](/bql-schema/types/scalars/float) 
The maximum amount of time, in milliseconds, to wait for the screenshot to be taken. Default timeout is 30 seconds, or 30000.

#### [screenshot.waitForImages](#wait-for-images)[Boolean](/bql-schema/types/scalars/boolean) 
When true, waits for all images on the page to load before taking the screenshot.
Default: False

### Type

#### [ScreenshotResponse](/bql-schema/types/objects/screenshot-response)  
The response returned after generating a Screenshot

---

# scroll

URL: https://docs.browserless.io/bql-schema/operations/mutations/scroll

Waits for a selector, then scrolls to it on the page or an x,y coordinate in pixels

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation ScrollToElement {
  goto(url: "https://example.com") {
    status
  }

  scroll(selector: "h1") {
    time
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
scroll(
  selector: String
  throughPage: Boolean = false
  timeout: Float
  visible: Boolean = false
  wait: Boolean = true
  x: Float
  y: Float
): ScrollResponse
```

### Arguments

#### [scroll.selector](#selector)[String](/bql-schema/types/scalars/string) 
The DOM selector of the element on the page you want to scroll to

#### [scroll.throughPage](#through-page)[Boolean](/bql-schema/types/scalars/boolean) 
Scrolls through the entire document in viewport-sized steps to trigger
lazy-loaded content, then returns to the top. Cannot be combined with
selector, x, or y.

#### [scroll.timeout](#timeout)[Float](/bql-schema/types/scalars/float) 
How long to wait for the element to appear before timing out, overriding any defaults. Default timeout is 30 seconds, or 30000.

#### [scroll.visible](#visible)[Boolean](/bql-schema/types/scalars/boolean) 
Whether or not to scroll to the element only if it's visible

#### [scroll.wait](#wait)[Boolean](/bql-schema/types/scalars/boolean) 
Whether or not to wait for the element, then scroll to it

#### [scroll.x](#x)[Float](/bql-schema/types/scalars/float) 
The X coordinate, in pixels, to scroll to

#### [scroll.y](#y)[Float](/bql-schema/types/scalars/float) 
The Y coordinate, in pixels, to scroll to

### Type

#### [ScrollResponse](/bql-schema/types/objects/scroll-response)  
Response returned after having scrolling inside the page

---

# select

URL: https://docs.browserless.io/bql-schema/operations/mutations/select

Selects a value from a dropdown or multiple select element

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation SelectOption {
  goto(url: "https://example.com") {
    status
  }

  single: select(
    selector: "select"
    value: "option1"
  ) {
    time
  }

  multi: select(
    selector: "select"
    value: ["option1", "option2"]
  ) {
    time
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
select(
  timeout: Float
  scroll: Boolean = true
  selector: String!
  value: StringOrArray!
  visible: Boolean = false
  wait: Boolean = true
): SelectResponse
```

### Arguments

#### [select.timeout](#timeout)[Float](/bql-schema/types/scalars/float) 
How long to wait for the element to appear before timing out on the handler, overriding any defaults. Default timeout is 30 seconds, or 30000.

#### [select.scroll](#scroll)[Boolean](/bql-schema/types/scalars/boolean) 
Whether or not to scroll to the select element prior to selecting, defaults to true

#### [select.selector](#selector)[String!](/bql-schema/types/scalars/string) 
The CSS selector of the element on the page you want to select a value from

#### [select.value](#value)[StringOrArray!](/bql-schema/types/scalars/string-or-array) 
The value or values to select from the dropdown or multiple select element

#### [select.visible](#visible)[Boolean](/bql-schema/types/scalars/boolean) 
Whether or not to select the element only if it's visible

#### [select.wait](#wait)[Boolean](/bql-schema/types/scalars/boolean) 
Whether or not to wait for the select to present in the DOM

### Type

#### [SelectResponse](/bql-schema/types/objects/select-response)  
The response returned after selecting a value from a dropdown or multiple select element

---

# setExtraHTTPHeaders

URL: https://docs.browserless.io/bql-schema/operations/mutations/set-extra-httpheaders

Sets the HTTP headers for the page

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation SetExtraHTTPHeaders {
  setExtraHTTPHeaders(headers: [
    {name: "x-browserless-secret-header", value: "55"},
    {name: "x-browserless-ping", value: "pong"}
  ]) {
    time
  }
  goto(url: "https://example.com") {
    status
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
setExtraHTTPHeaders(
  headers: [HeaderInput]
): HTTPHeadersResponse
```

### Arguments

#### [setExtraHTTPHeaders.headers](#headers)[[HeaderInput]](/bql-schema/types/inputs/header-input) 
The HTTP headers to set on the page

### Type

#### [HTTPHeadersResponse](/bql-schema/types/objects/httpheaders-response)  
The response returned after setting HTTP headers

---

# solve

URL: https://docs.browserless.io/bql-schema/operations/mutations/solve

Solves a captcha or other challenge. Can auto-detect the CAPTCHA type or solve a specific type.
Uses the same detection logic as BaaS to automatically identify the CAPTCHA type
and routes to the appropriate solver. Optionally accepts a specific type to solve.

Example (Auto-detection):
```graphql
mutation SolveAuto {
  goto(url: "https://protected.domain") {
    status
  }

  solve {
    found
    solved
    time
    token
  }
}
```

Example (Specific type):
```graphql
mutation SolveSpecific {
  goto(url: "https://protected.domain") {
    status
  }

  solve(type: recaptcha) {
    found
    solved
    time
    token
  }
}
```

```graphql
solve(
  type: CaptchaTypes
  timeout: Float
  wait: Boolean = true
): CaptchaResponse
```

### Arguments

#### [solve.type](#type)[CaptchaTypes](/bql-schema/types/enums/captcha-types) 
An enum of the type of captcha to look for and solve. If not provided, auto-detection will be used.

#### [solve.timeout](#timeout)[Float](/bql-schema/types/scalars/float) 
A time, in milliseconds, to wait for a captcha to appear. Only valid when wait = true.
If a captcha is found then this timer doesn't have an effect on timing-out the solve
Default timeout is 30 seconds, or 30000.

#### [solve.wait](#wait)[Boolean](/bql-schema/types/scalars/boolean) 
Whether or not to wait for the Captcha to be present on the page

### Type

#### [CaptchaResponse](/bql-schema/types/objects/captcha-response)  
Response returned after a captcha has been solved

---

# solveImageCaptcha

URL: https://docs.browserless.io/bql-schema/operations/mutations/solve-image-captcha

Solves an image captcha using caller-provided selectors. Unlike solve, which auto-detects known captcha types, this mutation works on any site by accepting custom captchaSelector and inputSelector arguments. It detects the captcha image, solves it, and fills the result into the specified input field.

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation SolveImageCaptcha {
  goto(url: "https://example.com") {
    status
  }
  solveImageCaptcha(
    captchaSelector: "img.captcha-image"
    inputSelector: "input#captcha-input"
    timeout: 60000
  ) {
    found
    solved
    time
    token
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
solveImageCaptcha(
  captchaSelector: String!
  inputSelector: String!
  timeout: Float
): CaptchaResponse
```

### Arguments

#### [solveImageCaptcha.captchaSelector](#captcha-selector)[String!](/bql-schema/types/scalars/string) 
The CSS selector for the captcha image element on the page

#### [solveImageCaptcha.inputSelector](#input-selector)[String!](/bql-schema/types/scalars/string) 
The CSS selector for the input field where the solved captcha value should be filled

#### [solveImageCaptcha.timeout](#timeout)[Float](/bql-schema/types/scalars/float) 
A time, in milliseconds, to wait for the captcha to appear and be solved. Default timeout is 30 seconds, or 30000.

### Type

#### [CaptchaResponse](/bql-schema/types/objects/captcha-response)  
Response returned after a captcha has been solved

---

# stopSessionRecording

URL: https://docs.browserless.io/bql-schema/operations/mutations/stop-session-recording

Stops the current session recording and processes the replay data, similar to closing the context. This mutation will stop RRWeb recording and upload the collected events.

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation StopRecording {
  goto(url: "https://example.com") {
    status
  }

  stopSessionRecording {
    success
    error
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
stopSessionRecording: StopSessionRecordingResponse
```

### Type

#### [StopSessionRecordingResponse](/bql-schema/types/objects/stop-session-recording-response)  
The response received after stopping session recording

---

# switchToWindow

URL: https://docs.browserless.io/bql-schema/operations/mutations/switch-to-window

Switches context to a popup window matching specified criteria

```graphql
switchToWindow(
  url: String
  title: String
  newest: Boolean = false
  timeout: Float = 5000
  waitUntil: WaitUntilGoto
): SwitchWindowResponse
```

### Arguments

#### [switchToWindow.url](#url)[String](/bql-schema/types/scalars/string) 
URL pattern to match against window URLs (supports wildcards)

#### [switchToWindow.title](#title)[String](/bql-schema/types/scalars/string) 
Window title pattern to match

#### [switchToWindow.newest](#newest)[Boolean](/bql-schema/types/scalars/boolean) 
Whether to target most recently opened window

#### [switchToWindow.timeout](#timeout)[Float](/bql-schema/types/scalars/float) 
Maximum time to wait for matching window (in milliseconds)

#### [switchToWindow.waitUntil](#wait-until)[WaitUntilGoto](/bql-schema/types/enums/wait-until-goto) 
When to consider the page fully-loaded and proceed with further execution

### Type

#### [SwitchWindowResponse](/bql-schema/types/objects/switch-window-response)  
Response from window switching operation

---

# text

URL: https://docs.browserless.io/bql-schema/operations/mutations/text

Returns the text content on the given page or by selector when specified

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation GetText {
  goto(url: "https://example.com") {
    status
  }

  selector: text(selector: "h1") {
      text
  }

  fullPage: text {
    text
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
text(
  timeout: Float
  selector: String = "html"
  visible: Boolean = false
  clean: CleanInput
): TextResponse
```

### Arguments

#### [text.timeout](#timeout)[Float](/bql-schema/types/scalars/float) 
The maximum amount of time, in milliseconds, to wait for the selector to appear, overriding any defaults. Default timeout is 30 seconds, or 30000.

#### [text.selector](#selector)[String](/bql-schema/types/scalars/string) 
The DOM selector of the given element you want to return the text of

#### [text.visible](#visible)[Boolean](/bql-schema/types/scalars/boolean) 
Whether or not to return the text content of the element only if it's visible

#### [text.clean](#clean)[CleanInput](/bql-schema/types/inputs/clean-input) 
Specifies conditions for "cleaning" the returned text, useful for minimizing the amount of markup returned for cases like LLMs and more. See nested options for parameters.

### Type

#### [TextResponse](/bql-schema/types/objects/text-response)  
Text content of a page

---

# title

URL: https://docs.browserless.io/bql-schema/operations/mutations/title

Returns the title of the page that the browser is currently at

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation GetTitle {
  goto(url: "https://example.com") {
    status
  }

  title {
    title
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
title: TitleResponse
```

### Type

#### [TitleResponse](/bql-schema/types/objects/title-response)  
Response returned after the page's title has been set or get

---

# type

URL: https://docs.browserless.io/bql-schema/operations/mutations/type

Types text into an element by scrolling to it, clicking it, then emitting key events for every character. Include a `\n` (or `\r`) in `text` to send an Enter/Return key press — useful for submitting search boxes or forms without a separate click. BrowserQL has no standalone key-press mutation, so non-Enter keys cannot be simulated; use `click` for non-keyboard submit interactions (for example, a submit button).

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation TypeText {
  goto(url: "https://example.com") {
    status
  }

  type(
    text: "Hello, World!"
    selector: "input[type='text']"
  ) {
    time
  }

  # Type a query and press Enter to submit
  searchAndSubmit: type(
    text: "browserless\n"
    selector: "input[name='q']"
  ) {
    time
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
type(
  text: String!
  selector: String!
  delay: [Int] = [50, 200]
  interactable: Boolean = true
  scroll: Boolean = true
  timeout: Float
  visible: Boolean = false
  wait: Boolean = true
): TypeResponse
```

### Arguments

#### [type.text](#text)[String!](/bql-schema/types/scalars/string) 
The text content you want to type into the element. A `\n` or `\r` character is sent as an Enter key press.

#### [type.selector](#selector)[String!](/bql-schema/types/scalars/string) 
The CSS selector of the element on the page you want to type text into

#### [type.delay](#delay)[[Int]](/bql-schema/types/scalars/int) 
The amount of delay between keystrokes in milliseconds. Values are used as a range and chosen at random

#### [type.interactable](#interactable)[Boolean](/bql-schema/types/scalars/boolean) 
Whether or not to check if element can be interacted with by hovering over it and seeing if the element
is available at that x and y position

#### [type.scroll](#scroll)[Boolean](/bql-schema/types/scalars/boolean) 
Whether or not to scroll to the element prior to typing, defaults to true

#### [type.timeout](#timeout)[Float](/bql-schema/types/scalars/float) 
How long to wait for the element to appear before timing out, overriding any defaults. Default timeout is 30 seconds, or 30000.

#### [type.visible](#visible)[Boolean](/bql-schema/types/scalars/boolean) 
Whether or not to type into the element only if it's visible

#### [type.wait](#wait)[Boolean](/bql-schema/types/scalars/boolean) 
Whether or not to wait for the element to present in the DOM

### Type

#### [TypeResponse](/bql-schema/types/objects/type-response)  
Response returned after having typed into an element

---

# url

URL: https://docs.browserless.io/bql-schema/operations/mutations/url

Returns the URL of the page that the browser is currently at

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation GetURL {
  goto(url: "https://example.com") {
    status
  }

  url {
    url
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
url: URLResponse
```

### Type

#### [URLResponse](/bql-schema/types/objects/urlresponse)  
Response returned after the URL of the page has been set or get

---

# userAgent

URL: https://docs.browserless.io/bql-schema/operations/mutations/user-agent

Sets the User-Agent string for the browser session

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation SetUserAgent {
  userAgent(userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15") {
    userAgent
    time
  }
  goto(url: "https://example.com") {
    status
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
userAgent(
  userAgent: String!
): UserAgentResponse
```

### Arguments

#### [userAgent.userAgent](#user-agent)[String!](/bql-schema/types/scalars/string) 
The User-Agent string to set for the browser session

### Type

#### [UserAgentResponse](/bql-schema/types/objects/user-agent-response)  
The response returned after setting the User-Agent

---

# verify

URL: https://docs.browserless.io/bql-schema/operations/mutations/verify

> **DEPRECATED**
> Use the `solve` mutation instead. This mutation will be removed in a future version.

⛔ &#x002A;&#x002A;DEPRECATED&#x002A;&#x002A; ⛔
This mutation is deprecated and will be removed in a future version.
Please use the `solve` mutation instead.

---

Clicks a verification button to assert human-like

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation VerifyCaptcha {
  goto(url: "https://protected.domain") {
    status
  }

  verify(type: cloudflare) {
    found
    solved
    time
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
verify(
  type: VerifyTypes!
  timeout: Float
  wait: Boolean = true
): CaptchaResponse @deprecated
```

### Arguments

#### [verify.type](#type)[VerifyTypes!](/bql-schema/types/enums/verify-types) 
An enum of the type of captcha to look for and solve

#### [verify.timeout](#timeout)[Float](/bql-schema/types/scalars/float) 
A time, in milliseconds, to wait for a verification to appear. Only valid when wait = true. Default timeout is 30 seconds, or 30000.

#### [verify.wait](#wait)[Boolean](/bql-schema/types/scalars/boolean) 
Whether or not to wait for the verification to be present on the page

### Type

#### [CaptchaResponse](/bql-schema/types/objects/captcha-response)  
Response returned after a captcha has been solved

---

# viewport

URL: https://docs.browserless.io/bql-schema/operations/mutations/viewport

Sets the viewport dimensions for the browser session

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation SetViewport {
  viewport(width: 1920, height: 1080) {
    width
    height
    time
  }
}
```

&#x002A;&#x002A;Mobile device simulation:&#x002A;&#x002A;
```graphql
mutation MobileViewport {
  viewport(width: 375, height: 667, deviceScaleFactor: 1, mobile: true) {
    width
    height
    deviceScaleFactor
    mobile
    time
  }
}
```

&#x002A;&#x002A;Desktop simulation:&#x002A;&#x002A;
```graphql
mutation DesktopViewport {
  viewport(width: 1920, height: 1080, mobile: false) {
    width
    height
    mobile
    time
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
viewport(
  width: Float!
  height: Float!
  deviceScaleFactor: Float = 1
  mobile: Boolean = false
  hasTouch: Boolean = false
  landscape: Boolean = false
): ViewportResponse
```

### Arguments

#### [viewport.width](#width)[Float!](/bql-schema/types/scalars/float) 
The width of the viewport in pixels

#### [viewport.height](#height)[Float!](/bql-schema/types/scalars/float) 
The height of the viewport in pixels

#### [viewport.deviceScaleFactor](#device-scale-factor)[Float](/bql-schema/types/scalars/float) 
The device scale factor, defaults to 1

#### [viewport.mobile](#mobile)[Boolean](/bql-schema/types/scalars/boolean) 
Whether to simulate a mobile device, defaults to false

#### [viewport.hasTouch](#has-touch)[Boolean](/bql-schema/types/scalars/boolean) 
Whether the viewport supports touch events, defaults to false

#### [viewport.landscape](#landscape)[Boolean](/bql-schema/types/scalars/boolean) 
Whether the viewport is in landscape orientation, defaults to false. Only takes
effect on a session with no active device-emulation profile. When a profile is
active (for example, a stealth session emulating a specific device), the
reported orientation is derived from that device's screen shape instead, and
this argument has no effect on it.

### Type

#### [ViewportResponse](/bql-schema/types/objects/viewport-response)  
Response returned after setting the viewport

---

# waitForEvent

URL: https://docs.browserless.io/bql-schema/operations/mutations/wait-for-event

Waits for a named event to fire on the page's document or window.

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation WaitForEvent {
  goto(url: "https://example.com", waitUntil: domContentLoaded) {
    status
  }

  # Attach the listener *before* the event fires. "load" already fired
  # during goto above, so wait for an event the page dispatches afterwards
  # (eg: a custom event emitted once async setup completes).
  waitForEvent(event: "app:ready") {
    time
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
waitForEvent(
  event: String!
  timeout: Float
): WaitForEvent
```

### Arguments

#### [waitForEvent.event](#event)[String!](/bql-schema/types/scalars/string) 
The name of the event to wait for, eg: "load" or a custom event name

#### [waitForEvent.timeout](#timeout)[Float](/bql-schema/types/scalars/float) 
The maximum amount of time, in milliseconds, to wait for the event to fire, overriding any defaults. Default timeout is 30 seconds, or 30000.

### Type

#### [WaitForEvent](/bql-schema/types/objects/wait-for-event)  
Response returned after having waited for an event to fire on the page

---

# waitForFunction

URL: https://docs.browserless.io/bql-schema/operations/mutations/wait-for-function

Waits for a JavaScript predicate, evaluated in the page, to return a truthy value. The predicate may be a statement or a function expression and can be async.

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation WaitForFunction {
  goto(url: "https://example.com") {
    status
  }

  waitForFunction(fn: "window.dataLayer && window.dataLayer.length > 0") {
    time
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
waitForFunction(
  fn: String!
  polling: Float
  pollingType: WaitForFunctionPolling
  timeout: Float
): WaitForFunction
```

### Arguments

#### [waitForFunction.fn](#fn)[String!](/bql-schema/types/scalars/string) 
The JavaScript predicate to evaluate in the page. May be a statement (eg: "window.ready === true") or a function expression (eg: "() =&#x003E; document.readyState === 'complete'")

#### [waitForFunction.polling](#polling)[Float](/bql-schema/types/scalars/float) 
The interval, in milliseconds, at which the predicate is re-evaluated. Defaults to 100ms

#### [waitForFunction.pollingType](#polling-type)[WaitForFunctionPolling](/bql-schema/types/enums/wait-for-function-polling) 
A browser-driven polling mode. Use this instead of polling for raf or mutation behavior.

#### [waitForFunction.timeout](#timeout)[Float](/bql-schema/types/scalars/float) 
The maximum amount of time, in milliseconds, to wait for the predicate to become truthy, overriding any defaults. Default timeout is 30 seconds, or 30000.

### Type

#### [WaitForFunction](/bql-schema/types/objects/wait-for-function)  
Response returned after having waited for a JavaScript predicate to become truthy

---

# waitForNavigation

URL: https://docs.browserless.io/bql-schema/operations/mutations/wait-for-navigation

Waits for a navigation even to fire, useful for clicking an element and waiting for a page load of some

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation WaitForNavigation {
  goto(url: "https://example.com") {
    status
  }

  waitForNavigation(waitUntil: load) {
    status
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
waitForNavigation(
  timeout: Float
  waitUntil: WaitUntilGoto = load
): HTTPResponse
```

### Arguments

#### [waitForNavigation.timeout](#timeout)[Float](/bql-schema/types/scalars/float) 
The maximum amount of time, in milliseconds, to wait for the page to load, overriding any defaults. Default timeout is 30 seconds, or 30000.

#### [waitForNavigation.waitUntil](#wait-until)[WaitUntilGoto](/bql-schema/types/enums/wait-until-goto) 
When to consider the page fully-loaded and proceed with further execution

### Type

#### [HTTPResponse](/bql-schema/types/objects/httpresponse)  
Response returned after a navigation event

---

# waitForNetworkIdle

URL: https://docs.browserless.io/bql-schema/operations/mutations/wait-for-network-idle

Wait for the page to have no more than the configured number of in-flight network requests for a period of time.

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
waitForNetworkIdle(
  idleTime: Float = 500
  concurrency: Int = 0
  timeout: Float
): WaitForTimeout
```

### Arguments

#### [waitForNetworkIdle.idleTime](#idle-time)[Float](/bql-schema/types/scalars/float) 
The number of milliseconds the network must remain idle.

#### [waitForNetworkIdle.concurrency](#concurrency)[Int](/bql-schema/types/scalars/int) 
The maximum number of in-flight requests allowed while considering the network idle.

#### [waitForNetworkIdle.timeout](#timeout)[Float](/bql-schema/types/scalars/float) 
The maximum amount of time, in milliseconds, to wait for network idle.

### Type

#### [WaitForTimeout](/bql-schema/types/objects/wait-for-timeout)  
Response returned after having waited for a selector to appear in the DOM

---

# waitForRequest

URL: https://docs.browserless.io/bql-schema/operations/mutations/wait-for-request

Waits for the browser to make a particular request

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation WaitForRequest {
  goto(url: "https://browserless.io") {
    status
  }

  waitForRequest(method: GET) {
    time
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
waitForRequest(
  method: Method
  operator: OperatorTypes = or
  timeout: Float
  url: String
): WaitForRequest
```

### Arguments

#### [waitForRequest.method](#method)[Method](/bql-schema/types/enums/method) 
The method of the request to wait for

#### [waitForRequest.operator](#operator)[OperatorTypes](/bql-schema/types/enums/operator-types) 
When applying arguments like URL or method, this operator will either "and" them together, or "or" them together. Default is "or"

#### [waitForRequest.timeout](#timeout)[Float](/bql-schema/types/scalars/float) 
How long to wait for the request to be made before timing out, overriding any defaults. Default timeout is 30 seconds, or 30000.

#### [waitForRequest.url](#url)[String](/bql-schema/types/scalars/string) 
The pattern of the request URL to wait for, using glob-style pattern-matching

### Type

#### [WaitForRequest](/bql-schema/types/objects/wait-for-request)  
Response returned after a particular network request has been sent

---

# waitForResponse

URL: https://docs.browserless.io/bql-schema/operations/mutations/wait-for-response

Waits for a particular network response to be made back to the browser

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation WaitForResponse {
  goto(url: "https://browserless.io") {
    status
  }

  waitForResponse(statuses: [200]) {
    time
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
waitForResponse(
  codes: [Int]
  operator: OperatorTypes = or
  statuses: [Int]
  url: String
): WaitForResponse
```

### Arguments

#### [waitForResponse.codes](#codes)[[Int]](/bql-schema/types/scalars/int) 
> **DEPRECATED**
> Use `statuses` field instead as it is more consistent in BrowserQL.

The HTTP Response code(s) of the URL to wait for. Can be a single HTTP code or a list of desired codes

#### [waitForResponse.operator](#operator)[OperatorTypes](/bql-schema/types/enums/operator-types) 
When applying arguments like URL or statuses, this operator will either "and" them together, or "or" them together. Default is "or"

#### [waitForResponse.statuses](#statuses)[[Int]](/bql-schema/types/scalars/int) 
The HTTP Response code(s) of the URL to wait for. Can be a single HTTP code or a list of desired codes

#### [waitForResponse.url](#url)[String](/bql-schema/types/scalars/string) 
The pattern of the response URL to wait for, using glob-style pattern-matching

### Type

#### [WaitForResponse](/bql-schema/types/objects/wait-for-response)  
Response returned after a particular network response has been received

---

# waitForSelector

URL: https://docs.browserless.io/bql-schema/operations/mutations/wait-for-selector

Waits for a given selector to be present in the DOM, with optional visibility

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation WaitForSelector {
  goto(url: "https://example.com") {
    status
  }

  waitForSelector(selector: "h1") {
    time
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
waitForSelector(
  selector: String!
  timeout: Float
  visible: Boolean = false
  hidden: Boolean = false
): WaitForSelector
```

### Arguments

#### [waitForSelector.selector](#selector)[String!](/bql-schema/types/scalars/string) 
The selector to wait for until present in the DOM

#### [waitForSelector.timeout](#timeout)[Float](/bql-schema/types/scalars/float) 
When waiting for a selector applies a timeout to wait for in milliseconds, overriding any defaults. Default timeout is 30 seconds, or 30000.

#### [waitForSelector.visible](#visible)[Boolean](/bql-schema/types/scalars/boolean) 
Whether or not to consider the element as present only if it's visible

#### [waitForSelector.hidden](#hidden)[Boolean](/bql-schema/types/scalars/boolean) 
Wait for the selector to become hidden, i.e. removed from the DOM or with
no visible bounding box. Mutually exclusive with "visible".

### Type

#### [WaitForSelector](/bql-schema/types/objects/wait-for-selector)  
Response returned after a particular selector has been found in the DOM

---

# waitForTimeout

URL: https://docs.browserless.io/bql-schema/operations/mutations/wait-for-timeout

Wait for a period of time, defined in milliseconds

&#x002A;&#x002A;Usage Example:&#x002A;&#x002A;
```graphql
mutation WaitForTimeout {
  waitForTimeout(time: 1000) {
    time
  }
}
```

&#x002A;&#x002A;Field Definition:&#x002A;&#x002A;

```graphql
waitForTimeout(
  time: Float!
): WaitForTimeout
```

### Arguments

#### [waitForTimeout.time](#time)[Float!](/bql-schema/types/scalars/float) 
The amount of time to wait for, in milliseconds

### Type

#### [WaitForTimeout](/bql-schema/types/objects/wait-for-timeout)  
Response returned after having waited for a selector to appear in the DOM

---

# watchEvent

URL: https://docs.browserless.io/bql-schema/operations/mutations/watch-event

Starts tracking a named document/window event in the current document and
before scripts execute in future documents. A later waitForEvent call returns
immediately when the tracked event has already fired.

```graphql
watchEvent(
  event: String!
): WaitForEvent
```

### Arguments

#### [watchEvent.event](#event)[String!](/bql-schema/types/scalars/string) 
The name of the event to track, eg: "load" or a custom event name

### Type

#### [WaitForEvent](/bql-schema/types/objects/wait-for-event)  
Response returned after having waited for an event to fire on the page

---

# browser

URL: https://docs.browserless.io/bql-schema/operations/queries/browser

The Version of the browser

```graphql
browser: String
```

### Type

#### [String](/bql-schema/types/scalars/string)  
The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.

---

# version

URL: https://docs.browserless.io/bql-schema/operations/queries/version

The Version of this server

```graphql
version: String
```

### Type

#### [String](/bql-schema/types/scalars/string)  
The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.

---

# console

URL: https://docs.browserless.io/bql-schema/operations/subscriptions/console

Streams console output as the page emits it. Mirrors page.on('console').

```graphql
console: ConsoleMessage
```

### Type

#### [ConsoleMessage](/bql-schema/types/objects/console-message)  
A console message emitted by the page. Mirrors puppeteer's ConsoleMessage,
sourced from CDP Runtime.consoleAPICalled and Runtime.exceptionThrown.

---

# request

URL: https://docs.browserless.io/bql-schema/operations/subscriptions/request

Streams each network request as it is issued. Mirrors page.on('request').
The optional filters mirror the request() mutation.

```graphql
request(
  url: [String]
  method: [Method]
  type: [ResourceType]
  operator: OperatorTypes = or
): RequestResponse
```

### Arguments

#### [request.url](#url)[[String]](/bql-schema/types/scalars/string) 
Glob-like URL pattern(s) to match against.

#### [request.method](#method)[[Method]](/bql-schema/types/enums/method) 
HTTP method(s) to match against.

#### [request.type](#type)[[ResourceType]](/bql-schema/types/enums/resource-type) 
Resource type(s) to match against.

#### [request.operator](#operator)[OperatorTypes](/bql-schema/types/enums/operator-types) 
Whether to "and" or "or" the supplied filters together.

### Type

#### [RequestResponse](/bql-schema/types/objects/request-response)  
Response returned from the request API

---

# response

URL: https://docs.browserless.io/bql-schema/operations/subscriptions/response

Streams each network response as it is received. Mirrors page.on('response').
The response body is NOT fetched by default (streaming many bodies is
expensive); request the `body` field in your selection set to trigger a
per-event fetch.

```graphql
response(
  url: [String]
  status: [Int]
  method: [Method]
  type: [ResourceType]
  operator: OperatorTypes = or
): ResponseResponse
```

### Arguments

#### [response.url](#url)[[String]](/bql-schema/types/scalars/string) 
Glob-like URL pattern(s) to match against.

#### [response.status](#status)[[Int]](/bql-schema/types/scalars/int) 
HTTP status code(s) to match against.

#### [response.method](#method)[[Method]](/bql-schema/types/enums/method) 
HTTP method(s) to match against.

#### [response.type](#type)[[ResourceType]](/bql-schema/types/enums/resource-type) 
Resource type(s) to match against.

#### [response.operator](#operator)[OperatorTypes](/bql-schema/types/enums/operator-types) 
Whether to "and" or "or" the supplied filters together.

### Type

#### [ResponseResponse](/bql-schema/types/objects/response-response)  
Response returned from the response API

---

# Responses

URL: https://docs.browserless.io/bql-schema/responses

This page presents reference for all mutations responses. For details on the mutations that produce these responses, see the [BQL language basics](/browserql/writing-bql/language-basics). To learn how to use these in practice, see [Getting Started with BrowserQL](/browserql/getting-started).

## `CaptchaResponse`

Represents the response structure for solving a captcha.

| **Field**   | **Type**    | **Description**                                             |
|-------------|-------------|-------------------------------------------------------------|
| `time`      | `Float`     | The total time it took to find and solve the captcha.        |
| `found`     | `Boolean`   | Indicates if a captcha was found.                           |
| `solved`    | `Boolean`   | Indicates if a captcha was found and solved.                |
| `token`     | `String`    | The solved token of the captcha response, if any is provided. |

## `ClickResponse`

Represents the response structure for clicking on an element.

| **Field**   | **Type**  | **Description**                                                                 |
|-------------|-----------|---------------------------------------------------------------------------------|
| `selector`  | `String`  | The selector text of the clicked element, if specified.                         |
| `x`         | `Float`   | The x-coordinate of the click, in pixels, on the page.                          |
| `y`         | `Float`   | The y-coordinate of the click, in pixels, on the page.                          |
| `time`      | `Float`   | The amount of time, in milliseconds, elapsed since the start of clicking to completion. |

## `CookieResponse`

Represents the response structure for setting and retrieving cookies.

| **Field**   | **Type**           | **Description**                                     |
|-------------|--------------------|-----------------------------------------------------|
| `cookies`   | `[StandardCookie]` | A standard cookie object with the values of the set cookies. |
| `time`      | `Float`            | The time it took to set the cookies.               |

## `DefaultResponse`

Represents the default response structure for methods.

| **Field**   | **Type**  | **Description**                                  |
|-------------|-----------|--------------------------------------------------|
| `timeout`   | `Float`   | The default timeout for all methods.             |

## `EvaluateResponse`

Represents the response structure for evaluating a script in the browser.

| **Field**   | **Type**  | **Description**                                     |
|-------------|-----------|-----------------------------------------------------|
| `value`     | `String`  | The returned value of the script, if any.           |
| `time`      | `Float`   | The time it took for the evaluate call to complete. |

## `HTMLResponse`

Represents the response structure for retrieving a page's HTML content.

| **Field**   | **Type**  | **Description**                                                                 |
|-------------|-----------|---------------------------------------------------------------------------------|
| `html`      | `String`  | The content of the page's HTML.                                                 |
| `time`      | `Float`   | The amount of time, in milliseconds, elapsed since the start of content retrieval to completion. |

## `HTTPResponse`

Represents the HTTP response structure for a page load.

| **Field**   | **Type**  | **Description**                                                                 |
|-------------|-----------|---------------------------------------------------------------------------------|
| `status`    | `Int`     | The status code of the response from the initial page load.                     |
| `text`      | `String`  | The status text of the response from the initial page load, generally `"ok"`.   |
| `url`       | `String`  | The final URL of the page after any potential redirects or URL rewrites.        |
| `time`      | `Float`   | The amount of time, in milliseconds, elapsed since the start of navigation to completion. |

## `HoverResponse`

Represents the response structure for hovering over an element.

| **Field**   | **Type**  | **Description**                                                                 |
|-------------|-----------|---------------------------------------------------------------------------------|
| `selector`  | `String`  | The selector text of the hovered element.                                       |
| `x`         | `Float`   | The x-coordinate in pixels, on the page.                                        |
| `y`         | `Float`   | The y-coordinate in pixels, on the page.                                        |
| `time`      | `Float`   | The amount of time, in milliseconds, elapsed since the start to completion.     |

## `JavaScriptResponse`

Represents the response structure for enabling or disabling JavaScript on a page.

| **Field**   | **Type**    | **Description**                                     |
|-------------|-------------|-----------------------------------------------------|
| `enabled`   | `Boolean`   | Whether JavaScript is enabled on the page.          |
| `time`      | `Float`     | The time it took to perform this action.            |

## `PDFResponse`

Represents the response structure for generating a PDF.

| **Field**    | **Type**  | **Description**                                     |
|--------------|-----------|-----------------------------------------------------|
| `base64`     | `String`  | Base64 encoded PDF content.                         |
| `size`       | `Float`   | The size of the resulting PDF in bytes.             |
| `time`       | `Float`   | The time it took to generate the PDF.               |

## `QuerySelectorResponse`

Represents the response structure for querying an element using `querySelector`.

| **Field**            | **Type**  | **Description**                                                                                     |
|----------------------|-----------|-----------------------------------------------------------------------------------------------------|
| `id`                 | `String`  | The element's identifier, reflecting the `id` global attribute.                                    |
| `childElementCount`  | `Float`   | The number of child elements of this element.                                                      |
| `className`          | `String`  | The value of the class attribute of the specified element.                                         |
| `innerHTML`          | `String`  | The HTML or XML markup contained within the element.                                               |
| `innerText`          | `String`  | The text contained within the element.                                                            |
| `localName`          | `String`  | The local part of the qualified name of the element.                                               |
| `outerHTML`          | `String`  | The serialized HTML fragment describing the element and its descendants.                           |

## `ReconnectionResponse`

Represents the response structure for reconnecting to a browser session.

| **Field**                 | **Type**    | **Description**                                                                                     |
|---------------------------|-------------|-----------------------------------------------------------------------------------------------------|
| `browserQLEndpoint`       | `String`    | The fully-qualified URL to reconnect future BrowserQL sessions. Token information might be required. |
| `browserWSEndpoint`       | `String`    | The fully-qualified URL of the browserWSEndpoint for use with libraries like Playwright or Puppeteer. Token information might be required. |
| `devtoolsFrontendUrl`     | `String`    | The fully-qualified URL of the devtools resources for remotely loading Chrome's developer tools.     |
| `webSocketDebuggerUrl`    | `String`    | The underlying page's WebSocketDebuggerUrl for libraries that operate on a page rather than a browser object. |

## `ScrollResponse`

Represents the response structure for scrolling to an element or coordinate.

| **Field**   | **Type**  | **Description**                                                                 |
|-------------|-----------|---------------------------------------------------------------------------------|
| `selector`  | `String`  | The CSS selector of the element on the page scrolled to.                        |
| `x`         | `Float`   | The x-coordinate, in pixels, to scroll to.                                      |
| `y`         | `Float`   | The y-coordinate, in pixels, to scroll to.                                      |
| `time`      | `Float`   | The amount of time, in milliseconds, elapsed since the start of scrolling to completion. |

## `ScreenshotResponse`

Represents the response structure for taking a screenshot.

| **Field**   | **Type**    | **Description**                                     |
|-------------|-------------|-----------------------------------------------------|
| `base64`    | `String`    | The base64 encoded image of the screenshot.         |
| `format`    | `String`    | The format of the screenshot.                       |
| `time`      | `Float`     | The time it took to take the screenshot.            |

## `SelectResponse`

Represents the response structure for selecting a value from a dropdown or multiple select element.

| **Field**    | **Type**          | **Description**                                                                                     |
|--------------|-------------------|-----------------------------------------------------------------------------------------------------|
| `selector`   | `String`          | The selector of the element you selected from.                                                     |
| `value`      | `StringOrArray`   | The value or values you selected from the select element.                                          |
| `time`       | `Float`           | The amount of time, in milliseconds, elapsed since the start of selecting to completion.            |

## `TextResponse`

Represents the response structure for retrieving the textual content of a page.

| **Field**   | **Type**  | **Description**                                                                 |
|-------------|-----------|---------------------------------------------------------------------------------|
| `text`      | `String`  | The textual content of the page.                                                |
| `time`      | `Float`   | The amount of time, in milliseconds, elapsed since the start of text retrieval to completion. |

## `TitleResponse`

Represents the response structure for retrieving the title of the current page.

| **Field**   | **Type**  | **Description**                     |
|-------------|-----------|-------------------------------------|
| `title`     | `String`  | The title of the current page.      |

## `TypeResponse`

Represents the response structure for typing text into an element.

| **Field**   | **Type**  | **Description**                                                                 |
|-------------|-----------|---------------------------------------------------------------------------------|
| `selector`  | `String`  | The selector of the element you typed into.                                     |
| `text`      | `String`  | The textual content that was typed.                                             |
| `x`         | `Float`   | The x-coordinate of the element, in pixels, on the page.                        |
| `y`         | `Float`   | The y-coordinate of the element, in pixels, on the page.                        |
| `time`      | `Float`   | The amount of time, in milliseconds, elapsed since the start of typing to completion. |

## `URLResponse`

Represents the response structure for retrieving the URL of the current page.

| **Field**   | **Type**  | **Description**                     |
|-------------|-----------|-------------------------------------|
| `url`       | `String`  | The URL of the current page.        |

## `VerifyResponse`

Represents the response structure for a verification action.

| **Field**   | **Type**    | **Description**                                             |
|-------------|-------------|-------------------------------------------------------------|
| `time`      | `Float`     | The total time it took to find and click the verification.   |
| `found`     | `Boolean`   | Indicates if a verification was found.                      |
| `solved`    | `Boolean`   | Indicates if a verification was found and clicked.          |

## `WaitForRequest`

Represents the response structure for waiting for a specific request.

| **Field**   | **Type**  | **Description**                                     |
|-------------|-----------|-----------------------------------------------------|
| `time`      | `Float`   | The period of time elapsed, in milliseconds, waited for. |
| `url`       | `String`  | The URL parameter used to match the response with.  |

## `WaitForResponse`

Represents the response structure for waiting for a specific response.

| **Field**   | **Type**  | **Description**                                     |
|-------------|-----------|-----------------------------------------------------|
| `time`      | `Float`   | The period of time elapsed, in milliseconds, waited for. |
| `status`    | `Int`     | The status code of the response.                    |
| `url`       | `String`  | The URL parameter used to match the response with.  |

## `WaitForSelector`

Represents the response structure for waiting for a selector.

| **Field**   | **Type**  | **Description**                                     |
|-------------|-----------|-----------------------------------------------------|
| `time`      | `Float`   | The period of time elapsed, in milliseconds, waited for. |
| `selector`  | `String`  | The selector waited for.                            |
| `x`         | `Float`   | The x-coordinate position, in pixels, left of the viewport. |
| `y`         | `Float`   | The y-coordinate position, in pixels, top of the viewport. |
| `width`     | `Float`   | The width, in pixels, of the element.               |
| `height`    | `Float`   | The height, in pixels, of the element.              |

## `WaitForTimeout`

Represents the response structure for waiting a specified period.

| **Field**   | **Type**  | **Description**                                     |
|-------------|-----------|-----------------------------------------------------|
| `time`      | `Float`   | The period of time elapsed, in milliseconds, waited for. |

---

# BrowserQL Schema

URL: https://docs.browserless.io/bql-schema/schema

# BrowserQL Schema

This document is a complete technical specification of our full-length GraphQL API, called BrowserQL. For more information on BrowserQL, check [BrowserQL Docs](/browserql/start).

This GraphQL schema contains every mutation, but to get you started the ones you'll use most will probably include:

- [goto](/bql-schema/operations/mutations/goto)
- [reject](/bql-schema/operations/mutations/reject)
- [proxy](/bql-schema/operations/mutations/proxy)
- [click](/bql-schema/operations/mutations/click)
- [type](/bql-schema/operations/mutations/type)
- [html](/bql-schema/operations/mutations/html)
- [reconnect](/bql-schema/operations/mutations/reconnect)

---

# Additional Schemas

URL: https://docs.browserless.io/bql-schema/schemas

This page presents reference for all remaining schemas. For the mutation definitions that use these types, see the [BQL language basics](/browserql/writing-bql/language-basics). For response types returned by mutations, see [responses](/bql-schema/responses).

## `CaptchaTypes`

Represents an enumeration of supported captcha types.

| **Enum Value** | **Description**              |
|----------------|------------------------------|
| `recaptcha`     | Represents the reCAPTCHA type.|
| `recaptchaV3`     | Represents the reCAPTCHA v3 type.|
| `geetest`     | Represents the GeeTest type.|
| `normal`     | Represents the Normal captcha type.|
| `friendlyCaptcha`     | Represents the friendlyCaptcha type.|
| `textCaptcha`     | Represents the textCaptcha type.|
| `amazonWaf`     | Represents the amazonWaf type.|

## `CookieInput`

Represents the structure for defining cookie input properties.

| **Field**     | **Type**          | **Description**                                                                                      |
|---------------|-------------------|------------------------------------------------------------------------------------------------------|
| `name`        | `String!`         | The cookie's name.                                                                                  |
| `value`       | `String!`         | The cookie's value.                                                                                 |
| `url`         | `String`          | The request-URI to associate with the cookie, affecting default domain, path, source port, and scheme. |
| `domain`      | `String`          | The domain associated with the cookie.                                                              |
| `path`        | `String`          | The path associated with the cookie.                                                                |
| `secure`      | `Boolean`         | Indicates if the cookie is secure.                                                                  |
| `httpOnly`    | `Boolean`         | Indicates if the cookie is HTTP-only.                                                               |
| `sameSite`    | `CookieSameSite`  | Specifies the `SameSite` policy for the cookie.                                                     |
| `expires`     | `Float`           | The expiration date of the cookie as a timestamp. Session cookie if not set.                        |

## `CookieSameSite`

Represents an enumeration of supported `SameSite` cookie policies.

| **Enum Value** | **Description**                     |
|----------------|-------------------------------------|
| `Strict`       | Strict `SameSite` policy.           |
| `Lax`          | Lax `SameSite` policy.             |
| `None`         | No `SameSite` policy enforced.      |

## `Method`

Represents an enumeration of supported HTTP methods for waiting.

| **Enum Value** | **Description**                     |
|----------------|-------------------------------------|
| `GET`          | HTTP GET method.                   |
| `HEAD`         | HTTP HEAD method.                  |
| `POST`         | HTTP POST method.                  |
| `PUT`          | HTTP PUT method.                   |
| `DELETE`       | HTTP DELETE method.                |
| `CONNECT`      | HTTP CONNECT method.               |
| `OPTIONS`      | HTTP OPTIONS method.               |
| `TRACE`        | HTTP TRACE method.                 |
| `PATCH`        | HTTP PATCH method.                 |

## `PDFPageFormat`

Represents an enumeration of supported PDF page formats.

| **Enum Value** | **Dimensions**               |
|----------------|------------------------------|
| `letter`       | 8.5in x 11in                |
| `legal`        | 8.5in x 14in                |
| `tabloid`      | 11in x 17in                 |
| `ledger`       | 17in x 11in                 |
| `a0`           | 33.1102in x 46.811in        |
| `a1`           | 23.3858in x 33.1102in       |
| `a2`           | 16.5354in x 23.3858in       |
| `a3`           | 11.6929in x 16.5354in       |
| `a4`           | 8.2677in x 11.6929in        |
| `a5`           | 5.8268in x 8.2677in         |
| `a6`           | 4.1339in x 5.8268in         |

## `RequestInput`

Represents the structure for specifying input for HTTP request monitoring.

| **Field**   | **Type**     | **Description**                                                                                      |
|-------------|--------------|------------------------------------------------------------------------------------------------------|
| `url`       | `String`     | The pattern of the request URL to wait for, using glob-style pattern-matching.                       |
| `method`    | `Method`     | The HTTP method of the request.                                                                      |

## `ResponseInput`

Represents the structure for specifying input for HTTP response monitoring.

| **Field**   | **Type**     | **Description**                                                                                      |
|-------------|--------------|------------------------------------------------------------------------------------------------------|
| `url`       | `String`     | The pattern of the response URL to wait for, using glob-style pattern-matching.                      |
| `codes`     | `[Int]`      | The HTTP response code(s) to wait for. Can be a single HTTP code or a list of desired codes.          |

## `ScreenshotClip`

Represents the structure for specifying a region to clip in a screenshot.

| **Field**   | **Type**  | **Description**                                     |
|-------------|-----------|-----------------------------------------------------|
| `x`         | `Float`   | The x-coordinate to start clipping, in pixels.      |
| `y`         | `Float`   | The y-coordinate to start clipping, in pixels.      |
| `width`     | `Float`   | The width of the clip, in pixels.                   |
| `height`    | `Float`   | The height of the clip, in pixels.                  |
| `scale`     | `Float`   | The scale factor of the clip.                       |

## `ScreenshotType`

Represents an enumeration of supported screenshot formats.

| **Enum Value** | **Description**                |
|----------------|--------------------------------|
| `jpeg`         | JPEG image format.             |
| `png`          | PNG image format.              |
| `webp`         | WEBP image format.             |

## `StandardCookie`

Represents the structure of a standard cookie object.

| **Field**     | **Type**          | **Description**                                                                                      |
|---------------|-------------------|------------------------------------------------------------------------------------------------------|
| `name`        | `String!`         | The cookie's name.                                                                                  |
| `value`       | `String!`         | The cookie's value.                                                                                 |
| `url`         | `String`          | The request-URI to associate with the cookie, affecting default domain, path, source port, and scheme. |
| `domain`      | `String`          | The domain associated with the cookie.                                                              |
| `path`        | `String`          | The path associated with the cookie.                                                                |
| `secure`      | `Boolean`         | Indicates if the cookie is secure.                                                                  |
| `httpOnly`    | `Boolean`         | Indicates if the cookie is HTTP-only.                                                               |
| `sameSite`    | `CookieSameSite`  | Specifies the `SameSite` policy for the cookie.                                                     |
| `expires`     | `Float`           | The expiration date of the cookie as a timestamp. Session cookie if not set.                        |

## `VerifyTypes`

Represents an enumeration of supported verification types.

| **Enum Value** | **Description**              |
|----------------|------------------------------|
| `cloudflare`   | Represents the Cloudflare verification type. |

## `WaitUntilGoto`

Represents an enumeration of options for determining when the page is considered loaded.

| **Enum Value**         | **Description**                                                                 |
|------------------------|---------------------------------------------------------------------------------|
| `commit`               | Fired when a network response is received and the document starts loading.      |
| `domContentLoaded`     | Fired when the `DOMContentLoaded` event is fired.                               |
| `load`                 | Fired when the `load` event occurs.                                             |
| `firstMeaningfulPaint` | Indicates when the primary content of the page is visible to the user.          |
| `firstContentfulPaint` | The render time of the largest image or text block visible in the viewport.     |
| `networkIdle`          | Fired when there are no network connections for at least 500 ms. Use with caution. |
| `interactiveTime`      | Chrome's best guess as to when the page becomes interactable. Use with caution. |

## `WaitUntilHistory`

Represents an enumeration of options for determining when the history event occurs.

| **Enum Value**        | **Description**                                                                 |
|-----------------------|---------------------------------------------------------------------------------|
| `commit`              | Fired when a network response is received and the document starts loading.      |
| `domContentLoaded`    | Fired when the `DOMContentLoaded` event is fired.                               |
| `load`                | Fired when the `load` event occurs.                                             |
| `networkIdle`         | Fired when there are no network connections for at least 500 ms. Use with caution. |

---

# deprecated

URL: https://docs.browserless.io/bql-schema/types/directives/deprecated

Marks an element of a GraphQL schema as no longer supported.

```graphql
directive @deprecated(
  reason: String = "No longer supported"
) on 
  | FIELD_DEFINITION
  | ARGUMENT_DEFINITION
  | INPUT_FIELD_DEFINITION
  | ENUM_VALUE
  | DIRECTIVE_DEFINITION
```

### Arguments

#### [deprecated.reason](#reason)[String](/bql-schema/types/scalars/string) 
Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax, as specified by [CommonMark](https://commonmark.org/).

---

# oneOf

URL: https://docs.browserless.io/bql-schema/types/directives/one-of

Indicates exactly one field must be supplied and this field must not be `null`.

```graphql
directive @oneOf on INPUT_OBJECT
```

---

# specifiedBy

URL: https://docs.browserless.io/bql-schema/types/directives/specified-by

Exposes a URL that specifies the behavior of this scalar.

```graphql
directive @specifiedBy(
  url: String!
) on SCALAR
```

### Arguments

#### [specifiedBy.url](#url)[String!](/bql-schema/types/scalars/string) 
The URL that specifies the behavior of this scalar.

---

# AttributeMode

URL: https://docs.browserless.io/bql-schema/types/enums/attribute-mode

Controls how the attributes field is interpreted in cleaning operations

```graphql
enum AttributeMode {
  ALLOW
  DENY
}
```

### Values

#### [AttributeMode.ALLOW](#allow) 
The attributes field specifies which attributes to keep (preserve only these)

#### [AttributeMode.DENY](#deny) 
The attributes field specifies which attributes to remove (remove only these)

### Member Of

[`CleanInput`](/bql-schema/types/inputs/clean-input)

---

# CaptchaTypes

URL: https://docs.browserless.io/bql-schema/types/enums/captcha-types

The different types of captchas that can be solved

```graphql
enum CaptchaTypes {
  cloudflare
  recaptcha
  recaptchaV3
  geetest
  normal
  numberCaptcha
  friendlyCaptcha
  capy
  textCaptcha
  amazonWaf
  datadome
  akamai
  lemin
  mtcaptcha
  slider
  audio
  funcaptcha
  click
  yandex
  math
  perimeterx
}
```

### Values

#### [CaptchaTypes.cloudflare](#cloudflare) 
cloudflare captcha type

#### [CaptchaTypes.recaptcha](#recaptcha) 
reCAPTCHA captcha type

#### [CaptchaTypes.recaptchaV3](#recaptcha-v3) 
reCAPTCHA v3 captcha type

#### [CaptchaTypes.geetest](#geetest) 
GeeTest captcha type

#### [CaptchaTypes.normal](#normal) 
Normal captcha type

#### [CaptchaTypes.numberCaptcha](#number-captcha) 
Number captcha type (numeric-only image challenge)

#### [CaptchaTypes.friendlyCaptcha](#friendly-captcha) 
friendlyCaptcha captcha type

#### [CaptchaTypes.capy](#capy) 
Capy captcha type

#### [CaptchaTypes.textCaptcha](#text-captcha) 
textCaptcha captcha type

#### [CaptchaTypes.amazonWaf](#amazon-waf) 
amazonWaf captcha type

#### [CaptchaTypes.datadome](#datadome) 
DataDome captcha type

#### [CaptchaTypes.akamai](#akamai) 
Akamai Bot Manager captcha type

#### [CaptchaTypes.lemin](#lemin) 
Lemin captcha type

#### [CaptchaTypes.mtcaptcha](#mtcaptcha) 
mtCaptcha captcha type

#### [CaptchaTypes.slider](#slider) 
Generic slider captcha type

#### [CaptchaTypes.audio](#audio) 
Audio captcha type (reCAPTCHA v2 audio accessibility fallback)

#### [CaptchaTypes.funcaptcha](#funcaptcha) 
FunCaptcha (Arkoselabs) captcha type

#### [CaptchaTypes.click](#click) 
Click captcha type (coordinate-based challenges)

#### [CaptchaTypes.yandex](#yandex) 
Yandex SmartCaptcha type

#### [CaptchaTypes.math](#math) 
Math captcha type (image containing a math expression to solve)

#### [CaptchaTypes.perimeterx](#perimeterx) 
PerimeterX / HUMAN Press & Hold captcha type

### Member Of

[`solve`](/bql-schema/operations/mutations/solve)

---

# ConsoleMessageType

URL: https://docs.browserless.io/bql-schema/types/enums/console-message-type

The kind of console message, mirroring the CDP Runtime.consoleAPICalled type
(and puppeteer's ConsoleMessageType). Uncaught exceptions are reported as `error`.

```graphql
enum ConsoleMessageType {
  log
  debug
  info
  error
  warning
  dir
  dirxml
  table
  trace
  clear
  startGroup
  startGroupCollapsed
  endGroup
  assert
  profile
  profileEnd
  count
  timeEnd
  verbose
}
```

### Values

#### [ConsoleMessageType.log](#log) 

#### [ConsoleMessageType.debug](#debug) 

#### [ConsoleMessageType.info](#info) 

#### [ConsoleMessageType.error](#error) 

#### [ConsoleMessageType.warning](#warning) 

#### [ConsoleMessageType.dir](#dir) 

#### [ConsoleMessageType.dirxml](#dirxml) 

#### [ConsoleMessageType.table](#table) 

#### [ConsoleMessageType.trace](#trace) 

#### [ConsoleMessageType.clear](#clear) 

#### [ConsoleMessageType.startGroup](#start-group) 

#### [ConsoleMessageType.startGroupCollapsed](#start-group-collapsed) 

#### [ConsoleMessageType.endGroup](#end-group) 

#### [ConsoleMessageType.assert](#assert) 

#### [ConsoleMessageType.profile](#profile) 

#### [ConsoleMessageType.profileEnd](#profile-end) 

#### [ConsoleMessageType.count](#count) 

#### [ConsoleMessageType.timeEnd](#time-end) 

#### [ConsoleMessageType.verbose](#verbose) 

### Member Of

[`ConsoleMessage`](/bql-schema/types/objects/console-message)

---

# CookiePriority

URL: https://docs.browserless.io/bql-schema/types/enums/cookie-priority

No description

```graphql
enum CookiePriority {
  Low
  Medium
  High
}
```

### Values

#### [CookiePriority.Low](#low) 

#### [CookiePriority.Medium](#medium) 

#### [CookiePriority.High](#high) 

### Member Of

[`CookieInput`](/bql-schema/types/inputs/cookie-input)

---

# CookieSameSite

URL: https://docs.browserless.io/bql-schema/types/enums/cookie-same-site

The values a cookie's SameSite attribute can hold

```graphql
enum CookieSameSite {
  Strict
  Lax
  None
}
```

### Values

#### [CookieSameSite.Strict](#strict) 
Cookies are only sent in a first-party context and not sent along with requests initiated by third party websites

#### [CookieSameSite.Lax](#lax) 
Cookies are withheld on cross-site sub-requests, such as calls to load images or frames, but are sent when a user navigates to the URL from an external site

#### [CookieSameSite.None](#none) 
Cookies are sent in all contexts, in responses to both first-party and cross-origin requests

### Member Of

[`CookieInput`](/bql-schema/types/inputs/cookie-input)  [`StandardCookie`](/bql-schema/types/objects/standard-cookie)

---

# CookieSourceScheme

URL: https://docs.browserless.io/bql-schema/types/enums/cookie-source-scheme

No description

```graphql
enum CookieSourceScheme {
  Unset
  NonSecure
  Secure
}
```

### Values

#### [CookieSourceScheme.Unset](#unset) 

#### [CookieSourceScheme.NonSecure](#non-secure) 

#### [CookieSourceScheme.Secure](#secure) 

### Member Of

[`CookieInput`](/bql-schema/types/inputs/cookie-input)

---

# CountryType

URL: https://docs.browserless.io/bql-schema/types/enums/country-type

The two-letter ISO code for the specified country

```graphql
enum CountryType {
  AD
  AE
  AF
  AG
  AL
  AM
  AO
  AR
  AT
  AU
  AW
  AX
  AZ
  BA
  BB
  BD
  BE
  BF
  BG
  BH
  BI
  BJ
  BM
  BN
  BO
  BQ
  BR
  BS
  BT
  BW
  BY
  BZ
  CA
  CD
  CG
  CH
  CI
  CL
  CM
  CN
  CO
  CR
  CU
  CV
  CW
  CY
  CZ
  DE
  DJ
  DK
  DM
  DO
  DZ
  EC
  EE
  EG
  ES
  ET
  FI
  FJ
  FO
  FR
  GA
  GB
  GD
  GE
  GF
  GG
  GH
  GI
  GM
  GN
  GP
  GR
  GT
  GU
  GY
  HK
  HN
  HR
  HT
  HU
  ID
  IE
  IL
  IM
  IN
  IQ
  IR
  IS
  IT
  JE
  JM
  JO
  JP
  KE
  KG
  KH
  KN
  KR
  KW
  KY
  KZ
  LA
  LB
  LC
  LI
  LK
  LR
  LS
  LT
  LU
  LV
  LY
  MA
  MC
  MD
  ME
  MG
  MK
  ML
  MM
  MN
  MO
  MQ
  MR
  MS
  MT
  MU
  MV
  MW
  MX
  MY
  MZ
  NA
  NC
  NG
  NI
  NL
  NO
  NP
  NZ
  OM
  PA
  PE
  PF
  PG
  PH
  PK
  PL
  PR
  PS
  PT
  PY
  QA
  RE
  RO
  RS
  RU
  RW
  SC
  SE
  SG
  SI
  SK
  SL
  SM
  SN
  SO
  SR
  SS
  ST
  SV
  SX
  SY
  SZ
  TC
  TG
  TH
  TJ
  TN
  TR
  TT
  TW
  TZ
  UA
  UG
  US
  UY
  UZ
  VC
  VE
  VG
  VI
  VN
  WS
  YT
  ZA
  ZM
  ZW
}
```

### Values

#### [CountryType.AD](#ad) 
Andorra

#### [CountryType.AE](#ae) 
United Arab Emirates

#### [CountryType.AF](#af) 
Afghanistan

#### [CountryType.AG](#ag) 
Antigua and Barbuda

#### [CountryType.AL](#al) 
Albania

#### [CountryType.AM](#am) 
Armenia

#### [CountryType.AO](#ao) 
Angola

#### [CountryType.AR](#ar) 
Argentina

#### [CountryType.AT](#at) 
Austria

#### [CountryType.AU](#au) 
Australia

#### [CountryType.AW](#aw) 
Aruba

#### [CountryType.AX](#ax) 
Åland Islands

#### [CountryType.AZ](#az) 
Azerbaijan

#### [CountryType.BA](#ba) 
Bosnia and Herzegovina

#### [CountryType.BB](#bb) 
Barbados

#### [CountryType.BD](#bd) 
Bangladesh

#### [CountryType.BE](#be) 
Belgium

#### [CountryType.BF](#bf) 
Burkina Faso

#### [CountryType.BG](#bg) 
Bulgaria

#### [CountryType.BH](#bh) 
Bahrain

#### [CountryType.BI](#bi) 
Burundi

#### [CountryType.BJ](#bj) 
Benin

#### [CountryType.BM](#bm) 
Bermuda

#### [CountryType.BN](#bn) 
Brunei Darussalam

#### [CountryType.BO](#bo) 
Plurinational State of Bolivia

#### [CountryType.BQ](#bq) 
Sint Eustatius and Saba Bonaire

#### [CountryType.BR](#br) 
Brazil

#### [CountryType.BS](#bs) 
Bahamas

#### [CountryType.BT](#bt) 
Bhutan

#### [CountryType.BW](#bw) 
Botswana

#### [CountryType.BY](#by) 
Belarus

#### [CountryType.BZ](#bz) 
Belize

#### [CountryType.CA](#ca) 
Canada

#### [CountryType.CD](#cd) 
Congo, The Democratic Republic of the

#### [CountryType.CG](#cg) 
Congo

#### [CountryType.CH](#ch) 
Switzerland

#### [CountryType.CI](#ci) 
Côte d'Ivoire

#### [CountryType.CL](#cl) 
Chile

#### [CountryType.CM](#cm) 
Cameroon

#### [CountryType.CN](#cn) 
China

#### [CountryType.CO](#co) 
Colombia

#### [CountryType.CR](#cr) 
Costa Rica

#### [CountryType.CU](#cu) 
Cuba

#### [CountryType.CV](#cv) 
Cabo Verde

#### [CountryType.CW](#cw) 
Curaçao

#### [CountryType.CY](#cy) 
Cyprus

#### [CountryType.CZ](#cz) 
Czechia

#### [CountryType.DE](#de) 
Germany

#### [CountryType.DJ](#dj) 
Djibouti

#### [CountryType.DK](#dk) 
Denmark

#### [CountryType.DM](#dm) 
Dominica

#### [CountryType.DO](#do) 
Dominican Republic

#### [CountryType.DZ](#dz) 
Algeria

#### [CountryType.EC](#ec) 
Ecuador

#### [CountryType.EE](#ee) 
Estonia

#### [CountryType.EG](#eg) 
Egypt

#### [CountryType.ES](#es) 
Spain

#### [CountryType.ET](#et) 
Ethiopia

#### [CountryType.FI](#fi) 
Finland

#### [CountryType.FJ](#fj) 
Fiji

#### [CountryType.FO](#fo) 
Faroe Islands

#### [CountryType.FR](#fr) 
France

#### [CountryType.GA](#ga) 
Gabon

#### [CountryType.GB](#gb) 
United Kingdom

#### [CountryType.GD](#gd) 
Grenada

#### [CountryType.GE](#ge) 
Georgia

#### [CountryType.GF](#gf) 
French Guiana

#### [CountryType.GG](#gg) 
Guernsey

#### [CountryType.GH](#gh) 
Ghana

#### [CountryType.GI](#gi) 
Gibraltar

#### [CountryType.GM](#gm) 
Gambia

#### [CountryType.GN](#gn) 
Guinea

#### [CountryType.GP](#gp) 
Guadeloupe

#### [CountryType.GR](#gr) 
Greece

#### [CountryType.GT](#gt) 
Guatemala

#### [CountryType.GU](#gu) 
Guam

#### [CountryType.GY](#gy) 
Guyana

#### [CountryType.HK](#hk) 
Hong Kong

#### [CountryType.HN](#hn) 
Honduras

#### [CountryType.HR](#hr) 
Croatia

#### [CountryType.HT](#ht) 
Haiti

#### [CountryType.HU](#hu) 
Hungary

#### [CountryType.ID](#id) 
Indonesia

#### [CountryType.IE](#ie) 
Ireland

#### [CountryType.IL](#il) 
Israel

#### [CountryType.IM](#im) 
Isle of Man

#### [CountryType.IN](#in) 
India

#### [CountryType.IQ](#iq) 
Iraq

#### [CountryType.IR](#ir) 
Iran, Islamic Republic of

#### [CountryType.IS](#is) 
Iceland

#### [CountryType.IT](#it) 
Italy

#### [CountryType.JE](#je) 
Jersey

#### [CountryType.JM](#jm) 
Jamaica

#### [CountryType.JO](#jo) 
Jordan

#### [CountryType.JP](#jp) 
Japan

#### [CountryType.KE](#ke) 
Kenya

#### [CountryType.KG](#kg) 
Kyrgyzstan

#### [CountryType.KH](#kh) 
Cambodia

#### [CountryType.KN](#kn) 
Saint Kitts and Nevis

#### [CountryType.KR](#kr) 
Korea, Republic of

#### [CountryType.KW](#kw) 
Kuwait

#### [CountryType.KY](#ky) 
Cayman Islands

#### [CountryType.KZ](#kz) 
Kazakhstan

#### [CountryType.LA](#la) 
Lao People's Democratic Republic

#### [CountryType.LB](#lb) 
Lebanon

#### [CountryType.LC](#lc) 
Saint Lucia

#### [CountryType.LI](#li) 
Liechtenstein

#### [CountryType.LK](#lk) 
Sri Lanka

#### [CountryType.LR](#lr) 
Liberia

#### [CountryType.LS](#ls) 
Lesotho

#### [CountryType.LT](#lt) 
Lithuania

#### [CountryType.LU](#lu) 
Luxembourg

#### [CountryType.LV](#lv) 
Latvia

#### [CountryType.LY](#ly) 
Libya

#### [CountryType.MA](#ma) 
Morocco

#### [CountryType.MC](#mc) 
Monaco

#### [CountryType.MD](#md) 
Moldova, Republic of

#### [CountryType.ME](#me) 
Montenegro

#### [CountryType.MG](#mg) 
Madagascar

#### [CountryType.MK](#mk) 
North Macedonia

#### [CountryType.ML](#ml) 
Mali

#### [CountryType.MM](#mm) 
Myanmar

#### [CountryType.MN](#mn) 
Mongolia

#### [CountryType.MO](#mo) 
Macao

#### [CountryType.MQ](#mq) 
Martinique

#### [CountryType.MR](#mr) 
Mauritania

#### [CountryType.MS](#ms) 
Montserrat

#### [CountryType.MT](#mt) 
Malta

#### [CountryType.MU](#mu) 
Mauritius

#### [CountryType.MV](#mv) 
Maldives

#### [CountryType.MW](#mw) 
Malawi

#### [CountryType.MX](#mx) 
Mexico

#### [CountryType.MY](#my) 
Malaysia

#### [CountryType.MZ](#mz) 
Mozambique

#### [CountryType.NA](#na) 
Namibia

#### [CountryType.NC](#nc) 
New Caledonia

#### [CountryType.NG](#ng) 
Nigeria

#### [CountryType.NI](#ni) 
Nicaragua

#### [CountryType.NL](#nl) 
Netherlands

#### [CountryType.NO](#no) 
Norway

#### [CountryType.NP](#np) 
Nepal

#### [CountryType.NZ](#nz) 
New Zealand

#### [CountryType.OM](#om) 
Oman

#### [CountryType.PA](#pa) 
Panama

#### [CountryType.PE](#pe) 
Peru

#### [CountryType.PF](#pf) 
French Polynesia

#### [CountryType.PG](#pg) 
Papua New Guinea

#### [CountryType.PH](#ph) 
Philippines

#### [CountryType.PK](#pk) 
Pakistan

#### [CountryType.PL](#pl) 
Poland

#### [CountryType.PR](#pr) 
Puerto Rico

#### [CountryType.PS](#ps) 
Palestine, State of

#### [CountryType.PT](#pt) 
Portugal

#### [CountryType.PY](#py) 
Paraguay

#### [CountryType.QA](#qa) 
Qatar

#### [CountryType.RE](#re) 
Réunion

#### [CountryType.RO](#ro) 
Romania

#### [CountryType.RS](#rs) 
Serbia

#### [CountryType.RU](#ru) 
Russian Federation

#### [CountryType.RW](#rw) 
Rwanda

#### [CountryType.SC](#sc) 
Seychelles

#### [CountryType.SE](#se) 
Sweden

#### [CountryType.SG](#sg) 
Singapore

#### [CountryType.SI](#si) 
Slovenia

#### [CountryType.SK](#sk) 
Slovakia

#### [CountryType.SL](#sl) 
Sierra Leone

#### [CountryType.SM](#sm) 
San Marino

#### [CountryType.SN](#sn) 
Senegal

#### [CountryType.SO](#so) 
Somalia

#### [CountryType.SR](#sr) 
Suriname

#### [CountryType.SS](#ss) 
South Sudan

#### [CountryType.ST](#st) 
Sao Tome and Principe

#### [CountryType.SV](#sv) 
El Salvador

#### [CountryType.SX](#sx) 
Sint Maarten (Dutch part)

#### [CountryType.SY](#sy) 
Syrian Arab Republic

#### [CountryType.SZ](#sz) 
Eswatini

#### [CountryType.TC](#tc) 
Turks and Caicos Islands

#### [CountryType.TG](#tg) 
Togo

#### [CountryType.TH](#th) 
Thailand

#### [CountryType.TJ](#tj) 
Tajikistan

#### [CountryType.TN](#tn) 
Tunisia

#### [CountryType.TR](#tr) 
Turkey

#### [CountryType.TT](#tt) 
Trinidad and Tobago

#### [CountryType.TW](#tw) 
Taiwan, Province of China

#### [CountryType.TZ](#tz) 
United Republic of Tanzania

#### [CountryType.UA](#ua) 
Ukraine

#### [CountryType.UG](#ug) 
Uganda

#### [CountryType.US](#us) 
United States of America

#### [CountryType.UY](#uy) 
Uruguay

#### [CountryType.UZ](#uz) 
Uzbekistan

#### [CountryType.VC](#vc) 
Saint Vincent and the Grenadines

#### [CountryType.VE](#ve) 
Bolivarian Republic of Venezuela

#### [CountryType.VG](#vg) 
Virgin Islands, British

#### [CountryType.VI](#vi) 
Virgin Islands, U.S.

#### [CountryType.VN](#vn) 
Vietnam

#### [CountryType.WS](#ws) 
Samoa

#### [CountryType.YT](#yt) 
Mayotte

#### [CountryType.ZA](#za) 
South Africa

#### [CountryType.ZM](#zm) 
Zambia

#### [CountryType.ZW](#zw) 
Zimbabwe

### Member Of

[`proxy`](/bql-schema/operations/mutations/proxy)

---

# LiveURLStreamType

URL: https://docs.browserless.io/bql-schema/types/enums/live-urlstream-type

The binary-type of the stream

```graphql
enum LiveURLStreamType {
  jpeg
  png
}
```

### Values

#### [LiveURLStreamType.jpeg](#jpeg) 
JPEG formatted binary type

#### [LiveURLStreamType.png](#png) 
PNG formatted binary type

### Member Of

[`liveURL`](/bql-schema/operations/mutations/live-url)

---

# MediaType

URL: https://docs.browserless.io/bql-schema/types/enums/media-type

The CSS media type to emulate for the page

```graphql
enum MediaType {
  screen
  print
}
```

### Values

#### [MediaType.screen](#screen) 
Emulate on-screen rendering

#### [MediaType.print](#print) 
Emulate print rendering

### Member Of

[`emulateMediaType`](/bql-schema/operations/mutations/emulate-media-type)

---

# Method

URL: https://docs.browserless.io/bql-schema/types/enums/method

The various HTTP-based methods to wait for

```graphql
enum Method {
  CONNECT
  DELETE
  GET
  HEAD
  OPTIONS
  PATCH
  POST
  PUT
  TRACE
}
```

### Values

#### [Method.CONNECT](#connect) 

#### [Method.DELETE](#delete) 

#### [Method.GET](#get) 

#### [Method.HEAD](#head) 

#### [Method.OPTIONS](#options) 

#### [Method.PATCH](#patch) 

#### [Method.POST](#post) 

#### [Method.PUT](#put) 

#### [Method.TRACE](#trace) 

### Member Of

[`fulfill`](/bql-schema/operations/mutations/fulfill)  [`proxy`](/bql-schema/operations/mutations/proxy)  [`reject`](/bql-schema/operations/mutations/reject)  [`request`](/bql-schema/operations/mutations/request)  [`request`](/bql-schema/operations/subscriptions/request)  [`RequestInput`](/bql-schema/types/inputs/request-input)  [`RequestResponse`](/bql-schema/types/objects/request-response)  [`response`](/bql-schema/operations/mutations/response)  [`response`](/bql-schema/operations/subscriptions/response)  [`ResponseResponse`](/bql-schema/types/objects/response-response)  [`waitForRequest`](/bql-schema/operations/mutations/wait-for-request)

---

# OperatorTypes

URL: https://docs.browserless.io/bql-schema/types/enums/operator-types

Logical operators for APIs that have conditions

```graphql
enum OperatorTypes {
  and
  or
}
```

### Values

#### [OperatorTypes.and](#and) 
"and" logical operator

#### [OperatorTypes.or](#or) 
"or" logical operator

### Member Of

[`fulfill`](/bql-schema/operations/mutations/fulfill)  [`if`](/bql-schema/operations/mutations/if)  [`ifnot`](/bql-schema/operations/mutations/ifnot)  [`proxy`](/bql-schema/operations/mutations/proxy)  [`reject`](/bql-schema/operations/mutations/reject)  [`request`](/bql-schema/operations/mutations/request)  [`request`](/bql-schema/operations/subscriptions/request)  [`response`](/bql-schema/operations/mutations/response)  [`response`](/bql-schema/operations/subscriptions/response)  [`waitForRequest`](/bql-schema/operations/mutations/wait-for-request)  [`waitForResponse`](/bql-schema/operations/mutations/wait-for-response)

---

# PDFPageFormat

URL: https://docs.browserless.io/bql-schema/types/enums/pdfpage-format

The values a pdf's Size attribute can hold

```graphql
enum PDFPageFormat {
  a0
  a1
  a2
  a3
  a4
  a5
  a6
  legal
  letter
  ledger
  tabloid
}
```

### Values

#### [PDFPageFormat.a0](#a-0) 
33.1102in x 46.811in

#### [PDFPageFormat.a1](#a-1) 
23.3858in x 33.1102in

#### [PDFPageFormat.a2](#a-2) 
16.5354in x 23.3858in

#### [PDFPageFormat.a3](#a-3) 
11.6929in x 16.5354in

#### [PDFPageFormat.a4](#a-4) 
8.2677in x 11.6929in

#### [PDFPageFormat.a5](#a-5) 
5.8268in x 8.2677in

#### [PDFPageFormat.a6](#a-6) 
4.1339in x 5.8268in

#### [PDFPageFormat.legal](#legal) 
8.5in x 14in

#### [PDFPageFormat.letter](#letter) 
8.5in x 11in

#### [PDFPageFormat.ledger](#ledger) 
17in x 11in

#### [PDFPageFormat.tabloid](#tabloid) 
11in x 17in

### Member Of

[`pdf`](/bql-schema/operations/mutations/pdf)

---

# ProxyNetwork

URL: https://docs.browserless.io/bql-schema/types/enums/proxy-network

The Browserless proxy network to route matching requests through.

```graphql
enum ProxyNetwork {
  residential
  datacenter
}
```

### Values

#### [ProxyNetwork.residential](#residential) 
Residential IP pool (default)

#### [ProxyNetwork.datacenter](#datacenter) 
Datacenter IP pool (cheaper, billed at 2 units/MB vs 6 units/MB for residential)

### Member Of

[`proxy`](/bql-schema/operations/mutations/proxy)

---

# ResourceType

URL: https://docs.browserless.io/bql-schema/types/enums/resource-type

The type of resource for a given network or HTTP request

```graphql
enum ResourceType {
  cspviolationreport
  document
  eventsource
  fetch
  font
  image
  manifest
  media
  other
  ping
  prefetch
  preflight
  script
  signedexchange
  stylesheet
  texttrack
  websocket
  xhr
}
```

### Values

#### [ResourceType.cspviolationreport](#cspviolationreport) 

#### [ResourceType.document](#document) 

#### [ResourceType.eventsource](#eventsource) 

#### [ResourceType.fetch](#fetch) 

#### [ResourceType.font](#font) 

#### [ResourceType.image](#image) 

#### [ResourceType.manifest](#manifest) 

#### [ResourceType.media](#media) 

#### [ResourceType.other](#other) 

#### [ResourceType.ping](#ping) 

#### [ResourceType.prefetch](#prefetch) 

#### [ResourceType.preflight](#preflight) 

#### [ResourceType.script](#script) 

#### [ResourceType.signedexchange](#signedexchange) 

#### [ResourceType.stylesheet](#stylesheet) 

#### [ResourceType.texttrack](#texttrack) 

#### [ResourceType.websocket](#websocket) 

#### [ResourceType.xhr](#xhr) 

### Member Of

[`fulfill`](/bql-schema/operations/mutations/fulfill)  [`proxy`](/bql-schema/operations/mutations/proxy)  [`reject`](/bql-schema/operations/mutations/reject)  [`request`](/bql-schema/operations/mutations/request)  [`request`](/bql-schema/operations/subscriptions/request)  [`RequestResponse`](/bql-schema/types/objects/request-response)  [`response`](/bql-schema/operations/mutations/response)  [`response`](/bql-schema/operations/subscriptions/response)  [`ResponseResponse`](/bql-schema/types/objects/response-response)

---

# ScreenshotType

URL: https://docs.browserless.io/bql-schema/types/enums/screenshot-type

The different types of screenshot formats

```graphql
enum ScreenshotType {
  jpeg
  png
  webp
}
```

### Values

#### [ScreenshotType.jpeg](#jpeg) 
JPEG image format

#### [ScreenshotType.png](#png) 
PNG image format

#### [ScreenshotType.webp](#webp) 
WebP image format

### Member Of

[`screenshot`](/bql-schema/operations/mutations/screenshot)

---

# VerifyTypes

URL: https://docs.browserless.io/bql-schema/types/enums/verify-types

The different types of verification methods that can be bypassed

```graphql
enum VerifyTypes {
  cloudflare
}
```

### Values

#### [VerifyTypes.cloudflare](#cloudflare) 
Cloudflare protection

### Member Of

[`verify`](/bql-schema/operations/mutations/verify)

---

# WaitForFunctionPolling

URL: https://docs.browserless.io/bql-schema/types/enums/wait-for-function-polling

Browser-driven polling modes for waitForFunction

```graphql
enum WaitForFunctionPolling {
  raf
  mutation
}
```

### Values

#### [WaitForFunctionPolling.raf](#raf) 
Evaluate on each animation frame

#### [WaitForFunctionPolling.mutation](#mutation) 
Evaluate after DOM mutations

### Member Of

[`waitForFunction`](/bql-schema/operations/mutations/wait-for-function)

---

# WaitUntilGoto

URL: https://docs.browserless.io/bql-schema/types/enums/wait-until-goto

Options for when to consider the page has loaded and proceed with further execution

```graphql
enum WaitUntilGoto {
  commit
  load
  domContentLoaded
  firstMeaningfulPaint
  firstContentfulPaint
  interactiveTime
  networkIdle
  networkAlmostIdle
}
```

### Values

#### [WaitUntilGoto.commit](#commit) 
Fired when network response is received and the document has started loading

#### [WaitUntilGoto.load](#load) 
Fired when the JavaScript 'load' event occurs

#### [WaitUntilGoto.domContentLoaded](#dom-content-loaded) 
Fired when the DOMContentLoaded event is fired

#### [WaitUntilGoto.firstMeaningfulPaint](#first-meaningful-paint) 
Indicates when the primary content of a page is visible to the user

#### [WaitUntilGoto.firstContentfulPaint](#first-contentful-paint) 
The render time of the largest image or text block visible in the viewport, relative to when the user first navigated to the page

#### [WaitUntilGoto.interactiveTime](#interactive-time) 
Use with caution: Chrome's best guess as to when the page becomes interactable

#### [WaitUntilGoto.networkIdle](#network-idle) 
Use with caution: Fired when there are no network connections for at least 500 ms. A page that holds a long-lived connection open never reaches this state, so the wait is bounded: once the page reaches networkAlmostIdle it settles for networkAlmostIdle after a second in which nothing arrives and no resource the page is still waiting on remains outstanding, and a page that never falls silent is given at most 30 seconds more, or half the timeout still unspent if that is smaller

#### [WaitUntilGoto.networkAlmostIdle](#network-almost-idle) 
Use with caution: Fired when there are no more than 2 network connections for at least 500 ms

### Member Of

[`goto`](/bql-schema/operations/mutations/goto)  [`switchToWindow`](/bql-schema/operations/mutations/switch-to-window)  [`waitForNavigation`](/bql-schema/operations/mutations/wait-for-navigation)

---

# WaitUntilHistory

URL: https://docs.browserless.io/bql-schema/types/enums/wait-until-history

The different stages of the browser's loading process

```graphql
enum WaitUntilHistory {
  commit
  domContentLoaded
  load
  networkIdle
  networkAlmostIdle
}
```

### Values

#### [WaitUntilHistory.commit](#commit) 
Fired when network response is received and the document started loading

#### [WaitUntilHistory.domContentLoaded](#dom-content-loaded) 
Fired when the DOMContentLoaded event is fired

#### [WaitUntilHistory.load](#load) 
Fired when the 'load' event occurs

#### [WaitUntilHistory.networkIdle](#network-idle) 
Use with caution: Fired when there are no network connections for at least 500 ms. A page that holds a long-lived connection open never reaches this state, so the wait is bounded: once the page reaches networkAlmostIdle it settles for networkAlmostIdle after a second in which nothing arrives and no resource the page is still waiting on remains outstanding, and a page that never falls silent is given at most 30 seconds more, or half the timeout still unspent if that is smaller

#### [WaitUntilHistory.networkAlmostIdle](#network-almost-idle) 
Use with caution: Fired when there are no more than 2 network connections for at least 500 ms

### Member Of

[`back`](/bql-schema/operations/mutations/back)  [`content`](/bql-schema/operations/mutations/content)  [`forward`](/bql-schema/operations/mutations/forward)  [`reload`](/bql-schema/operations/mutations/reload)

---

# CleanInput

URL: https://docs.browserless.io/bql-schema/types/inputs/clean-input

Options for cleaning up the DOM prior to exporting its content. Many options are available, and this query can destructively remove non-text DOM nodes, DOM attributes, and gratuitous whitespace characters. Since these operations are destructive in their nature it's recommended to run them at the very end of your query in order to preserve page functionality

```graphql
input CleanInput {
  removeNonTextNodes: Boolean
  removeAttributes: Boolean
  removeRegex: Boolean
  selectors: [String!]
  attributes: [String]
  mode: AttributeMode
  regexes: [String!]
}
```

### Fields

#### [CleanInput.removeNonTextNodes](#remove-non-text-nodes)[Boolean](/bql-schema/types/scalars/boolean) 
When true (default is true) this will remove non-textual nodes from the DOM like scripts, links, video, canvas, etc. You may override this by specifying a `selectors` argument for DOM selectors to remove.

#### [CleanInput.removeAttributes](#remove-attributes)[Boolean](/bql-schema/types/scalars/boolean) 
When true (default is false) this will remove all attributes on all DOM nodes. Useful for "cleaning" up all HTML markup but preserving the structure overall. You can specify specific attributes to remove with `attributes` argument

#### [CleanInput.removeRegex](#remove-regex)[Boolean](/bql-schema/types/scalars/boolean) 
Removes any characters in the HTML by a regex pattern and arn in order. By default this is true and removes newlines, returns, tabs, multi-spaces and HTML comments in that order. You may supply your own regex by using the `regexes` argument

#### [CleanInput.selectors](#selectors)[[String!]](/bql-schema/types/scalars/string) 
A list of selectors to remove from the page when `removeNonTextNodes` is set to true (`true` by default).

#### [CleanInput.attributes](#attributes)[[String]](/bql-schema/types/scalars/string) 
A list of attributes to remove from all DOM nodes. When this isn't specified, and `removeAttributes` is true, all attributes on all DOM nodes are removed. `removeNonTextNodes` must be set to `true` for this to take effect

#### [CleanInput.mode](#mode)[AttributeMode](/bql-schema/types/enums/attribute-mode) 
Controls how the `attributes` field is interpreted. When set to "allow", the `attributes` field specifies which attributes to keep. When set to "deny" (default), the `attributes` field specifies which attributes to remove. Defaults to "deny" for backward compatibility.

#### [CleanInput.regexes](#regexes)[[String!]](/bql-schema/types/scalars/string) 
When `removeRegex` is set to "true" this list of regex items, without the beginning and ending `/`, are removed from the page. These are each run in order and replaced with a single space character to preserve some of their contents

### Member Of

[`html`](/bql-schema/operations/mutations/html)  [`text`](/bql-schema/operations/mutations/text)

---

# CookieInput

URL: https://docs.browserless.io/bql-schema/types/inputs/cookie-input

The cookie to be sent to the page

```graphql
input CookieInput {
  domain: String
  expires: Float
  httpOnly: Boolean
  name: String!
  path: String
  priority: CookiePriority
  sourceScheme: CookieSourceScheme
  partitionKey: CookiePartitionKeyInput
  sameSite: CookieSameSite
  secure: Boolean
  url: String
  value: String!
}
```

### Fields

#### [CookieInput.domain](#domain)[String](/bql-schema/types/scalars/string) 
Cookie domain

#### [CookieInput.expires](#expires)[Float](/bql-schema/types/scalars/float) 
Cookie expiration date, session cookie if not set

#### [CookieInput.httpOnly](#http-only)[Boolean](/bql-schema/types/scalars/boolean) 
True if cookie is http-only

#### [CookieInput.name](#name)[String!](/bql-schema/types/scalars/string) 
Cookie name

#### [CookieInput.path](#path)[String](/bql-schema/types/scalars/string) 
Cookie path

#### [CookieInput.priority](#priority)[CookiePriority](/bql-schema/types/enums/cookie-priority) 

#### [CookieInput.sourceScheme](#source-scheme)[CookieSourceScheme](/bql-schema/types/enums/cookie-source-scheme) 

#### [CookieInput.partitionKey](#partition-key)[CookiePartitionKeyInput](/bql-schema/types/inputs/cookie-partition-key-input) 

#### [CookieInput.sameSite](#same-site)[CookieSameSite](/bql-schema/types/enums/cookie-same-site) 
Cookie SameSite type

#### [CookieInput.secure](#secure)[Boolean](/bql-schema/types/scalars/boolean) 
True if cookie is secure

#### [CookieInput.url](#url)[String](/bql-schema/types/scalars/string) 
The request-URI to associate with the setting of the cookie. This value can affect the default domain, path, source port, and source scheme values of the created cookie

#### [CookieInput.value](#value)[String!](/bql-schema/types/scalars/string) 
Cookie value

### Member Of

[`cookies`](/bql-schema/operations/mutations/cookies)

---

# CookiePartitionKeyInput

URL: https://docs.browserless.io/bql-schema/types/inputs/cookie-partition-key-input

No description

```graphql
input CookiePartitionKeyInput {
  sourceOrigin: String!
  hasCrossSiteAncestor: Boolean
}
```

### Fields

#### [CookiePartitionKeyInput.sourceOrigin](#source-origin)[String!](/bql-schema/types/scalars/string) 

#### [CookiePartitionKeyInput.hasCrossSiteAncestor](#has-cross-site-ancestor)[Boolean](/bql-schema/types/scalars/boolean) 

### Member Of

[`CookieInput`](/bql-schema/types/inputs/cookie-input)

---

# HeaderInput

URL: https://docs.browserless.io/bql-schema/types/inputs/header-input

A HTTP header input type

```graphql
input HeaderInput {
  name: String!
  value: String!
}
```

### Fields

#### [HeaderInput.name](#name)[String!](/bql-schema/types/scalars/string) 
The name of the HTTP header

#### [HeaderInput.value](#value)[String!](/bql-schema/types/scalars/string) 
The value of the HTTP header

### Member Of

[`fulfill`](/bql-schema/operations/mutations/fulfill)  [`setExtraHTTPHeaders`](/bql-schema/operations/mutations/set-extra-httpheaders)

---

# HTMLResourcePathInput

URL: https://docs.browserless.io/bql-schema/types/inputs/htmlresource-path-input

A resource URL and its replacement archive path when rewriting HTML for export.

```graphql
input HTMLResourcePathInput {
  url: String!
  path: String!
}
```

### Fields

#### [HTMLResourcePathInput.url](#url)[String!](/bql-schema/types/scalars/string) 
The absolute resource URL found in the page.

#### [HTMLResourcePathInput.path](#path)[String!](/bql-schema/types/scalars/string) 
The relative path used for the resource in the exported archive.

### Member Of

[`HTMLRewriteInput`](/bql-schema/types/inputs/htmlrewrite-input)

---

# HTMLRewriteInput

URL: https://docs.browserless.io/bql-schema/types/inputs/htmlrewrite-input

Options for rewriting page resource URLs before returning HTML.

```graphql
input HTMLRewriteInput {
  baseURL: String!
  resources: [HTMLResourcePathInput!]!
}
```

### Fields

#### [HTMLRewriteInput.baseURL](#base-url)[String!](/bql-schema/types/scalars/string) 
The base URL used to resolve relative resource URLs.

#### [HTMLRewriteInput.resources](#resources)[[HTMLResourcePathInput!]!](/bql-schema/types/inputs/htmlresource-path-input) 
The resources to rewrite from absolute URLs to archive-relative paths.

### Member Of

[`html`](/bql-schema/operations/mutations/html)

---

# RequestInput

URL: https://docs.browserless.io/bql-schema/types/inputs/request-input

The specific request to perform a conditional action on

```graphql
input RequestInput {
  method: Method
  url: String
}
```

### Fields

#### [RequestInput.method](#method)[Method](/bql-schema/types/enums/method) 
The HTTP Method of the request

#### [RequestInput.url](#url)[String](/bql-schema/types/scalars/string) 
The pattern of the request URL to wait for, using glob-style pattern-matching

### Member Of

[`if`](/bql-schema/operations/mutations/if)  [`ifnot`](/bql-schema/operations/mutations/ifnot)

---

# ResponseInput

URL: https://docs.browserless.io/bql-schema/types/inputs/response-input

The specific response to perform a conditional action on

```graphql
input ResponseInput {
  statuses: [Int]
  codes: [Int] @deprecated
  url: String
}
```

### Fields

#### [ResponseInput.statuses](#statuses)[[Int]](/bql-schema/types/scalars/int) 
The HTTP Response code(s) of the URL to wait for. Can be a single HTTP code or a list of desired codes

#### [ResponseInput.codes](#codes)[[Int]](/bql-schema/types/scalars/int) 
> **DEPRECATED**
> Use `statuses` field instead as it is more consistent in BrowserQL.

The HTTP Response code(s) of the URL to wait for. Can be a single HTTP code or a list of desired codes

#### [ResponseInput.url](#url)[String](/bql-schema/types/scalars/string) 
The pattern of the response URL to wait for, using glob-style pattern-matching

### Member Of

[`if`](/bql-schema/operations/mutations/if)  [`ifnot`](/bql-schema/operations/mutations/ifnot)

---

# ScreenshotClip

URL: https://docs.browserless.io/bql-schema/types/inputs/screenshot-clip

The clipping to be applied to the screenshot

```graphql
input ScreenshotClip {
  height: Float
  scale: Float
  width: Float
  x: Float
  y: Float
}
```

### Fields

#### [ScreenshotClip.height](#height)[Float](/bql-schema/types/scalars/float) 
The height of the clip, in pixels

#### [ScreenshotClip.scale](#scale)[Float](/bql-schema/types/scalars/float) 
The scale factor of the clip

#### [ScreenshotClip.width](#width)[Float](/bql-schema/types/scalars/float) 
The width of the clip, in pixels

#### [ScreenshotClip.x](#x)[Float](/bql-schema/types/scalars/float) 
The x coordinate to start clipping, in pixels

#### [ScreenshotClip.y](#y)[Float](/bql-schema/types/scalars/float) 
The y coordinate to start clipping, in pixels

### Member Of

[`screenshot`](/bql-schema/operations/mutations/screenshot)

---

# AddScriptTagResponse

URL: https://docs.browserless.io/bql-schema/types/objects/add-script-tag-response

Response returned after injecting a script tag

```graphql
type AddScriptTagResponse {
  time: Float
}
```

### Fields

#### [AddScriptTagResponse.time](#time)[Float](/bql-schema/types/scalars/float) 
The period of time elapsed, in milliseconds

### Returned By

[`addScriptTag`](/bql-schema/operations/mutations/add-script-tag)

---

# AddStyleTagResponse

URL: https://docs.browserless.io/bql-schema/types/objects/add-style-tag-response

Response returned after injecting a style tag

```graphql
type AddStyleTagResponse {
  time: Float
}
```

### Fields

#### [AddStyleTagResponse.time](#time)[Float](/bql-schema/types/scalars/float) 
The period of time elapsed, in milliseconds

### Returned By

[`addStyleTag`](/bql-schema/operations/mutations/add-style-tag)

---

# Attribute

URL: https://docs.browserless.io/bql-schema/types/objects/attribute

No description

```graphql
type Attribute {
  name: String
  value: String
}
```

### Fields

#### [Attribute.name](#name)[String](/bql-schema/types/scalars/string) 

#### [Attribute.value](#value)[String](/bql-schema/types/scalars/string) 

### Member Of

[`MapSelectorResponse`](/bql-schema/types/objects/map-selector-response)

---

# CaptchaResponse

URL: https://docs.browserless.io/bql-schema/types/objects/captcha-response

Response returned after a captcha has been solved

```graphql
type CaptchaResponse {
  found: Boolean
  solved: Boolean
  time: Float
  token: String
  error: String
}
```

### Fields

#### [CaptchaResponse.found](#found)[Boolean](/bql-schema/types/scalars/boolean) 
If a captcha was found or not

#### [CaptchaResponse.solved](#solved)[Boolean](/bql-schema/types/scalars/boolean) 
If a captcha was found, whether or not it was solved

#### [CaptchaResponse.time](#time)[Float](/bql-schema/types/scalars/float) 
The total time it took to find, and solve, the captcha

#### [CaptchaResponse.token](#token)[String](/bql-schema/types/scalars/string) 
The solved token of the response, if any is provided

#### [CaptchaResponse.error](#error)[String](/bql-schema/types/scalars/string) 
Error message if something went wrong, providing guidance on how to fix the issue

### Returned By

[`solve`](/bql-schema/operations/mutations/solve)  [`solveImageCaptcha`](/bql-schema/operations/mutations/solve-image-captcha)  [`verify`](/bql-schema/operations/mutations/verify)

---

# ClickResponse

URL: https://docs.browserless.io/bql-schema/types/objects/click-response

Response returned after having clicked on an element

```graphql
type ClickResponse {
  selector: String
  time: Float
  x: Float
  y: Float
}
```

### Fields

#### [ClickResponse.selector](#selector)[String](/bql-schema/types/scalars/string) 
The selector text if specified

#### [ClickResponse.time](#time)[Float](/bql-schema/types/scalars/float) 
The amount of time, in milliseconds, elapsed since the start of clicking to completion

#### [ClickResponse.x](#x)[Float](/bql-schema/types/scalars/float) 
The X coordinate of the click, in pixels, on the page

#### [ClickResponse.y](#y)[Float](/bql-schema/types/scalars/float) 
The Y coordinate of the click, in pixels, on the page

### Returned By

[`checkbox`](/bql-schema/operations/mutations/checkbox)  [`click`](/bql-schema/operations/mutations/click)

---

# CloseLiveURLResponse

URL: https://docs.browserless.io/bql-schema/types/objects/close-live-urlresponse

The response returned after closing a Live URL

```graphql
type CloseLiveURLResponse {
  liveURLId: String!
}
```

### Fields

#### [CloseLiveURLResponse.liveURLId](#live-urlid)[String!](/bql-schema/types/scalars/string) 
The ID of the closed Live URL

### Returned By

[`closeLiveURL`](/bql-schema/operations/mutations/close-live-url)

---

# ConsoleMessage

URL: https://docs.browserless.io/bql-schema/types/objects/console-message

A console message emitted by the page. Mirrors puppeteer's ConsoleMessage,
sourced from CDP Runtime.consoleAPICalled and Runtime.exceptionThrown.

```graphql
type ConsoleMessage {
  type: ConsoleMessageType
  text: String
  args: [String]
  location: ConsoleMessageLocation
  timestamp: Float
}
```

### Fields

#### [ConsoleMessage.type](#type)[ConsoleMessageType](/bql-schema/types/enums/console-message-type) 
The kind of console message.

#### [ConsoleMessage.text](#text)[String](/bql-schema/types/scalars/string) 
The joined, human-readable message text (equivalent to puppeteer's msg.text()).

#### [ConsoleMessage.args](#args)[[String]](/bql-schema/types/scalars/string) 
Each console argument, JSON-encoded and in order (equivalent to msg.args()).

#### [ConsoleMessage.location](#location)[ConsoleMessageLocation](/bql-schema/types/objects/console-message-location) 
The source location the message originated from.

#### [ConsoleMessage.timestamp](#timestamp)[Float](/bql-schema/types/scalars/float) 
Wall-clock milliseconds since the Unix epoch when the message was emitted.

### Returned By

[`console`](/bql-schema/operations/subscriptions/console)

---

# ConsoleMessageLocation

URL: https://docs.browserless.io/bql-schema/types/objects/console-message-location

The source location a console message originated from.

```graphql
type ConsoleMessageLocation {
  url: String
  lineNumber: Int
  columnNumber: Int
}
```

### Fields

#### [ConsoleMessageLocation.url](#url)[String](/bql-schema/types/scalars/string) 

#### [ConsoleMessageLocation.lineNumber](#line-number)[Int](/bql-schema/types/scalars/int) 

#### [ConsoleMessageLocation.columnNumber](#column-number)[Int](/bql-schema/types/scalars/int) 

### Member Of

[`ConsoleMessage`](/bql-schema/types/objects/console-message)

---

# CookieResponse

URL: https://docs.browserless.io/bql-schema/types/objects/cookie-response

The response returned after setting or getting cookies

```graphql
type CookieResponse {
  cookies: [StandardCookie]
  time: Float
}
```

### Fields

#### [CookieResponse.cookies](#cookies)[[StandardCookie]](/bql-schema/types/objects/standard-cookie) 
A standard cookie object with the values of the set cookies

#### [CookieResponse.time](#time)[Float](/bql-schema/types/scalars/float) 
The time it took to set and return the cookies

### Returned By

[`cookies`](/bql-schema/operations/mutations/cookies)

---

# DefaultResponse

URL: https://docs.browserless.io/bql-schema/types/objects/default-response

Default response for all methods

```graphql
type DefaultResponse {
  timeout: Float
}
```

### Fields

#### [DefaultResponse.timeout](#timeout)[Float](/bql-schema/types/scalars/float) 
The default timeout for all methods. Default timeout is 30 seconds, or 30000.

### Returned By

[`preferences`](/bql-schema/operations/mutations/preferences)

---

# EmulateMediaTypeResponse

URL: https://docs.browserless.io/bql-schema/types/objects/emulate-media-type-response

Response returned after emulating a CSS media type

```graphql
type EmulateMediaTypeResponse {
  type: String
  time: Float
}
```

### Fields

#### [EmulateMediaTypeResponse.type](#type)[String](/bql-schema/types/scalars/string) 
The media type that was emulated

#### [EmulateMediaTypeResponse.time](#time)[Float](/bql-schema/types/scalars/float) 
The period of time elapsed, in milliseconds

### Returned By

[`emulateMediaType`](/bql-schema/operations/mutations/emulate-media-type)

---

# EvaluateResponse

URL: https://docs.browserless.io/bql-schema/types/objects/evaluate-response

Response returned after evaluating a script

```graphql
type EvaluateResponse {
  time: Float
  value: String
}
```

### Fields

#### [EvaluateResponse.time](#time)[Float](/bql-schema/types/scalars/float) 
The time it took for the evaluate call to happen

#### [EvaluateResponse.value](#value)[String](/bql-schema/types/scalars/string) 
The returned value of the script, if any

### Returned By

[`evaluate`](/bql-schema/operations/mutations/evaluate)

---

# FulfillResponse

URL: https://docs.browserless.io/bql-schema/types/objects/fulfill-response

Response returned after registering a request-fulfillment interceptor

```graphql
type FulfillResponse {
  time: Float
}
```

### Fields

#### [FulfillResponse.time](#time)[Float](/bql-schema/types/scalars/float) 
The period of time elapsed, in milliseconds

### Returned By

[`fulfill`](/bql-schema/operations/mutations/fulfill)

---

# Headers

URL: https://docs.browserless.io/bql-schema/types/objects/headers

An object representing the header's name and underlying value

```graphql
type Headers {
  name: String
  value: String
}
```

### Fields

#### [Headers.name](#name)[String](/bql-schema/types/scalars/string) 

#### [Headers.value](#value)[String](/bql-schema/types/scalars/string) 

### Member Of

[`RequestResponse`](/bql-schema/types/objects/request-response)  [`ResponseResponse`](/bql-schema/types/objects/response-response)

---

# HoverResponse

URL: https://docs.browserless.io/bql-schema/types/objects/hover-response

Response returned after having hovered over an element

```graphql
type HoverResponse {
  selector: String
  time: Float
  x: Float
  y: Float
}
```

### Fields

#### [HoverResponse.selector](#selector)[String](/bql-schema/types/scalars/string) 
The selector text

#### [HoverResponse.time](#time)[Float](/bql-schema/types/scalars/float) 
The amount of time, in milliseconds, elapsed since the start to completion

#### [HoverResponse.x](#x)[Float](/bql-schema/types/scalars/float) 
The X coordinate in pixels, on the page

#### [HoverResponse.y](#y)[Float](/bql-schema/types/scalars/float) 
The Y coordinate in pixels, on the page

### Returned By

[`hover`](/bql-schema/operations/mutations/hover)

---

# HTMLResponse

URL: https://docs.browserless.io/bql-schema/types/objects/htmlresponse

HTML content of a page

```graphql
type HTMLResponse {
  html: String
  time: Float
}
```

### Fields

#### [HTMLResponse.html](#html)[String](/bql-schema/types/scalars/string) 
The content of the page's HTML

#### [HTMLResponse.time](#time)[Float](/bql-schema/types/scalars/float) 
The amount of time, in milliseconds, elapsed since the start of content retrieval to completion

### Returned By

[`html`](/bql-schema/operations/mutations/html)

---

# HTTPHeaders

URL: https://docs.browserless.io/bql-schema/types/objects/httpheaders

An object representing the header's name and underlying value

```graphql
type HTTPHeaders {
  name: String
  value: String
}
```

### Fields

#### [HTTPHeaders.name](#name)[String](/bql-schema/types/scalars/string) 

#### [HTTPHeaders.value](#value)[String](/bql-schema/types/scalars/string) 

### Member Of

[`HTTPHeadersResponse`](/bql-schema/types/objects/httpheaders-response)

---

# HTTPHeadersResponse

URL: https://docs.browserless.io/bql-schema/types/objects/httpheaders-response

The response returned after setting HTTP headers

```graphql
type HTTPHeadersResponse {
  headers: [HTTPHeaders]
  time: Float
}
```

### Fields

#### [HTTPHeadersResponse.headers](#headers)[[HTTPHeaders]](/bql-schema/types/objects/httpheaders) 
The HTTP headers that were set

#### [HTTPHeadersResponse.time](#time)[Float](/bql-schema/types/scalars/float) 
The time it took to set the HTTP headers

### Returned By

[`setExtraHTTPHeaders`](/bql-schema/operations/mutations/set-extra-httpheaders)

---

# HTTPResponse

URL: https://docs.browserless.io/bql-schema/types/objects/httpresponse

Response returned after a navigation event

```graphql
type HTTPResponse {
  ip: String
  port: Int
  status: Int
  time: Float
  text: String
  url: String
}
```

### Fields

#### [HTTPResponse.ip](#ip)[String](/bql-schema/types/scalars/string) 
The remote IP address that served the initial page-load response

#### [HTTPResponse.port](#port)[Int](/bql-schema/types/scalars/int) 
The remote port that served the initial page-load response

#### [HTTPResponse.status](#status)[Int](/bql-schema/types/scalars/int) 
The status code response of the initial page-load

#### [HTTPResponse.time](#time)[Float](/bql-schema/types/scalars/float) 
The amount of time, in milliseconds, elapsed since the start of navigation to completion

#### [HTTPResponse.text](#text)[String](/bql-schema/types/scalars/string) 
The status text of the response from the initial page-load. Generally 'ok'

#### [HTTPResponse.url](#url)[String](/bql-schema/types/scalars/string) 
The final URL of the page after any potential redirects or URL rewrites

### Returned By

[`authenticate`](/bql-schema/operations/mutations/authenticate)  [`back`](/bql-schema/operations/mutations/back)  [`content`](/bql-schema/operations/mutations/content)  [`forward`](/bql-schema/operations/mutations/forward)  [`goto`](/bql-schema/operations/mutations/goto)  [`reload`](/bql-schema/operations/mutations/reload)  [`waitForNavigation`](/bql-schema/operations/mutations/wait-for-navigation)  

### Member Of

[`SwitchWindowResponse`](/bql-schema/types/objects/switch-window-response)

---

# JavaScriptResponse

URL: https://docs.browserless.io/bql-schema/types/objects/java-script-response

The response returned after enabling or disabling JavaScript on the page

```graphql
type JavaScriptResponse {
  enabled: Boolean
  time: Float
}
```

### Fields

#### [JavaScriptResponse.enabled](#enabled)[Boolean](/bql-schema/types/scalars/boolean) 
Whether or not JavaScript is enabled on the page

#### [JavaScriptResponse.time](#time)[Float](/bql-schema/types/scalars/float) 
The time it took to perform this action

### Returned By

[`javaScriptEnabled`](/bql-schema/operations/mutations/java-script-enabled)

---

# LiveURLResponse

URL: https://docs.browserless.io/bql-schema/types/objects/live-urlresponse

The response from the Live-URL query

```graphql
type LiveURLResponse {
  liveURLId: String
  liveURL: String
  timeout: Float
}
```

### Fields

#### [LiveURLResponse.liveURLId](#live-urlid)[String](/bql-schema/types/scalars/string) 
A unique ID representing the session. Useful for storing and associations in other APIs

#### [LiveURLResponse.liveURL](#live-url)[String](/bql-schema/types/scalars/string) 
The fully-qualified URL to share with the end-user to allow them to live-stream the browser

#### [LiveURLResponse.timeout](#timeout)[Float](/bql-schema/types/scalars/float) 
How long (ms) the Live URL stays valid. Equal to the requested timeout, or to the session's remaining time when that is shorter

### Returned By

[`liveURL`](/bql-schema/operations/mutations/live-url)

---

# LoadSecretResponse

URL: https://docs.browserless.io/bql-schema/types/objects/load-secret-response

Response returned after attempting to fill a credential resolved from a
1Password integration. The resolved value is never included in the response.

```graphql
type LoadSecretResponse {
  ok: Boolean!
  error: String
  message: String
  time: Float
}
```

### Fields

#### [LoadSecretResponse.ok](#ok)[Boolean!](/bql-schema/types/scalars/boolean) 
Whether the credential was resolved and filled into the target element.

#### [LoadSecretResponse.error](#error)[String](/bql-schema/types/scalars/string) 
Error code when ok is false: DomainNotAllowed, SelectorNotFound,
NoFocusedElement, TargetNotFillable, CredentialNotResolved (catch-all),
InvalidToken, BadCredentialReference, VaultNotFound, ItemNotFound,
FieldNotFound, UnknownResolveError, CredentialIntegrationExpired,
VaultUnreachable (transport failures), AuditWriteFailed, or
OtpTimeout (no fresh emailed code arrived for a mail:// reference).

#### [LoadSecretResponse.message](#message)[String](/bql-schema/types/scalars/string) 
Human-readable explanation when ok is false.

#### [LoadSecretResponse.time](#time)[Float](/bql-schema/types/scalars/float) 
The amount of time, in milliseconds, the operation took.

### Returned By

[`loadSecret`](/bql-schema/operations/mutations/load-secret)

---

# MapSelectorResponse

URL: https://docs.browserless.io/bql-schema/types/objects/map-selector-response

Response returned from a Map Selector

```graphql
type MapSelectorResponse {
  innerHTML: String
  innerText: String
  id: String
  class: [String]
  attribute(
    name: String
  ): Attribute
  mapSelector(
    selector: String!
    timeout: Float
    wait: Boolean = true
  ): [MapSelectorResponse]
}
```

### Fields

#### [MapSelectorResponse.innerHTML](#inner-html)[String](/bql-schema/types/scalars/string) 
The innerHTML of the selected DOM Node

#### [MapSelectorResponse.innerText](#inner-text)[String](/bql-schema/types/scalars/string) 
The innerText of the selected DOM Node, eg, the raw textual content

#### [MapSelectorResponse.id](#id)[String](/bql-schema/types/scalars/string) 
The ID attribute's value, if any, of the node

#### [MapSelectorResponse.class](#class)[[String]](/bql-schema/types/scalars/string) 
The class attribute's value, if any, of the node represented as an array of strings

#### [MapSelectorResponse.attribute](#attribute)[Attribute](/bql-schema/types/objects/attribute) 
Retrieve an attribute by the name of the attribute itself, eg, "data-test-id"
##### [MapSelectorResponse.attribute.name](#map-selector-response-attribute-name)[String](/bql-schema/types/scalars/string) 

#### [MapSelectorResponse.mapSelector](#map-selector)[[MapSelectorResponse]](/bql-schema/types/objects/map-selector-response) 
You can further map nested DOM nodes as well. For instance, given a parent ".product" node, you can map further nodes like ".price" or ".shipping" as examples. This will give you items appropriately nested by their parent node for better hierarchical representation of data
##### [MapSelectorResponse.mapSelector.selector](#map-selector-response-map-selector-selector)[String!](/bql-schema/types/scalars/string) 
A query-selector-al compatible string, or JavaScript code that returns an DOM NodeList. Examples include:
- All `<button />` Elements:
  `selector: "button"`

- A JavaScript snippet that returns a button element
  `selector: "document.querySelectorAll('button')"`

##### [MapSelectorResponse.mapSelector.timeout](#map-selector-response-map-selector-timeout)[Float](/bql-schema/types/scalars/float) 
How long to wait for the element to appear before timing out, overriding any defaults. Default timeout is 30 seconds, or 30000.

##### [MapSelectorResponse.mapSelector.wait](#map-selector-response-map-selector-wait)[Boolean](/bql-schema/types/scalars/boolean) 
Whether or not to wait for the selectors to present in the DOM

### Returned By

[`mapSelector`](/bql-schema/operations/mutations/map-selector)  

### Member Of

[`MapSelectorResponse`](/bql-schema/types/objects/map-selector-response)

---

# MarkdownResponse

URL: https://docs.browserless.io/bql-schema/types/objects/markdown-response

Response returned after converting the page content to Markdown

```graphql
type MarkdownResponse {
  markdown: String
  time: Float
}
```

### Fields

#### [MarkdownResponse.markdown](#markdown)[String](/bql-schema/types/scalars/string) 
The page content converted to Markdown

#### [MarkdownResponse.time](#time)[Float](/bql-schema/types/scalars/float) 
The period of time elapsed, in milliseconds

### Returned By

[`markdown`](/bql-schema/operations/mutations/markdown)

---

# Mutation

URL: https://docs.browserless.io/bql-schema/types/objects/mutation

# Mutation

This document is a complete technical specification of every mutation in the BrowserQL API. To get you started, the ones you'll use most will probably include:

- [goto](/bql-schema/operations/mutations/goto)
- [reject](/bql-schema/operations/mutations/reject)
- [proxy](/bql-schema/operations/mutations/proxy)
- [click](/bql-schema/operations/mutations/click)
- [type](/bql-schema/operations/mutations/type)
- [html](/bql-schema/operations/mutations/html)
- [reconnect](/bql-schema/operations/mutations/reconnect)

---

# PDFResponse

URL: https://docs.browserless.io/bql-schema/types/objects/pdfresponse

The response returned after generating a PDF

```graphql
type PDFResponse {
  base64: String
  size: Float
  time: Float
}
```

### Fields

#### [PDFResponse.base64](#base-64)[String](/bql-schema/types/scalars/string) 
Base64 encoded PDF content

#### [PDFResponse.size](#size)[Float](/bql-schema/types/scalars/float) 
The size of the resulting PDF in bytes

#### [PDFResponse.time](#time)[Float](/bql-schema/types/scalars/float) 
The time it took to generate the PDF

### Returned By

[`pdf`](/bql-schema/operations/mutations/pdf)

---

# ProxyResponse

URL: https://docs.browserless.io/bql-schema/types/objects/proxy-response

Response returned after setting up the proxy patterns

```graphql
type ProxyResponse {
  time: Float
}
```

### Fields

#### [ProxyResponse.time](#time)[Float](/bql-schema/types/scalars/float) 

### Returned By

[`proxy`](/bql-schema/operations/mutations/proxy)

---

# QuerySelectorResponse

URL: https://docs.browserless.io/bql-schema/types/objects/query-selector-response

The response returned after querying the page for a selector

```graphql
type QuerySelectorResponse {
  childElementCount: Float
  className: String
  id: String
  innerHTML: String
  innerText: String
  localName: String
  outerHTML: String
}
```

### Fields

#### [QuerySelectorResponse.childElementCount](#child-element-count)[Float](/bql-schema/types/scalars/float) 
The Element.childElementCount read-only property returns the number of child elements of this element

#### [QuerySelectorResponse.className](#class-name)[String](/bql-schema/types/scalars/string) 
The className property of the Element interface gets and sets the value of the class attribute of the specified element

#### [QuerySelectorResponse.id](#id)[String](/bql-schema/types/scalars/string) 
The id property of the Element interface represents the element's identifier, reflecting the id global attribute

#### [QuerySelectorResponse.innerHTML](#inner-html)[String](/bql-schema/types/scalars/string) 
The Element property innerHTML gets the HTML or XML markup contained within the element

#### [QuerySelectorResponse.innerText](#inner-text)[String](/bql-schema/types/scalars/string) 
The Element property innerText gets the text contained within the element

#### [QuerySelectorResponse.localName](#local-name)[String](/bql-schema/types/scalars/string) 
The Element.localName read-only property returns the local part of the qualified name of an element

#### [QuerySelectorResponse.outerHTML](#outer-html)[String](/bql-schema/types/scalars/string) 
The outerHTML attribute of the Element DOM interface gets the serialized HTML fragment describing the element including its descendants. It can also be set to replace the element with nodes parsed from the given string

### Returned By

[`querySelector`](/bql-schema/operations/mutations/query-selector)  [`querySelectorAll`](/bql-schema/operations/mutations/query-selector-all)

---

# ReconnectionResponse

URL: https://docs.browserless.io/bql-schema/types/objects/reconnection-response

The response received after attempting to reconnect a BrowserQL session

```graphql
type ReconnectionResponse {
  browserQLEndpoint: String
  browserWSEndpoint: String
  devtoolsFrontendUrl: String
  webSocketDebuggerUrl: String
}
```

### Fields

#### [ReconnectionResponse.browserQLEndpoint](#browser-qlendpoint)[String](/bql-schema/types/scalars/string) 
The fully-qualified URL to reconnect future BrowserQL sessions, eg: https://chrome.browserless.io/bql/$id. Please note that token information is not returned by this API and might be required

#### [ReconnectionResponse.browserWSEndpoint](#browser-wsendpoint)[String](/bql-schema/types/scalars/string) 
The fully-qualified URL of the browserWSEndpoint which can be used with other libraries like playwright or puppeteer. Please note that token information is not returned by this API and might be required

#### [ReconnectionResponse.devtoolsFrontendUrl](#devtools-frontend-url)[String](/bql-schema/types/scalars/string) 
The fully-qualified URL of the devtools resources for loading Chrome's developer tools remotely

#### [ReconnectionResponse.webSocketDebuggerUrl](#web-socket-debugger-url)[String](/bql-schema/types/scalars/string) 
The underlying page's webSocketDebuggerUrl, useful for hooking libraries that operate on a page and not a browser object

### Returned By

[`reconnect`](/bql-schema/operations/mutations/reconnect)

---

# RejectResponse

URL: https://docs.browserless.io/bql-schema/types/objects/reject-response

The response parameters for the reject mutation

```graphql
type RejectResponse {
  time: Float
  enabled: Boolean
}
```

### Fields

#### [RejectResponse.time](#time)[Float](/bql-schema/types/scalars/float) 

#### [RejectResponse.enabled](#enabled)[Boolean](/bql-schema/types/scalars/boolean) 

### Returned By

[`reject`](/bql-schema/operations/mutations/reject)

---

# RequestResponse

URL: https://docs.browserless.io/bql-schema/types/objects/request-response

Response returned from the request API

```graphql
type RequestResponse {
  url: String
  method: Method
  type: ResourceType
  headers: [Headers]
}
```

### Fields

#### [RequestResponse.url](#url)[String](/bql-schema/types/scalars/string) 
The final URL of the request

#### [RequestResponse.method](#method)[Method](/bql-schema/types/enums/method) 
The HTTP method of the request

#### [RequestResponse.type](#type)[ResourceType](/bql-schema/types/enums/resource-type) 
The type of request that was made

#### [RequestResponse.headers](#headers)[[Headers]](/bql-schema/types/objects/headers) 
The headers of the request

### Returned By

[`request`](/bql-schema/operations/mutations/request)  [`request`](/bql-schema/operations/subscriptions/request)

---

# ResponseResponse

URL: https://docs.browserless.io/bql-schema/types/objects/response-response

Response returned from the response API

```graphql
type ResponseResponse {
  status: Int
  url: String
  method: Method
  type: ResourceType
  headers: [Headers]
  body: String
  base64Encoded: Boolean!
}
```

### Fields

#### [ResponseResponse.status](#status)[Int](/bql-schema/types/scalars/int) 
The HTTP status code of the response

#### [ResponseResponse.url](#url)[String](/bql-schema/types/scalars/string) 
The final URL of the response

#### [ResponseResponse.method](#method)[Method](/bql-schema/types/enums/method) 
The HTTP method of the request that facilitated the response

#### [ResponseResponse.type](#type)[ResourceType](/bql-schema/types/enums/resource-type) 
The type of response that was received

#### [ResponseResponse.headers](#headers)[[Headers]](/bql-schema/types/objects/headers) 
The response headers returned

#### [ResponseResponse.body](#body)[String](/bql-schema/types/scalars/string) 
The body of the response, represented as a string when possible or base64 for binary-type requests like images

#### [ResponseResponse.base64Encoded](#base-64-encoded)[Boolean!](/bql-schema/types/scalars/boolean) 
Whether the response body is base64 encoded.

### Returned By

[`response`](/bql-schema/operations/mutations/response)  [`response`](/bql-schema/operations/subscriptions/response)

---

# SaveSecretResponse

URL: https://docs.browserless.io/bql-schema/types/objects/save-secret-response

Response returned after attempting to create a Login item in a 1Password
integration. The supplied password is never included in the response.

```graphql
type SaveSecretResponse {
  ok: Boolean!
  ref: String
  error: String
  message: String
  time: Float
}
```

### Fields

#### [SaveSecretResponse.ok](#ok)[Boolean!](/bql-schema/types/scalars/boolean) 
Whether the Login item was created.

#### [SaveSecretResponse.ref](#ref)[String](/bql-schema/types/scalars/string) 
ID-based op:// reference to the created password field.

#### [SaveSecretResponse.error](#error)[String](/bql-schema/types/scalars/string) 
Stable error code when ok is false.

#### [SaveSecretResponse.message](#message)[String](/bql-schema/types/scalars/string) 
Human-readable explanation when ok is false.

#### [SaveSecretResponse.time](#time)[Float](/bql-schema/types/scalars/float) 
The amount of time, in milliseconds, the operation took.

### Returned By

[`saveSecret`](/bql-schema/operations/mutations/save-secret)

---

# ScreenshotResponse

URL: https://docs.browserless.io/bql-schema/types/objects/screenshot-response

The response returned after generating a Screenshot

```graphql
type ScreenshotResponse {
  base64: String
  format: String
  time: Float
}
```

### Fields

#### [ScreenshotResponse.base64](#base-64)[String](/bql-schema/types/scalars/string) 
The base64 encoded image of the screenshot

#### [ScreenshotResponse.format](#format)[String](/bql-schema/types/scalars/string) 
The format of the screenshot

#### [ScreenshotResponse.time](#time)[Float](/bql-schema/types/scalars/float) 
The time it took to take the screenshot

### Returned By

[`screenshot`](/bql-schema/operations/mutations/screenshot)

---

# ScrollResponse

URL: https://docs.browserless.io/bql-schema/types/objects/scroll-response

Response returned after having scrolling inside the page

```graphql
type ScrollResponse {
  selector: String
  time: Float
  x: Float
  y: Float
}
```

### Fields

#### [ScrollResponse.selector](#selector)[String](/bql-schema/types/scalars/string) 
The CSS selector of the element on the page you want to scroll to

#### [ScrollResponse.time](#time)[Float](/bql-schema/types/scalars/float) 
The amount of time, in milliseconds, elapsed since the start of scrolling to completion

#### [ScrollResponse.x](#x)[Float](/bql-schema/types/scalars/float) 
The X coordinate, in pixels, to scroll to

#### [ScrollResponse.y](#y)[Float](/bql-schema/types/scalars/float) 
The Y coordinate, in pixels, to scroll to

### Returned By

[`scroll`](/bql-schema/operations/mutations/scroll)

---

# SelectResponse

URL: https://docs.browserless.io/bql-schema/types/objects/select-response

The response returned after selecting a value from a dropdown or multiple select element

```graphql
type SelectResponse {
  selector: String
  time: Float
  value: StringOrArray
}
```

### Fields

#### [SelectResponse.selector](#selector)[String](/bql-schema/types/scalars/string) 
The selector of the element you selected from

#### [SelectResponse.time](#time)[Float](/bql-schema/types/scalars/float) 
The amount of time, in milliseconds, elapsed since the start of selecting to completion

#### [SelectResponse.value](#value)[StringOrArray](/bql-schema/types/scalars/string-or-array) 
The value or values you selected from the select element

### Returned By

[`select`](/bql-schema/operations/mutations/select)

---

# StandardCookie

URL: https://docs.browserless.io/bql-schema/types/objects/standard-cookie

A standard cookie

```graphql
type StandardCookie {
  domain: String
  expires: Float
  httpOnly: Boolean
  name: String!
  path: String
  sameSite: CookieSameSite
  secure: Boolean
  value: String!
  url: String
}
```

### Fields

#### [StandardCookie.domain](#domain)[String](/bql-schema/types/scalars/string) 
Cookie domain

#### [StandardCookie.expires](#expires)[Float](/bql-schema/types/scalars/float) 
Cookie expiration date, session cookie if not set

#### [StandardCookie.httpOnly](#http-only)[Boolean](/bql-schema/types/scalars/boolean) 
True if cookie is http-only

#### [StandardCookie.name](#name)[String!](/bql-schema/types/scalars/string) 
Cookie name

#### [StandardCookie.path](#path)[String](/bql-schema/types/scalars/string) 
Cookie path

#### [StandardCookie.sameSite](#same-site)[CookieSameSite](/bql-schema/types/enums/cookie-same-site) 
Cookie SameSite type

#### [StandardCookie.secure](#secure)[Boolean](/bql-schema/types/scalars/boolean) 
True if cookie is secure

#### [StandardCookie.value](#value)[String!](/bql-schema/types/scalars/string) 
Cookie value

#### [StandardCookie.url](#url)[String](/bql-schema/types/scalars/string) 
The request-URI to associate with the setting of the cookie. This value can affect the default domain, path, source port, and source scheme values of the created cookie

### Member Of

[`CookieResponse`](/bql-schema/types/objects/cookie-response)

---

# StopSessionRecordingResponse

URL: https://docs.browserless.io/bql-schema/types/objects/stop-session-recording-response

The response received after stopping session recording

```graphql
type StopSessionRecordingResponse {
  success: Boolean
  error: String
}
```

### Fields

#### [StopSessionRecordingResponse.success](#success)[Boolean](/bql-schema/types/scalars/boolean) 
Whether the recording was successfully stopped and uploaded

#### [StopSessionRecordingResponse.error](#error)[String](/bql-schema/types/scalars/string) 
Error message if the operation failed

### Returned By

[`stopSessionRecording`](/bql-schema/operations/mutations/stop-session-recording)

---

# SwitchWindowResponse

URL: https://docs.browserless.io/bql-schema/types/objects/switch-window-response

Response from window switching operation

```graphql
type SwitchWindowResponse {
  success: Boolean!
  error: String
  title: String
  url: String
  navigation: HTTPResponse
}
```

### Fields

#### [SwitchWindowResponse.success](#success)[Boolean!](/bql-schema/types/scalars/boolean) 

#### [SwitchWindowResponse.error](#error)[String](/bql-schema/types/scalars/string) 

#### [SwitchWindowResponse.title](#title)[String](/bql-schema/types/scalars/string) 

#### [SwitchWindowResponse.url](#url)[String](/bql-schema/types/scalars/string) 

#### [SwitchWindowResponse.navigation](#navigation)[HTTPResponse](/bql-schema/types/objects/httpresponse) 

### Returned By

[`switchToWindow`](/bql-schema/operations/mutations/switch-to-window)

---

# TextResponse

URL: https://docs.browserless.io/bql-schema/types/objects/text-response

Text content of a page

```graphql
type TextResponse {
  text: String
  time: Float
}
```

### Fields

#### [TextResponse.text](#text)[String](/bql-schema/types/scalars/string) 
The textual content of the page

#### [TextResponse.time](#time)[Float](/bql-schema/types/scalars/float) 
The amount of time, in milliseconds, elapsed since the start of text retrieval to completion

### Returned By

[`text`](/bql-schema/operations/mutations/text)

---

# TitleResponse

URL: https://docs.browserless.io/bql-schema/types/objects/title-response

Response returned after the page's title has been set or get

```graphql
type TitleResponse {
  title: String
}
```

### Fields

#### [TitleResponse.title](#title)[String](/bql-schema/types/scalars/string) 
The title of the current page

### Returned By

[`title`](/bql-schema/operations/mutations/title)

---

# TypeResponse

URL: https://docs.browserless.io/bql-schema/types/objects/type-response

Response returned after having typed into an element

```graphql
type TypeResponse {
  selector: String
  text: String
  time: Float
  x: Float
  y: Float
}
```

### Fields

#### [TypeResponse.selector](#selector)[String](/bql-schema/types/scalars/string) 
The selector of the element you typed into

#### [TypeResponse.text](#text)[String](/bql-schema/types/scalars/string) 
The textual content that was typed

#### [TypeResponse.time](#time)[Float](/bql-schema/types/scalars/float) 
The amount of time, in milliseconds, elapsed since the start of typing to completion

#### [TypeResponse.x](#x)[Float](/bql-schema/types/scalars/float) 
The X coordinate of the element, in pixels, on the page

#### [TypeResponse.y](#y)[Float](/bql-schema/types/scalars/float) 
The Y coordinate of the element, in pixels, on the page

### Returned By

[`type`](/bql-schema/operations/mutations/type)

---

# URLResponse

URL: https://docs.browserless.io/bql-schema/types/objects/urlresponse

Response returned after the URL of the page has been set or get

```graphql
type URLResponse {
  url: String
}
```

### Fields

#### [URLResponse.url](#url)[String](/bql-schema/types/scalars/string) 
The URL of the current page

### Returned By

[`url`](/bql-schema/operations/mutations/url)

---

# UserAgentResponse

URL: https://docs.browserless.io/bql-schema/types/objects/user-agent-response

The response returned after setting the User-Agent

```graphql
type UserAgentResponse {
  userAgent: String
  time: Float
}
```

### Fields

#### [UserAgentResponse.userAgent](#user-agent)[String](/bql-schema/types/scalars/string) 
The User-Agent string that was set

#### [UserAgentResponse.time](#time)[Float](/bql-schema/types/scalars/float) 
The time it took to set the User-Agent

### Returned By

[`userAgent`](/bql-schema/operations/mutations/user-agent)

---

# VerifyResponse

URL: https://docs.browserless.io/bql-schema/types/objects/verify-response

Response returned after a verification method has been bypassed

```graphql
type VerifyResponse {
  found: Boolean
  solved: Boolean
  time: Float
}
```

### Fields

#### [VerifyResponse.found](#found)[Boolean](/bql-schema/types/scalars/boolean) 
If a verification was found or not

#### [VerifyResponse.solved](#solved)[Boolean](/bql-schema/types/scalars/boolean) 
If a verification was found, whether or not it was clicked

#### [VerifyResponse.time](#time)[Float](/bql-schema/types/scalars/float) 
The total time it took to find, and click, the verification

---

# ViewportResponse

URL: https://docs.browserless.io/bql-schema/types/objects/viewport-response

Response returned after setting the viewport

```graphql
type ViewportResponse {
  width: Float
  height: Float
  deviceScaleFactor: Float
  mobile: Boolean
  hasTouch: Boolean
  landscape: Boolean
  time: Float
}
```

### Fields

#### [ViewportResponse.width](#width)[Float](/bql-schema/types/scalars/float) 
The width of the viewport in pixels

#### [ViewportResponse.height](#height)[Float](/bql-schema/types/scalars/float) 
The height of the viewport in pixels

#### [ViewportResponse.deviceScaleFactor](#device-scale-factor)[Float](/bql-schema/types/scalars/float) 
The device scale factor

#### [ViewportResponse.mobile](#mobile)[Boolean](/bql-schema/types/scalars/boolean) 
Whether the device is in mobile mode

#### [ViewportResponse.hasTouch](#has-touch)[Boolean](/bql-schema/types/scalars/boolean) 
Whether the viewport supports touch events

#### [ViewportResponse.landscape](#landscape)[Boolean](/bql-schema/types/scalars/boolean) 
Whether the viewport is in landscape orientation

#### [ViewportResponse.time](#time)[Float](/bql-schema/types/scalars/float) 
The time it took to set the viewport

### Returned By

[`viewport`](/bql-schema/operations/mutations/viewport)

---

# WaitForEvent

URL: https://docs.browserless.io/bql-schema/types/objects/wait-for-event

Response returned after having waited for an event to fire on the page

```graphql
type WaitForEvent {
  time: Float
}
```

### Fields

#### [WaitForEvent.time](#time)[Float](/bql-schema/types/scalars/float) 
The period of time elapsed, in milliseconds, waited for

### Returned By

[`waitForEvent`](/bql-schema/operations/mutations/wait-for-event)  [`watchEvent`](/bql-schema/operations/mutations/watch-event)

---

# WaitForFunction

URL: https://docs.browserless.io/bql-schema/types/objects/wait-for-function

Response returned after having waited for a JavaScript predicate to become truthy

```graphql
type WaitForFunction {
  time: Float
}
```

### Fields

#### [WaitForFunction.time](#time)[Float](/bql-schema/types/scalars/float) 
The period of time elapsed, in milliseconds, waited for

### Returned By

[`waitForFunction`](/bql-schema/operations/mutations/wait-for-function)

---

# WaitForRequest

URL: https://docs.browserless.io/bql-schema/types/objects/wait-for-request

Response returned after a particular network request has been sent

```graphql
type WaitForRequest {
  time: Float
  url: String
}
```

### Fields

#### [WaitForRequest.time](#time)[Float](/bql-schema/types/scalars/float) 
The period of time elapsed, in milliseconds, waited for

#### [WaitForRequest.url](#url)[String](/bql-schema/types/scalars/string) 
The URL parameter used to match the response with

### Returned By

[`waitForRequest`](/bql-schema/operations/mutations/wait-for-request)

---

# WaitForResponse

URL: https://docs.browserless.io/bql-schema/types/objects/wait-for-response

Response returned after a particular network response has been received

```graphql
type WaitForResponse {
  time: Float
  status: Int
  url: String
}
```

### Fields

#### [WaitForResponse.time](#time)[Float](/bql-schema/types/scalars/float) 
The period of time elapsed, in milliseconds, waited for

#### [WaitForResponse.status](#status)[Int](/bql-schema/types/scalars/int) 
The status code response of the response

#### [WaitForResponse.url](#url)[String](/bql-schema/types/scalars/string) 
The URL parameter used to match the response with

### Returned By

[`waitForResponse`](/bql-schema/operations/mutations/wait-for-response)

---

# WaitForSelector

URL: https://docs.browserless.io/bql-schema/types/objects/wait-for-selector

Response returned after a particular selector has been found in the DOM

```graphql
type WaitForSelector {
  height: Float
  selector: String
  time: Float
  y: Float
  x: Float
  width: Float
}
```

### Fields

#### [WaitForSelector.height](#height)[Float](/bql-schema/types/scalars/float) 
The height, in pixels, of the element

#### [WaitForSelector.selector](#selector)[String](/bql-schema/types/scalars/string) 
The selector waited for

#### [WaitForSelector.time](#time)[Float](/bql-schema/types/scalars/float) 
The period of time elapsed, in milliseconds, waited for

#### [WaitForSelector.y](#y)[Float](/bql-schema/types/scalars/float) 
The position, in pixels, top of the viewport

#### [WaitForSelector.x](#x)[Float](/bql-schema/types/scalars/float) 
The position, in pixels, left of the viewport

#### [WaitForSelector.width](#width)[Float](/bql-schema/types/scalars/float) 
The width, in pixels, of the element

### Returned By

[`waitForSelector`](/bql-schema/operations/mutations/wait-for-selector)

---

# WaitForTimeout

URL: https://docs.browserless.io/bql-schema/types/objects/wait-for-timeout

Response returned after having waited for a selector to appear in the DOM

```graphql
type WaitForTimeout {
  time: Float
}
```

### Fields

#### [WaitForTimeout.time](#time)[Float](/bql-schema/types/scalars/float) 
The period of time elapsed, in milliseconds, waited for

### Returned By

[`waitForNetworkIdle`](/bql-schema/operations/mutations/wait-for-network-idle)  [`waitForTimeout`](/bql-schema/operations/mutations/wait-for-timeout)

---

# Boolean

URL: https://docs.browserless.io/bql-schema/types/scalars/boolean

The `Boolean` scalar type represents `true` or `false`.

```graphql
scalar Boolean
```

### Member Of

[`CaptchaResponse`](/bql-schema/types/objects/captcha-response)  [`checkbox`](/bql-schema/operations/mutations/checkbox)  [`CleanInput`](/bql-schema/types/inputs/clean-input)  [`click`](/bql-schema/operations/mutations/click)  [`CookieInput`](/bql-schema/types/inputs/cookie-input)  [`CookiePartitionKeyInput`](/bql-schema/types/inputs/cookie-partition-key-input)  [`hover`](/bql-schema/operations/mutations/hover)  [`html`](/bql-schema/operations/mutations/html)  [`if`](/bql-schema/operations/mutations/if)  [`include`](/bql-schema/operations/directives/include)  [`javaScriptEnabled`](/bql-schema/operations/mutations/java-script-enabled)  [`JavaScriptResponse`](/bql-schema/types/objects/java-script-response)  [`liveURL`](/bql-schema/operations/mutations/live-url)  [`LoadSecretResponse`](/bql-schema/types/objects/load-secret-response)  [`mapSelector`](/bql-schema/operations/mutations/map-selector)  [`markdown`](/bql-schema/operations/mutations/markdown)  [`pdf`](/bql-schema/operations/mutations/pdf)  [`proxy`](/bql-schema/operations/mutations/proxy)  [`querySelector`](/bql-schema/operations/mutations/query-selector)  [`querySelectorAll`](/bql-schema/operations/mutations/query-selector-all)  [`reject`](/bql-schema/operations/mutations/reject)  [`RejectResponse`](/bql-schema/types/objects/reject-response)  [`request`](/bql-schema/operations/mutations/request)  [`response`](/bql-schema/operations/mutations/response)  [`ResponseResponse`](/bql-schema/types/objects/response-response)  [`SaveSecretResponse`](/bql-schema/types/objects/save-secret-response)  [`screenshot`](/bql-schema/operations/mutations/screenshot)  [`scroll`](/bql-schema/operations/mutations/scroll)  [`select`](/bql-schema/operations/mutations/select)  [`skip`](/bql-schema/operations/directives/skip)  [`solve`](/bql-schema/operations/mutations/solve)  [`StandardCookie`](/bql-schema/types/objects/standard-cookie)  [`StopSessionRecordingResponse`](/bql-schema/types/objects/stop-session-recording-response)  [`switchToWindow`](/bql-schema/operations/mutations/switch-to-window)  [`SwitchWindowResponse`](/bql-schema/types/objects/switch-window-response)  [`text`](/bql-schema/operations/mutations/text)  [`type`](/bql-schema/operations/mutations/type)  [`verify`](/bql-schema/operations/mutations/verify)  [`VerifyResponse`](/bql-schema/types/objects/verify-response)  [`viewport`](/bql-schema/operations/mutations/viewport)  [`ViewportResponse`](/bql-schema/types/objects/viewport-response)  [`waitForSelector`](/bql-schema/operations/mutations/wait-for-selector)

---

# Float

URL: https://docs.browserless.io/bql-schema/types/scalars/float

The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE&#x005F;floating&#x005F;point).

```graphql
scalar Float
```

### Member Of

[`AddScriptTagResponse`](/bql-schema/types/objects/add-script-tag-response)  [`AddStyleTagResponse`](/bql-schema/types/objects/add-style-tag-response)  [`back`](/bql-schema/operations/mutations/back)  [`CaptchaResponse`](/bql-schema/types/objects/captcha-response)  [`checkbox`](/bql-schema/operations/mutations/checkbox)  [`click`](/bql-schema/operations/mutations/click)  [`ClickResponse`](/bql-schema/types/objects/click-response)  [`ConsoleMessage`](/bql-schema/types/objects/console-message)  [`content`](/bql-schema/operations/mutations/content)  [`CookieInput`](/bql-schema/types/inputs/cookie-input)  [`CookieResponse`](/bql-schema/types/objects/cookie-response)  [`DefaultResponse`](/bql-schema/types/objects/default-response)  [`EmulateMediaTypeResponse`](/bql-schema/types/objects/emulate-media-type-response)  [`evaluate`](/bql-schema/operations/mutations/evaluate)  [`EvaluateResponse`](/bql-schema/types/objects/evaluate-response)  [`forward`](/bql-schema/operations/mutations/forward)  [`FulfillResponse`](/bql-schema/types/objects/fulfill-response)  [`goto`](/bql-schema/operations/mutations/goto)  [`hover`](/bql-schema/operations/mutations/hover)  [`HoverResponse`](/bql-schema/types/objects/hover-response)  [`html`](/bql-schema/operations/mutations/html)  [`HTMLResponse`](/bql-schema/types/objects/htmlresponse)  [`HTTPHeadersResponse`](/bql-schema/types/objects/httpheaders-response)  [`HTTPResponse`](/bql-schema/types/objects/httpresponse)  [`JavaScriptResponse`](/bql-schema/types/objects/java-script-response)  [`liveURL`](/bql-schema/operations/mutations/live-url)  [`LiveURLResponse`](/bql-schema/types/objects/live-urlresponse)  [`LoadSecretResponse`](/bql-schema/types/objects/load-secret-response)  [`mapSelector`](/bql-schema/operations/mutations/map-selector)  [`markdown`](/bql-schema/operations/mutations/markdown)  [`MarkdownResponse`](/bql-schema/types/objects/markdown-response)  [`pdf`](/bql-schema/operations/mutations/pdf)  [`PDFResponse`](/bql-schema/types/objects/pdfresponse)  [`preferences`](/bql-schema/operations/mutations/preferences)  [`ProxyResponse`](/bql-schema/types/objects/proxy-response)  [`querySelector`](/bql-schema/operations/mutations/query-selector)  [`querySelectorAll`](/bql-schema/operations/mutations/query-selector-all)  [`QuerySelectorResponse`](/bql-schema/types/objects/query-selector-response)  [`reconnect`](/bql-schema/operations/mutations/reconnect)  [`RejectResponse`](/bql-schema/types/objects/reject-response)  [`reload`](/bql-schema/operations/mutations/reload)  [`request`](/bql-schema/operations/mutations/request)  [`response`](/bql-schema/operations/mutations/response)  [`SaveSecretResponse`](/bql-schema/types/objects/save-secret-response)  [`screenshot`](/bql-schema/operations/mutations/screenshot)  [`ScreenshotClip`](/bql-schema/types/inputs/screenshot-clip)  [`ScreenshotResponse`](/bql-schema/types/objects/screenshot-response)  [`scroll`](/bql-schema/operations/mutations/scroll)  [`ScrollResponse`](/bql-schema/types/objects/scroll-response)  [`select`](/bql-schema/operations/mutations/select)  [`SelectResponse`](/bql-schema/types/objects/select-response)  [`solve`](/bql-schema/operations/mutations/solve)  [`solveImageCaptcha`](/bql-schema/operations/mutations/solve-image-captcha)  [`StandardCookie`](/bql-schema/types/objects/standard-cookie)  [`switchToWindow`](/bql-schema/operations/mutations/switch-to-window)  [`text`](/bql-schema/operations/mutations/text)  [`TextResponse`](/bql-schema/types/objects/text-response)  [`type`](/bql-schema/operations/mutations/type)  [`TypeResponse`](/bql-schema/types/objects/type-response)  [`UserAgentResponse`](/bql-schema/types/objects/user-agent-response)  [`verify`](/bql-schema/operations/mutations/verify)  [`VerifyResponse`](/bql-schema/types/objects/verify-response)  [`viewport`](/bql-schema/operations/mutations/viewport)  [`ViewportResponse`](/bql-schema/types/objects/viewport-response)  [`waitForEvent`](/bql-schema/operations/mutations/wait-for-event)  [`WaitForEvent`](/bql-schema/types/objects/wait-for-event)  [`waitForFunction`](/bql-schema/operations/mutations/wait-for-function)  [`WaitForFunction`](/bql-schema/types/objects/wait-for-function)  [`waitForNavigation`](/bql-schema/operations/mutations/wait-for-navigation)  [`waitForNetworkIdle`](/bql-schema/operations/mutations/wait-for-network-idle)  [`waitForRequest`](/bql-schema/operations/mutations/wait-for-request)  [`WaitForRequest`](/bql-schema/types/objects/wait-for-request)  [`WaitForResponse`](/bql-schema/types/objects/wait-for-response)  [`waitForSelector`](/bql-schema/operations/mutations/wait-for-selector)  [`WaitForSelector`](/bql-schema/types/objects/wait-for-selector)  [`waitForTimeout`](/bql-schema/operations/mutations/wait-for-timeout)  [`WaitForTimeout`](/bql-schema/types/objects/wait-for-timeout)

---

# FloatOrString

URL: https://docs.browserless.io/bql-schema/types/scalars/float-or-string

Holds the value of a either a float or a string

```graphql
scalar FloatOrString
```

### Member Of

[`pdf`](/bql-schema/operations/mutations/pdf)

---

# Int

URL: https://docs.browserless.io/bql-schema/types/scalars/int

The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.

```graphql
scalar Int
```

### Member Of

[`ConsoleMessageLocation`](/bql-schema/types/objects/console-message-location)  [`fulfill`](/bql-schema/operations/mutations/fulfill)  [`HTTPResponse`](/bql-schema/types/objects/httpresponse)  [`liveURL`](/bql-schema/operations/mutations/live-url)  [`response`](/bql-schema/operations/mutations/response)  [`response`](/bql-schema/operations/subscriptions/response)  [`ResponseInput`](/bql-schema/types/inputs/response-input)  [`ResponseResponse`](/bql-schema/types/objects/response-response)  [`type`](/bql-schema/operations/mutations/type)  [`waitForNetworkIdle`](/bql-schema/operations/mutations/wait-for-network-idle)  [`waitForResponse`](/bql-schema/operations/mutations/wait-for-response)  [`WaitForResponse`](/bql-schema/types/objects/wait-for-response)

---

# String

URL: https://docs.browserless.io/bql-schema/types/scalars/string

The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.

```graphql
scalar String
```

### Returned By

[`browser`](/bql-schema/operations/queries/browser)  [`version`](/bql-schema/operations/queries/version)  

### Member Of

[`addScriptTag`](/bql-schema/operations/mutations/add-script-tag)  [`addStyleTag`](/bql-schema/operations/mutations/add-style-tag)  [`Attribute`](/bql-schema/types/objects/attribute)  [`authenticate`](/bql-schema/operations/mutations/authenticate)  [`CaptchaResponse`](/bql-schema/types/objects/captcha-response)  [`checkbox`](/bql-schema/operations/mutations/checkbox)  [`CleanInput`](/bql-schema/types/inputs/clean-input)  [`click`](/bql-schema/operations/mutations/click)  [`ClickResponse`](/bql-schema/types/objects/click-response)  [`closeLiveURL`](/bql-schema/operations/mutations/close-live-url)  [`CloseLiveURLResponse`](/bql-schema/types/objects/close-live-urlresponse)  [`ConsoleMessage`](/bql-schema/types/objects/console-message)  [`ConsoleMessageLocation`](/bql-schema/types/objects/console-message-location)  [`content`](/bql-schema/operations/mutations/content)  [`CookieInput`](/bql-schema/types/inputs/cookie-input)  [`CookiePartitionKeyInput`](/bql-schema/types/inputs/cookie-partition-key-input)  [`deprecated`](/bql-schema/types/directives/deprecated)  [`EmulateMediaTypeResponse`](/bql-schema/types/objects/emulate-media-type-response)  [`evaluate`](/bql-schema/operations/mutations/evaluate)  [`EvaluateResponse`](/bql-schema/types/objects/evaluate-response)  [`export`](/bql-schema/operations/directives/export)  [`fulfill`](/bql-schema/operations/mutations/fulfill)  [`goto`](/bql-schema/operations/mutations/goto)  [`HeaderInput`](/bql-schema/types/inputs/header-input)  [`Headers`](/bql-schema/types/objects/headers)  [`hover`](/bql-schema/operations/mutations/hover)  [`HoverResponse`](/bql-schema/types/objects/hover-response)  [`html`](/bql-schema/operations/mutations/html)  [`HTMLResourcePathInput`](/bql-schema/types/inputs/htmlresource-path-input)  [`HTMLResponse`](/bql-schema/types/objects/htmlresponse)  [`HTMLRewriteInput`](/bql-schema/types/inputs/htmlrewrite-input)  [`HTTPHeaders`](/bql-schema/types/objects/httpheaders)  [`HTTPResponse`](/bql-schema/types/objects/httpresponse)  [`if`](/bql-schema/operations/mutations/if)  [`ifnot`](/bql-schema/operations/mutations/ifnot)  [`liveURL`](/bql-schema/operations/mutations/live-url)  [`LiveURLResponse`](/bql-schema/types/objects/live-urlresponse)  [`loadSecret`](/bql-schema/operations/mutations/load-secret)  [`LoadSecretResponse`](/bql-schema/types/objects/load-secret-response)  [`mapSelector`](/bql-schema/operations/mutations/map-selector)  [`MapSelectorResponse`](/bql-schema/types/objects/map-selector-response)  [`markdown`](/bql-schema/operations/mutations/markdown)  [`MarkdownResponse`](/bql-schema/types/objects/markdown-response)  [`pdf`](/bql-schema/operations/mutations/pdf)  [`PDFResponse`](/bql-schema/types/objects/pdfresponse)  [`proxy`](/bql-schema/operations/mutations/proxy)  [`querySelector`](/bql-schema/operations/mutations/query-selector)  [`querySelectorAll`](/bql-schema/operations/mutations/query-selector-all)  [`QuerySelectorResponse`](/bql-schema/types/objects/query-selector-response)  [`ReconnectionResponse`](/bql-schema/types/objects/reconnection-response)  [`reject`](/bql-schema/operations/mutations/reject)  [`request`](/bql-schema/operations/mutations/request)  [`request`](/bql-schema/operations/subscriptions/request)  [`RequestInput`](/bql-schema/types/inputs/request-input)  [`RequestResponse`](/bql-schema/types/objects/request-response)  [`response`](/bql-schema/operations/mutations/response)  [`response`](/bql-schema/operations/subscriptions/response)  [`ResponseInput`](/bql-schema/types/inputs/response-input)  [`ResponseResponse`](/bql-schema/types/objects/response-response)  [`saveSecret`](/bql-schema/operations/mutations/save-secret)  [`SaveSecretResponse`](/bql-schema/types/objects/save-secret-response)  [`screenshot`](/bql-schema/operations/mutations/screenshot)  [`ScreenshotResponse`](/bql-schema/types/objects/screenshot-response)  [`scroll`](/bql-schema/operations/mutations/scroll)  [`ScrollResponse`](/bql-schema/types/objects/scroll-response)  [`select`](/bql-schema/operations/mutations/select)  [`SelectResponse`](/bql-schema/types/objects/select-response)  [`solveImageCaptcha`](/bql-schema/operations/mutations/solve-image-captcha)  [`specifiedBy`](/bql-schema/types/directives/specified-by)  [`StandardCookie`](/bql-schema/types/objects/standard-cookie)  [`StopSessionRecordingResponse`](/bql-schema/types/objects/stop-session-recording-response)  [`switchToWindow`](/bql-schema/operations/mutations/switch-to-window)  [`SwitchWindowResponse`](/bql-schema/types/objects/switch-window-response)  [`text`](/bql-schema/operations/mutations/text)  [`TextResponse`](/bql-schema/types/objects/text-response)  [`TitleResponse`](/bql-schema/types/objects/title-response)  [`type`](/bql-schema/operations/mutations/type)  [`TypeResponse`](/bql-schema/types/objects/type-response)  [`URLResponse`](/bql-schema/types/objects/urlresponse)  [`userAgent`](/bql-schema/operations/mutations/user-agent)  [`UserAgentResponse`](/bql-schema/types/objects/user-agent-response)  [`waitForEvent`](/bql-schema/operations/mutations/wait-for-event)  [`waitForFunction`](/bql-schema/operations/mutations/wait-for-function)  [`waitForRequest`](/bql-schema/operations/mutations/wait-for-request)  [`WaitForRequest`](/bql-schema/types/objects/wait-for-request)  [`waitForResponse`](/bql-schema/operations/mutations/wait-for-response)  [`WaitForResponse`](/bql-schema/types/objects/wait-for-response)  [`waitForSelector`](/bql-schema/operations/mutations/wait-for-selector)  [`WaitForSelector`](/bql-schema/types/objects/wait-for-selector)  [`watchEvent`](/bql-schema/operations/mutations/watch-event)

---

# StringOrArray

URL: https://docs.browserless.io/bql-schema/types/scalars/string-or-array

Holds the value of a either a string or an array of strings

```graphql
scalar StringOrArray
```

### Member Of

[`select`](/bql-schema/operations/mutations/select)  [`SelectResponse`](/bql-schema/types/objects/select-response)

---

# Running JavaScript with evaluate

URL: https://docs.browserless.io/browserql/advanced-config/multi-line-javascript

The `evaluate` mutation runs JavaScript in the browser's page environment and returns the result. You can pass either a single expression or a multi-line statement body, and `evaluate` figures out which form you used.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Familiarity with [BQL language basics](/browserql/writing-bql/language-basics)

## Single expressions

For a single expression, write the expression and `evaluate` returns its value. You don't need a `return` statement:

```graphql
mutation EvaluateExpression {
  goto(url: "https://example.com") {
    status
  }

  evaluate(content: "document.title") {
    value
  }
}
```

## Multi-line statement bodies

For anything longer than one expression, use triple quotes (`"""`) and write your code as a normal statement body. Use your own `return` to pass a value back. No IIFE (Immediately Invoked Function Expression) wrapper is required:

```graphql
mutation MultiLineEvaluate {
  goto(url: "https://example.com") {
    status
  }

  evaluate(content: """
    const title = document.title;
    const url = window.location.href;
    return JSON.stringify({ title, url });
  """) {
    value
  }
}
```

Earlier versions only accepted single expressions, so multi-line code had to be wrapped in an IIFE by hand. That's no longer necessary. Existing IIFE-wrapped scripts still work, so you don't need to rewrite anything that already runs.

**Example use cases:**
- Extract complex data from the DOM
- Manipulate page elements dynamically, such as deleting or adding elements to the HTML
- Perform calculations or transformations

## Advanced Examples

### Extracting Complex Data

```graphql
mutation ExtractComplexData {
  goto(url: "https://example.com") {
    status
  }

  evaluate(content: """
    try {
      // Extract all links with their text and href.
      const links = Array.from(document.querySelectorAll('a')).map(link => ({
        text: link.textContent.trim(),
        href: link.href,
        isExternal: !link.href.includes(window.location.hostname)
      }));

      // Get page metadata.
      const meta = {
        title: document.title,
        description: document.querySelector('meta[name="description"]')?.content || '',
        keywords: document.querySelector('meta[name="keywords"]')?.content || '',
        viewport: document.querySelector('meta[name="viewport"]')?.content || ''
      };

      return JSON.stringify({ links, meta, error: null });
    } catch (e) {
      return JSON.stringify({ links: null, meta: null, error: (e?.message ?? String(e)) });
    }
  """) {
    value
  }
}
```

### Dynamic Element Manipulation

```graphql
mutation ManipulateElements {
  goto(url: "https://example.com") {
    status
  }

  evaluate(content: """
    try {
      // Remove all ads.
      const ads = document.querySelectorAll('[class*="ad"], [id*="ad"], [class*="banner"]');
      ads.forEach(ad => ad.remove());

      // Add a custom element.
      const customDiv = document.createElement('div');
      customDiv.innerHTML = '<h2>Custom Content Added by BrowserQL</h2>';
      customDiv.style.cssText = 'background: #f0f0f0; padding: 20px; margin: 20px 0; border-radius: 5px;';
      document.body.insertBefore(customDiv, document.body.firstChild);

      return JSON.stringify({
        removedAds: ads.length,
        addedElement: 'Custom div added to page',
        error: null
      });
    } catch (e) {
      return JSON.stringify({ removedAds: null, addedElement: null, error: (e?.message ?? String(e)) });
    }
  """) {
    value
  }
}
```

### Data Transformation and Calculations

```graphql
mutation DataTransformation {
  goto(url: "https://example.com") {
    status
  }

  evaluate(content: """
    try {
      // Extract and transform price data.
      const priceElements = document.querySelectorAll('[class*="price"], [class*="cost"]');
      const prices = Array.from(priceElements).map(el => {
        const text = el.textContent;
        const price = parseFloat(text.replace(/[^0-9.]/g, ''));
        return {
          originalText: text,
          numericValue: price,
          currency: text.match(/[$€£¥]/)?.[0] || 'unknown'
        };
      }).filter(p => !isNaN(p.numericValue));

      // Calculate statistics.
      const values = prices.map(p => p.numericValue);
      const total = values.reduce((sum, value) => sum + value, 0);
      const average = values.length > 0 ? total / values.length : 0;
      const min = values.length > 0 ? Math.min(...values) : null;
      const max = values.length > 0 ? Math.max(...values) : null;

      return JSON.stringify({
        prices,
        statistics: { total, average, min, max, count: prices.length },
        error: null
      });
    } catch (e) {
      return JSON.stringify({ prices: null, statistics: null, error: (e?.message ?? String(e)) });
    }
  """) {
    value
  }
}
```

### Handling Links That Open in New Tabs

When automating interactions with links that have `target="_blank"`, you can modify them to open in the same browser session. This simplifies your script by avoiding the need to switch tabs or manage multiple page contexts.

```graphql
mutation avoidNewTab {
  goto(url: "https://www.example.com", waitUntil: networkIdle) {
    status
  }
  evaluate(content: """
    const link = document.querySelector('a');
    if (link && link.target === '_blank') {
      link.target = '_self';
    }
  """) {
    time
  }
  click(selector: "a") {
    time
  }
}
```

This approach modifies links from opening in new tabs to opening in the same session, keeping your automation workflow simple and avoiding complex tab management.

## Best Practices

1. **Pick the right form**: Use a bare expression when you need a single value, and a multi-line body with your own `return` for anything more involved.
2. **Return meaningful data**: In a statement body, `return` the data you need. Without a `return`, `value` comes back empty.
3. **Handle errors gracefully**: Wrap operations in try-catch blocks so a failure returns a useful message instead of breaking the mutation.
4. **Keep it focused**: Write focused, single-purpose scripts for better maintainability.

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>403 Forbidden</code> response?</summary>

Your API token is missing or malformed. Pass it as a `?token=` query parameter. Check your [account dashboard](https://browserless.io/account/) to verify the token is active.

</details>

<details>
<summary>My query times out before completing</summary>

Increase the `timeout` parameter on your mutation or use a more specific `waitUntil` condition. Long-running pages may need `networkIdle` instead of `load`.

</details>

## Next steps

Ready to explore more advanced BrowserQL features? Check out these related topics:

  <a href="/browserql/use-cases/scrape-and-extract-data" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Data Extraction</h3>
      Extract structured data from web pages using mapSelector, querySelectorAll, and network response interception.
    
  </a>

  <a href="/browserql/advanced-config/parsing-libraries" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Parsing Libraries</h3>
      Integrate BrowserQL with popular parsing libraries like Beautiful Soup, Scrapy, and Cheerio.
    
  </a>

---

# Parsing HTML with External Libraries

URL: https://docs.browserless.io/browserql/advanced-config/parsing-libraries

This guide shows how to retrieve HTML with BrowserQL, handling any JavaScript rendering, bot protection, or interaction needed to reach it, then parse the result with Beautiful Soup, Scrapy, or Cheerio.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Familiarity with [BQL language basics](/browserql/writing-bql/language-basics)

## Example

The query below navigates to `https://browserless.io/`, clicks the Pricing link, and returns the page HTML.

```graphql
mutation RetrieveHTML {
  goto(url: "https://browserless.io/") {
    status
  }
  click(selector: "a[href=\"/pricing\"]") {
    time
  }

  html {
    html
  }
}
```

## Beautiful Soup

Beautiful Soup is a Python library for parsing HTML and XML. Use it when you need to extract structured data from pages that require real browser interactions before the content is available.

```python

from bs4 import BeautifulSoup

url = 'https://browserless.io/'
token = 'YOUR_API_TOKEN_HERE'
timeout = 5 * 60 * 1000

query = '''
mutation RetrieveHTML($url: String!) {
    goto(url: $url) {
        status
    }
    click(selector: "a[href=\"/pricing\"]") {
        time
    }
    html {
        html
    }
}
'''

variables = {"url": url}
endpoint = f'https://production-sfo.browserless.io/chromium/bql?timeout={timeout}&token={token}'

payload = {"query": query, "variables": variables}

response = requests.post(endpoint, json=payload, headers={'content-type': 'application/json'})
html_content = response.json()['data']['html']['html']

soup = BeautifulSoup(html_content, 'html.parser')
plans = [tag.text.strip() for tag in soup.find_all('div', class_='tag_price margin-bottom margin-large')]

print(plans)
```

## Scrapy

Scrapy is a Python web crawling framework with a built-in selector engine and data pipeline system. Pair it with Browserless to scrape pages that block traditional crawlers.

```python

from scrapy.selector import Selector

url = 'https://browserless.io/'
token = 'YOUR_API_TOKEN_HERE'
timeout = 5 * 60 * 1000

query = '''
mutation RetrieveHTML($url: String!) {
    goto(url: $url) {
        status
    }
    click(selector: "a[href=\"/pricing\"]") {
        time
    }
    html {
        html
    }
}
'''

variables = {"url": url}
endpoint = f'https://production-sfo.browserless.io/chromium/bql?timeout={timeout}&token={token}'

payload = {"query": query, "variables": variables}

response = requests.post(endpoint, json=payload, headers={'content-type': 'application/json'})
html_content = response.json()['data']['html']['html']

selector = Selector(text=html_content)
plans = selector.css('.tag_price.margin-bottom.margin-large::text').getall()

print(plans)
```

## Cheerio

Cheerio is a jQuery-like HTML parser for Node.js. Use it to extract data from HTML returned by Browserless without running a second browser instance. If you prefer a no-code approach, the [Smart Scrape API](/rest-apis/smart-scrape) can return structured content directly.

```js

const url = 'https://browserless.io/';
const token = 'YOUR_API_TOKEN_HERE';
const timeout = 5 * 60 * 1000;

const queryParams = new URLSearchParams({ timeout, token }).toString();

const query = `
  mutation RetrieveHTML($url: String!) {
    goto(url: $url) {
      status
    }
    click(selector: "a[href=\"/pricing\"]") {
      time
    }
    html {
      html
    }
  }
`;

const endpoint = `https://production-sfo.browserless.io/chromium/bql?${queryParams}`;

const options = {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ query, variables: { url } }),
};

(async () => {
  try {
    const response = await fetch(endpoint, options);
    const { data } = await response.json();

    const $ = cheerio.load(data.html.html);
    const plans = [];
    $('.tag_price.margin-bottom.margin-large').each((_, element) => {
      plans.push($(element).text().trim());
    });

    console.log(plans);
  } catch (error) {
    console.error('Error fetching or parsing HTML:', error);
  }
})();
```

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>403 Forbidden</code> response?</summary>

Your API token is missing or malformed. Pass it as a `?token=` query parameter. Check your [account dashboard](https://browserless.io/account/) to verify the token is active.

</details>

<details>
<summary>My query times out before completing</summary>

Increase the `timeout` parameter on your mutation or use a more specific `waitUntil` condition. Long-running pages may need `networkIdle` instead of `load`.

</details>

## Next steps

  <a href="/browserql/writing-bql/language-basics" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BQL language basics</h3>
      learn BQL mutation structure and syntax
    
  </a>
  <a href="/browserql/bot-detection/overview" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Bot detection</h3>
      stealth, proxies, and CAPTCHA solving
    
  </a>

---

# BQL Best Practices

URL: https://docs.browserless.io/browserql/best-practices

This guide covers essential patterns that will make your browser automation more reliable and maintainable. If you're new to BrowserQL, start with the [Writing BrowserQL guide](/browserql/writing-bql/language-basics) first.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Familiarity with [BQL language basics](/browserql/writing-bql/language-basics)

## Prefer variables over hardcoded values

Use GraphQL variables to keep your BQL operations reusable and easy to maintain. Variables let you parameterize values like URLs, selectors, and input data, so you can avoid hardcoding and repeating them across queries.

{/*ARCADE EMBED START*/}

  <iframe
    src="https://demo.arcade.software/UAmR7aN2o7ezKrFWIwRU?embed&embed_mobile=tab&embed_desktop=inline&show_copy_link=true"
    title="Test a Form Submission Script Using Variables and GraphQL"
    frameborder="0"
    loading="lazy"
    webkitallowfullscreen
    mozallowfullscreen
    allowfullscreen
    allow="clipboard-write"
    className='arcade-embed-iframe'
  />

{/*ARCADE EMBED END*/}

  
**✅ Good - Using Variables:**

```graphql
# Avoiding concatenating strings by using GraphQL variables
mutation LoginUser($email: String!, $password: String!, $loginUrl: String!) {
  goto(url: $loginUrl, waitUntil: firstContentfulPaint) {
    status
  }

  typeEmail: type(
    selector: "input[type='email']"
    text: $email
  ) {
    time
  }

  typePassword: type(
    selector: "input[type='password']"
    text: $password
  ) {
    time
  }

  submitLogin: click(selector: "button[type='submit']") {
    x
    y
  }
}
```

    Then, send a `variables` object in your JSON payload [read more about them here](https://graphql.org/learn/queries/#variables)

```json
{
  "email": "user@example.com",
  "password": "securepassword123",
  "loginUrl": "https://example.com/login"
}
```

  
  
**❌ Bad - Hardcoded Values:**

```graphql
mutation LoginUser {
  goto(url: "https://example.com/login", waitUntil: firstContentfulPaint) {
    status
  }

  # Hardcoding your credentials!
  typeEmail: type(
    selector: "input[type='email']"
    text: "user@example.com"
  ) {
    time
  }

  typePassword: type(
    selector: "input[type='password']"
    text: "securepassword123"
  ) {
    time
  }

  submitLogin: click(selector: "button[type='submit']") {
    x
    y
  }
}
```
  

## Prefer semantic selectors over flaky CSS classes

Prefer semantic selectors for more reliable automations. Use stable HTML attributes, roles, or other meaningful selectors that are less likely to change, and avoid relying on CSS classes that can break with UI updates.

  
**✅ Good - Semantic Selectors:**

```graphql
mutation AddProductToCart($url: String!) {
  goto(url: $url, waitUntil: networkIdle) {
    status
  }
  # Use HTML attributes for more reliable scraping
  click(selector: "[data-testid='add-to-cart-button']") {
    selector
  }
  # Use HTML roles instead of classes for waiting for a particular selector
  waitForSelector(
    selector: "[role='status'][aria-label='cart-item-count']"
    visible: true
  ) {
    selector
  }
  html(visible: true) {
    html
  }
}
```
  
  
**❌ Bad - Flaky CSS Classes:**

```graphql
mutation AddProductToCart($url: String!) {
  goto(url: $url) {
    status
  }

  # Use an unstable, minified class name
  click(selector: ".btn-345xy") {
    selector
  }

  # Wait for cart count using a brittle nth-child selector
  waitForSelector(selector: ".header > div:nth-child(3) > span", visible: true) {
    selector
  }
  html(visible: true) {
    html
  }
}
```
  

## Reject unnecessary resources

Block unnecessary resources to improve performance. Use the reject mutation to prevent loading ads, images, videos, and other non-essential requests, reducing bandwidth usage and speeding up execution.
Reducing bandwidth usage can lower proxy costs when proxies are enabled, but blocking too many resources can trigger bot detection on some sites.

  
**✅ Good - Block Unnecessary Resources:**

```graphql
# Make sure to enable the Adblock parameter in the editor!
mutation Search($url: String!, $query: String!) {
  # Reject images, styles and media right away
  reject(
    type: [image, stylesheet, media]
  ) {
    time
  }
  goto(url: $url, waitUntil: networkIdle) {
    status
  }
  type(text: $query, selector: "input[name='q']") {
    selector
  }
  click(selector: "button[type='submit']") {
    selector
  }
  waitForNavigation {
    status
  }
  html(visible: true) {
    html
  }
}
```
  
  
**❌ Bad - Loading Everything:**

```graphql
mutation Search($url: String!, $query: String!) {
  # Waits until all images, styles, and requests are done loading
  # before continuing
  goto(url: $url, waitUntil: networkIdle) {
    status
  }
  type(text: $query, selector: "input[name='q']") {
    selector
  }
  click(selector: "button[type='submit']") {
    selector
  }
  waitForNavigation {
    status
  }
  html(visible: true) {
    html
  }
}
```
  

## FAQ & Troubleshooting

<details>
<summary>How do I handle pages that load content dynamically?</summary>

Use wait mutations like `waitForSelector`, `waitForTimeout`, or `waitForResponse` to pause execution until the content you need is present. BQL also supports response interception to capture API data directly.

</details>

<details>
<summary>How do I target elements inside an iframe or shadow DOM?</summary>

Use a deep selector: prefix any selector with `< ` (for example `selector: "< button.submit"`) and interaction mutations like `click` and `type` traverse all iframes and shadow DOMs, open or closed. See [deep selectors](/browserql/use-cases/submitting-forms#deep-selectors-for-iframes-and-shadow-dom) for URL-scoped variants and limitations. To wait for iframe content, use `waitForSelector` on the `iframe` element itself first, then interact through deep selectors.

</details>

<details>
<summary>Should I use the stealth route for all queries?</summary>

The stealth route adds overhead for fingerprint randomization and entropy injection. Use the standard `/chromium/bql` route for sites without bot detection, and switch to `/stealth/bql` only when you encounter blocks or CAPTCHAs.

</details>

## Next steps

  <a href="/browserql/writing-bql/language-basics" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Language basics</h3>
      BQL mutation structure, aliases, and actions
    
  </a>
  <a href="/browserql/bot-detection/overview" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Bot detection</h3>
      stealth, proxies, and CAPTCHA solving
    
  </a>
  <a href="/bql-schema/operations/mutations/goto" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BQL API reference</h3>
      full list of available mutations
    
  </a>

---

# OS emulation

URL: https://docs.browserless.io/browserql/bot-detection/os-emulation

BrowserQL sessions run on Linux, so they expose a Linux fingerprint by default. Bot detection systems cross-check signals like the User-Agent, `navigator.platform`, UA Client Hints, GPU renderer, and media devices, and flag sessions where these don't line up. The `emulationOs` query parameter switches the session to a coherent operating system identity — set it to `windows` or `macos` to move off the default Linux fingerprint.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Familiarity with [BrowserQL basics](/browserql/writing-bql/language-basics)

`emulationOs` is a URL query parameter on the endpoint. It is not a header, a launch option, or a mutation, and it behaves identically whether you call BQL over HTTP POST or WebSocket. It works on any [regional endpoint](/baas/connection-url-patterns#regional-endpoints), on stealth and BQL routes, on the unblock and scrape REST APIs (`/unblock`, `/smart-scrape`), and on the Agent route (`/chromium/agent`). On a plain route like `/chromium` or `/chrome` it is ignored rather than rejected, and the session keeps its default identity.

## Emulating an OS on BQL routes

Every BQL route — `/chromium/bql`, `/chrome/bql`, and `/stealth/bql` — runs a stealth-hardened browser. `emulationOs` selects the operating system identity the session presents: pass `windows` or `macos` to apply that platform's full fingerprint stack, or omit it to keep the native Linux identity.

```bash
# Windows identity on /stealth/bql
curl -X POST \
  'https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE&emulationOs=windows' \
  -H 'Content-Type: application/json' \
  -d '{
    "query": "mutation { goto(url: \"https://example.com\") { status } }"
  }'
```

## Accepted values

| Value | Emulated identity |
|-------|-------------------|
| `windows` | Windows / Chrome on Win64 |
| `macos` | macOS / Chrome on Intel Mac |
| `linux` | Native Linux identity (the default when omitted) |
| `android` | Mobile Chrome on Android — see [Android (mobile) emulation](#android-mobile-emulation) |

Desktop `windows`, `macos`, and `linux` and mobile `android` identities are supported. iPhone, iOS, and Safari are not. Values are exact-match and lowercase: any other value, including a typo or wrong casing like `macOS`, is silently ignored. The session behaves as if `emulationOs` wasn't set, so double-check the spelling, because there's no error to catch.

## What gets spoofed

When `emulationOs` is set, the session presents one internally consistent set of signals for the target OS:

- **User-Agent** (e.g. `Windows NT 10.0; Win64; x64`)
- **UA Client Hints** — `Sec-CH-UA-Platform`, `Sec-CH-UA-Platform-Version`, `getHighEntropyValues()`
- **`navigator.platform`** — `Win32`, `MacIntel`, or `Linux x86_64`
- **GPU / WebGL renderer** and **CPU core count** (`navigator.hardwareConcurrency`) for the platform
- **Media devices, speech-synthesis voices, and AudioContext latency** for the OS

These signals stay consistent for the session's lifetime, including across navigations and new windows.

## What it doesn't change

`emulationOs` only sets OS-derived browser signals. What stays untouched depends on the identity.

### Desktop identities

For `windows`, `macos`, and `linux`, `emulationOs` does **not** change:

- **Timezone, locale, and `Accept-Language`** — these follow your [proxy](/browserql/bot-detection/proxies)'s location, not the emulated OS.
- **Screen and viewport size** — set these on the client.
- **WebRTC handling** — applied uniformly to every stealth session.

### Android emulation

`emulationOs=android` does set screen and viewport size: it applies a high-DPI mobile viewport (see [Android (mobile) emulation](#android-mobile-emulation)). Everything else stays the same as desktop. It does **not** change:

- **Timezone, locale, and `Accept-Language`** — these follow your proxy's location, not the emulated OS.
- **WebRTC handling** — applied uniformly to every stealth session.

An emulated identity served with a mismatched timezone or locale is itself a detection signal, so pair `emulationOs` with a region-matched proxy for a fully coherent profile.

## Using with the BQL IDE

In the [BQL IDE](https://browserless.io/account/bql/), select the Stealth browser (which uses `/stealth/bql`) and append `emulationOs` to the endpoint URL in your session settings:

```
https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE&emulationOs=macos
```

Without `emulationOs` the session keeps the native Linux identity; append `&emulationOs=windows` or `&emulationOs=macos` to switch.

## Using with the WebSocket BQL route

The BQL WebSocket route speaks GraphQL over WebSocket: send `{ "query": "..." }` JSON messages and read `{ "data": ... }` replies (this is not CDP, so Puppeteer/Playwright clients don't apply here). Pass `emulationOs` in the connection URL. From Node.js with the [`ws`](https://www.npmjs.com/package/ws) package:

```js

const TOKEN = "YOUR_API_TOKEN_HERE";

const ws = new WebSocket(
  `wss://production-sfo.browserless.io/stealth/bql?token=${TOKEN}&emulationOs=windows`
);

ws.on("open", () => {
  ws.send(JSON.stringify({
    query: `mutation { goto(url: "https://example.com") { status } }`,
  }));
});

ws.on("message", (data) => console.log(JSON.parse(data.toString())));
```

## Combining with other BQL features

`emulationOs` composes with other BQL mutations:

- **`userAgent()` mutation** overrides the User-Agent for the rest of the session. For a desktop Windows or macOS Chrome/Chromium User-Agent, `navigator.platform` and UA Client Hints are derived from it automatically to stay coherent; a Linux, mobile, or non-Chromium User-Agent is applied as-is and leaves the emulated platform and Client Hints unchanged — so keep the User-Agent consistent with the emulated OS.
- **`proxy` mutation** adds a residential or datacenter proxy. The OS fingerprint and proxy are independent layers — match the proxy region to the emulated OS for a coherent timezone and locale.
- **`solve()` mutation** for CAPTCHA solving works normally on emulated sessions.

```graphql
mutation {
  goto(url: "https://protected-site.com") { status }
  solve(wait: true) { solved }
}
```

The above query, sent to `/stealth/bql?emulationOs=windows`, runs with a full Windows identity and solves any detected CAPTCHA.

## Android (mobile) emulation

Set `emulationOs=android` to make a BQL session present as mobile Chrome on Android instead of a desktop OS. Add `emulatedDevice=<slug>` to pick a specific phone, or omit it and Browserless picks a real device for you and keeps it consistent for the whole session. Android emulation runs on the Chromium and Brave stealth engines. iPhone, iOS, and Safari are not supported.

### Enabling Android emulation

`emulationOs` and `emulatedDevice` are connection query-string parameters on the endpoint URL, not BQL mutation arguments. They work the same over HTTP POST and WebSocket on any BQL route.

BQL over HTTP POST — put the parameters on the URL and send a normal GraphQL body:

```bash
curl -X POST \
  'https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE&emulationOs=android&emulatedDevice=galaxy-s25-ultra' \
  -H 'Content-Type: application/json' \
  -d '{
    "query": "mutation { goto(url: \"https://example.com\") { status } title }"
  }'
```

BQL over WebSocket:

```bash
wss://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE&emulationOs=android&emulatedDevice=pixel-8-pro
```

Omit `emulatedDevice` and Browserless picks a device for you:

```bash
wss://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE&emulationOs=android
```

The session then runs mutations as normal, exactly as it does with `emulationOs=windows`. The same parameters also work when connecting Puppeteer or Playwright to a [stealth route](/baas/bot-detection/os-emulation#android-mobile-emulation).

### Choosing a device

`emulatedDevice` takes one of the supported Android slugs — for example `pixel-8-pro` or `galaxy-s25-ultra`. See [Supported mobile devices](/baas/bot-detection/supported-mobile-devices) for the full list. Omitting `emulatedDevice` is fine: Browserless picks a real device automatically and keeps it stable across reconnects within the session.

### What gets emulated

`emulationOs=android` shifts the whole identity to a coherent mobile persona:

- **Mobile User-Agent and Client Hints** — the browser reports as Chrome on Android, and the specific model is exposed through UA Client Hints.
- **Phone screen size and pixel ratio** — a high-DPI mobile viewport.
- **Touch input** — coarse-pointer and no-hover behavior.
- **Portrait orientation.**
- **Motion sensors** — accelerometer, gyroscope, and orientation.
- **Mobile GPU / WebGL renderer identity**, Android system fonts, mobile media devices and voices, and Android-appropriate battery and network signals.

### Rules and errors

- `emulatedDevice` requires `emulationOs=android`. Sending `emulatedDevice` on a stealth session without it returns an **HTTP 400** with the message: `emulatedDevice requires emulationOs=android on a stealth request (a /stealth/*, BQL, or /chromium/agent route).`
- Device slugs are **case-sensitive** and matched exactly. Use them as listed (`pixel-8`, not `Pixel-8` or `pixel8`).
- An unrecognized `emulatedDevice` value is **not** an error. Browserless falls back to an automatically chosen device.
- `emulationOs` accepts only the exact lowercase values `windows`, `macos`, `linux`, and `android`. Any other value is silently ignored. A typo shows up as a default-identity fingerprint, not as an error.

### Limitations

- Android only — no iPhone, iOS, or Safari.
- All emulated devices are modern flagship phones in portrait orientation.

## FAQ & Troubleshooting

### Does `emulationOs` work on plain (non-stealth) routes?

No. `emulationOs` only takes effect on stealth routes, the unblock and scrape REST APIs (`/unblock`, `/chromium/unblock`, `/chrome/unblock`, `/smart-scrape`), BQL routes, and the Agent route (`/chromium/agent`). On a plain route (`/chromium`, `/chrome`) it is ignored rather than rejected, so the session silently keeps its default identity. Use a `/stealth` route, `/unblock`, `/smart-scrape`, a `/bql` route, or `/chromium/agent`.

### Can I verify what OS identity my session is reporting?

Yes. Use a JavaScript `evaluate` in BQL to check the exposed APIs:

```graphql
mutation {
  goto(url: "https://example.com") { status }
  evaluate(content: "JSON.stringify({ platform: navigator.platform, ua: navigator.userAgent })")
}
```

With `emulationOs=windows`, this returns `{"platform":"Win32","ua":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) ..."}`.

## Next Steps

  <a href="/browserql/bot-detection/stealth" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Stealth Route</h3>
      How the /stealth/bql route and BQL stealth hardening work.
    
  </a>

  <a href="/browserql/bot-detection/proxies" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Proxies</h3>
      Add residential proxies for IP-level evasion alongside OS emulation.
    
  </a>

  <a href="/browserql/bot-detection/solving-captchas" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Solving CAPTCHAs</h3>
      Handle Cloudflare, reCAPTCHA, and other challenges automatically.
    
  </a>

---

# Bot Detection

URL: https://docs.browserless.io/browserql/bot-detection/overview

BrowserQL includes built-in tools for bypassing bot detection on protected and heavily monitored pages. This section covers the stealth route, proxies, CAPTCHA solving, and browser fingerprinting.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Familiarity with [BQL language basics](/browserql/writing-bql/language-basics)

## Using a proxy

BrowserQL supports residential and third-party proxies for bypassing IP-based blocks and location restrictions. See the [Proxies documentation](/browserql/bot-detection/proxies) for configuration and usage.

## Solving CAPTCHAs

BrowserQL includes built-in support for solving Cloudflare, reCAPTCHA, and other challenge types. See the [Solving CAPTCHAs documentation](/browserql/bot-detection/solving-captchas) for techniques and form submission patterns.

## Recommended strategy for protected sites

When working with heavily protected sites, layer your bypass techniques progressively rather than enabling everything at once. This helps you identify what each site requires while minimizing resource usage.

**Start with stealth**: Run your queries on the [stealth route](/browserql/bot-detection/stealth) (`/stealth/bql`) first. If you still encounter blocks, add the techniques below.

**Add proxies when needed**: If you encounter IP-based blocks or CAPTCHAs, enable residential proxies via the `proxy` mutation. Residential proxies are more effective than datacenter IPs at bypassing detection, though they consume additional units per MB of traffic.

**Use conditional CAPTCHA solving**: When CAPTCHAs appear, use the `if` mutation with a selector to trigger solving only when a CAPTCHA is present. For example, wrap your `solve` mutation in `if(selector: "#g-recaptcha-response")` to conditionally execute it. This saves time and resources compared to always attempting to solve.

**Set generous timeouts**: CAPTCHA solving can take 30 seconds to several minutes depending on the challenge type. Use generous `timeout` values on your `solve` mutations. When in doubt, omit the timeout parameter to use the default.

## Human-like fingerprints

BrowserQL automatically configures browser sessions to appear indistinguishable from genuine user activity. You can further customize browser binaries, ad-blocking, and mouse and typing behavior patterns. These options are available in [Launch Parameters](/browserql/launch-parameters).

## FAQ & Troubleshooting

<details>
<summary>My script is still getting blocked after enabling stealth</summary>

Stealth alone may not be enough for heavily protected sites. Combine the `/stealth/bql` endpoint with residential proxies and human-like behavior. Some sites also require solving CAPTCHAs before proceeding.

</details>

<details>
<summary>Which browser binary should I use for bot-detected sites?</summary>

Start with `/stealth/bql`. If that's blocked, try `/chrome/bql` with residential proxies. The stealth binary has the most comprehensive fingerprint mitigations, but some sites specifically check for a genuine Chrome user agent.

</details>

## Next steps

  <a href="/browserql/bot-detection/solving-captchas" className="next-step-card-link">
    
      <h3 className="next-step-card-title">CAPTCHA solving</h3>
      handle reCAPTCHA, Cloudflare, and image-based challenges
    
  </a>
  <a href="/browserql/bot-detection/proxies" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Proxies</h3>
      residential and external proxy routing
    
  </a>
  <a href="/browserql/bot-detection/stealth" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Stealth route</h3>
      fingerprint mitigations and entropy injection
    
  </a>

---

# Proxies

URL: https://docs.browserless.io/browserql/bot-detection/proxies

BrowserQL supports built-in residential and datacenter proxies, as well as external third-party proxies. Use built-in proxies for IP rotation, geographic routing, and session stickiness. Use external proxies to route traffic through your own proxy provider.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Units remaining on your plan (built-in proxies consume units per MB of traffic, on every plan including free)

## Built-In Proxies

Browserless offers two built-in proxy networks:

| Network | Description | Cost |
|---|---|---|
| **Residential** (default) | Routes through real residential IP addresses. Harder to detect. | **6 units/MB** |
| **Datacenter** | Routes through datacenter IPs. Cheaper but more easily detected. | **2 units/MB** |

Both networks support the same targeting options:

- **Proxy country**: Route requests through a specific country. The datacenter pool covers fewer countries than residential; if a country has no datacenter coverage, requests won't exit from the requested location.
- **Proxy city**: Route requests through a specific city (Scale plan required).
- **Proxy state/province**: Route requests through a specific state or province (Scale plan required).
- **Sticky proxying**: Reuse the same IP across multiple requests in a session. This is the **default** on BrowserQL — pass `proxySticky=false` (or `proxy(sticky: false)`) to opt out and rotate IPs instead.

> **Proxy Cost**
> Residential proxies consume **6 units per MB** and datacenter proxies consume **2 units per MB** of traffic.

### Enable via the IDE

1. Open the **Session Settings Panel**.
2. Under **Proxy Network**, select `Residential` or `Datacenter`.
3. Set **Sticky Proxying** to reuse the same IP, or set **Proxy Country** to target a specific location.

### Enable Programmatically

Add proxy parameters directly to your BQL endpoint URL:

  
**Residential Proxy:**

```bash
https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE&proxy=residential
```
  
  
**Datacenter Proxy:**

```bash
https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE&proxy=datacenter
```
  
  
**Country-Specific Proxy:**

```bash
https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us
```
  
  
**Rotate IPs (Opt Out of Sticky):**

```bash
https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us&proxySticky=false
```
  

**Available parameters:**

- `proxy=residential`: Enables residential proxy routing (6 units/MB).
- `proxy=datacenter`: Enables datacenter proxy routing (2 units/MB).
- `proxyCountry=us`: Routes through <a href="/data/proxy-countries.json" download="proxy-countries.json">a specific country</a>. Accepts [ISO 3166 country codes](/bql-schema/types/enums/country-type).
- `proxyCity=chicago`: Routes through a specific city within the selected country.
- `proxyState=california`: Routes through a specific state or province within the selected country. Remove spaces and lowercase the value, for example `newbrunswick` for "New Brunswick".
- `proxySticky=true`: Maintains the same proxy IP across the session. **BrowserQL sessions are sticky by default**, so you rarely need to set this — pass `proxySticky=false` to opt out and rotate IPs instead. `proxySticky` is a real boolean: `false` or `0` disables it (previously any present value enabled it).
- `proxyLocaleMatch=true`: Sets the browser language to match the proxy location (for example, Portuguese for Brazil).
- `proxyPreset=px_gov01`: Applies a website-specific proxy preset (residential only). Use `px_gov01` for government websites or `px_ipv6` for Google domains such as Maps and YouTube.
- `externalProxyServer`: Routes traffic through your own proxy server. Format: `http(s)://[username:password@]host:port`. URL-encode the value.

> **Sticky by default**
> BrowserQL sessions pin one stable exit IP for the whole session by default. Anti-bot systems like Akamai and DataDome bind their sensor data and cookies to the egress IP, so a mid-session IP rotation can re-trigger the block. Keep sticky enabled when evading detection; pass `proxySticky=false` (or `proxy(sticky: false)`) only when you specifically want a fresh IP per request.

> **City- and State-Level Proxying**
> City- and state-level proxying require a **Scale plan** (500k+ units). Plans under 500k units receive a `401` error.
> 
> To get available cities:
> 
> - All supported cities: `https://production-sfo.browserless.io/proxy/cities?token=YOUR_TOKEN`
> - Cities for a specific country: `https://production-sfo.browserless.io/proxy/cities?country=US&token=YOUR_TOKEN`

### BQL `proxy` Mutation: Network Argument

When using the BQL `proxy` mutation, you can select the proxy network with the `network` argument:

```graphql
mutation DatacenterProxy {
  proxy(network: datacenter, url: "*", country: US) {
    time
  }
  goto(url: "https://example.com", waitUntil: load) {
    status
  }
}
```

The `network` argument accepts `residential` (default) or `datacenter`. It cannot be combined with the `server` argument.

## External Proxies

Use the `proxy` mutation to route traffic through your own proxy server. The mutation accepts a server URI and lets you scope which requests get proxied by URL pattern, HTTP method, or request type.

The server URI follows the format `protocol://username:password@host:port`. Omit the credentials if your proxy doesn't require authentication.

### Proxy All Requests

Use `url: "*"` to route all requests through the proxy:

```graphql
mutation ExternalProxy {
  proxy(server: "http://john:1337@myproxy.com:1234" url: "*") {
    time
  }
  goto(url: "https://nordvpn.com/what-is-my-ip/", waitUntil: load) {
    status
  }
  waitForTimeout(time: 5000) {
    time
  }
}
```

### Scope Requests by Type

You can limit proxying to specific request types instead of all traffic. This is useful when you only need to proxy document loads and API calls, not static assets:

```graphql
mutation ProxyDocumentAndXHR {
  proxy(server: "http://john:1337@myproxy.com:1234" type: [document, xhr]) {
    time
  }
  goto(url: "https://nordvpn.com/what-is-my-ip/", waitUntil: load) {
    status
  }
  waitForTimeout(time: 5000) {
    time
  }
}
```

### Mix and Match Proxies

You can define multiple `proxy` mutations in a single query. BrowserQL uses the first proxy that matches a given request, so order matters.

The example below routes document and XHR requests through a residential proxy and all other requests through a datacenter proxy. The residential proxy is listed first so it takes priority for those request types:

```graphql
mutation MixedProxies {
  # Proxy document and XHR through residential
  residential: proxy(server: "http://john:1337@residential.proxy.com:1234" type: [document, xhr]) {
    time
  }
  # Proxy all other requests through datacenter
  datacenter: proxy(server: "http://john:1337@datacenter.proxy.com:1234" url: "*") {
    time
  }
  goto(url: "https://nordvpn.com/what-is-my-ip/", waitUntil: load) {
    status
  }
  waitForTimeout(time: 5000) {
    time
  }
}
```

## Reducing Bandwidth

The `reject` mutation blocks requests before they're sent. Use it to skip loading images, media, or other assets you don't need. This reduces bandwidth consumption and can speed up page loads.

> **Bot Detection Risk**
> Blocking requests can trigger bot detection on some sites. Avoid rejecting resources the target site uses for fingerprinting or session validation. Test your configuration carefully before relying on it in production.

You can reject requests by:

- **URL pattern**: Glob-style patterns to match specific domains or paths.
- **HTTP method**: Block specific request methods such as GET or POST.
- **Resource type**: Filter by type, such as `image`, `media`, or `script`.
- **Operator**: Use `and` to require all conditions to match, or `or` to match any one of them.

  
**Rejecting Images and Media:**

```graphql
mutation RejectImages {
  reject(type: [image, media]) {
    enabled
    time
  }
  goto(url: "https://en.wikipedia.org/wiki/Main_Page", waitUntil: firstContentfulPaint) {
    status
    time
  }
}
```
  
  
**Rejecting Media from a Specific Domain:**

```graphql
mutation Reject {
  reject(
    operator: and
    type: image
    url: "*google.com*"
  ) {
    enabled
    time
  }
  goto(url: "https://en.wikipedia.org/wiki/Main_Page", waitUntil: firstContentfulPaint) {
    status
    time
  }
}
```
  

> **Tip**
> The `reject` mutation only blocks requests during query execution. Assets may still appear to load in the BrowserQL editor after the mutation completes.

## FAQ & Troubleshooting

<details>
<summary>My proxy isn't working or I'm getting connection errors</summary>

Built-in proxies are available on every plan, including free, but they consume units per MB of traffic. Check that your account has units remaining. If using an external proxy, check that the `server`, `username`, and `password` fields are correct and the proxy is reachable.

</details>

<details>
<summary>How are proxy units calculated?</summary>

Built-in proxy usage is billed per MB of traffic routed through the proxy: 6 units/MB for residential and 2 units/MB for datacenter. External proxies don't consume proxy units. Check your [dashboard](https://browserless.io/account/) for current usage.

</details>

## Next steps

  <a href="/browserql/bot-detection/solving-captchas" className="next-step-card-link">
    
      <h3 className="next-step-card-title">CAPTCHA solving</h3>
      handle CAPTCHAs with built-in solving
    
  </a>
  <a href="/browserql/use-cases/scrape-and-extract-data" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Data extraction</h3>
      combine proxies with scraping workflows
    
  </a>
  <a href="/browserql/use-cases/performing-searches" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Performing searches</h3>
      location-aware searches at scale
    
  </a>

---

# CAPTCHA Solving

URL: https://docs.browserless.io/browserql/bot-detection/solving-captchas

BrowserQL includes built-in support for CAPTCHA challenges, including those embedded in iframes or shadow DOMs, and in most cases the `solve` mutation handles them with no configuration required. For custom image-based CAPTCHAs that standard detection can't handle, use `solveImageCaptcha`.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Familiarity with [BQL language basics](/browserql/writing-bql/language-basics)

> **DEPRECATION WARNING**
> The `verify` mutation is deprecated and will be removed in a future version. Use the `solve` mutation instead.

## Solve

The [`solve`](/bql-schema/operations/mutations/solve) mutation detects and solves CAPTCHAs on a page. It automatically identifies the CAPTCHA type, so you don't need to specify it.

> **Info**
> When solving reCAPTCHAs, the checkbox may not appear ticked after solving. This is expected. After solving, click the form's submit button to proceed.

```graphql
mutation SolveCaptcha {
  goto(url: "https://protected.domain") {
    status
  }

  solve {
    found
    solved
    time
  }
}
```

### Specifying a type

The `type` argument is optional. Specifying the CAPTCHA type can reduce verification time by a few milliseconds when you know which type is present. See the full list of supported types in the [CAPTCHA types schema reference](/bql-schema/types/enums/captcha-types).

```graphql
mutation SolveCaptchaWithType {
  goto(url: "https://protected.domain") {
    status
  }

  solve(type: recaptcha) {
    found
    solved
    time
  }
}
```

### Form submission after solving

After solving a CAPTCHA, click the submit button to proceed:

```graphql
mutation SolveAndSubmit {
  goto(url: "https://protected.domain") {
    status
  }

  solve {
    found
    solved
    time
  }

  click(selector: "button[type='submit']") {
    time
  }
}
```

If no submit button is available, trigger form submission with the `evaluate` mutation:

```graphql
mutation SolveAndTriggerSubmit {
  goto(url: "https://protected.domain") {
    status
  }

  solve {
    found
    solved
    time
  }

  evaluate(content: "window.onSubmit()") {
    time
  }
}
```

Replace `window.onSubmit()` with the JavaScript function that submits the form on your target site.

## Solving Image CAPTCHAs

Use [`solveImageCaptcha`](/bql-schema/operations/mutations/solve-image-captcha) when `solve` doesn't detect a CAPTCHA on a page. This happens with custom image-based challenges, typically legacy sites that display a distorted text image and require users to type what they see. Unlike `solve`, this mutation requires CSS selectors for both the CAPTCHA image and the input field where the solution is entered.

```graphql
mutation SolveImageCaptcha {
  goto(url: "https://captcha.com/demos/features/captcha-demo.aspx", waitUntil: networkIdle) {
    status
  }

  solveImageCaptcha(
    captchaSelector: "#demoCaptcha_CaptchaImage"
    inputSelector: "#captchaCode"
    timeout: 30000
  ) {
    found
    solved
    time
    token
  }
}
```

### Form submission after solving

After solving, submit the form by clicking the submit button:

```graphql
mutation SolveImageCaptchaAndSubmit {
  goto(url: "https://captcha.com/demos/features/captcha-demo.aspx", waitUntil: networkIdle) {
    status
  }

  solveImageCaptcha(
    captchaSelector: "#demoCaptcha_CaptchaImage"
    inputSelector: "#captchaCode"
  ) {
    found
    solved
    time
    token
  }

  click(selector: "#validateCaptchaButton") {
    time
  }
}
```

## FAQ & Troubleshooting

<details>
<summary>Which CAPTCHA types can <code>solve</code> handle?</summary>

The full list lives in the [CaptchaTypes enum](/bql-schema/types/enums/captcha-types). It includes Cloudflare, reCAPTCHA (v2 and v3), GeeTest, DataDome, Akamai, Amazon WAF, slider CAPTCHAs, image/text CAPTCHAs, and more. You rarely need to pick one: calling `solve` with no `type` auto-detects any supported CAPTCHA type on the page and routes it to the right solver.

</details>

<details>
<summary>The <code>solve</code> mutation returns <code>found: false</code></summary>

The CAPTCHA element isn't in the DOM when `solve` runs. Add a `waitForSelector` before `solve` to ensure the CAPTCHA has loaded. If the CAPTCHA is inside an iframe, BQL should still detect it, but verify the page has fully rendered.

</details>

<details>
<summary>CAPTCHA solving is taking too long</summary>

Some challenges (especially image-based ones) can take 30 seconds to several minutes. Set a generous `timeout` on the `solve` mutation or omit it to use the default. For `solveImageCaptcha`, processing time depends on the number of images and their complexity.

</details>

## Next steps

  <a href="/bql-schema/types/enums/captcha-types" className="next-step-card-link">
    
      <h3 className="next-step-card-title">CAPTCHA types reference</h3>
      all supported CAPTCHA types in the BQL schema
    
  </a>
  <a href="/browserql/bot-detection/proxies" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Proxies</h3>
      residential and external proxy routing
    
  </a>
  <a href="/browserql/bot-detection/stealth" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Stealth route</h3>
      fingerprint mitigations and entropy injection
    
  </a>

---

# Stealth Route

URL: https://docs.browserless.io/browserql/bot-detection/stealth

The stealth route (`/stealth/bql`) is a hardened endpoint for BrowserQL that applies comprehensive browser fingerprint mitigations and entropy injection. It replaces the default `/bql` route and requires no additional configuration to activate its protections.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Familiarity with [BQL language basics](/browserql/writing-bql/language-basics)

## Using the stealth route

To use the stealth route with BrowserQL, select the Stealth browser in the [BQL IDE settings](https://browserless.io/account/bql/), or specify the stealth endpoint when running programmatically:

```bash
# Stealth (Recommended)

curl -X POST \
  https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE \
  -H 'Content-Type: application/json' \
  -d '{
    "query": "query { goto(url: \"https://turo.com\") { status } }"
  }'
```

For a full layered strategy including proxies, CAPTCHA solving, and fingerprint configuration, see [Bot Detection Overview](/browserql/bot-detection/overview).

## FAQ & Troubleshooting

<details>
<summary>The stealth route is slower than the standard route</summary>

Expected. The stealth route applies fingerprint randomization and entropy injection on every request, which adds overhead. If the target site doesn't have bot detection, use `/chromium/bql` or `/chrome/bql` for faster execution.

</details>

<details>
<summary>I'm still getting blocked on the stealth route</summary>

Add residential proxies and CAPTCHA solving. Some sites use multi-layered detection that requires IP rotation in addition to fingerprint mitigations. See [Bot Detection Overview](/browserql/bot-detection/overview) for the full layered strategy.

</details>

## Next steps

  <a href="/browserql/bot-detection/solving-captchas" className="next-step-card-link">
    
      <h3 className="next-step-card-title">CAPTCHA solving</h3>
      handle reCAPTCHA, Cloudflare, and image-based challenges
    
  </a>
  <a href="/browserql/bot-detection/proxies" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Proxies</h3>
      residential and external proxy routing
    
  </a>
  <a href="/browserql/launch-parameters" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure browser binaries and behavior
    
  </a>

---

# Quickstart

URL: https://docs.browserless.io/browserql/getting-started

Follow these steps to run your first BrowserQL query:

- Go to the [Browserless dashboard](https://www.browserless.io/signup/email?plan=free)
    - Sign up or log in to your account
    - Copy your API token from the dashboard

    ![API Key Dashboard](/img/browserql/api-key-dashboard.png)

  Access the [BQL web editor](https://browserless.io/account/bql). [Learn more about using the IDE](/browserql/using-the-ide/ide-features).

  Copy and paste this example query into the IDE to scrape the first headline from Hacker News, and click play:

```graphql
mutation ScrapeHN {
  goto(url: "https://news.ycombinator.com", waitUntil: firstMeaningfulPaint) {
    status
    time
  }
  firstHeadline: text(selector: ".titleline") {
    text
  }
}
```

    **Click Run in the IDE to execute this query.**

    Here's what each part does:
    - `mutation` names the script and defines it as a browser action
    - `goto` navigates to the URL and waits for `firstMeaningfulPaint` to fire
    - `status` and `time` are returned once the wait condition is met
    - `firstHeadline` is an alias for the `text` action
    - `text(selector)` extracts the text content of the matched element

    **Response**

```json
{
  "data": {
"viewport": {
  "width": 1366,
  "height": 768,
  "time": 2
},
"goto": {
  "status": 200,
  "time": 1467
},
"firstHeadline": {
  "text": "Browserless is awesome! (https://github.com/browserless)"
}
  }
}
```

> **Tip**
> New to GraphQL? The [Language Basics](/browserql/writing-bql/language-basics) page covers the key concepts.

## Exporting Your Query

Once you've written a query in the IDE, you can export it to run programmatically from your own code, in whatever language you use. Here's what the Hacker News example looks like as an HTTP request:

  
**cURL:**

```bash
curl --request POST \
  --url 'https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{"query":"mutation ScrapeHN {\n  goto(url: \"https://news.ycombinator.com\", waitUntil: firstMeaningfulPaint) {\n    status\n    time\n  }\n  firstHeadline: text(selector: \".titleline\") {\n    text\n  }\n}"}'
```
  
  
**Axios:**

```js
import axios from 'axios';

const response = await axios.post(
  'https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE',
  {
    query: `mutation ScrapeHN {
  goto(url: "https://news.ycombinator.com", waitUntil: firstMeaningfulPaint) {
    status
    time
  }
  firstHeadline: text(selector: ".titleline") {
    text
  }
}`,
  },
  { headers: { 'Content-Type': 'application/json' } }
);

console.log(response.data);
```
  
  
**Fetch:**

```js
const response = await fetch(
  'https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      query: `mutation ScrapeHN {
  goto(url: "https://news.ycombinator.com", waitUntil: firstMeaningfulPaint) {
    status
    time
  }
  firstHeadline: text(selector: ".titleline") {
    text
  }
}`,
    }),
  }
);

const data = await response.json();
console.log(data);
```
  

The IDE can generate these export snippets for you automatically. [Learn how to export and use queries in your code](/browserql/using-the-ide/using-api-calls).

## Using the BAP TypeScript SDK

For a Puppeteer-like TypeScript API, install the official Browser Automation Protocol (BAP) SDK:

```bash
npm install @browserless.io/bap-ts
```

The same Hacker News flow becomes a sequence of strongly typed methods:

```typescript

const TOKEN = "YOUR_API_TOKEN_HERE";

const browser = Browserless.connect({
  browserWSEndpoint: "wss://production-sfo.browserless.io/chromium/bql",
  token: TOKEN,
});

const page = await browser.newPage();

// The same wait condition as the `goto` mutation above.
await page.goto("https://news.ycombinator.com", {
  waitUntil: "firstMeaningfulPaint",
});

const firstHeadline = await page.text({ selector: ".titleline" });
console.log(firstHeadline.text);

await browser.close();
```

**Response**

```text
Browserless is awesome! (https://github.com/browserless)
```

Each method builds the same BQL mutation you wrote in the IDE and sends it over one WebSocket, so the results match. See the [BAP Quickstart](/bap/getting-started) for setup details and the [generated `Page` reference](/bap/typescript/reference/classes/Page) for every available operation.

## FAQ & Troubleshooting

<details>
<summary>What do I need to get started with BrowserQL?</summary>

A Browserless API token from a free account at browserless.io. No local browser installation is required. BQL runs entirely in the cloud.

</details>

<details>
<summary>My query returns a connection error</summary>

Verify your API token is valid and your plan is active in the [dashboard](https://browserless.io/account/). Check that you're using the correct endpoint URL format: `wss://production-sfo.browserless.io/chromium/bql?token=YOUR_TOKEN`.

</details>

<details>
<summary>Can I hand off a BQL session to Puppeteer or Playwright?</summary>

Yes. Use the `reconnect` mutation to get a WebSocket endpoint you can pass to Puppeteer or Playwright. See [Puppeteer & Playwright handoff](/browserql/session-management/puppeteer-playwright).

</details>

## Next steps

  <a href="/browserql/writing-bql/language-basics" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Writing BQL</h3>
      learn mutations, aliases, and BQL syntax
    
  </a>
  <a href="/browserql/using-the-ide/ide-features" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BQL IDE</h3>
      built-in docs, query history, and code export
    
  </a>
  <a href="/browserql/launch-parameters" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure browser launch options
    
  </a>

---

# Launch Parameters

URL: https://docs.browserless.io/browserql/launch-parameters

Launch parameters configure how browsers start and behave in your sessions. You can pass them as individual query parameters or as a single JSON `launch` parameter in the URL.

If you've used Puppeteer or Playwright, these are the cloud equivalent of `browser.launch()` options.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Familiarity with [BQL language basics](/browserql/writing-bql/language-basics)

## Passing Launch Options

There are two ways to pass options to the browser: as individual query parameters in the URL, or as a JSON object via the `launch` parameter.

- **Query parameters** are passed directly in the URL (e.g., `&proxy=residential&humanlike=true`). Best for simple, standalone settings.
- **The `launch` object** is a JSON string passed as a single `launch` query parameter, either URL-encoded or base64-encoded. Use it for browser-level options like `headless` and `stealth`, or array flags like `args: [...]` whose brackets, quotes, and commas require encoding.

Both methods can be used together. Browserless merges them, with individual query parameters taking precedence.

## Query Parameters

These are passed directly in the URL, e.g. `?token=YOUR_TOKEN&proxy=residential`.

## The `launch` Object

Options in the `launch` object are passed as an encoded JSON string. This is the equivalent of Puppeteer's `launch({ options })`, passed as a query parameter to the cloud service. See the [full list of options](/open-api/chromium-bql-execute) in the API reference.

### Encoding the `launch` Value

The `launch` value must be encoded before being appended to the URL. You can use URL encoding or base64. Both work the same way. Base64 is simpler because it avoids manually escaping brackets, quotes, and commas.

**URL encoding**

  
**JavaScript:**

```js
const launch = JSON.stringify({
  args: ["--window-size=1280,720"],
});

const url = `https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE&launch=${encodeURIComponent(launch)}`;
```
  
  
**Python:**

```python
import json
import urllib.parse

launch = json.dumps({
    "args": ["--window-size=1280,720"],
})

url = f"https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE&launch={urllib.parse.quote(launch)}"
```
  
  
**cURL:**

```bash
curl --request POST \
  --url 'https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE&launch=%7B%22args%22%3A%5B%22--window-size%3D1280%2C720%22%5D%7D' \
  --header 'Content-Type: application/json' \
  --data '{"query":"mutation { goto(url: \"https://example.com\") { status } }"}'
```
  

**Base64 encoding**

  
**JavaScript:**

```js
const launch = btoa(JSON.stringify({
  args: ["--window-size=1280,720"],
}));

const url = `https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE&launch=${launch}`;
```
  
  
**Python:**

```python
import json
import base64

launch = base64.b64encode(json.dumps({
    "args": ["--window-size=1280,720"],
}).encode()).decode()

url = f"https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE&launch={launch}"
```
  
  
**cURL:**

```bash
LAUNCH=$(echo -n '{"args":["--window-size=1280,720"]}' | base64)

curl --request POST \
  --url "https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE&launch=${LAUNCH}" \
  --header 'Content-Type: application/json' \
  --data '{"query":"mutation { goto(url: \"https://example.com\") { status } }"}'
```
  

### Chrome Flags

Chrome flags are passed via the `args` array inside the `launch` object. Encode the value before use (see [Encoding the `launch` Value](#encoding-the-launch-value) above).

Before encoding:
```json
{"args":["--window-size=1920,1080","--lang=en-US"]}
```

Encoded:
```bash
?launch=%7B%22args%22%3A%5B%22--window-size%3D1920%2C1080%22%2C%22--lang%3Den-US%22%5D%7D
```

Enterprise plans support the full set of [Chrome command-line switches](https://peter.sh/experiments/chromium-command-line-switches/). The following flags are available to all accounts:

- `--disable-features`
- `--disable-setuid-sandbox`
- `--disable-site-isolation-trials`
- `--disable-web-security`
- `--enable-features`
- `--font-render-hinting`
- `--force-color-profile`
- `--lang`
- `--proxy-bypass-list`
- `--proxy-server`
- `--window-size`

## Restricting ad-block rulesets (`blockAdsInclude`)

When you pass `blockAds=true`, Browserless loads uBlock Origin Lite and compiles
all **57 filter rulesets (~69,000 rules)** on **every fresh browser launch** — a
CPU-bound cost that adds up when many browsers start at once.

`blockAdsInclude` lets a request load **only the rulesets it needs**, so Chrome
compiles fewer of them and the browser starts faster.

It is **opt-in**: if you don't pass it, `blockAds=true` behaves exactly as before —
the full extension with all 57 rulesets.

```text
# no blockAdsInclude — full extension, unchanged
?blockAds=true

# only the recommended core lists — faster launch
?blockAds=true&blockAdsInclude=ublock-filters,easylist,easyprivacy,pgl,ublock-badware,urlhaus-full
```

- Applies only when `blockAds=true`.
- Value is a comma-separated list **or** a JSON array of ruleset ids.
- Unknown ids are rejected with `400 Bad Request`.

> **Note**
> `blockAdsInclude` is currently honored on the BrowserQL endpoints
> (`/chromium/bql`, `/chrome/bql`, `/stealth/bql`), the CDP WebSocket connection,
> and the `/unblock`, `/screenshot`, and `/pdf` REST endpoints. Persistent
> `/session` connections and endpoints that use the bundled open-source uBlock
> build still load the full ruleset set. Where it isn't honored, `blockAds=true`
> continues to work with the full extension.

### Recommended set

These 6 language-neutral lists cover general ads, trackers, and malware — enough
for typical traffic. Pass them for a faster launch:

```text
blockAdsInclude=ublock-filters,easylist,easyprivacy,pgl,ublock-badware,urlhaus-full
```

### What each group blocks

| Group | What it blocks | When to include |
|---|---|---|
| **Recommended (6)** | general ads, third-party trackers, malicious/badware URLs (language-neutral) | almost always — the recommended set |
| **Locale (37)** | region/language-specific ad lists (e.g. German, Japanese) | only if you scrape sites in that language |
| **Optional (14)** | cookie-consent banners, overlays, social/chat/AI widgets, notifications, experimental | only if you want annoyance removal |

### Full ruleset reference

Include only the locales your traffic actually targets — the locale lists are the
bulk of the rules and therefore most of the startup cost. (This list reflects the
bundled uBlock Origin Lite build and can change when the extension is updated.)

**Recommended (6)** — the core set.

| id | list |
|---|---|
| `ublock-filters` | uBlock filters – Ads, trackers, and more |
| `easylist` | EasyList |
| `easyprivacy` | EasyPrivacy |
| `pgl` | Peter Lowe – Ads, trackers, and more |
| `ublock-badware` | uBlock filters – Badware risks |
| `urlhaus-full` | Malicious URL Blocklist |

<details>
<summary>Optional (14) — opt-in (annoyances, mobile, experimental)</summary>

| id | list |
|---|---|
| `adguard-mobile` | AdGuard/uBO – Mobile Ads |
| `block-lan` | Block Outsider Intrusion into LAN |
| `dpollock-0` | Dan Pollock's hosts file |
| `adguard-spyware-url` | AdGuard/uBO – URL Tracking Protection |
| `annoyances-ai` | EasyList – AI Widgets |
| `annoyances-cookies` | EasyList/uBO – Cookie Notices |
| `annoyances-overlays` | EasyList/uBO – Overlay Notices |
| `annoyances-social` | EasyList – Social Widgets |
| `annoyances-widgets` | EasyList – Chat Widgets |
| `annoyances-others` | EasyList – Other Annoyances |
| `annoyances-notifications` | EasyList – Notifications |
| `ublock-experimental` | uBlock filters – Experimental |
| `ubol-tests` | uBO Lite Test Filters |
| `rus-1` | RU AdList: Counters |

</details>

<details>
<summary>Locale (37) — region/language-specific ad lists</summary>

| id | list | languages |
|---|---|---|
| `alb-0` | Adblock List for Albania | sq |
| `ara-0` | Liste AR | ar kab |
| `bgr-0` | Bulgarian Adblock list | bg mk |
| `chn-0` | AdGuard Chinese | ug zh |
| `cze-0` | EasyList Czech and Slovak | cs sk |
| `deu-0` | EasyList Germany | de dsb hsb lb rm |
| `est-0` | Eesti saitidele kohandatud filter | et |
| `fin-0` | Adblock List for Finland | fi |
| `fra-0` | AdGuard Français | ar br ff fr lb oc son |
| `grc-0` | Greek AdBlock Filter | el |
| `hrv-0` | Serbo-Croatian filters | bs hr sr |
| `hun-0` | hufilter | hu |
| `idn-0` | ABPindo | id ms |
| `ind-0` | IndianList | as bn gu hi kn ml mr ne pa si ta te |
| `irn-0` | PersianBlocker | fa ps tg |
| `isl-0` | Icelandic ABP List | is |
| `isr-0` | EasyList Hebrew | he |
| `ita-0` | EasyList Italy | it lij |
| `jpn-1` | AdGuard Japanese | ja |
| `kor-1` | List-KR Classic | ko |
| `ltu-0` | EasyList Lithuania | lt |
| `lva-0` | Latvian List | lv |
| `mkd-0` | Macedonian adBlock Filters | mk |
| `nld-0` | AdGuard Dutch | af fy nl |
| `nor-0` | Nordiske filtre | nb nn no da is |
| `pol-0` | Oficjalne Polskie Filtry | szl pl |
| `pol-3` | CERT.PL's Warning List | szl pl |
| `rou-1` | Romanian Ad (ROad) Block List Light | ro md |
| `rus-0` | RU AdList | be kk tt ru uz |
| `spa-0` | EasyList Spanish | es ca eu gl … |
| `spa-1` | AdGuard Spanish/Portuguese | es pt ca … |
| `svn-0` | Slovenian List | sl |
| `swe-1` | Frellwit's Swedish Filter | sv |
| `tha-0` | EasyList Thailand | th |
| `tur-0` | AdGuard Turkish | tr |
| `ukr-0` | AdGuard Ukrainian | uk |
| `vie-1` | ABPVN List | vi |

</details>

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>403 Forbidden</code> response?</summary>

Your API token is missing or malformed. Pass it as a `?token=` query parameter. Check your [account dashboard](https://browserless.io/account/) to verify the token is active.

</details>

<details>
<summary>My query times out before completing</summary>

Increase the `timeout` parameter on your mutation or use a more specific `waitUntil` condition. Long-running pages may need `networkIdle` instead of `load`.

</details>

## Next steps

  <a href="/browserql/bot-detection/overview" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Bot detection</h3>
      learn how Browserless handles stealth, proxies, and CAPTCHA solving
    
  </a>
  <a href="/browserql/writing-bql/language-basics" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Writing BQL queries</h3>
      covers GraphQL mutations, aliases, and BQL syntax
    
  </a>
  <a href="/browserql/using-the-ide/ide-features" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Using the IDE</h3>
      use the query editor, run sessions live, and export code to your project
    
  </a>

---

# Cookie Management

URL: https://docs.browserless.io/browserql/session-management/injecting-cookies

Cookie management is essential for maintaining session state, authentication, and personalized user experiences during browser automation. BrowserQL's `cookies` mutation lets you set cookies before navigation and retrieve them after page interactions.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Familiarity with [BQL language basics](/browserql/writing-bql/language-basics)

## Cookie operations

The `cookies` mutation in BrowserQL serves dual purposes:
- **Setting cookies**: Pass an array of `CookieInput` objects to inject cookies into the browser
- **Getting cookies**: Call without parameters to retrieve all cookies from the current page

### Cookie Input Structure

When setting cookies, use the `CookieInput` type with the following properties:

- `name` (required): Cookie name
- `value` (required): Cookie value
- `domain`: Cookie domain (e.g., "example.com")
- `path`: Cookie path (defaults to "/")
- `secure`: Boolean indicating if cookie requires HTTPS
- `httpOnly`: Boolean for HTTP-only cookies
- `sameSite`: Cookie SameSite policy (`Strict`, `Lax`, or `None`)
- `expires`: Expiration timestamp (session cookie if not set)
- `url`: Request URI to associate with the cookie

### Cookie Response

The mutation returns a `CookieResponse` containing:
- `cookies`: Array of `StandardCookie` objects with name, value, domain, path, and security properties
- `time`: Execution time in milliseconds

## Complete Cookie Management Example

Here's a comprehensive example demonstrating cookie injection and extraction using the test site setcookie.net:

  
**BrowserQL:**

```graphql
mutation cookies {
  setCookies: cookies(cookies: [
    {
      name: "test_cookie"
      value: "123456"
      domain: "setcookie.net"
      path: "/"
      secure: true
    }
  ]) {
    cookies {
      name
      value
    }
  }
  waitForTimeout(time: 3000) {
    time
  }
  goto(url: "https://setcookie.net/", waitUntil: networkIdle) {
    status
  }

  getCookies: cookies {
    cookies {
      name
      value
      domain
      path
      secure
    }
  }
}
```
  
  
**JavaScript:**

```javascript
import fetch from 'node-fetch';

const API_KEY = "YOUR_API_TOKEN_HERE";
const BQL_ENDPOINT = `https://production-sfo.browserless.io/chromium/bql?token=${API_KEY}`;

const cookieQuery = `
mutation cookies {
  setCookies: cookies(cookies: [
    {
      name: "test_cookie"
      value: "123456"
      domain: "setcookie.net"
      path: "/"
      secure: true
    }
  ]) {
    cookies {
      name
      value
    }
  }
  waitForTimeout(time: 3000) {
    time
  }
  goto(url: "https://setcookie.net/", waitUntil: networkIdle) {
    status
  }

  getCookies: cookies {
    cookies {
      name
      value
      domain
      path
      secure
    }
  }
}`;

async function manageCookies() {
  const response = await fetch(BQL_ENDPOINT, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ query: cookieQuery })
  });

  const result = await response.json();
  console.log('Cookie Management Result:', JSON.stringify(result, null, 2));

  // Access the set cookies
  console.log('Set Cookies:', result.data.setCookies.cookies);

  // Access the retrieved cookies
  console.log('Retrieved Cookies:', result.data.getCookies.cookies);
}

manageCookies().catch(console.error);
```
  
  
**Python:**

```python
import requests
import json

API_KEY = "YOUR_API_TOKEN_HERE"
BQL_ENDPOINT = f"https://production-sfo.browserless.io/chromium/bql?token={API_KEY}"

cookie_query = """
mutation cookies {
  setCookies: cookies(cookies: [
    {
      name: "test_cookie"
      value: "123456"
      domain: "setcookie.net"
      path: "/"
      secure: true
    }
  ]) {
    cookies {
      name
      value
    }
  }
  waitForTimeout(time: 3000) {
    time
  }
  goto(url: "https://setcookie.net/", waitUntil: networkIdle) {
    status
  }

  getCookies: cookies {
    cookies {
      name
      value
      domain
      path
      secure
    }
  }
}
"""

def manage_cookies():
    response = requests.post(
        BQL_ENDPOINT,
        headers={
            'Content-Type': 'application/json'
        },
        json={'query': cookie_query}
    )

    result = response.json()
    print('Cookie Management Result:', json.dumps(result, indent=2))

    # Access the set cookies
    print('Set Cookies:', result['data']['setCookies']['cookies'])

    # Access the retrieved cookies
    print('Retrieved Cookies:', result['data']['getCookies']['cookies'])

if __name__ == "__main__":
    manage_cookies()
```
  

## Common Use Cases

### Authentication Cookies

Set authentication cookies before navigating to protected pages:

```graphql
mutation setAuthCookie {
  cookies(cookies: [
    {
      name: "session_token"
      value: "abc123xyz789"
      domain: "example.com"
      path: "/"
      secure: true
      httpOnly: true
    }
  ]) {
    cookies {
      name
      value
    }
  }
  goto(url: "https://example.com/dashboard") {
    status
  }
}
```

> **HttpOnly Cookie Limitations**
> HttpOnly cookies are protected by the browser and cannot be accessed or modified via client-side JavaScript or automation frameworks like BrowserQL, Puppeteer, or Playwright. This security design prevents cross-site scripting (XSS) attacks from stealing authentication tokens. If your workflow requires persisting HttpOnly cookies for authentication across sessions, you should use our [persisting sessions](/baas/session-management/persisting-state) feature, which maintains all browser state including HttpOnly cookies between reconnections.

### Preference Cookies

Inject user preference cookies to customize page behavior:

```graphql
mutation setPreferences {
  cookies(cookies: [
    {
      name: "theme"
      value: "dark"
      domain: "example.com"
    },
    {
      name: "language"
      value: "en-US"
      domain: "example.com"
    }
  ]) {
    cookies {
      name
      value
    }
  }
}
```

### Cookie Extraction for Analysis

Retrieve cookies after user interactions to analyze session data:

```graphql
mutation extractCookies {
  goto(url: "https://example.com/login") {
    status
  }
  typeUsername: type(selector: "#username", text: "user@example.com") {
    time
  }
  typePassword: type(selector: "#password", text: "password123") {
    time
  }
  click(selector: "#login-button") {
    time
  }
  waitForSelector(selector: ".dashboard") {
    time
  }

  cookies {
    cookies {
      name
      value
      domain
      expires
      secure
      httpOnly
    }
  }
}
```

## FAQ & Troubleshooting

<details>
<summary>My cookies aren't being applied to the page</summary>

Set cookies before the `goto` mutation. Cookies set after navigation apply to subsequent requests but won't retroactively affect the current page load. Also verify the `domain` matches the target site.

</details>

<details>
<summary>Can I read cookies set by the page after navigation?</summary>

Yes. Call the `cookies` mutation without `set` arguments after navigation to retrieve all cookies for the current page, including those set by JavaScript or server responses.

</details>

## Next steps

  <a href="/browserql/session-management/persisting-state" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Persisting state</h3>
      long-lived sessions that maintain state across queries
    
  </a>
  <a href="/browserql/session-management/reconnect-to-browserless" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Session reconnect</h3>
      reuse browser sessions across multiple requests
    
  </a>

---

# Persisting State

URL: https://docs.browserless.io/browserql/session-management/persisting-state

This guide covers creating a long-lived browser session with the Session REST API, running BQL queries against it, and cleaning up when you're done. It also covers session configuration options, how long state persists by plan, and best practices for managing sessions.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Familiarity with [BQL reconnect](/browserql/session-management/reconnect-to-browserless)

## Comparison with BQL reconnect

The Session API and the BQL `reconnect` mutation both maintain browser continuity, but they work differently.

| Feature | Session API + BQL | BQL Reconnect |
|---------|-------------------|---------------|
| **Session Creation** | Explicit via REST API | Implicit with first query |
| **Lifecycle Control** | Programmatic start/stop | Timeout-based only |
| **State Management** | Persistent across disconnections | Requires active connection |
| **Proxy Configuration** | Set once, applies to all queries | Per-query configuration |

## Persisting State Workflow

POST to `/session`. Save the returned object. It contains the URLs you need for BQL queries and session cleanup.

> **Stealth is optional**
> `browserQL` is returned for all sessions and accepts queries whether or not stealth is enabled. The examples below pass `stealth: true` because it helps on bot-protected targets; omit it if you don't need it.

**JavaScript:**

```javascript
const response = await fetch(
  "https://production-sfo.browserless.io/session?token=YOUR_API_TOKEN_HERE",
  {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      ttl: 300000,
      stealth: true,
    }),
  }
);

if (!response.ok) {
  throw new Error(`Failed to create session: ${response.status}`);
}

const session = await response.json();
console.log("Session created:", session.id);
```

**Python:**

```python

response = requests.post(
    "https://production-sfo.browserless.io/session",
    params={"token": "YOUR_API_TOKEN_HERE"},
    json={
        "ttl": 300000,
        "stealth": True,
    },
)

response.raise_for_status()
session = response.json()
print("Session created:", session["id"])
```

**cURL:**

```bash
curl -X POST "https://production-sfo.browserless.io/session?token=YOUR_API_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "ttl": 300000,
    "stealth": true
  }'
```

POST BQL mutations to `session.browserQL`. This is a fully-qualified URL returned for all sessions that inherits all session properties (proxy, stealth mode, etc.).

**JavaScript:**

```javascript
const query = `
  mutation SetDarkMode {
    goto(url: "https://docs.browserless.io/", waitUntil: networkIdle) {
      status
    }
    click(selector: "div.toggle_vylO.colorModeToggle_x44X > button") {
      time
    }
  }
`;

const bqlResponse = await fetch(session.browserQL, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ query }),
});

const result = await bqlResponse.json();
console.log("Dark mode toggled:", result.data);
```

**Python:**

```python
query = """
mutation SetDarkMode {
  goto(url: "https://docs.browserless.io/", waitUntil: networkIdle) {
    status
  }
  click(selector: "div.toggle_vylO.colorModeToggle_x44X > button") {
    time
  }
}
"""

bql_response = requests.post(
    session["browserQL"],
    json={"query": query},
)

result = bql_response.json()
print("Dark mode toggled:", result["data"])
```

You can POST to `session.browserQL` as many times as needed across multiple script runs. State persists across all of them. Only send a DELETE to `session.stop` when you want to permanently discard the session data. Sessions also expire automatically when their TTL elapses.

**JavaScript:**

```javascript
const stopResponse = await fetch(session.stop, { method: "DELETE" });

if (stopResponse.ok) {
  console.log("Session stopped.");
}
```

**Python:**

```python
stop_response = requests.delete(session["stop"])
stop_response.raise_for_status()
print("Session stopped.")
```

**cURL:**

```bash
# Use the stop URL from the session creation response

curl -X DELETE "SESSION_STOP_URL_HERE"
```

## Complete Example

This example creates a session, toggles dark mode in one BQL query, then verifies the theme preference persists in a second query against the same session.

**JavaScript:**

```javascript
async function main() {
  // 1. Create a stealth session
  const response = await fetch(
    "https://production-sfo.browserless.io/session?token=YOUR_API_TOKEN_HERE",
    {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ ttl: 300000, stealth: true }),
    }
  );

  if (!response.ok) {
    throw new Error(`Failed to create session: ${response.status}`);
  }

  const session = await response.json();
  console.log("Session created:", session.id);

  // 2. First query: navigate and toggle dark mode
  const toggleQuery = `
    mutation SetDarkMode {
      goto(url: "https://docs.browserless.io/", waitUntil: networkIdle) {
        status
      }
      click(selector: "div.toggle_vylO.colorModeToggle_x44X > button") {
        time
      }
    }
  `;

  const toggleResponse = await fetch(session.browserQL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ query: toggleQuery }),
  });

  const toggleResult = await toggleResponse.json();
  console.log("Dark mode toggled:", toggleResult.data.click.time);

  // 3. Second query: verify theme persists (reuse same session.browserQL URL)
  const verifyQuery = `
    mutation VerifyTheme {
      goto(url: "https://docs.browserless.io/", waitUntil: networkIdle) {
        status
      }
      theme: evaluate(content: "return localStorage.getItem('theme');") {
        value
      }
    }
  `;

  const verifyResponse = await fetch(session.browserQL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ query: verifyQuery }),
  });

  const verifyResult = await verifyResponse.json();
  console.log("Theme persisted:", verifyResult.data.theme.value);

  // 4. Clean up
  await fetch(session.stop, { method: "DELETE" });
  console.log("Session stopped.");
}

main().catch(console.error);
```

**Python:**

```python

TOKEN = "YOUR_API_TOKEN_HERE"

# 1. Create a stealth session
response = requests.post(
    "https://production-sfo.browserless.io/session",
    params={"token": TOKEN},
    json={"ttl": 300000, "stealth": True},
)
response.raise_for_status()
session = response.json()
print("Session created:", session["id"])

# 2. First query: navigate and toggle dark mode
toggle_query = """
mutation SetDarkMode {
  goto(url: "https://docs.browserless.io/", waitUntil: networkIdle) {
    status
  }
  click(selector: "div.toggle_vylO.colorModeToggle_x44X > button") {
    time
  }
}
"""

toggle_response = requests.post(
    session["browserQL"],
    json={"query": toggle_query},
)
toggle_result = toggle_response.json()
print("Dark mode toggled:", toggle_result["data"]["click"]["time"])

# 3. Second query: verify theme persists (reuse same session.browserQL URL)
verify_query = """
mutation VerifyTheme {
  goto(url: "https://docs.browserless.io/", waitUntil: networkIdle) {
    status
  }
  theme: evaluate(content: "return localStorage.getItem('theme');") {
    value
  }
}
"""

verify_response = requests.post(
    session["browserQL"],
    json={"query": verify_query},
)
verify_result = verify_response.json()
print("Theme persisted:", verify_result["data"]["theme"]["value"])

# 4. Clean up
requests.delete(session["stop"])
print("Session stopped.")
```

## Session Response Schema

A successful `POST /session` returns a JSON object with these fields:

| Property | Type | Description |
|----------|------|-------------|
| `id` | string | Unique session identifier |
| `connect` | string | WebSocket URL for CDP-based libraries (Puppeteer, Playwright). Token is pre-embedded. |
| `browserQL` | string | URL for running BQL queries against this session. Returned for all sessions. Token is pre-embedded. |
| `stop` | string | URL for session termination via DELETE request. Token is pre-embedded. |
| `ttl` | number | Session time-to-live in milliseconds |
| `cloudEndpointId` | string \| null | Encrypted cloud endpoint ID for the session. Present on Browserless Cloud, `null` when self-hosted. |

## Persisted Session Data Duration

Session data (cookies, localStorage, cache) persists for the duration of the session TTL, up to the plan maximum:

| Plan | Maximum Session Lifetime |
|------|--------------------------|
| Free | 1 day |
| Prototyping | 7 days |
| Starter | 30 days |
| Scale | 90 days |
| Enterprise | Custom |

You can also limit persistence duration by passing a lower `ttl` value to the session API.

## Session Configuration Options

All session configuration options apply to both WebSocket connections and BQL queries:

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `ttl` | number | _required_ | Time-to-live in milliseconds. Max varies by plan. |
| `stealth` | boolean | false | Enables stealth mode for bot-protected targets. Optional; BQL queries work without it. |
| `headless` | boolean | false | Run browser in headless mode. An omitted value resolves to `false`, so a session launches headful unless you pass `true`. Ignored when `stealth` is true. |
| `browser` | string | `'chromium'` | Browser type: `'chromium'`, `'chrome'`, or `'stealth'`. The `'stealth'` browser cannot be combined with the `stealth` property. |
| `blockAds` | boolean | false | Enable ad blocking. |
| `args` | string[] | `[]` | Additional Chrome launch arguments. |
| `proxy` | object | null | Proxy configuration. When set, applies to all BQL queries and WebSocket connections for this session. See [proxy docs](/baas/launch-options) for the full configuration shape. |

## Best Practices

1. **Store session URLs together.** Save `session.id`, `session.browserQL`, and `session.stop` as a unit after session creation. You need all three for queries, cleanup, and debugging.

2. **Delete sessions when done.** Send a DELETE to `session.stop` when your workflow completes. Relying on TTL expiry alone wastes resources and can hit session limits.

3. **Handle expired sessions.** If a POST to `session.browserQL` returns a non-2xx status other than `429`, the session has likely expired or been deleted; create a new session and retry. A `429 "already being accessed"` means the session is alive but still held by the previous request, so retry the same session after a short backoff instead.

## FAQ & Troubleshooting

<details>
<summary>My session returns a non-2xx status when I try to use it</summary>

If the status is anything other than `429`, the session has expired or been deleted. Create a new session and retry. Sessions expire based on your plan's TTL (up to 90 days). A `429` means the live session is still attached to another client; see the next entry.

</details>

<details>
<summary>How do I clean up sessions I'm no longer using?</summary>

Send a DELETE request to the `session.stop` URL. Don't rely on TTL expiry alone, as idle sessions consume resources and can hit your session limit.

</details>

<details>
<summary>I get <code>429 "already being accessed"</code> even though I send requests one at a time</summary>

Only one client can be attached to a session at a time, and the attachment isn't released the instant your previous request's HTTP response arrives. The browser detaches shortly after each request completes, so back-to-back requests can catch the session still held by the previous one. Retry with a short backoff (a few hundred milliseconds is usually enough). If a session is done for good, DELETE it via `session.stop` instead of leaving it to time out.

</details>

## Next steps

  <a href="/browserql/session-management/reconnect-to-browserless" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Session reconnect</h3>
      lightweight session reuse with the reconnect mutation
    
  </a>
  <a href="/browserql/session-management/injecting-cookies" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Cookie management</h3>
      set cookies before navigation
    
  </a>

---

# Reconnect using Puppeteer & Playwright

URL: https://docs.browserless.io/browserql/session-management/puppeteer-playwright

The `reconnect` mutation returns a WebSocket endpoint you can pass to Puppeteer or Playwright. This lets you use BQL for unblocking (solving CAPTCHAs, bypassing bot detection) and then hand off control to your library for everything else.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Puppeteer or Playwright installed locally
- Familiarity with [BQL reconnect](/browserql/session-management/reconnect-to-browserless)

> **Reconnecting with More BQL**
> You can also reconnect to run more BQL queries instead of connecting a library. See the [Reconnect to Browserless](/browserql/session-management/reconnect-to-browserless) guide.

## The Reconnect Mutation

Run your BQL mutations (navigation, unblocking, etc.) and call `reconnect` to get a `browserWSEndpoint`. This is the WebSocket URL you pass to Puppeteer's `connect()` or Playwright's `connectOverCDP()`.

The `timeout` argument controls how long (in milliseconds) the browser waits for a library connection. If nothing connects within this window, the browser shuts down and returns a `404`.

```graphql
mutation Reconnect {
  goto(url: "https://www.browserless.io/practice-form/", waitUntil: networkIdle) {
    status
  }

  # Uncomment to solve Cloudflare before handing off:
  # solve(type: cloudflare) {
  #   found
  #   solved
  #   time
  # }

  reconnect(timeout: 30000) {
    browserWSEndpoint
  }
}
```

## Connecting with Libraries

The `browserWSEndpoint` from the response connects to any library that supports the Chrome DevTools Protocol (CDP). This includes Puppeteer (`puppeteer.connect()`) and Playwright (`chromium.connectOverCDP()`).

Execute your BQL query first, extract the `browserWSEndpoint`, then pass it to your library. The examples below navigate to a page with BQL, reconnect, and take a screenshot.

### Reconnect Using Puppeteer

```js

const url = 'https://www.browserless.io/practice-form';
const token = 'YOUR_API_TOKEN_HERE';
const timeout = 5 * 60 * 1000;

const queryParams = new URLSearchParams({
  timeout,
  token,
}).toString();

const query = `
  mutation Reconnect($url: String!) {
    goto(url: $url, waitUntil: networkIdle) {
      status
    }
    reconnect(timeout: 30000) {
      browserWSEndpoint
    }
  }
`;

const variables = { url };

const endpoint = `https://production-sfo.browserless.io/chromium/bql?${queryParams}`;

const options = {
  method: 'POST',
  headers: {
    'content-type': 'application/json'
  },
  body: JSON.stringify({
    query,
    variables,
  }),
};

try {
  console.log(`Running BQL Query: ${url}`);

  const response = await fetch(endpoint, options);

  if (!response.ok) {
    throw new Error(`Got non-ok response:\n` + (await response.text()));
  }

  const { data } = await response.json();
  const browserWSEndpoint = data.reconnect.browserWSEndpoint
  console.log(`Got OK response! Connecting puppeteer to ${browserWSEndpoint}`);
  const browser = await puppeteer.connect({
    browserWSEndpoint,
  });
  console.log(`Connected to ${await browser.version()}`);
  const pages = await browser.pages();
  const page = pages.find((p) => p.url().includes(url));
  await page.screenshot({ fullPage: true, path: 'temp.png' });
  await browser.close();
} catch (error) {
  console.error(error);
}
```

### Reconnect Using Playwright

  
**JavaScript:**

```js
import playwright from 'playwright-core';

const url = 'https://www.browserless.io/practice-form';
const token = 'YOUR_API_TOKEN_HERE';
const timeout = 5 * 60 * 1000;

const queryParams = new URLSearchParams({
  timeout,
  token,
}).toString();

const query = `
  mutation Reconnect($url: String!) {
    goto(url: $url, waitUntil: networkIdle) {
      status
    }
    reconnect(timeout: 30000) {
      browserWSEndpoint
    }
  }
`;

const variables = { url };

const endpoint =
  `https://production-sfo.browserless.io/chromium/bql?${queryParams}`;

const options = {
  method: 'POST',
  headers: {
    'content-type': 'application/json',
  },
  body: JSON.stringify({
    query,
    variables,
  }),
};

(async () => {
  try {
    console.log(`Running BQL Query: ${url}`);

    const response = await fetch(endpoint, options);

    if (!response.ok) {
      throw new Error(`Got non-ok response:\n` + (await response.text()));
    }

    const { data } = await response.json();
    const browserWSEndpoint = data.reconnect.browserWSEndpoint;

    console.log(`Got OK response! Connecting Playwright to ${browserWSEndpoint}`);

    const browser = await playwright.chromium.connectOverCDP(browserWSEndpoint);
    const context = browser.contexts()[0];
    const page = context.pages().find((p) => p.url().includes(url));

    if (!page) {
      throw new Error(`Could not find a page matching ${url}`);
    }

    await page.screenshot({ fullPage: true, path: 'temp.png' });

    await browser.close();
  } catch (error) {
    console.error(error);
  }
})();
```
  
  
**Python:**

```python
import requests
from playwright.sync_api import sync_playwright

url = "https://www.browserless.io/practice-form"
token = "YOUR_API_TOKEN_HERE"
timeout = 5 * 60 * 1000

query_params = f"token={token}&timeout={timeout}"
endpoint = f"https://production-sfo.browserless.io/chromium/bql?{query_params}"

query = """
  mutation Reconnect($url: String!) {
    goto(url: $url, waitUntil: networkIdle) {
      status
    }
    reconnect(timeout: 30000) {
      browserWSEndpoint
    }
  }
"""

variables = {"url": url}

headers = {"content-type": "application/json"}

body = {"query": query, "variables": variables}

try:
    print(f"Running BQL Query: {url}")
    response = requests.post(endpoint, json=body, headers=headers)

    if response.status_code != 200:
        raise Exception(f"Got non-ok response:\n{response.text}")

    data = response.json().get("data")
    browser_ws_endpoint = data["reconnect"]["browserWSEndpoint"]
    print(f"Got OK response! Connecting Playwright to {browser_ws_endpoint}")

    with sync_playwright() as playwright:
        browser = playwright.chromium.connect_over_cdp(browser_ws_endpoint)
        context = browser.contexts[0]
        page = next((p for p in context.pages if url in p.url), None)

        if not page:
            raise Exception(f"Could not find a page matching {url}")

        page.screenshot(path="temp.png", full_page=True)

        browser.close()

except Exception as e:
    print(f"Error: {e}")
```
  
  
**Java:**

```java
package com.example;

import com.microsoft.playwright.*;
import com.google.gson.*;
import java.net.http.*;
import java.net.URI;
import java.util.*;

public class PlaywrightBQL {
    public static void main(String[] args) {
        String url = "https://www.browserless.io/practice-form";
        String token = "YOUR_API_TOKEN_HERE";
        int timeout = 5 * 60 * 1000;

        String queryParams = String.format("token=%s&timeout=%d", token, timeout);
        String endpoint = String.format("https://production-sfo.browserless.io/chromium/bql?%s", queryParams);

        String query = """
                mutation Reconnect($url: String!) {
                  goto(url: $url, waitUntil: networkIdle) {
                    status
                  }
                  reconnect(timeout: 30000) {
                    browserWSEndpoint
                  }
                }
                """;

        JsonObject variables = new JsonObject();
        variables.addProperty("url", url);

        JsonObject body = new JsonObject();
        body.addProperty("query", query);
        body.add("variables", variables);

        try {
            // Send GraphQL request
            HttpClient client = HttpClient.newHttpClient();
            HttpRequest request = HttpRequest.newBuilder()
                    .uri(URI.create(endpoint))
                    .header("Content-Type", "application/json")
                    .POST(HttpRequest.BodyPublishers.ofString(body.toString()))
                    .build();

            HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
            if (response.statusCode() != 200) {
                throw new RuntimeException("Failed request: " + response.body());
            }

            // Parse response
            JsonObject data = JsonParser.parseString(response.body()).getAsJsonObject().getAsJsonObject("data");
            String browserWSEndpoint = data.getAsJsonObject("reconnect").get("browserWSEndpoint").getAsString();

            System.out.println("Connecting Playwright to " + browserWSEndpoint);

            try (Playwright playwright = Playwright.create()) {
                Browser browser = playwright.chromium().connectOverCDP(browserWSEndpoint);
                BrowserContext context = browser.contexts().get(0);
                Page page = context.pages().stream()
                        .filter(p -> p.url().contains(url))
                        .findFirst()
                        .orElseThrow(() -> new RuntimeException("Page not found"));

                page.screenshot(new Page.ScreenshotOptions().setPath("temp.png").setFullPage(true));
                browser.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
```
  
  
**C#:**

```csharp
using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Playwright;

class Program
{
    static async System.Threading.Tasks.Task Main(string[] args)
    {
        string url = "https://www.browserless.io/practice-form";
        string token = "YOUR_API_TOKEN_HERE";
        int timeout = 5 * 60 * 1000;

        string queryParams = $"token={token}&timeout={timeout}";
        string endpoint = $"https://production-sfo.browserless.io/chromium/bql?{queryParams}";

        string query = @"
        mutation Reconnect($url: String!) {
          goto(url: $url, waitUntil: networkIdle) {
            status
          }
          reconnect(timeout: 30000) {
            browserWSEndpoint
          }
        }";

        var body = new
        {
            query = query,
            variables = new { url }
        };

        try
        {
            // Send GraphQL request
            using var client = new HttpClient();
            var requestContent = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
            var response = await client.PostAsync(endpoint, requestContent);

            if (!response.IsSuccessStatusCode)
            {
                throw new Exception($"Request failed: {await response.Content.ReadAsStringAsync()}");
            }

            var jsonResponse = JsonNode.Parse(await response.Content.ReadAsStringAsync());
            string browserWSEndpoint = jsonResponse["data"]["reconnect"]["browserWSEndpoint"].ToString();

            Console.WriteLine($"Connecting Playwright to {browserWSEndpoint}");

            // Connect to Playwright
            using var playwright = await Playwright.CreateAsync();
            var browser = await playwright.Chromium.ConnectOverCDPAsync(browserWSEndpoint);
            var context = browser.Contexts[0];
            var page = context.Pages.Find(p => p.Url.Contains(url)) ?? throw new Exception("Page not found");

            // Take screenshot
            await page.ScreenshotAsync(new PageScreenshotOptions { Path = "temp.png", FullPage = true });

            await browser.CloseAsync();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error: {ex.Message}");
        }
    }
}
```
  

## FAQ & Troubleshooting

<details>
<summary>Puppeteer/Playwright fails to connect with a <code>404</code> error</summary>

The reconnect timeout expired before your library connected. Increase the `timeout` value in the `reconnect` mutation, or reduce the delay between your BQL query and the library connection.

</details>

<details>
<summary>My page state is lost after handoff</summary>

State (cookies, localStorage, navigation history) persists through the handoff. If you're seeing a blank page, make sure you're accessing the correct page in the browser context. Use `browser.pages()` (Puppeteer) or `context.pages` (Playwright) to find the active page.

</details>

## Next steps

  <a href="/browserql/session-management/reconnect-to-browserless" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Session reconnect</h3>
      reuse BQL sessions without library handoff
    
  </a>
  <a href="/browserql/session-management/persisting-state" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Persisting state</h3>
      long-lived sessions with the Session REST API
    
  </a>
  <a href="/baas/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BaaS overview</h3>
      run full Puppeteer/Playwright scripts without BQL
    
  </a>

---

# Reconnect to Session

URL: https://docs.browserless.io/browserql/session-management/reconnect-to-browserless

The `reconnect` mutation lets you reuse a running browser session across multiple BrowserQL queries instead of launching a new browser for every request. The session preserves cookies, cache, and all page state between requests, which cuts proxy bandwidth, eliminates redundant page loads, and reduces the chance of triggering bot detection.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Familiarity with [BQL language basics](/browserql/writing-bql/language-basics)

## How reconnects work

The [`reconnect`](/bql-schema/operations/mutations/reconnect) mutation returns a `browserQLEndpoint` URL. You send your next query to that URL to reuse the same browser. Each reconnect resets the session's idle timer, so the browser stays alive as long as you keep reconnecting before the timeout expires, up to your plan's maximum session duration.

```graphql
reconnect(timeout: 30000) {
  browserQLEndpoint
}
```

The `timeout` value is in milliseconds. The response includes the endpoint URL for your next query.

**Response**

```json
{
  "data": {
    "reconnect": {
      "browserQLEndpoint": "https://production-sfo.browserless.io/e/53.../chromium/bql/05..."
    }
  }
}
```

### Start a Session

Send your first query to open a browser session. Include the `reconnect` mutation to get a reusable endpoint URL.

```js

const API_KEY = "YOUR_API_TOKEN";
const BQL_ENDPOINT = `https://production-sfo.browserless.io/chromium/bql?token=${API_KEY}`;

const sessionQuery = `
mutation StartSession {
  goto(url: "https://example.com", waitUntil: networkIdle) {
    status
  }
  reconnect(timeout: 30000) {
    browserQLEndpoint
  }
}`;

async function startSession() {
  const response = await fetch(BQL_ENDPOINT, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ query: sessionQuery }),
  });

  const data = await response.json();
  const reconnectUrl = data.data.reconnect.browserQLEndpoint;
  console.log("Reconnect URL:", reconnectUrl);
  return reconnectUrl;
}

startSession();
```

### Use the Reconnect URL

Send your next query to the reconnect URL instead of the original endpoint.

:::important API Token Required
The `browserQLEndpoint` returned by the reconnect mutation does not include your API token. Append `?token=YOUR_API_TOKEN` to the reconnect URL before sending subsequent requests.
:::

```js
const RECONNECT_BQL_ENDPOINT = "YOUR_RECONNECT_BQL_ENDPOINT" + "?token=YOUR_API_TOKEN";

const scrapeQuery = `
mutation FetchData {
  text(selector: ".product-title") {
    text
  }
}`;

async function fetchData() {
  const response = await fetch(RECONNECT_BQL_ENDPOINT, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ query: scrapeQuery }),
  });

  const data = await response.json();
  console.log("Fetched Data:", data.data.text.text);
}

fetchData();
```

### Full Example

This example starts a session, scrapes multiple pages, and periodically reconnects to reset the idle timer. Reconnecting keeps the browser alive between queries, but never past your plan's maximum session duration.

  
**Javascript:**

```js
import fetch from 'node-fetch';

const API_KEY = "YOUR_API_TOKEN";
const BQL_ENDPOINT = `https://production-sfo.browserless.io/chromium/bql?token=${API_KEY}`;

const sessionQuery = `
mutation StartSession {
  goto(url: "https://example.com", waitUntil: networkIdle) {
    status
  }
  reconnect(timeout: 30000) {
    browserQLEndpoint
  }
}`;

async function startSession() {
  const response = await fetch(BQL_ENDPOINT, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ query: sessionQuery }),
  });

  const data = await response.json();
  return data.data.reconnect.browserQLEndpoint;
}

async function fetchData(reconnectUrl) {
  const scrapeQuery = `
  mutation FetchData {
    text(selector: ".product-title") {
      text
    }
  }`;

  const response = await fetch(reconnectUrl + "?token=" + API_KEY, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ query: scrapeQuery }),
  });

  const data = await response.json();
  console.log("Fetched Data:", data.data.text.text);
}

// Reconnect before the timeout to extend the session
async function refreshSession(reconnectUrl) {
  const refreshQuery = `
  mutation RefreshSession {
    reconnect(timeout: 30000) {
      browserQLEndpoint
    }
  }`;

  const response = await fetch(reconnectUrl + "?token=" + API_KEY, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ query: refreshQuery }),
  });

  const data = await response.json();
  return data.data.reconnect.browserQLEndpoint;
}

(async () => {
  let reconnectUrl = await startSession();
  let pagesScraped = 0;
  const REFRESH_INTERVAL = 20;

  for (let i = 0; i < 100; i++) {
    // Reconnect periodically to reset the idle timer
    if (pagesScraped >= REFRESH_INTERVAL) {
      reconnectUrl = await refreshSession(reconnectUrl);
      pagesScraped = 0;
    }

    await fetchData(reconnectUrl);
    console.log(`Scraped page ${i + 1}`);
    pagesScraped++;
  }
})();
```
  
  
**Python:**

```python
import requests

API_KEY = "YOUR_API_TOKEN"
BQL_ENDPOINT = "https://production-sfo.browserless.io/chromium/bql"

session_query = """
mutation StartSession {
  goto(url: "https://example.com", waitUntil: networkIdle) {
    status
  }
  reconnect(timeout: 30000) {
    browserQLEndpoint
  }
}
"""

scrape_query = """
mutation FetchData {
  text(selector: ".product-title") {
    text
  }
}
"""

refresh_query = """
mutation RefreshSession {
  reconnect(timeout: 30000) {
    browserQLEndpoint
  }
}
"""

def start_session():
    headers = {"Content-Type": "application/json"}
    response = requests.post(
        f"{BQL_ENDPOINT}?token={API_KEY}",
        json={"query": session_query},
        headers=headers,
    )
    response.raise_for_status()
    return response.json()["data"]["reconnect"]["browserQLEndpoint"]

def fetch_data(reconnect_url):
    headers = {"Content-Type": "application/json"}
    response = requests.post(
        f"{reconnect_url}?token={API_KEY}",
        json={"query": scrape_query},
        headers=headers,
    )
    response.raise_for_status()
    data = response.json()["data"]["text"]["text"]
    print("Fetched Data:", data)
    return data

# Reconnect before the timeout to extend the session
def refresh_session(reconnect_url):
    headers = {"Content-Type": "application/json"}
    response = requests.post(
        f"{reconnect_url}?token={API_KEY}",
        json={"query": refresh_query},
        headers=headers,
    )
    response.raise_for_status()
    return response.json()["data"]["reconnect"]["browserQLEndpoint"]

def main():
    reconnect_url = start_session()
    pages_scraped = 0
    REFRESH_INTERVAL = 20

    for i in range(100):
        # Reconnect periodically to reset the idle timer
        if pages_scraped >= REFRESH_INTERVAL:
            reconnect_url = refresh_session(reconnect_url)
            pages_scraped = 0

        fetch_data(reconnect_url)
        print(f"Scraped page {i + 1}")
        pages_scraped += 1

if __name__ == "__main__":
    main()
```
  
  
**Java:**

```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

public class BrowserlessScraper {

    static final String API_KEY = "YOUR_API_TOKEN";
    static final String BQL_ENDPOINT = "https://production-sfo.browserless.io/chromium/bql";
    static final ObjectMapper mapper = new ObjectMapper();
    static final HttpClient client = HttpClient.newHttpClient();

    static String executeQuery(String url, String query) throws Exception {
        String payload = mapper.writeValueAsString(new Query(query));
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(url))
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(payload))
            .build();
        HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
        return response.body();
    }

    static String startSession() throws Exception {
        String sessionQuery = """
        mutation StartSession {
          goto(url: "https://example.com", waitUntil: networkIdle) { status }
          reconnect(timeout: 30000) { browserQLEndpoint }
        }""";
        String json = executeQuery(BQL_ENDPOINT + "?token=" + API_KEY, sessionQuery);
        return mapper.readTree(json).path("data").path("reconnect").path("browserQLEndpoint").asText();
    }

    static String fetchData(String reconnectUrl) throws Exception {
        String scrapeQuery = """
        mutation FetchData {
          text(selector: ".product-title") { text }
        }""";
        String json = executeQuery(reconnectUrl + "?token=" + API_KEY, scrapeQuery);
        return mapper.readTree(json).path("data").path("text").path("text").asText();
    }

    // Reconnect before the timeout to extend the session
    static String refreshSession(String reconnectUrl) throws Exception {
        String refreshQuery = """
        mutation RefreshSession {
          reconnect(timeout: 30000) { browserQLEndpoint }
        }""";
        String json = executeQuery(reconnectUrl + "?token=" + API_KEY, refreshQuery);
        return mapper.readTree(json).path("data").path("reconnect").path("browserQLEndpoint").asText();
    }

    public static void main(String[] args) throws Exception {
        String reconnectUrl = startSession();
        int pagesScraped = 0;
        int REFRESH_INTERVAL = 20;

        for (int i = 0; i < 100; i++) {
            // Reconnect periodically to reset the idle timer
            if (pagesScraped >= REFRESH_INTERVAL) {
                reconnectUrl = refreshSession(reconnectUrl);
                pagesScraped = 0;
            }

            String data = fetchData(reconnectUrl);
            System.out.printf("Scraped page %d: %s%n", i + 1, data);
            pagesScraped++;
        }
    }

    static class Query {
        public String query;
        Query(String q) { this.query = q; }
    }
}
```
  
  
**C#:**

```csharp
using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

class Program
{
    const string ApiKey = "YOUR_API_TOKEN";
    const string BqlEndpoint = "https://production-sfo.browserless.io/chromium/bql";

    const string sessionQuery = @"
    mutation StartSession {
      goto(url: ""https://example.com"", waitUntil: networkIdle) { status }
      reconnect(timeout: 30000) { browserQLEndpoint }
    }";

    const string scrapeQuery = @"
    mutation FetchData {
      text(selector: "".product-title"") { text }
    }";

    const string refreshQuery = @"
    mutation RefreshSession {
      reconnect(timeout: 30000) { browserQLEndpoint }
    }";

    static async Task<string> ExecuteQuery(string url, string query)
    {
        using var client = new HttpClient();
        var content = new StringContent(
            JsonSerializer.Serialize(new { query }),
            Encoding.UTF8,
            "application/json"
        );
        var response = await client.PostAsync(url, content);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadAsStringAsync();
    }

    static async Task Main()
    {
        var json = JsonDocument.Parse(
            await ExecuteQuery($"{BqlEndpoint}?token={ApiKey}", sessionQuery)
        );
        var reconnectUrl = json.RootElement
            .GetProperty("data")
            .GetProperty("reconnect")
            .GetProperty("browserQLEndpoint")
            .GetString();

        int pagesScraped = 0;
        int REFRESH_INTERVAL = 20;

        for (int i = 0; i < 100; i++)
        {
            // Reconnect periodically to reset the idle timer
            if (pagesScraped >= REFRESH_INTERVAL)
            {
                json = JsonDocument.Parse(
                    await ExecuteQuery($"{reconnectUrl}?token={ApiKey}", refreshQuery)
                );
                reconnectUrl = json.RootElement
                    .GetProperty("data")
                    .GetProperty("reconnect")
                    .GetProperty("browserQLEndpoint")
                    .GetString();
                pagesScraped = 0;
            }

            json = JsonDocument.Parse(
                await ExecuteQuery($"{reconnectUrl}?token={ApiKey}", scrapeQuery)
            );
            var data = json.RootElement
                .GetProperty("data")
                .GetProperty("text")
                .GetProperty("text")
                .GetString();
            Console.WriteLine($"Scraped page {i + 1}: {data}");
            pagesScraped++;
        }
    }
}
```
  

## Reconnect Timeout Limits

The `timeout` parameter in the `reconnect` mutation controls how long the browser stays alive waiting for a reconnect. Omitting it keeps the default of 30 seconds.

## Terminate sessions after reconnecting

After your work is done, send a `DELETE` request to terminate the session immediately. Sessions that are not explicitly closed continue to occupy a concurrency slot until the timeout expires, which can cause `429 Too Many Requests` errors when you run many sessions in a loop.

Build the terminate URL from the `browserQLEndpoint` by replacing the browser-type segment (`/chromium/bql/`, `/chrome/bql/`, or `/stealth/bql/`) with `/browser/<browserId>`. Keep the `/e/<encodedToken>/` prefix intact. The path swap happens inside that prefix, not at the root:

```bash
# browserQLEndpoint (returned by reconnect)
https://<host>/e/<encodedToken>/chromium/bql/<browserId>

# terminateURL (replace only the inner path segment)
https://<host>/e/<encodedToken>/browser/<browserId>
```

```js
const TOKEN = process.env.BROWSERLESS_TOKEN;
const HOST = 'https://production-sfo.browserless.io';

const res = await fetch(`${HOST}/chromium/bql?token=${TOKEN}`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    query: `mutation {
      goto(url: "https://example.com", waitUntil: networkIdle) { status }
      reconnect(timeout: 60000) { browserQLEndpoint }
    }`,
  }),
});

const { data } = await res.json();
const { browserQLEndpoint } = data.reconnect;

// ... do your work using browserQLEndpoint ...

const browserId = browserQLEndpoint.split('/').pop();
const terminateURL = browserQLEndpoint.replace(/\/(chromium|chrome|stealth)\/bql\/[^/]+$/, `/browser/${browserId}`);

await fetch(`${terminateURL}?token=${TOKEN}`, { method: 'DELETE' });
```

## Reconnect to Puppeteer or Playwright

`reconnect` also returns a `browserWSEndpoint` you can pass to `puppeteer.connect()` or `playwright.chromium.connectOverCDP()` to attach a CDP-based library to the same session, with all state carried over.

See [Reconnect using Puppeteer & Playwright](/browserql/session-management/puppeteer-playwright) for the full guide.

## FAQ & Troubleshooting

<details>
<summary>My reconnect endpoint returns a connection error</summary>

The session has expired. Each reconnect resets the idle timer, but if the timeout elapses before your next request, the browser is terminated. Increase the `timeout` value or reconnect more frequently. A session that has reached your plan's maximum session duration is terminated regardless of the reconnect timeout.

</details>

<details>
<summary>Can I reconnect to a session from a different machine?</summary>

Yes. The `browserQLEndpoint` URL is fully qualified and includes the session identifier. Any client that can reach the Browserless API can send queries to it.

</details>

## Next steps

  <a href="/browserql/session-management/puppeteer-playwright" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Puppeteer & Playwright handoff</h3>
      attach CDP libraries to a BQL session
    
  </a>
  <a href="/browserql/session-management/persisting-state" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Persisting state</h3>
      long-lived sessions with the Session REST API
    
  </a>
  <a href="/browserql/session-management/injecting-cookies" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Cookie management</h3>
      set cookies before navigation
    
  </a>

---

# Session Replay

URL: https://docs.browserless.io/browserql/session-management/session-replay

This guide covers how to enable Session Replay for your BQL sessions, view the recordings, and understand its limitations. It also explains how Session Replay differs from BaaS Screen Recording.

- A [paid Browserless plan](https://www.browserless.io/pricing) (Prototyping, Starter, Scale, or Enterprise) — Session Replay is not available on the free plan
- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## What is Session Replay

![Session Replay Demo](/img/session_replay.gif)

**Session Replay** captures BQL sessions as interactive replays viewable in your Browserless dashboard. Using RRWeb technology, it records DOM mutations, mouse movements, clicks, scrolling, keyboard input, console logs, and network requests. The recordings are lightweight and can be played back at any speed.

This is different from Screen Recording (`record=true`), which captures WebM video files and is only available for [BaaS connections](/baas/start), not BQL.

Use Session Replay for debugging BQL automation workflows, creating visual documentation, and troubleshooting mutations with recorded evidence.

## What Gets Recorded

Session Replay captures DOM events (all visual changes to the page), user interactions (mouse movements, clicks, scrolling, keyboard input), console logs (all console output with timestamps), and network requests (HTTP requests and responses).

## Recording a Session with BQL

### Connection String Configuration

To enable session recording with BQL, add the `replay=true` parameter to your connection URL. Recording starts automatically when you connect.

```bash
https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE&replay=true
```

> **Recording Starts Automatically**
> When you connect with `replay=true`, recording begins immediately. There is no need to call a start mutation. The recording continues until the BQL query completes or you explicitly stop it.

### Basic Recording Example

  
**CURL:**

```bash
curl --request POST \
  --url 'https://production-sfo.browserless.io/chrome/bql?token=YOUR_API_TOKEN_HERE&replay=true' \
  --header 'Content-Type: application/json' \
  --data '{
    "query": "mutation replay {\n  goto(url: \"https://www.browserless.io\") {\n    time\n  }\n  click(selector: \"a\") {\n    time\n  }\n  waitForTimeout(time: 2000) {\n    time\n  }\n}",
    "variables": {},
    "operationName": "replay"
  }'
```
  
  
**JavaScript:**

```javascript
import fetch from 'node-fetch';

const API_KEY = "YOUR_API_TOKEN_HERE";
const BQL_ENDPOINT = `https://production-sfo.browserless.io/chromium/bql?replay=true&token=${API_KEY}`;

const recordingQuery = `
mutation replay {
  goto(url: "https://www.browserless.io") {
    time
  }
  click(selector:"a"){
    time
  }
  waitForTimeout(time:2000){
    time
  }
}`;

const recordSession = async () => {
  const response = await fetch(BQL_ENDPOINT, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ query: recordingQuery,
"operationName":"replay" })
  });

  const result = await response.json();
  console.log('Recording Result:', JSON.stringify(result, null, 2));

  // Session recording automatically stops when the BQL query completes
  console.log('Session recorded successfully');
};

recordSession().catch(console.error);
```
  
  
**Python:**

```python
import requests
import json

API_KEY = "YOUR_API_TOKEN_HERE"
BQL_ENDPOINT = f"https://production-sfo.browserless.io/chromium/bql?replay=true&token={API_KEY}"

recording_query = """
mutation RecordSession {
  goto(url: "https://www.example.com") {
    status
    time
  }

  click(selector: "button.example") {
    time
    x
    y
  }

  type(selector: "input[name='search']", text: "browserless") {
    time
  }

  waitForSelector(selector: ".search-results") {
    time
  }
}
"""

def record_session():
    response = requests.post(
        BQL_ENDPOINT,
        headers={
            'Content-Type': 'application/json'
        },
        json={'query': recording_query}
    )

    result = response.json()
    print('Recording Result:', json.dumps(result, indent=2))

    # Session recording automatically stops when the BQL query completes
    print('Session recorded successfully')

if __name__ == "__main__":
    record_session()
```
  

### Stop Recording Manually

For long-running sessions or when you need precise control over when recording stops, use the `stopSessionRecording` mutation. This mutation stops the recording and uploads the captured data to your dashboard.

  
**CURL:**

```bash
curl --request POST \
  --url 'https://production-sfo.browserless.io/chrome/bql?token=YOUR_API_TOKEN_HERE&replay=true' \
  --header 'Content-Type: application/json' \
  --data '{
    "query": "mutation StopRecording {\n  goto(url: \"https://www.browserless.io\") {\n    status\n    time\n  }\n  waitForTimeout(time: 5000) {\n    time\n  }\n  stopSessionRecording {\n    success\n    error\n  }\n}",
    "variables": {},
    "operationName": "StopRecording"
  }'
```
  
  
**JavaScript:**

```javascript
import fetch from 'node-fetch';

const API_KEY = "YOUR_API_TOKEN_HERE";
const BQL_ENDPOINT = `https://production-sfo.browserless.io/chromium/bql?replay=true&token=${API_KEY}`;

const recordingQuery = `
mutation StopRecording {
  goto(url: "https://www.browserless.io") {
    status
    time
  }
  waitForTimeout(time: 5000) {
    time
  }
  stopSessionRecording {
    success
    error
  }
}`;

const recordSession = async () => {
  const response = await fetch(BQL_ENDPOINT, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      query: recordingQuery,
      operationName: "StopRecording"
    })
  });

  const result = await response.json();
  console.log('Recording Result:', JSON.stringify(result, null, 2));

  if (result.data?.stopSessionRecording?.success) {
    console.log('Session recording stopped and uploaded successfully');
  }
};

recordSession().catch(console.error);
```
  

The `stopSessionRecording` mutation returns a response with `success` (boolean indicating if the recording was stopped successfully) and `error` (error message if the operation failed, null otherwise).

## Viewing Replays

After your BQL query completes (or after calling `stopSessionRecording`), the replay is automatically uploaded and available in your [Browserless dashboard](https://www.browserless.io/account/session-replay). Navigate to the Session Replay section to find your recorded sessions. Each replay includes a timeline showing all captured events, console logs, and network activity.

## Navigation Handling

Session Replay automatically handles page navigations within your BQL session. When mutations like `goto` navigate to a new page, the recording system reinjects the RRWeb recorder and continues capturing events. All data from multiple page navigations is aggregated into a single replay.

## Limitations

- Cross-origin iframes may not be fully captured due to browser security restrictions.
- Very long sessions with high DOM activity may produce large replay files.
- WebGL or canvas content may not be perfectly reproduced in playback.

## FAQ & Troubleshooting

<details>
<summary>My session replay is empty or not appearing in the dashboard</summary>

Ensure your BQL query completed successfully. If the browser session was terminated before the recording could upload, the replay won't appear. Check that your API token is on a [paid plan](https://www.browserless.io/pricing) (Prototyping, Starter, Scale, or Enterprise) — Session Replay isn't available on the free plan.

</details>

<details>
<summary>Can I use Session Replay with BaaS connections?</summary>

No. Session Replay is BQL-only. For BaaS connections, use [Screen Recording](/baas/monitor-sessions/screen-recording) instead, which captures WebM video files.

</details>

## Next steps

  <a href="/browserql/best-practices" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BQL best practices</h3>
      guidance on writing reliable BQL scripts
    
  </a>
  <a href="/browserql/using-the-ide/ide-features" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BQL IDE</h3>
      debug queries with the browser-based editor
    
  </a>
  <a href="/baas/monitor-sessions/screen-recording" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Screen recording (BaaS)</h3>
      WebM video capture for BaaS sessions
    
  </a>

---

# BrowserQL

URL: https://docs.browserless.io/browserql/start

BrowserQL (BQL) is the GraphQL query language and protocol behind Browserless's stealth-first automation: [BAP](/bap)'s TypeScript and Python SDKs build these same mutations under the hood. You write GraphQL mutations against managed browsers to navigate pages, interact with elements, extract data, and bypass bot detection.

If you're writing TypeScript or Python, use [BAP](/bap) instead of hand-writing GraphQL: it wraps these same mutations in a typed, Puppeteer- or Playwright-shaped API. Reach for BrowserQL directly when you're in another language, generating BQL from a tool, or working in the hosted IDE below.

## How it works

You write GraphQL mutations that describe what the browser should do. Browserless runs the browser, manages stealth configuration, and returns structured results. Run queries through the [BQL IDE](/browserql/using-the-ide/ide-features) or send them as HTTP requests from your code.

Here's a minimal example that navigates to Hacker News and extracts the page text:

```graphql
mutation ScrapeHN {
  goto(url: "https://news.ycombinator.com", waitUntil: domContentLoaded) {
    status
  }
  text {
    text
  }
}
```

## When to use BrowserQL directly

BrowserQL is built for sites that actively resist automation. Use it when Puppeteer or Playwright scripts get blocked, when you need automatic CAPTCHA solving, or when you need human-like behavior at scale. For simple automation on permissive sites, Puppeteer or Playwright alone may be enough.

## Key capabilities

- [Navigation and waiting](/browserql/writing-bql/language-basics): Go to URLs, wait for selectors, network requests, or custom timeouts before proceeding.
- [Interaction](/browserql/writing-bql/language-basics): Click, type, scroll, and trigger human-like events.
- [Data extraction](/browserql/use-cases/scrape-and-extract-data): Return text, attributes, and structured JSON from the page.
- [Screenshots and PDFs](/browserql/use-cases/screenshots): Capture full pages or specific elements at any point in a query.
- [CAPTCHA solving](/browserql/bot-detection/solving-captchas): Automatically detect and solve reCAPTCHA and image-based challenges.
- [Stealth and fingerprinting](/browserql/bot-detection/stealth): Evade bot detection with entropy injection, browser fingerprint mitigations, and the `/stealth/bql` endpoint.
- [Proxies](/browserql/bot-detection/proxies): Route traffic through built-in residential proxies or your own proxy provider.
- [Session handoff](/browserql/session-management/puppeteer-playwright): Pass a BQL session to Puppeteer or Playwright via the `reconnect` mutation.

## Browser endpoints

Three endpoints are available, each using a different browser binary:

- **[`/chromium/bql`](/open-api/chromium-bql-execute)**: Open-source Chromium with no telemetry and a minimal fingerprint surface. Best for most headless scraping and automation.
- **[`/chrome/bql`](/open-api/chrome-bql-execute)**: A genuine Google Chrome build. Use when your target site requires or explicitly detects the real Chrome browser, or needs built-in video codec support.
- **[`/stealth/bql`](/open-api/stealth-bql-execute)**: A privacy-hardened browser with fingerprint randomization, ad and tracker blocking, and a distinct user agent. Use when Chrome or Chromium are blocked.

## Authentication

Requests to BrowserQL require an API token passed as a query parameter. See the [Quickstart guide](/browserql/getting-started) to get your token and run your first query.

## Session limits

BrowserQL session durations depend on your subscription plan. The [reconnect](/browserql/session-management/reconnect-to-browserless) maximum matches the session maximum on every plan:

## FAQ & Troubleshooting

<details>
<summary>What's the difference between BrowserQL and BAP?</summary>

BrowserQL is the GraphQL protocol. [BAP](/bap) is a typed SDK, in TypeScript or Python, built on top of it. Every BAP method sends a BQL mutation under the hood, so anything documented here works through BAP too. Use BrowserQL directly when you're not writing TypeScript or Python, want to generate BQL from another tool, or are working in the hosted IDE.

</details>

<details>
<summary>How is BrowserQL different from Puppeteer or Playwright?</summary>

BrowserQL is a declarative GraphQL API: you describe what the browser should do rather than scripting step-by-step. It includes built-in stealth, CAPTCHA solving, and human-like behavior that would require extra libraries in Puppeteer or Playwright.

</details>

<details>
<summary>Does BrowserQL handle bot detection?</summary>

Yes. BrowserQL includes built-in stealth capabilities, CAPTCHA solving, proxy rotation, and browser fingerprint randomization. Use the `/stealth/bql` endpoint for maximum anti-detection on protected sites.

</details>

<details>
<summary>Why am I getting a <code>403 Forbidden</code> response?</summary>

Your API token is missing or malformed. Pass it as a `?token=` query parameter, not in the request body. Check your [account dashboard](https://browserless.io/account/) to verify the token is active.

</details>

<details>
<summary>My query returns empty data</summary>

The page likely requires JavaScript to render. Add a `waitForSelector` or `waitForEvent` mutation before extracting content to ensure the page is fully loaded.

</details>

## Next steps

  <a href="/bap" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BAP overview</h3>
      use a typed SDK instead of hand-writing GraphQL
    
  </a>
  <a href="/browserql/getting-started" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Quickstart</h3>
      set up your account and run your first query
    
  </a>
  <a href="/browserql/writing-bql/language-basics" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Writing BQL</h3>
      learn GraphQL mutations, aliases, and BQL syntax
    
  </a>
  <a href="/browserql/bot-detection/overview" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Bot detection</h3>
      bypass CAPTCHAs, fingerprinting, and anti-bot systems
    
  </a>
  <a href="/browserql/using-the-ide/ide-features" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BQL IDE</h3>
      write and test queries in the browser-based editor
    
  </a>

---

# PDF Generation

URL: https://docs.browserless.io/browserql/use-cases/pdfs

The [`pdf` mutation](/bql-schema/operations/mutations/pdf) navigates to a URL and generates a PDF. Browserless returns the file as a base64-encoded string with options for page format, margins, headers, footers, and background printing.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Familiarity with [BQL language basics](/browserql/writing-bql/language-basics)

## Basic Usage

Navigate to a URL and capture the page as a PDF. [Wait for elements to load](/browserql/writing-bql/waiting-for-things) before generating to avoid incomplete content.

  
**BQL Query:**

```graphql
mutation PDF {
  goto(url: "https://example.com") {
    status
  }

  pdf(displayHeaderFooter: true, printBackground: false, format: a0) {
    base64
  }
}
```
  
  
**BAP TypeScript:**

```typescript
import Browserless from "@browserless.io/bap-ts";

const TOKEN = "YOUR_API_TOKEN_HERE";

const browser = Browserless.connect({
  browserWSEndpoint: "wss://production-sfo.browserless.io/chromium/bql",
  token: TOKEN,
});

const page = await browser.newPage();
await page.goto("https://example.com");

// Returns the bytes as a Uint8Array. `path` also writes the file, Node.js only.
await page.pdf({
  path: "page.pdf",
  displayHeaderFooter: true,
  printBackground: false,
  format: "a0",
});

await browser.close();
```
  
  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{"query":"mutation PDF {\n  goto(url: \"https://example.com\") {\n    status\n  }\n\n  pdf(displayHeaderFooter: true, printBackground: false, format: a0) {\n    base64\n  }\n}","variables":{},"operationName":"PDF"}'
```
  
  
**Javascript:**

```js
const endpoint = "https://production-sfo.browserless.io/chromium/bql";
const token = "YOUR_API_TOKEN_HERE";

const options = {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({"query":"mutation PDF {\n  goto(url: \"https://example.com\") {\n    status\n  }\n\n  pdf(displayHeaderFooter: true, printBackground: false, format: a0) {\n    base64\n  }\n}","variables":{},"operationName":"PDF"})
};

const url = `${endpoint}?token=${token}`;
const response = await fetch(url, options);
const data = await response.json();

console.log(data);

```
  
  
**Python:**

```python
import requests

endpoint = "https://production-sfo.browserless.io/chromium/bql"
query_string = {
    "token": "YOUR_API_TOKEN_HERE",
}
headers = {
    "Content-Type": "application/json",
}
payload = {
    "query": "mutation PDF {\n  goto(url: \"https://example.com\") {\n    status\n  }\n\n  pdf(displayHeaderFooter: true, printBackground: false, format: a0) {\n    base64\n  }\n}",
    "variables": {},
    "operationName": "PDF",
}

response = requests.post(endpoint, params=query_string, headers=headers, json=payload)
print(response.json())
```
  
  
**Java:**

```java
String url = "https://production-sfo.browserless.io/chromium/bql";
String token = "YOUR_API_TOKEN_HERE";
String endpoint = url + "?token=" + token;

String payload = """
{"query":"mutation PDF {\n  goto(url: \"https://example.com\") {\n    status\n  }\n\n  pdf(displayHeaderFooter: true, printBackground: false, format: a0) {\n    base64\n  }\n}","variables":{},"operationName":"PDF"}
""";

HttpResponse response = Unirest.post(endpoint)
    .header("Content-Type", "application/json")
    .body(payload)
    .asString();
```
  
  
**C#:**

```csharp
string url = "https://production-sfo.browserless.io/chromium/bql";
string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"{url}?token={token}";

var payload = new
{
    query = @"mutation PDF {
  goto(url: ""https://example.com"") {
    status
  }

  pdf(displayHeaderFooter: true, printBackground: false, format: a0) {
    base64
  }
}",
    variables = "",
    operationName = "PDF"
};

using (var client = new HttpClient())
{
    var jsonPayload = System.Text.Json.JsonSerializer.Serialize(payload);
    var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");

    var response = await client.PostAsync(endpoint, content);
    string responseBody = await response.Content.ReadAsStringAsync();

    Console.WriteLine(responseBody);
}
```
  

## Setting HTML Content

The `content` mutation sets the page HTML directly, without navigating to a URL. Use it to generate PDFs from dynamic or server-rendered markup.

  
**BQL Query:**

```graphql
mutation PDF {
  content(html: "<h1>Hello, World!</h1>") {
    status
  }

  pdf(displayHeaderFooter: true, printBackground: false, format: a0) {
    base64
  }
}
```
  
  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{"query":"mutation PDF {\ncontent(html: \"<h1>Hello, World!</h1>\") {\n  status\n}\n\npdf(displayHeaderFooter: true, printBackground: false, format: a0) {\n  base64\n}\n}","variables":{},"operationName":"PDF"}'
```
  
  
**Javascript:**

```js
const endpoint = "https://production-sfo.browserless.io/chromium/bql";
const token = "YOUR_API_TOKEN_HERE";

const options = {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({"query":"mutation PDF {\ncontent(html: \"<h1>Hello, World!</h1>\") {\n  status\n}\n\npdf(displayHeaderFooter: true, printBackground: false, format: a0) {\n  base64\n}\n}","variables":{},"operationName":"PDF"})
};

const url = `${endpoint}?token=${token}`;
const response = await fetch(url, options);
const data = await response.json();

console.log(data);
```
  
  
**Python:**

```python
import requests

endpoint = "https://production-sfo.browserless.io/chromium/bql"
query_string = {
    "token": "YOUR_API_TOKEN_HERE",
}
headers = {
    "Content-Type": "application/json",
}
payload = {
    "query": "mutation PDF {\ncontent(html: \"<h1>Hello, World!</h1>\") {\n  status\n}\n\npdf(displayHeaderFooter: true, printBackground: false, format: a0) {\n  base64\n}\n}",
    "variables": {},
    "operationName": "PDF",
}

response = requests.post(endpoint, params=query_string, headers=headers, json=payload)
print(response.json())
```

  
  
**Java:**

```java
String url = "https://production-sfo.browserless.io/chromium/bql";
String token = "YOUR_API_TOKEN_HERE";
String endpoint = url + "?token=" + token;

String payload = """
{"query":"mutation PDF {\ncontent(html: \"<h1>Hello, World!</h1>\") {\n  status\n}\n\npdf(displayHeaderFooter: true, printBackground: false, format: a0) {\n  base64\n}\n}","variables":{},"operationName":"PDF"}
""";

HttpResponse response = Unirest.post(endpoint)
    .header("Content-Type", "application/json")
    .body(payload)
    .asString();
```
  
  
**C#:**

```csharp
string url = "https://production-sfo.browserless.io/chromium/bql";
string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"{url}?token={token}";

var payload = new
{
    query = @"mutation PDF {
content(html: ""<h1>Hello, World!</h1>"") {
  status
}

pdf(displayHeaderFooter: true, printBackground: false, format: a0) {
  base64
}
}",
    variables = "",
    operationName = "PDF"
};

using (var client = new HttpClient())
{
    var jsonPayload = System.Text.Json.JsonSerializer.Serialize(payload);
    var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");

    var response = await client.PostAsync(endpoint, content);
    string responseBody = await response.Content.ReadAsStringAsync();

    Console.WriteLine(responseBody);
}
```
  

## Adding Header and Footer Templates

Custom header and footer templates let you inject dynamic content into PDFs. Templates support special CSS classes: `pageNumber`, `totalPages`, `title`, `url`, and `date`.

> **Font Size**
> Specify font sizes explicitly in header and footer templates (e.g., `font-size: 12pt`). The default text size is too small to read in most PDF viewers.

  
**BQL Query:**

```graphql
mutation addFooterAndHeaderToPDF {
  goto(url: "https://www.example.com", waitUntil: networkIdle) {
    status
  }
  invoice: pdf(
    format: a4
    displayHeaderFooter: true
    headerTemplate: ""
    footerTemplate: "/"
    printBackground: true
  ) {
    base64
  }
}
```
  
  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{"query":"mutation addFooterAndHeaderToPDF {\n  goto(url: \"https://www.example.com\", waitUntil: networkIdle) {\n    status\n  }\n  invoice: pdf(\n    format: a4\n    displayHeaderFooter: true\n    headerTemplate: \"\"\n    footerTemplate: \"/\"\n    printBackground: true\n  ) {\n    base64\n  }\n}","variables":{},"operationName":"addFooterAndHeaderToPDF"}'
```
  
  
**Javascript:**

```js
const endpoint = "https://production-sfo.browserless.io/chromium/bql";
const token = "YOUR_API_TOKEN_HERE";

const options = {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({"query":"mutation addFooterAndHeaderToPDF {\n  goto(url: \"https://www.example.com\", waitUntil: networkIdle) {\n    status\n  }\n  invoice: pdf(\n    format: a4\n    displayHeaderFooter: true\n    headerTemplate: \"\"\n    footerTemplate: \"/\"\n    printBackground: true\n  ) {\n    base64\n  }\n}","variables":{},"operationName":"addFooterAndHeaderToPDF"})
};

const url = `${endpoint}?token=${token}`;
const response = await fetch(url, options);
const data = await response.json();

console.log(data);
```
  
  
**Python:**

```python
import requests

endpoint = "https://production-sfo.browserless.io/chromium/bql"
query_string = {
    "token": "YOUR_API_TOKEN_HERE",
}
headers = {
    "Content-Type": "application/json",
}
payload = {
    "query": "mutation addFooterAndHeaderToPDF {\n  goto(url: \"https://www.example.com\", waitUntil: networkIdle) {\n    status\n  }\n  invoice: pdf(\n    format: a4\n    displayHeaderFooter: true\n    headerTemplate: \"\"\n    footerTemplate: \"/\"\n    printBackground: true\n  ) {\n    base64\n  }\n}",
    "variables": {},
    "operationName": "addFooterAndHeaderToPDF",
}

response = requests.post(endpoint, params=query_string, headers=headers, json=payload)
print(response.json())
```
  
  
**Java:**

```java
String url = "https://production-sfo.browserless.io/chromium/bql";
String token = "YOUR_API_TOKEN_HERE";
String endpoint = String.format("%s?token=%s", url, token);

HttpResponse response = Unirest.post(endpoint)
    .header("Content-Type", "application/json")
    .body("{\"query\":\"mutation addFooterAndHeaderToPDF {\\n  goto(url: \\\"https://www.example.com\\\", waitUntil: networkIdle) {\\n    status\\n  }\\n  invoice: pdf(\\n    format: a4\\n    displayHeaderFooter: true\\n    headerTemplate: \\\"\\\"\\n    footerTemplate: \\\"/\\\"\\n    printBackground: true\\n  ) {\\n    base64\\n  }\\n}\",\"variables\":\"\",\"operationName\":\"addFooterAndHeaderToPDF\"}")
    .asString();
```
  
  
**C#:**

```csharp
string url = "https://production-sfo.browserless.io/chromium/bql";
string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"{url}?token={token}";

var payload = new
{
    query = @"mutation addFooterAndHeaderToPDF {
  goto(url: ""https://www.example.com"", waitUntil: networkIdle) {
    status
  }
  invoice: pdf(
    format: a4
    displayHeaderFooter: true
    headerTemplate: """"
    footerTemplate: ""/""
    printBackground: true
  ) {
    base64
  }
}",
    variables = "",
    operationName = "addFooterAndHeaderToPDF"
};

using (var client = new HttpClient())
{
    var jsonPayload = System.Text.Json.JsonSerializer.Serialize(payload);
    var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");

    var response = await client.PostAsync(endpoint, content);
    string responseBody = await response.Content.ReadAsStringAsync();

    Console.WriteLine(responseBody);
}
```
  

## FAQ & Troubleshooting

<details>
<summary>Can BrowserQL generate PDFs from any web page?</summary>

Yes. BrowserQL renders the page in a real browser and generates a PDF using Chrome's print-to-PDF engine. This handles JavaScript-rendered content, custom fonts, and complex layouts.

</details>

<details>
<summary>How do I add headers and footers to generated PDFs?</summary>

Use the headerTemplate and footerTemplate parameters in the pdf mutation. These accept HTML strings with special classes like pageNumber and totalPages for dynamic content.

</details>

<details>
<summary>Can I generate a PDF from raw HTML instead of a URL?</summary>

Yes. Use the content mutation to load HTML directly into the browser, then call the pdf mutation. This is useful for generating invoices, reports, or documents from dynamic HTML.

</details>

## Next steps

  <a href="/browserql/writing-bql/waiting-for-things" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Waiting properly</h3>
      BrowserQL's built-in waiters for more reliable automations
    
  </a>
  <a href="/browserql/best-practices#reject-unnecessary-resources" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Reject unnecessary resources</h3>
      block ads, images, and non-essential content
    
  </a>
  <a href="/browserql/best-practices" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Best BQL practices</h3>
      essential patterns for reliable and maintainable browser automation
    
  </a>

---

# Search Automation

URL: https://docs.browserless.io/browserql/use-cases/performing-searches

The [`click`](/bql-schema/operations/mutations/click) and [`type`](/bql-schema/operations/mutations/type) mutations let you interact with search bars, dismiss popups, and submit queries. BrowserQL uses level 4 CSS selectors and handles wait conditions automatically.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Familiarity with [BQL language basics](/browserql/writing-bql/language-basics)

## Basic Usage

The example below navigates to Booking.com, dismisses a sign-in modal, types a destination, and submits the search.

  
**BQL Query:**

```graphql
mutation SearchBooking {
  goto(url: "https://www.booking.com", waitUntil: networkIdle) {
    status
  }

  closeModal: click(
    selector: "button[aria-label='Dismiss sign-in info.']"
    visible: true
    timeout: 5000
  ) {
    time
  }

  type(
    text: "New York"
    selector: "input[name='ss'][placeholder='Where are you going?']"
  ) {
    selector
  }

  submitSearch: click(
    selector: "button[type='submit']"
    visible: true
  ) {
    selector
    time
  }
}
```
  
  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{"query":"mutation SearchBooking {\n  goto(url: \"https://www.booking.com\", waitUntil: networkIdle) {\n    status\n  }\n\n  closeModal: click(\n    selector: \"button[aria-label='\''Dismiss sign-in info.'\'']\"\n    visible: true\n    timeout: 5000\n  ) {\n    time\n  }\n\n  type(\n    text: \"New York\"\n    selector: \"input[name='\''ss'\''][placeholder='\''Where are you going?'\'']\"\n  ) {\n    selector\n  }\n\n  submitSearch: click(\n    selector: \"button[type='\''submit'\'']\"\n    visible: true\n  ) {\n    selector\n    time\n  }\n}","variables":{},"operationName":"SearchBooking"}'
```
  
  
**Javascript:**

```js
const endpoint = "https://production-sfo.browserless.io/chromium/bql";
const token = "YOUR_API_TOKEN_HERE";

const options = {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    query: `mutation SearchBooking {
  goto(url: "https://www.booking.com", waitUntil: networkIdle) {
    status
  }

  closeModal: click(
    selector: "button[aria-label='Dismiss sign-in info.']"
    visible: true
    timeout: 5000
  ) {
    time
  }

  type(
    text: "New York"
    selector: "input[name='ss'][placeholder='Where are you going?']"
  ) {
    selector
  }

  submitSearch: click(
    selector: "button[type='submit']"
    visible: true
  ) {
    selector
    time
  }
}`,
    variables: "",
    operationName: "SearchBooking",
  })
};

const url = `${endpoint}?token=${token}`;
const response = await fetch(url, options);
const data = await response.json();

console.log(data);
```
  
  
**Python:**

```python
import requests

endpoint = "https://production-sfo.browserless.io/chromium/bql"
query_string = {
    "token": "YOUR_API_TOKEN_HERE",
}
headers = {
    "Content-Type": "application/json",
}
payload = {
    "query": """mutation SearchBooking {
  goto(url: "https://www.booking.com", waitUntil: networkIdle) {
    status
  }

  closeModal: click(
    selector: "button[aria-label='Dismiss sign-in info.']"
    visible: true
    timeout: 5000
  ) {
    time
  }

  type(
    text: "New York"
    selector: "input[name='ss'][placeholder='Where are you going?']"
  ) {
    selector
  }

  submitSearch: click(
    selector: "button[type='submit']"
    visible: true
  ) {
    selector
    time
  }
}""",
    "variables": {},
    "operationName": "SearchBooking",
}

response = requests.post(endpoint, params=query_string, headers=headers, json=payload)
print(response.json())
```
  
  
**Java:**

```java
String url = "https://production-sfo.browserless.io/chromium/bql";
String token = "YOUR_API_TOKEN_HERE";
String endpoint = String.format("%s?token=%s", url, token);

HttpResponse response = Unirest.post(endpoint)
    .header("Content-Type", "application/json")
    .body("{\"query\":\"mutation SearchBooking {\\n  goto(url: \\\"https://www.booking.com\\\", waitUntil: networkIdle) {\\n    status\\n  }\\n\\n  closeModal: click(\\n    selector: \\\"button[aria-label='Dismiss sign-in info.']\\\",\\n    visible: true,\\n    timeout: 5000\\n  ) {\\n    time\\n  }\\n\\n  type(\\n    text: \\\"New York\\\",\\n    selector: \\\"input[name='ss'][placeholder='Where are you going?']\\\"\\n  ) {\\n    selector\\n  }\\n\\n  submitSearch: click(\\n    selector: \\\"button[type='submit']\\\",\\n    visible: true\\n  ) {\\n    selector\\n    time\\n  }\\n}\",\"variables\":\"\",\"operationName\":\"SearchBooking\"}")
    .asString();
```
  
  
**C#:**

```csharp
string url = "https://production-sfo.browserless.io/chromium/bql";
string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"{url}?token={token}";

var payload = new
{
    query = @"mutation SearchBooking {
  goto(url: ""https://www.booking.com"", waitUntil: networkIdle) {
    status
  }

  closeModal: click(
    selector: ""button[aria-label='Dismiss sign-in info.']""
    visible: true
    timeout: 5000
  ) {
    time
  }

  type(
    text: ""New York""
    selector: ""input[name='ss'][placeholder='Where are you going?']""
  ) {
    selector
  }

  submitSearch: click(
    selector: ""button[type='submit']""
    visible: true
  ) {
    selector
    time
  }
}",
    variables = "",
    operationName = "SearchBooking"
};

using (HttpClient httpClient = new HttpClient())
{
    var jsonPayload = System.Text.Json.JsonSerializer.Serialize(payload);
    var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");

    var response = await httpClient.PostAsync(endpoint, content);
    string responseBody = await response.Content.ReadAsStringAsync();
    Console.WriteLine(responseBody);
}
```
  

## Waiting for Search Results

After submitting a search, use [`waitForNavigation`](/bql-schema/operations/mutations/wait-for-navigation) to pause until the results page loads, then extract the page HTML. See [Wait Conditions](/browserql/writing-bql/waiting-for-things) for more options to wait.

  
**BQL Query:**

```graphql
mutation SearchAndExtract {
  goto(url: "https://www.booking.com", waitUntil: networkIdle) {
    status
  }

  closeModal: click(
    selector: "button[aria-label='Dismiss sign-in info.']"
    visible: true
    timeout: 5000
  ) {
    time
  }

  type(
    text: "New York"
    selector: "input[name='ss'][placeholder='Where are you going?']"
  ) {
    selector
  }

  submitSearch: click(
    selector: "button[type='submit']"
    visible: true
  ) {
    selector
    time
  }

  waitForNavigation {
    time
  }

  html {
    html
  }
}
```
  
  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{"query":"mutation SearchAndExtract {\n  goto(url: \"https://www.booking.com\", waitUntil: networkIdle) {\n    status\n  }\n\n  closeModal: click(\n    selector: \"button[aria-label='\''Dismiss sign-in info.'\'']\"\n    visible: true\n    timeout: 5000\n  ) {\n    time\n  }\n\n  type(\n    text: \"New York\"\n    selector: \"input[name='\''ss'\''][placeholder='\''Where are you going?'\'']\"\n  ) {\n    selector\n  }\n\n  submitSearch: click(\n    selector: \"button[type='\''submit'\'']\"\n    visible: true\n  ) {\n    selector\n    time\n  }\n\n  waitForNavigation {\n    time\n  }\n\n  html {\n    html\n  }\n}","variables":{},"operationName":"SearchAndExtract"}'
```
  
  
**Javascript:**

```js
const endpoint = "https://production-sfo.browserless.io/chromium/bql";
const token = "YOUR_API_TOKEN_HERE";

const options = {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    query: `mutation SearchAndExtract {
  goto(url: "https://www.booking.com", waitUntil: networkIdle) {
    status
  }

  closeModal: click(
    selector: "button[aria-label='Dismiss sign-in info.']"
    visible: true
    timeout: 5000
  ) {
    time
  }

  type(
    text: "New York"
    selector: "input[name='ss'][placeholder='Where are you going?']"
  ) {
    selector
  }

  submitSearch: click(
    selector: "button[type='submit']"
    visible: true
  ) {
    selector
    time
  }

  waitForNavigation {
    time
  }

  html {
    html
  }
}`,
    variables: "",
    operationName: "SearchAndExtract",
  })
};

const url = `${endpoint}?token=${token}`;
const response = await fetch(url, options);
const data = await response.json();

console.log(data);
```
  
  
**Python:**

```python
import requests

endpoint = "https://production-sfo.browserless.io/chromium/bql"
query_string = {
    "token": "YOUR_API_TOKEN_HERE",
}
headers = {
    "Content-Type": "application/json",
}
payload = {
    "query": """mutation SearchAndExtract {
  goto(url: "https://www.booking.com", waitUntil: networkIdle) {
    status
  }

  closeModal: click(
    selector: "button[aria-label='Dismiss sign-in info.']"
    visible: true
    timeout: 5000
  ) {
    time
  }

  type(
    text: "New York"
    selector: "input[name='ss'][placeholder='Where are you going?']"
  ) {
    selector
  }

  submitSearch: click(
    selector: "button[type='submit']"
    visible: true
  ) {
    selector
    time
  }

  waitForNavigation {
    time
  }

  html {
    html
  }
}""",
    "variables": {},
    "operationName": "SearchAndExtract",
}

response = requests.post(endpoint, params=query_string, headers=headers, json=payload)
print(response.json())
```
  
  
**Java:**

```java
String url = "https://production-sfo.browserless.io/chromium/bql";
String token = "YOUR_API_TOKEN_HERE";
String endpoint = String.format("%s?token=%s", url, token);

HttpResponse response = Unirest.post(endpoint)
    .header("Content-Type", "application/json")
    .body("{\"query\":\"mutation SearchAndExtract {\\n  goto(url: \\\"https://www.booking.com\\\", waitUntil: networkIdle) {\\n    status\\n  }\\n\\n  closeModal: click(\\n    selector: \\\"button[aria-label='Dismiss sign-in info.']\\\",\\n    visible: true,\\n    timeout: 5000\\n  ) {\\n    time\\n  }\\n\\n  type(\\n    text: \\\"New York\\\",\\n    selector: \\\"input[name='ss'][placeholder='Where are you going?']\\\"\\n  ) {\\n    selector\\n  }\\n\\n  submitSearch: click(\\n    selector: \\\"button[type='submit']\\\",\\n    visible: true\\n  ) {\\n    selector\\n    time\\n  }\\n\\n  waitForNavigation {\\n    time\\n  }\\n\\n  html {\\n    html\\n  }\\n}\",\"variables\":\"\",\"operationName\":\"SearchAndExtract\"}")
    .asString();
```
  
  
**C#:**

```csharp
string url = "https://production-sfo.browserless.io/chromium/bql";
string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"{url}?token={token}";

var payload = new
{
    query = @"mutation SearchAndExtract {
  goto(url: ""https://www.booking.com"", waitUntil: networkIdle) {
    status
  }

  closeModal: click(
    selector: ""button[aria-label='Dismiss sign-in info.']""
    visible: true
    timeout: 5000
  ) {
    time
  }

  type(
    text: ""New York""
    selector: ""input[name='ss'][placeholder='Where are you going?']""
  ) {
    selector
  }

  submitSearch: click(
    selector: ""button[type='submit']""
    visible: true
  ) {
    selector
    time
  }

  waitForNavigation {
    time
  }

  html {
    html
  }
}",
    variables = "",
    operationName = "SearchAndExtract"
};

using (HttpClient httpClient = new HttpClient())
{
    var jsonPayload = System.Text.Json.JsonSerializer.Serialize(payload);
    var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");

    var response = await httpClient.PostAsync(endpoint, content);
    string responseBody = await response.Content.ReadAsStringAsync();
    Console.WriteLine(responseBody);
}
```
  

## Handling Conditional Elements

The [`if`](/bql-schema/operations/mutations/if) mutation runs its nested block only when a selector is present at the time of execution. Use it to interact with elements that appear conditionally, such as date pickers, banners, or upsell prompts.

> **`if` does not wait**
> `if` evaluates the selector immediately. It does not wait for the element to appear. If the element loads asynchronously, add a `waitForSelector` call before the `if` block to ensure the element has time to render.

```graphql
mutation ConditionalDatePicker {
  goto(url: "https://www.booking.com", waitUntil: networkIdle) {
    status
  }

  type(
    text: "New York"
    selector: "input[name='ss'][placeholder='Where are you going?']"
  ) {
    selector
  }

  submitSearch: click(
    selector: "button[type='submit']"
    visible: true
  ) {
    time
  }

  waitForNavigation {
    time
  }

  ifDateFlexible: if(selector: "#flexible-searchboxdatepicker-tab-trigger", visible: true) {
    clickFlexible: click(
      selector: "#flexible-searchboxdatepicker-tab-trigger"
    ) {
      time
    }
  }

  html {
    html
  }
}
```

Use [`ifnot`](/bql-schema/operations/mutations/ifnot) to run a block only when a selector is absent.

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>403 Forbidden</code> response?</summary>

Your API token is missing or malformed. Pass it as a `?token=` query parameter. Check your [account dashboard](https://browserless.io/account/) to verify the token is active.

</details>

<details>
<summary>My query times out before completing</summary>

Increase the `timeout` parameter on your mutation or use a more specific `waitUntil` condition. Long-running pages may need `networkIdle` instead of `load`.

</details>

## Next steps

  <a href="/browserql/writing-bql/waiting-for-things" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Waiting for Things</h3>
      Control when BrowserQL proceeds: wait for selectors, navigation events, or network responses.
    
  </a>

  <a href="/browserql/use-cases/scrape-and-extract-data" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Data Extraction</h3>
      Extract HTML, JSON, or raw API responses from search result pages.
    
  </a>

  <a href="/browserql/best-practices" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Best BQL Practices</h3>
      Patterns for reliable, maintainable browser automation with BrowserQL.
    
  </a>

---

# Data Scraping and Extraction

URL: https://docs.browserless.io/browserql/use-cases/scrape-and-extract-data

BrowserQL offers three main approaches to extracting data: grab the page HTML with the [`html`](/bql-schema/operations/mutations/html) mutation, map DOM elements to a structured JSON with [`mapSelector`](/bql-schema/operations/mutations/map-selector) or [`querySelectorAll`](/bql-schema/operations/mutations/query-selector-all), or intercept raw API responses with the [`response`](/bql-schema/operations/mutations/response) mutation. Choose the approach that fits your downstream processing needs.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Familiarity with [BQL language basics](/browserql/writing-bql/language-basics)

## Basic Usage

The `html` mutation returns the full page HTML. [Wait for the page to load](/browserql/writing-bql/waiting-for-things) before extracting to avoid empty results.

  
**BQL Query:**

```graphql
mutation ExtractHTML {
  goto(url: "https://www.browserless.io/", waitUntil: domContentLoaded) {
    status
  }

  html {
    html
  }
}
```
  
  
**BAP TypeScript:**

```typescript
import Browserless from "@browserless.io/bap-ts";

const TOKEN = "YOUR_API_TOKEN_HERE";

const browser = Browserless.connect({
  browserWSEndpoint: "wss://production-sfo.browserless.io/chromium/bql",
  token: TOKEN,
});

const page = await browser.newPage();
await page.goto("https://www.browserless.io/");

// Pass a `selector` to scope the HTML to one part of the page.
const result = await page.html();
console.log(result.html);

await browser.close();
```
  
  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{"query":"mutation ExtractHTML {\n  goto(url: \"https://www.browserless.io/\", waitUntil: domContentLoaded) {\n    status\n  }\n\n  html {\n    html\n  }\n}","variables":{},"operationName":"ExtractHTML"}'
```
  
  
**Javascript:**

```js
const endpoint = "https://production-sfo.browserless.io/chromium/bql";
const token = "YOUR_API_TOKEN_HERE";

const options = {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    query: `mutation ExtractHTML {
  goto(url: "https://www.browserless.io/", waitUntil: domContentLoaded) {
    status
  }

  html {
    html
  }
}`,
    variables: "",
    operationName: "ExtractHTML",
  })
};

const url = `${endpoint}?token=${token}`;
const response = await fetch(url, options);
const data = await response.json();

console.log(data);
```
  
  
**Python:**

```python
import requests

endpoint = "https://production-sfo.browserless.io/chromium/bql"
query_string = {
    "token": "YOUR_API_TOKEN_HERE",
}
headers = {
    "Content-Type": "application/json",
}
payload = {
    "query": """mutation ExtractHTML {
  goto(url: "https://www.browserless.io/", waitUntil: domContentLoaded) {
    status
  }

  html {
    html
  }
}""",
    "variables": {},
    "operationName": "ExtractHTML",
}

response = requests.post(endpoint, params=query_string, headers=headers, json=payload)
print(response.json())
```
  
  
**Java:**

```java
String url = "https://production-sfo.browserless.io/chromium/bql";
String token = "YOUR_API_TOKEN_HERE";
String endpoint = String.format("%s?token=%s", url, token);

HttpResponse response = Unirest.post(endpoint)
    .header("Content-Type", "application/json")
    .body("{\"query\":\"mutation ExtractHTML {\\n  goto(url: \\\"https://www.browserless.io/\\\", waitUntil: domContentLoaded) {\\n    status\\n  }\\n\\n  html {\\n    html\\n  }\\n}\",\"variables\":\"\",\"operationName\":\"ExtractHTML\"}")
    .asString();
```
  
  
**C#:**

```csharp
string url = "https://production-sfo.browserless.io/chromium/bql";
string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"{url}?token={token}";

var payload = new
{
    query = @"mutation ExtractHTML {
  goto(url: ""https://www.browserless.io/"", waitUntil: domContentLoaded) {
    status
  }

  html {
    html
  }
}",
    variables = "",
    operationName = "ExtractHTML"
};

using (HttpClient httpClient = new HttpClient())
{
    var jsonPayload = System.Text.Json.JsonSerializer.Serialize(payload);
    var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");

    var response = await httpClient.PostAsync(endpoint, content);
    string responseBody = await response.Content.ReadAsStringAsync();
    Console.WriteLine(responseBody);
}
```
  

### Targeting a Specific Element

Pass a `selector` to return HTML from a single element instead of the full page:

```graphql
html(selector: ".navbar_container") {
  html
}
```

### Cleaning the HTML

The `clean` argument strips non-text nodes (scripts, video, canvas), DOM attributes, and excess whitespace. It can reduce payload size by up to 1,000x:

```graphql
html(clean: {
  removeAttributes: true
  removeNonTextNodes: true
}) {
  html
}
```

## Creating a JSON with `mapSelector`

`mapSelector` is designed for pages with repetitive, hierarchical structure: product listings, comment threads, search results, or any repeating pattern. It iterates over a NodeList, similar to `document.querySelectorAll`, and returns a structured array of objects. Use it to extract attributes, text content, or nested elements.

The query below navigates to Hacker News and extracts the `href` of every post link:

  
**BQL Query:**

```graphql
mutation ScrapeHackerNews {
  goto(
    url: "https://news.ycombinator.com"
    waitUntil: firstContentfulPaint
  ) {
    status
  }

  posts: mapSelector(selector: ".submission .titleline > a", wait: true) {
    link: attribute(name: "href") {
      value
    }
  }
}
```
  
  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{"query":"mutation ScrapeHackerNews {\n  goto(\n    url: \"https://news.ycombinator.com\"\n    waitUntil: firstContentfulPaint\n  ) {\n    status\n  }\n\n  posts: mapSelector(selector: \".submission .titleline > a\", wait: true) {\n    link: attribute(name: \"href\") {\n      value\n    }\n  }\n}","variables":{},"operationName":"ScrapeHackerNews"}'
```
  
  
**Javascript:**

```js
const endpoint = "https://production-sfo.browserless.io/chromium/bql";
const token = "YOUR_API_TOKEN_HERE";

const options = {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    query: `mutation ScrapeHackerNews {
  goto(
    url: "https://news.ycombinator.com"
    waitUntil: firstContentfulPaint
  ) {
    status
  }

  posts: mapSelector(selector: ".submission .titleline > a", wait: true) {
    link: attribute(name: "href") {
      value
    }
  }
}`,
    variables: "",
    operationName: "ScrapeHackerNews",
  })
};

const url = `${endpoint}?token=${token}`;
const response = await fetch(url, options);
const data = await response.json();

console.log(data);
```
  
  
**Python:**

```python
import requests

endpoint = "https://production-sfo.browserless.io/chromium/bql"
query_string = {
    "token": "YOUR_API_TOKEN_HERE",
}
headers = {
    "Content-Type": "application/json",
}
payload = {
    "query": """mutation ScrapeHackerNews {
  goto(
    url: "https://news.ycombinator.com"
    waitUntil: firstContentfulPaint
  ) {
    status
  }

  posts: mapSelector(selector: ".submission .titleline > a", wait: true) {
    link: attribute(name: "href") {
      value
    }
  }
}""",
    "variables": {},
    "operationName": "ScrapeHackerNews",
}

response = requests.post(endpoint, params=query_string, headers=headers, json=payload)
print(response.json())
```
  
  
**Java:**

```java
String url = "https://production-sfo.browserless.io/chromium/bql";
String token = "YOUR_API_TOKEN_HERE";
String endpoint = String.format("%s?token=%s", url, token);

HttpResponse response = Unirest.post(endpoint)
    .header("Content-Type", "application/json")
    .body("{\"query\":\"mutation ScrapeHackerNews {\\n  goto(\\n    url: \\\"https://news.ycombinator.com\\\"\\n    waitUntil: firstContentfulPaint\\n  ) {\\n    status\\n  }\\n\\n  posts: mapSelector(selector: \\\".submission .titleline > a\\\", wait: true) {\\n    link: attribute(name: \\\"href\\\") {\\n      value\\n    }\\n  }\\n}\",\"variables\":\"\",\"operationName\":\"ScrapeHackerNews\"}")
    .asString();
```
  
  
**C#:**

```csharp
string url = "https://production-sfo.browserless.io/chromium/bql";
string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"{url}?token={token}";

var payload = new
{
    query = @"mutation ScrapeHackerNews {
  goto(
    url: ""https://news.ycombinator.com""
    waitUntil: firstContentfulPaint
  ) {
    status
  }

  posts: mapSelector(selector: "".submission .titleline > a"", wait: true) {
    link: attribute(name: ""href"") {
      value
    }
  }
}",
    variables = "",
    operationName = "ScrapeHackerNews"
};

using (HttpClient httpClient = new HttpClient())
{
    var jsonPayload = System.Text.Json.JsonSerializer.Serialize(payload);
    var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");

    var response = await httpClient.PostAsync(endpoint, content);
    string responseBody = await response.Content.ReadAsStringAsync();
    Console.WriteLine(responseBody);
}
```
  

**Response**

```json
{
  "data": {
    "posts": [
      { "link": { "value": "https://churchofturing.github.io/landscapeoflisp.html" } },
      { "link": { "value": "https://www.jjj.de/fxt/fxtbook.pdf" } },
      { "link": { "value": "https://ereader-swedish.fly.dev/" } }
    ]
  }
}
```

Nest `mapSelector` calls to traverse hierarchical DOM structures. The example below extracts post authors and scores from each submission:

```graphql
mutation ScrapeHackerNewsMetadata {
  goto(url: "https://news.ycombinator.com") {
    status
  }

  posts: mapSelector(selector: ".subtext .subline") {
    author: mapSelector(selector: ".hnuser") {
      authorName: innerText
    }

    score: mapSelector(selector: ".score") {
      score: innerText
    }
  }
}
```

`mapSelector` always returns an array, whether one or many elements match. It returns `null` when no elements are found.

## Scraping Network Responses

The `response` mutation records HTTP responses made by the browser, filtered by URL pattern, method, or resource type. BQL waits for the response automatically.

The example below captures the raw document response from a page load:

  
**BQL Query:**

```graphql
mutation DocumentResponses {
  goto(url: "https://example.com/", waitUntil: load) {
    status
  }

  response(type: document) {
    url
    body
    headers {
      name
      value
    }
  }
}
```
  
  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{"query":"mutation DocumentResponses {\n  goto(url: \"https://example.com/\", waitUntil: load) {\n    status\n  }\n  response(type: document) {\n    url\n    body\n    headers {\n      name\n      value\n    }\n  }\n}","variables":{},"operationName":"DocumentResponses"}'
```
  
  
**Javascript:**

```js
const endpoint = "https://production-sfo.browserless.io/chromium/bql";
const token = "YOUR_API_TOKEN_HERE";

const options = {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    query: `mutation DocumentResponses {
  goto(url: "https://example.com/", waitUntil: load) {
    status
  }

  response(type: document) {
    url
    body
    headers {
      name
      value
    }
  }
}`,
    variables: "",
    operationName: "DocumentResponses",
  })
};

const url = `${endpoint}?token=${token}`;
const response = await fetch(url, options);
const data = await response.json();

console.log(data);
```
  
  
**Python:**

```python
import requests

endpoint = "https://production-sfo.browserless.io/chromium/bql"
query_string = {
    "token": "YOUR_API_TOKEN_HERE",
}
headers = {
    "Content-Type": "application/json",
}
payload = {
    "query": """mutation DocumentResponses {
  goto(url: "https://example.com/", waitUntil: load) {
    status
  }

  response(type: document) {
    url
    body
    headers {
      name
      value
    }
  }
}""",
    "variables": {},
    "operationName": "DocumentResponses",
}

response = requests.post(endpoint, params=query_string, headers=headers, json=payload)
print(response.json())
```
  
  
**Java:**

```java
String url = "https://production-sfo.browserless.io/chromium/bql";
String token = "YOUR_API_TOKEN_HERE";
String endpoint = String.format("%s?token=%s", url, token);

HttpResponse response = Unirest.post(endpoint)
    .header("Content-Type", "application/json")
    .body("{\"query\":\"mutation DocumentResponses {\\n  goto(url: \\\"https://example.com/\\\", waitUntil: load) {\\n    status\\n  }\\n  response(type: document) {\\n    url\\n    body\\n    headers {\\n      name\\n      value\\n    }\\n  }\\n}\",\"variables\":\"\",\"operationName\":\"DocumentResponses\"}")
    .asString();
```
  
  
**C#:**

```csharp
string url = "https://production-sfo.browserless.io/chromium/bql";
string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"{url}?token={token}";

var payload = new
{
    query = @"mutation DocumentResponses {
  goto(url: ""https://example.com/"", waitUntil: load) {
    status
  }

  response(type: document) {
    url
    body
    headers {
      name
      value
    }
  }
}",
    variables = "",
    operationName = "DocumentResponses"
};

using (HttpClient httpClient = new HttpClient())
{
    var jsonPayload = System.Text.Json.JsonSerializer.Serialize(payload);
    var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");

    var response = await httpClient.PostAsync(endpoint, content);
    string responseBody = await response.Content.ReadAsStringAsync();
    Console.WriteLine(responseBody);
}
```
  

Filter by `method` and `operator` to narrow responses to a specific type. The example below captures only XHR GET responses:

  
**BQL Query:**

```graphql
mutation AJAXGetCalls {
  goto(url: "https://msn.com/", waitUntil: load) {
    status
  }

  response(type: xhr, method: GET, operator: and) {
    url
    type
    method
    body
    headers {
      name
      value
    }
  }
}
```
  
  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{"query":"mutation AJAXGetCalls {\n  goto(url: \"https://msn.com/\", waitUntil: load) {\n    status\n  }\n  response(type: xhr, method: GET, operator: and) {\n    url\n    type\n    method\n    body\n    headers {\n      name\n      value\n    }\n  }\n}","variables":{},"operationName":"AJAXGetCalls"}'
```
  
  
**Javascript:**

```js
const endpoint = "https://production-sfo.browserless.io/chromium/bql";
const token = "YOUR_API_TOKEN_HERE";

const options = {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    query: `mutation AJAXGetCalls {
  goto(url: "https://msn.com/", waitUntil: load) {
    status
  }

  response(type: xhr, method: GET, operator: and) {
    url
    type
    method
    body
    headers {
      name
      value
    }
  }
}`,
    variables: "",
    operationName: "AJAXGetCalls",
  })
};

const url = `${endpoint}?token=${token}`;
const response = await fetch(url, options);
const data = await response.json();

console.log(data);
```
  
  
**Python:**

```python
import requests

endpoint = "https://production-sfo.browserless.io/chromium/bql"
query_string = {
    "token": "YOUR_API_TOKEN_HERE",
}
headers = {
    "Content-Type": "application/json",
}
payload = {
    "query": """mutation AJAXGetCalls {
  goto(url: "https://msn.com/", waitUntil: load) {
    status
  }

  response(type: xhr, method: GET, operator: and) {
    url
    type
    method
    body
    headers {
      name
      value
    }
  }
}""",
    "variables": {},
    "operationName": "AJAXGetCalls",
}

response = requests.post(endpoint, params=query_string, headers=headers, json=payload)
print(response.json())
```
  
  
**Java:**

```java
String url = "https://production-sfo.browserless.io/chromium/bql";
String token = "YOUR_API_TOKEN_HERE";
String endpoint = String.format("%s?token=%s", url, token);

HttpResponse response = Unirest.post(endpoint)
    .header("Content-Type", "application/json")
    .body("{\"query\":\"mutation AJAXGetCalls {\\n  goto(url: \\\"https://msn.com/\\\", waitUntil: load) {\\n    status\\n  }\\n  response(type: xhr, method: GET, operator: and) {\\n    url\\n    type\\n    method\\n    body\\n    headers {\\n      name\\n      value\\n    }\\n  }\\n}\",\"variables\":\"\",\"operationName\":\"AJAXGetCalls\"}")
    .asString();
```
  
  
**C#:**

```csharp
string url = "https://production-sfo.browserless.io/chromium/bql";
string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"{url}?token={token}";

var payload = new
{
    query = @"mutation AJAXGetCalls {
  goto(url: ""https://msn.com/"", waitUntil: load) {
    status
  }

  response(type: xhr, method: GET, operator: and) {
    url
    type
    method
    body
    headers {
      name
      value
    }
  }
}",
    variables = "",
    operationName = "AJAXGetCalls"
};

using (HttpClient httpClient = new HttpClient())
{
    var jsonPayload = System.Text.Json.JsonSerializer.Serialize(payload);
    var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");

    var response = await httpClient.PostAsync(endpoint, content);
    string responseBody = await response.Content.ReadAsStringAsync();
    Console.WriteLine(responseBody);
}
```
  

## Using `querySelectorAll`

The `querySelectorAll` mutation returns an array of matched elements with their HTML properties. Use it when you need fast element extraction without the nested mapping of `mapSelector`.

  
**BQL Query:**

```graphql
mutation FindLinks {
  goto(url: "https://browserless.io") {
    status
  }

  links: querySelectorAll(selector: "a") {
    innerText
    outerHTML
  }
}
```
  
  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{"query":"mutation FindLinks {\n  goto(url: \"https://browserless.io\") {\n    status\n  }\n\n  links: querySelectorAll(selector: \"a\") {\n    innerText\n    outerHTML\n  }\n}","variables":{},"operationName":"FindLinks"}'
```
  
  
**Javascript:**

```js
const endpoint = "https://production-sfo.browserless.io/chromium/bql";
const token = "YOUR_API_TOKEN_HERE";

const options = {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    query: `mutation FindLinks {
  goto(url: "https://browserless.io") {
    status
  }

  links: querySelectorAll(selector: "a") {
    innerText
    outerHTML
  }
}`,
    variables: "",
    operationName: "FindLinks",
  })
};

const url = `${endpoint}?token=${token}`;
const response = await fetch(url, options);
const data = await response.json();

console.log(data);
```
  
  
**Python:**

```python
import requests

endpoint = "https://production-sfo.browserless.io/chromium/bql"
query_string = {
    "token": "YOUR_API_TOKEN_HERE",
}
headers = {
    "Content-Type": "application/json",
}
payload = {
    "query": """mutation FindLinks {
  goto(url: "https://browserless.io") {
    status
  }

  links: querySelectorAll(selector: "a") {
    innerText
    outerHTML
  }
}""",
    "variables": {},
    "operationName": "FindLinks",
}

response = requests.post(endpoint, params=query_string, headers=headers, json=payload)
print(response.json())
```
  
  
**Java:**

```java
String url = "https://production-sfo.browserless.io/chromium/bql";
String token = "YOUR_API_TOKEN_HERE";
String endpoint = String.format("%s?token=%s", url, token);

HttpResponse response = Unirest.post(endpoint)
    .header("Content-Type", "application/json")
    .body("{\"query\":\"mutation FindLinks {\\n  goto(url: \\\"https://browserless.io\\\") {\\n    status\\n  }\\n\\n  links: querySelectorAll(selector: \\\"a\\\") {\\n    innerText\\n    outerHTML\\n  }\\n}\",\"variables\":\"\",\"operationName\":\"FindLinks\"}")
    .asString();
```
  
  
**C#:**

```csharp
string url = "https://production-sfo.browserless.io/chromium/bql";
string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"{url}?token={token}";

var payload = new
{
    query = @"mutation FindLinks {
  goto(url: ""https://browserless.io"") {
    status
  }

  links: querySelectorAll(selector: ""a"") {
    innerText
    outerHTML
  }
}",
    variables = "",
    operationName = "FindLinks"
};

using (HttpClient httpClient = new HttpClient())
{
    var jsonPayload = System.Text.Json.JsonSerializer.Serialize(payload);
    var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");

    var response = await httpClient.PostAsync(endpoint, content);
    string responseBody = await response.Content.ReadAsStringAsync();
    Console.WriteLine(responseBody);
}
```
  

Each result includes `innerHTML`, `innerText`, `outerHTML`, `id`, `className`, and `childElementCount`. Use `innerText` to get visible text, or `outerHTML` to get the full element markup.

## Processing Data with JavaScript

The `evaluate` mutation runs JavaScript in the browser context and returns the result. Use it when you need calculations, filtering, or transformations that go beyond what `mapSelector` or `querySelectorAll` support.

```graphql
mutation CountHeadings {
  goto(url: "https://browserless.io") {
    status
  }

  headingCount: evaluate(
    content: "document.querySelectorAll('h2').length"
  ) {
    value
  }
}
```

The `content` field accepts either a single expression (returned automatically) or a multi-line statement body. For a statement body, write your code directly and use `return` to pass values back. No IIFE wrapper is required. For examples using `await`, external scripts, and complex transformations, see [Multi-line JavaScript](/browserql/advanced-config/multi-line-javascript).

## FAQ & Troubleshooting

<details>
<summary>What methods does BrowserQL offer for data extraction?</summary>

BrowserQL provides HTML extraction, CSS selector-based queries via mapSelector and querySelectorAll, response interception to capture API data, and JavaScript evaluation for custom extraction logic.

</details>

<details>
<summary>Can BrowserQL scrape JavaScript-rendered pages?</summary>

Yes. BQL runs a full browser that executes JavaScript before extraction. This means single-page apps, dynamically loaded content, and client-side rendered pages are all fully supported.

</details>

<details>
<summary>How do I handle pagination when scraping?</summary>

Use BQL's click and waitForSelector mutations to navigate through pages, or intercept the underlying API responses that supply the paginated data. You can also use conditional logic to loop until no more pages remain.

</details>

<details>
<summary>Does BrowserQL support scraping behind login walls?</summary>

Yes. Use the type and click mutations to fill login forms, or inject cookies from a previous session with the setCookies mutation. BQL persists session state across queries when using session management.

</details>

## Next steps

  <a href="/browserql/advanced-config/multi-line-javascript" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Multi-line JavaScript Evaluation</h3>
      Run custom JavaScript in the browser context for complex data processing and transformation.
    
  </a>

  <a href="/browserql/writing-bql/waiting-for-things" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Waiting for Things</h3>
      Control when BrowserQL proceeds to avoid scraping incomplete or empty pages.
    
  </a>

  <a href="/browserql/best-practices" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Best BQL Practices</h3>
      Patterns for reliable, maintainable browser automation with BrowserQL.
    
  </a>

---

# Screenshots

URL: https://docs.browserless.io/browserql/use-cases/screenshots

The [`screenshot` mutation](/bql-schema/operations/mutations/screenshot) navigates to a URL and captures a screenshot. Browserless returns the image as a base64-encoded `png`, `jpg`, or `webp`.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Familiarity with [BQL language basics](/browserql/writing-bql/language-basics)

## Basic Usage

Navigate to a URL and capture the page. [Wait for elements to load](/browserql/writing-bql/waiting-for-things) before capturing to avoid blank or incomplete screenshots.

  
**BQL Query:**

```graphql
mutation Screenshot {
  goto(url: "https://example.com") {
    status
  }

  screenshot(omitBackground:true) {
    base64
  }
}
```
  
  
**BAP TypeScript:**

```typescript
import Browserless from "@browserless.io/bap-ts";

const TOKEN = "YOUR_API_TOKEN_HERE";

const browser = Browserless.connect({
  browserWSEndpoint: "wss://production-sfo.browserless.io/chromium/bql",
  token: TOKEN,
});

const page = await browser.newPage();
await page.goto("https://example.com");

// Returns the bytes as a Uint8Array. `path` also writes the file, Node.js only.
await page.screenshot({
  path: "screenshot.png",
  omitBackground: true,
});

await browser.close();
```
  
  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{"query":"mutation Screenshot {\n  goto(url: \"https://example.com\") {\n    status\n  }\n\n  screenshot(omitBackground:true) {\n    base64\n  }\n}","variables":{},"operationName":"Screenshot"}'
```
  
  
**Javascript:**

```js
const endpoint = "https://production-sfo.browserless.io/chromium/bql";
const token = "YOUR_API_TOKEN_HERE";

const options = {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({"query":"mutation Screenshot {\n  goto(url: \"https://example.com\") {\n    status\n  }\n\n  screenshot(omitBackground:true) {\n    base64\n  }\n}","variables":{},"operationName":"Screenshot"})
};

const url = `${endpoint}?token=${token}`;
const response = await fetch(url, options);
const data = await response.json();

console.log(data);
```
  
  
**Python:**

```python
import requests

endpoint = "https://production-sfo.browserless.io/chromium/bql"
query_string = {
    "token": "YOUR_API_TOKEN_HERE",
}
headers = {
    "Content-Type": "application/json",
}
payload = {
    "query": "mutation Screenshot {\n  goto(url: \"https://example.com\") {\n    status\n  }\n\n  screenshot(omitBackground:true) {\n    base64\n  }\n}",
    "variables": {},
    "operationName": "Screenshot",
}

response = requests.post(endpoint, params=query_string, headers=headers, json=payload)
print(response.json())
```
  
  
**Java:**

```java
String url = "https://production-sfo.browserless.io/chromium/bql";
String token = "YOUR_API_TOKEN_HERE";
String endpoint = url + "?token=" + token;

String payload = """
{"query":"mutation Screenshot {\n  goto(url: \"https://example.com\") {\n    status\n  }\n\n  screenshot(omitBackground:true) {\n    base64\n  }\n}","variables":{},"operationName":"Screenshot"}
""";

HttpResponse response = Unirest.post(endpoint)
    .header("Content-Type", "application/json")
    .body(payload)
    .asString();
```
  
  
**C#:**

```csharp
string url = "https://production-sfo.browserless.io/chromium/bql";
string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"{url}?token={token}";

var payload = new
{
    query = @"mutation Screenshot {
  goto(url: ""https://example.com"") {
    status
  }

  screenshot(omitBackground: true) {
    base64
  }
}",
    variables = "",
    operationName = "Screenshot"
};

using (HttpClient httpClient = new HttpClient())
{
    var jsonPayload = System.Text.Json.JsonSerializer.Serialize(payload);
    var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");

    var response = await httpClient.PostAsync(endpoint, content);
    string responseBody = await response.Content.ReadAsStringAsync();
    Console.WriteLine(responseBody);
}
```
  

## Setting HTML Content

The `content` mutation sets the page HTML directly, without navigating to a URL. Use it to screenshot dynamically generated markup.

  
**BQL Query:**

```graphql
mutation Screenshot {
  content(html: "<h1>Hello, World!</h1>") {
    status
  }

  screenshot(type: webp) {
    base64
  }
}
```
  
  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{"query":"mutation Screenshot {\n  content(html: \"<h1>Hello, World!</h1>\") {\n    status\n  }\n\n  screenshot(type: webp) {\n    base64\n  }\n}","variables":{},"operationName":"Screenshot"}'
```
  
  
**Javascript:**

```js
const endpoint = "https://production-sfo.browserless.io/chromium/bql";
const token = "YOUR_API_TOKEN_HERE";

const options = {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({"query":"mutation Screenshot {\n  content(html: \"<h1>Hello, World!</h1>\") {\n    status\n  }\n\n  screenshot(type: webp) {\n    base64\n  }\n}","variables":{},"operationName":"Screenshot"})
};

const url = `${endpoint}?token=${token}`;
const response = await fetch(url, options);
const data = await response.json();

console.log(data);
```
  
  
**Python:**

```python
import requests

endpoint = "https://production-sfo.browserless.io/chromium/bql"
query_string = {
    "token": "YOUR_API_TOKEN_HERE",
}
headers = {
    "Content-Type": "application/json",
}
payload = {
    "query": "mutation Screenshot {\n  content(html: \"<h1>Hello, World!</h1>\") {\n    status\n  }\n\n  screenshot(type: webp) {\n    base64\n  }\n}",
    "variables": {},
    "operationName": "Screenshot",
}

response = requests.post(endpoint, params=query_string, headers=headers, json=payload)
print(response.json())
```

  
  
**Java:**

```java
String url = "https://production-sfo.browserless.io/chromium/bql";
String token = "YOUR_API_TOKEN_HERE";
String endpoint = url + "?token=" + token;

String payload = """
{"query":"mutation Screenshot {\n  content(html: \"<h1>Hello, World!</h1>\") {\n    status\n  }\n\n  screenshot(type: webp) {\n    base64\n  }\n}","variables":{},"operationName":"Screenshot"}
""";

HttpResponse response = Unirest.post(endpoint)
    .header("Content-Type", "application/json")
    .body(payload)
    .asString();
```
  
  
**C#:**

```csharp
string url = "https://production-sfo.browserless.io/chromium/bql";
string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"{url}?token={token}";

var payload = new
{
    query = @"mutation Screenshot {
  content(html: ""<h1>Hello, World!</h1>"") {
    status
  }

  screenshot(type: webp) {
    base64
  }
}",
    variables = "",
    operationName = "Screenshot"
};

using (var client = new HttpClient())
{
    var jsonPayload = System.Text.Json.JsonSerializer.Serialize(payload);
    var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");

    var response = await client.PostAsync(endpoint, content);
    string responseBody = await response.Content.ReadAsStringAsync();

    Console.WriteLine(responseBody);
}
```
  

## Element-specific Screenshots

The `selector` parameter captures a specific element instead of the full page. Use it to focus on a component, widget, or section.

  
**BQL Query:**

```graphql
mutation ElementScreenshot {
  goto(url: "https://example.com") {
    status
  }

  screenshot(selector: "h1") {
    base64
  }
}
```
  
  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{"query":"mutation ElementScreenshot {\n  goto(url: \"https://example.com\") {\n    status\n  }\n\n  screenshot(selector: \"h1\") {\n    base64\n  }\n}","variables":{},"operationName":"ElementScreenshot"}'
```
  
  
**Javascript:**

```js
const endpoint = "https://production-sfo.browserless.io/chromium/bql";
const token = "YOUR_API_TOKEN_HERE";

const options = {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({"query":"mutation ElementScreenshot {\n  goto(url: \"https://example.com\") {\n    status\n  }\n\n  screenshot(selector: \"h1\") {\n    base64\n  }\n}","variables":{},"operationName":"ElementScreenshot"})
};

const url = `${endpoint}?token=${token}`;
const response = await fetch(url, options);
const data = await response.json();

console.log(data);
```
  
  
**Python:**

```python
import requests

endpoint = "https://production-sfo.browserless.io/chromium/bql"
query_string = {
    "token": "YOUR_API_TOKEN_HERE",
}
headers = {
    "Content-Type": "application/json",
}
payload = {
    "query": "mutation ElementScreenshot {\n  goto(url: \"https://example.com\") {\n    status\n  }\n\n  screenshot(selector: \"h1\") {\n    base64\n  }\n}",
    "variables": {},
    "operationName": "ElementScreenshot",
}

response = requests.post(endpoint, params=query_string, headers=headers, json=payload)
print(response.json())
```
  
  
**Java:**

```java
String url = "https://production-sfo.browserless.io/chromium/bql";
String token = "YOUR_API_TOKEN_HERE";
String endpoint = String.format("%s?token=%s", url, token);

HttpResponse response = Unirest.post(endpoint)
    .header("Content-Type", "application/json")
    .body("{\"query\":\"mutation ElementScreenshot {\\n  goto(url: \\\"https://example.com\\\") {\\n    status\\n  }\\n\\n  screenshot(selector: \\\"h1\\\") {\\n    base64\\n  }\\n}\",\"variables\":\"\",\"operationName\":\"ElementScreenshot\"}")
    .asString();
```
  
  
**C#:**

```csharp
string url = "https://production-sfo.browserless.io/chromium/bql";
string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"{url}?token={token}";

var payload = new
{
    query = @"mutation ElementScreenshot {
  goto(url: ""https://example.com"") {
    status
  }

  screenshot(selector: ""h1"") {
    base64
  }
}",
    variables = "",
    operationName = "ElementScreenshot"
};

using (HttpClient httpClient = new HttpClient())
{
    var jsonPayload = System.Text.Json.JsonSerializer.Serialize(payload);
    var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");

    var response = await httpClient.PostAsync(endpoint, content);
    string responseBody = await response.Content.ReadAsStringAsync();
    Console.WriteLine(responseBody);
}
```
  

## Transparent Background Screenshots

Set `omitBackground: true` to capture a transparent background instead of the default white. Requires `type: png`. JPEG does not support transparency.

  
**BQL Query:**

```graphql
mutation TransparentScreenshot {
  goto(url: "https://example.com") {
    status
  }

  screenshot(omitBackground: true, type: png) {
    base64
  }
}
```
  
  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{"query":"mutation TransparentScreenshot {\n  goto(url: \"https://example.com\") {\n    status\n  }\n\n  screenshot(omitBackground: true, type: png) {\n    base64\n  }\n}","variables":{},"operationName":"TransparentScreenshot"}'
```
  
  
**Javascript:**

```js
const endpoint = "https://production-sfo.browserless.io/chromium/bql";
const token = "YOUR_API_TOKEN_HERE";

const options = {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({"query":"mutation TransparentScreenshot {\n  goto(url: \"https://example.com\") {\n    status\n  }\n\n  screenshot(omitBackground: true, type: png) {\n    base64\n  }\n}","variables":{},"operationName":"TransparentScreenshot"})
};

const url = `${endpoint}?token=${token}`;
const response = await fetch(url, options);
const data = await response.json();

console.log(data);
```
  
  
**Python:**

```python
import requests

endpoint = "https://production-sfo.browserless.io/chromium/bql"
query_string = {
    "token": "YOUR_API_TOKEN_HERE",
}
headers = {
    "Content-Type": "application/json",
}
payload = {
    "query": "mutation TransparentScreenshot {\n  goto(url: \"https://example.com\") {\n    status\n  }\n\n  screenshot(omitBackground: true, type: png) {\n    base64\n  }\n}",
    "variables": {},
    "operationName": "TransparentScreenshot",
}

response = requests.post(endpoint, params=query_string, headers=headers, json=payload)
print(response.json())
```
  
  
**Java:**

```java
String url = "https://production-sfo.browserless.io/chromium/bql";
String token = "YOUR_API_TOKEN_HERE";
String endpoint = String.format("%s?token=%s", url, token);

HttpResponse response = Unirest.post(endpoint)
    .header("Content-Type", "application/json")
    .body("{\"query\":\"mutation TransparentScreenshot {\\n  goto(url: \\\"https://example.com\\\") {\\n    status\\n  }\\n\\n  screenshot(omitBackground: true, type: png) {\\n    base64\\n  }\\n}\",\"variables\":\"\",\"operationName\":\"TransparentScreenshot\"}")
    .asString();
```
  
  
**C#:**

```csharp
string url = "https://production-sfo.browserless.io/chromium/bql";
string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"{url}?token={token}";

var payload = new
{
    query = @"mutation TransparentScreenshot {
  goto(url: ""https://example.com"") {
    status
  }

  screenshot(omitBackground: true, type: png) {
    base64
  }
}",
    variables = "",
    operationName = "TransparentScreenshot"
};

using (HttpClient httpClient = new HttpClient())
{
    var jsonPayload = System.Text.Json.JsonSerializer.Serialize(payload);
    var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");

    var response = await httpClient.PostAsync(endpoint, content);
    string responseBody = await response.Content.ReadAsStringAsync();
    Console.WriteLine(responseBody);
}
```
  

## Clipped Region Screenshots

The `clip` parameter captures a rectangular region defined by pixel coordinates. Use it when the target area does not correspond to a single CSS selector.

  
**BQL Query:**

```graphql
mutation ClippedScreenshot {
  goto(url: "https://example.com") {
    status
  }

  screenshot(clip: { x: 100, y: 200, width: 800, height: 600 }) {
    base64
  }
}
```
  
  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{"query":"mutation ClippedScreenshot {\n  goto(url: \"https://example.com\") {\n    status\n  }\n\n  screenshot(clip: { x: 100, y: 200, width: 800, height: 600 }) {\n    base64\n  }\n}","variables":{},"operationName":"ClippedScreenshot"}'
```
  
  
**Javascript:**

```js
const endpoint = "https://production-sfo.browserless.io/chromium/bql";
const token = "YOUR_API_TOKEN_HERE";

const options = {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({"query":"mutation ClippedScreenshot {\n  goto(url: \"https://example.com\") {\n    status\n  }\n\n  screenshot(clip: { x: 100, y: 200, width: 800, height: 600 }) {\n    base64\n  }\n}","variables":{},"operationName":"ClippedScreenshot"})
};

const url = `${endpoint}?token=${token}`;
const response = await fetch(url, options);
const data = await response.json();

console.log(data);
```
  
  
**Python:**

```python
import requests

endpoint = "https://production-sfo.browserless.io/chromium/bql"
query_string = {
    "token": "YOUR_API_TOKEN_HERE",
}
headers = {
    "Content-Type": "application/json",
}
payload = {
    "query": "mutation ClippedScreenshot {\n  goto(url: \"https://example.com\") {\n    status\n  }\n\n  screenshot(clip: { x: 100, y: 200, width: 800, height: 600 }) {\n    base64\n  }\n}",
    "variables": {},
    "operationName": "ClippedScreenshot",
}

response = requests.post(endpoint, params=query_string, headers=headers, json=payload)
print(response.json())
```
  
  
**Java:**

```java
String url = "https://production-sfo.browserless.io/chromium/bql";
String token = "YOUR_API_TOKEN_HERE";
String endpoint = String.format("%s?token=%s", url, token);

HttpResponse response = Unirest.post(endpoint)
    .header("Content-Type", "application/json")
    .body("{\"query\":\"mutation ClippedScreenshot {\\n  goto(url: \\\"https://example.com\\\") {\\n    status\\n  }\\n\\n  screenshot(clip: { x: 100, y: 200, width: 800, height: 600 }) {\\n    base64\\n  }\\n}\",\"variables\":\"\",\"operationName\":\"ClippedScreenshot\"}")
    .asString();
```
  
  
**C#:**

```csharp
string url = "https://production-sfo.browserless.io/chromium/bql";
string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"{url}?token={token}";

var payload = new
{
    query = @"mutation ClippedScreenshot {
  goto(url: ""https://example.com"") {
    status
  }

  screenshot(clip: { x: 100, y: 200, width: 800, height: 600 }) {
    base64
  }
}",
    variables = "",
    operationName = "ClippedScreenshot"
};

using (HttpClient httpClient = new HttpClient())
{
    var jsonPayload = System.Text.Json.JsonSerializer.Serialize(payload);
    var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");

    var response = await httpClient.PostAsync(endpoint, content);
    string responseBody = await response.Content.ReadAsStringAsync();
    Console.WriteLine(responseBody);
}
```
  

## FAQ & Troubleshooting

<details>
<summary>What screenshot formats does BrowserQL support?</summary>

BrowserQL captures screenshots as PNG or JPEG. You can take full-page screenshots, capture specific elements by CSS selector, clip custom regions, and render pages with transparent backgrounds.

</details>

<details>
<summary>Can I take a screenshot of a specific element?</summary>

Yes. Use the screenshot mutation with a selector parameter to capture just a specific DOM element. BQL waits for the element to be visible before capturing.

</details>

<details>
<summary>How do I capture a full-page screenshot?</summary>

Set fullPage to true in the screenshot mutation. BQL scrolls the entire page and captures all content, not just the visible viewport.

</details>

<details>
<summary>Can I take screenshots of pages with bot protection?</summary>

Yes. Use the /stealth/bql endpoint with proxies enabled to bypass bot detection before taking the screenshot. BQL handles fingerprint randomization and CAPTCHA solving automatically.

</details>

## Next steps

  <a href="/browserql/writing-bql/waiting-for-things" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Waiting Properly</h3>
      Learn about BrowserQL's built-in waiters for more reliable automations.
    
  </a>

  <a href="/browserql/best-practices#reject-unnecessary-resources" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Reject Unnecessary Resources</h3>
      Block ads, images, and other non-essential content to improve performance.
    
  </a>

  <a href="/browserql/best-practices" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Best BQL Practices</h3>
      Master essential patterns for reliable and maintainable browser automation.
    
  </a>

---

# Form Submission

URL: https://docs.browserless.io/browserql/use-cases/submitting-forms

The [`type`](/bql-schema/operations/mutations/type), [`click`](/bql-schema/operations/mutations/click), [`select`](/bql-schema/operations/mutations/select), [`checkbox`](/bql-schema/operations/mutations/checkbox), and [`solve`](/bql-schema/operations/mutations/solve) mutations let you fill, submit, and bypass CAPTCHAs in forms. BrowserQL handles element waiting, scrolling, and human-like input timing automatically.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Familiarity with [BQL language basics](/browserql/writing-bql/language-basics)

## Basic Usage

The example below navigates to a form, fills two fields, selects a dropdown option, solves a CAPTCHA, and then clicks submit. For explicit control over timing, see [Waiting for Things](/browserql/writing-bql/waiting-for-things).

  
**BQL Query:**

```graphql
mutation FormExample {
  goto(url: "https://www.browserless.io/practice-form") {
    status
  }

  typeEmail: type(text: "john@email.com", selector: "#Email") {
    time
  }

  typeMessage: type(
    selector: "#Message"
    text: "Watching BrowserQL do cool things!"
  ) {
    time
  }

  subject: select(selector: "select#Subject", value: "Support") {
    selector
  }

  solve {
    time
    solved
  }

  submitForm: click(selector: "button[type='submit']") {
    time
  }
}
```
  
  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{"query":"mutation FormExample {\n  goto(url: \"https://www.browserless.io/practice-form\") {\n    status\n  }\n\n  typeEmail: type(text: \"john@email.com\", selector: \"#Email\") {\n    time\n  }\n\n  typeMessage: type(\n    selector: \"#Message\"\n    text: \"Watching BrowserQL do cool things!\"\n  ) {\n    time\n  }\n\n  subject: select(selector: \"select#Subject\", value: \"Support\") {\n    selector\n  }\n\n  solve {\n    time\n    solved\n  }\n}","variables":{},"operationName":"FormExample"}'
```
  
  
**Javascript:**

```js
const endpoint = "https://production-sfo.browserless.io/chromium/bql";
const token = "YOUR_API_TOKEN_HERE";

const options = {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    query: `mutation FormExample {
  goto(url: "https://www.browserless.io/practice-form") {
    status
  }

  typeEmail: type(text: "john@email.com", selector: "#Email") {
    time
  }

  typeMessage: type(
    selector: "#Message"
    text: "Watching BrowserQL do cool things!"
  ) {
    time
  }

  subject: select(selector: "select#Subject", value: "Support") {
    selector
  }

  solve {
    time
    solved
  }

  submitForm: click(selector: "button[type='submit']") {
    time
  }
}`,
    variables: "",
    operationName: "FormExample",
  })
};

const url = `${endpoint}?token=${token}`;
const response = await fetch(url, options);
const data = await response.json();

console.log(data);
```
  
  
**Python:**

```python
import requests

endpoint = "https://production-sfo.browserless.io/chromium/bql"
query_string = {
    "token": "YOUR_API_TOKEN_HERE",
}
headers = {
    "Content-Type": "application/json",
}
payload = {
    "query": """mutation FormExample {
  goto(url: "https://www.browserless.io/practice-form") {
    status
  }

  typeEmail: type(text: "john@email.com", selector: "#Email") {
    time
  }

  typeMessage: type(
    selector: "#Message"
    text: "Watching BrowserQL do cool things!"
  ) {
    time
  }

  subject: select(selector: "select#Subject", value: "Support") {
    selector
  }

  solve {
    time
    solved
  }

  submitForm: click(selector: "button[type='submit']") {
    time
  }
}""",
    "variables": {},
    "operationName": "FormExample",
}

response = requests.post(endpoint, params=query_string, headers=headers, json=payload)
print(response.json())
```
  
  
**Java:**

```java
String url = "https://production-sfo.browserless.io/chromium/bql";
String token = "YOUR_API_TOKEN_HERE";
String endpoint = String.format("%s?token=%s", url, token);

HttpResponse response = Unirest.post(endpoint)
    .header("Content-Type", "application/json")
    .body("{\"query\":\"mutation FormExample {\\n  goto(url: \\\"https://www.browserless.io/practice-form\\\") {\\n    status\\n  }\\n\\n  typeEmail: type(text: \\\"john@email.com\\\", selector: \\\"#Email\\\") {\\n    time\\n  }\\n\\n  typeMessage: type(\\n    selector: \\\"#Message\\\"\\n    text: \\\"Watching BrowserQL do cool things!\\\"\\n  ) {\\n    time\\n  }\\n\\n  subject: select(selector: \\\"select#Subject\\\", value: \\\"Support\\\") {\\n    selector\\n  }\\n\\n  solve {\\n    time\\n    solved\\n  }\\n\\n  submitForm: click(selector: \\\"button[type='submit']\\\") {\\n    time\\n  }\\n}\\",\"variables\":\"\",\"operationName\":\"FormExample\"}")
    .asString();
```
  
  
**C#:**

```csharp
string url = "https://production-sfo.browserless.io/chromium/bql";
string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"{url}?token={token}";

var payload = new
{
    query = @"mutation FormExample {
  goto(url: ""https://www.browserless.io/practice-form"") {
    status
  }

  typeEmail: type(text: ""john@email.com"", selector: ""#Email"") {
    time
  }

  typeMessage: type(
    selector: ""#Message""
    text: ""Watching BrowserQL do cool things!""
  ) {
    time
  }

  subject: select(selector: ""select#Subject"", value: ""Support"") {
    selector
  }

  solve {
    time
    solved
  }

  submitForm: click(selector: ""button[type='submit']"") {
    time
  }
}",
    variables = "",
    operationName = "FormExample"
};

using (HttpClient httpClient = new HttpClient())
{
    var jsonPayload = System.Text.Json.JsonSerializer.Serialize(payload);
    var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");

    var response = await httpClient.PostAsync(endpoint, content);
    string responseBody = await response.Content.ReadAsStringAsync();
    Console.WriteLine(responseBody);
}
```
  

## Deep Selectors for Iframes and Shadow DOM

Deep selectors target elements inside iframes or shadow DOMs. Add `< ` at the start of any selector to enable deep traversal.

```graphql
selector: "< [optional URL pattern] css-selector"
```

- **`<`** signals a deep selector.
- **URL pattern** (optional) matches the iframe source URL with glob syntax.
- **CSS selector** identifies the target element inside the matched frame.

  
**Iframe by URL glob:**

    Target a button inside any iframe from cnn.com:

```graphql
selector: "< *cnn.com* button#submit"
```
  
  
**Iframe by path:**

    Click an anchor inside a Google verification iframe:

```graphql
selector: "< *google.com/api/verify* a[class*='now']"
```
  
  
**Any iframe:**

    Find an element with a `price` attribute across all iframes and the top-level page:

```graphql
selector: "< [price]"
```
  
  
**Exact iframe URL:**

    Find a span inside an iframe at an exact URL:

```graphql
selector: "< https://amazon.com/ span.submit.order"
```
  

> **Restrictions**
> - Parent-child combinators (e.g., `div > span`) are not supported in deep selectors.
> - Each deep selector must target a single node.

## Humanized Typing and Clicking

BrowserQL types with randomized delays between keystrokes to mimic human behavior. Two ways to control this:

**Query parameter:** Add `humanlike=true` to the connection URL to enable human-like behavior globally for the session.

**Per-mutation delay:** Set a `delay` range on the `type` mutation to control keystroke timing explicitly.

```graphql
mutation HumanizedForm {
  goto(url: "https://www.browserless.io/practice-form") {
    status
  }

  # Randomize keystroke delay between 80ms and 200ms
  typeMessage: type(
    selector: "#Message"
    text: "Hello, this looks human!"
    delay: [80, 200]
  ) {
    time
  }
}
```

For session-level configuration, see [Session Settings](/browserql/using-the-ide/session-settings#configuring-session-settings).

## Keyboard Events

BrowserQL does not have a dedicated mutation for sending keyboard events like Enter, Tab, or arrow keys. The recommended approach for form submission is to use `click` on the submit button (as shown in the examples above).

If you need to simulate keyboard events, you can use the [`evaluate`](/browserql/advanced-config/multi-line-javascript) mutation to dispatch `KeyboardEvent` objects:

> **Warning**
> Events created via `evaluate` have `isTrusted: false`, and some sites may ignore untrusted events for security reasons.

```graphql
mutation KeyboardEnter {
  goto(url: "https://example.com/form") {
    status
  }

  type(text: "search query", selector: "input[name='q']") {
    time
  }

  pressEnter: evaluate(content: """
    const enterEvent = new KeyboardEvent('keydown', {
      key: 'Enter',
      code: 'Enter',
      keyCode: 13,
      which: 13,
      bubbles: true,
      cancelable: true
    });
    document.activeElement.dispatchEvent(enterEvent);
  """) {
    value
  }
}
```

## FAQ & Troubleshooting

<details>
<summary>How do I upload a file to a form?</summary>

BQL has no file-transfer mutation, so there's no way to get a file from your machine into the page. `type` only enters text, and `evaluate` runs inside the page's JavaScript context, which can't read your local filesystem, so uploads attempted this way silently never start. Use BQL for the parts it's good at (unblocking, logging in), then hand the session to Puppeteer or Playwright with the [`reconnect` mutation](/browserql/session-management/puppeteer-playwright) and call `uploadFile` (Puppeteer) or `setInputFiles` (Playwright) there.

</details>

<details>
<summary>Why am I getting a <code>403 Forbidden</code> response?</summary>

Your API token is missing or malformed. Pass it as a `?token=` query parameter. Check your [account dashboard](https://browserless.io/account/) to verify the token is active.

</details>

<details>
<summary>My query times out before completing</summary>

Increase the `timeout` parameter on your mutation or use a more specific `waitUntil` condition. Long-running pages may need `networkIdle` instead of `load`.

</details>

## Next steps

  <a href="/browserql/bot-detection/solving-captchas" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Solving CAPTCHAs</h3>
      Use the solve mutation to automatically detect and bypass CAPTCHAs during form flows.
    
  </a>

  <a href="/browserql/writing-bql/waiting-for-things" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Waiting for Things</h3>
      Control when BrowserQL proceeds: wait for selectors, navigation events, or network responses.
    
  </a>

  <a href="/browserql/best-practices" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Best BQL Practices</h3>
      Patterns for reliable, maintainable browser automation with BrowserQL.
    
  </a>

---

# BQL IDE Features

URL: https://docs.browserless.io/browserql/using-the-ide/ide-features

# BrowserQL IDE

The BrowserQL IDE is a browser-based editor for writing, testing, and running BQL queries against the Browserless API. It includes built-in schema documentation, query history, multi-language code export, and keyboard shortcuts.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Global Settings

In the Global Settings tab, you can define the following settings:

- **BrowserQL Endpoint**: Choose between one of the available BQL Endpoints.
- **API Token**: Your API token goes here. This value is required to successfully execute your queries.
- **Persist Headers**: Choose whether to save the custom HTTP request headers from the Headers panel to your browser's local storage between page reloads. Your browser always saves the API token and endpoint separately, regardless of this setting. Only enable this on a device you trust.
- **Show Results**: Choose between automatically showing the results after executing a query or not.
- **Theme**: Adjust how the interface looks, between light or dark mode.
- **Wait before close**: Set how long the browser session stays open after a query finishes. Options are `10s`, `30s`, `60s`, and `5m`. Use this when you need to inspect the browser state or observe a page mid-automation after execution completes.
- **Clear storage**: Remove all locally stored data and start fresh.

## Built-in Documentation

The BrowserQL Editor includes complete documentation of all available mutations, responses, and schemas. You can access it in the top-left corner of the IDE window. Navigate through mutations and schemas by clicking in each one, and use the search input at the top to find the desired information.

> **Example code**
> Each mutation in the Built-in Documentation presents a working example code to show you how the mutation works. Copy it, and try the query out to see what each BQL mutation can do.

## Export Query as Code

The **Export Query as Code** feature turns any of your queries into code in your preferred language. This ensures your query has the exact same settings and configurations as what you run in the BQL IDE, avoiding mismatches between environments. Open this option in the sidebar next to the code tab, or use the `Ctrl+Shift+E` / `Cmd+Shift+E` shortcut.

The list below shows the available languages you can export your queries:

## History

Right below the documentation, there is a history of every query you executed. Open it by clicking it, and you can retrieve any query you created. You can also save as favorite, and rename a query in the history tab.

Your browser stores query history in localStorage, so it persists across sessions. Closing and reopening the browser does not erase it. To wipe all history, use **Clear storage** in Global Settings. From the history panel, star a query to save it as a favorite, or double-click a name to rename it.

## Keyboard Shortcuts

The BrowserQL IDE supports keyboard shortcuts for common actions. Open the Keyboard Shortcuts panel in the IDE to see the full list, or refer to the table below.

| Shortcut | Action |
|----------|--------|
| Ctrl + Enter / Cmd + Enter | Execute query |
| Ctrl + F / Cmd + F | Search in editor |
| Ctrl + Shift + E / Cmd + Shift + E | Export query |
| Ctrl + Shift + M | Merge fragment definitions into operation |
| Ctrl + Shift + R | Re-fetch schema using introspection |
| Alt + Shift + P | Prettify editors |
| Alt + T | New Tab |
| Alt + W | Close Tab |

## FAQ & Troubleshooting

<details>
<summary>The IDE isn't loading or shows a blank page</summary>

Clear your browser cache and reload. The IDE requires a modern browser with JavaScript enabled. Disable browser extensions that block scripts if the editor fails to render.

</details>

<details>
<summary>My query runs in the IDE but fails via API</summary>

The IDE may have session settings (proxy, stealth, viewport) applied that aren't included in your API call. Check your IDE session settings and include them as `launch` parameters in your HTTP request.

</details>

## Next steps

  <a href="/browserql/using-the-ide/session-settings" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Session settings</h3>
      configure browser, proxy, and viewport per session
    
  </a>
  <a href="/browserql/using-the-ide/using-api-calls" className="next-step-card-link">
    
      <h3 className="next-step-card-title">API calls</h3>
      export IDE queries to production code
    
  </a>
  <a href="/browserql/writing-bql/language-basics" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Writing BQL</h3>
      learn BQL mutation structure and syntax
    
  </a>

---

# IDE Session Settings

URL: https://docs.browserless.io/browserql/using-the-ide/session-settings

Each tab in the BrowserQL Editor has its own session settings. These settings control browser choice, proxy routing, viewport dimensions, and behavior options for that tab only.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Access to the [BQL IDE](https://browserless.io/account/bql)

## Configuring session settings

Open the settings panel from any editor tab to configure the following options. To carry these settings into production, use [Export Query as Code](/browserql/using-the-ide/ide-features#export-query-as-code), which generates code that reflects the exact settings configured here.

### Browser

Choose the browser binary the session runs on.

- **Chromium**: The open-source Chromium build. Use this as a baseline when site compatibility is not a concern.
- **Chrome**: Google's Chrome binary. Supports additional codecs not included in Chromium. Use this when a site reports your browser is unsupported, for example on streaming platforms like Twitch.
- **Stealth**: An in-house Chromium build with comprehensive fingerprinting and realistic browser API values. Use this when targeting sites with bot detection, CAPTCHAs, or Cloudflare protection.

### Human-like Behavior

When enabled, the session produces smooth, randomized mouse movements and typing patterns that resemble real user input. Disable this when raw execution speed matters more than evasion.

### Adblock

When enabled, the session blocks ads at the network level. This reduces page noise, speeds up loads, and keeps scraping workflows focused on content.

### Block Consent Modals

When enabled, the session automatically dismisses cookie consent dialogs and similar popups before your query runs.

### Residential Proxying

When enabled, all browser traffic routes through a residential proxy network. This makes requests appear to originate from real consumer devices and helps access geo-restricted content or bypass IP-based blocking.

Residential proxying consumes significantly more units than a standard session. See the [Using a Proxy](/browserql/bot-detection/proxies) guide for details.

### Sticky Proxy

*(Requires Residential Proxying to be enabled.)*

When enabled, the session routes all requests through the same IP address for the duration of the connection. Use this when the target site ties state to a specific IP, for example when maintaining a logged-in session across multiple requests.

BrowserQL sessions use a sticky exit IP by default, so a single stable IP is used even without changing this setting. This matters for anti-bot systems like Akamai and DataDome, which bind their sensor data and cookies to the egress IP — a mid-session IP rotation can re-trigger the block. Turn this off (or pass `proxySticky=false` / `proxy(sticky: false)` in code) only when you specifically want to rotate IPs.

### Proxy Country

*(Requires Residential Proxying to be enabled.)*

Select the country the proxy IP should appear to originate from. Over 180 countries are available. Leave unset to let the proxy network choose automatically.

### Locale Match

*(Requires Residential Proxying to be enabled.)*

When enabled, the browser's `Accept-Language` header is set to match the selected proxy country. For example, selecting Brazil sets the language to Portuguese. Use this when the target site serves different content based on browser locale.

### Viewport Mode

Controls how the browser viewport is sized during the session.

- **Fixed**: Pins the viewport to the dimensions you select in Viewport Size. A consistent viewport ensures the page renders the same CSS layout in the IDE and in production. If the viewport differs between your IDE session and your programmatic run, the site may switch to a different layout and break selectors that worked during development.
- **Responsive**: Adapts to the IDE panel size. Useful for interactive exploration, but not recommended when building automation scripts.

### Viewport Size

*(Only available when Viewport Mode is set to Fixed.)*

Choose a preset viewport size for the session:

| Preset | Dimensions |
|---|---|
| Desktop | 1920 × 1080 |
| Laptop | 1366 × 768 (default) |
| Tablet | 1024 × 768 |

## FAQ & Troubleshooting

<details>
<summary>My proxy setting isn't being applied</summary>

Session settings are per-tab. Check that you configured the proxy in the correct tab. Built-in proxies are available on every plan, including free, but they consume units per MB of traffic, so check that your account has units remaining.

</details>

<details>
<summary>Changes to session settings don't take effect</summary>

Session settings apply to the next query execution. If you changed settings mid-session, run a new query to see the updated configuration.

</details>

## Next steps

  <a href="/browserql/using-the-ide/using-api-calls" className="next-step-card-link">
    
      <h3 className="next-step-card-title">API calls</h3>
      export IDE queries to production code
    
  </a>
  <a href="/browserql/writing-bql/language-basics" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Writing BQL</h3>
      BQL script structure, actions, and aliases
    
  </a>
  <a href="/browserql/bot-detection/overview" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Bot detection</h3>
      stealth, proxies, and CAPTCHA solving
    
  </a>

---

# Export BQL as API calls

URL: https://docs.browserless.io/browserql/using-the-ide/using-api-calls

BrowserQL allows you to design queries in the **IDE** and then export them as ready-to-use API calls in multiple programming languages.
This makes it easy to test your logic in the IDE and then integrate it directly into your applications.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- A working BQL query (test it in the [BQL IDE](https://browserless.io/account/bql) first)

> **Recommended Endpoint**
> For production use with bot detection bypass, use the **`/stealth/bql`** route instead of the standard `/bql` route. The stealth routes provide advanced anti-detection capabilities for better success rates with protected sites. [Explore more stealth routes](/browserql/bot-detection/stealth).

### Exporting Your Query

Once your query is working in the IDE, follow these steps to ensure reliable results outside of the BQL IDE:

1. Click **Export as Code**.
2. Select your preferred **language**.
3. Copy the generated snippet, which includes the **endpoint, token, headers, and payload**.
4. **For bot detection bypass**: Replace `/chromium/bql` or `/chrome/bql` with `/stealth/bql` in the endpoint URL.

### Example BQL Query

The following query navigates to [Hacker News](https://news.ycombinator.com), clicks the **Jobs** tab, and extracts the HTML of the page:

```graphql
mutation ExtractJobsHTML {
  goto(url: "https://news.ycombinator.com", waitUntil: firstMeaningfulPaint) {
    status
  }

  click(selector: "span.pagetop > a[href='jobs']") {
    x
    y
  }

  html {
    html
  }
}
```

### Exported Query as API Call

Here’s what the exported query looks like in different languages:

  
**JavaScript:**

```javascript
const url = "https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE";
const options = {
  method: "POST",
  headers: {
    'Content-Type': 'application/json'
  },
  body: '{"query":"mutation ExtractJobsHTML {\\\\n  goto(url: \\\\"https://news.ycombinator.com\\\\", waitUntil: firstMeaningfulPaint) { status }\\\\n  click(selector: \\\\"span.pagetop > a[href=\\'jobs\\']\\\\") { x y }\\\\n  html { html }\\\\n}","variables":null,"operationName":"ExtractJobsHTML"}'
};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```
  
  
**Python:**

```python
import http.client
import json

query = """
mutation ExtractJobsHTML {
  goto(url: "https://news.ycombinator.com", waitUntil: firstMeaningfulPaint) {
    status
  }

  click(selector: "span.pagetop > a[href='jobs']") {
    x
    y
  }

  html {
    html
  }
}
"""

conn = http.client.HTTPSConnection("production-sfo.browserless.io")
payload = json.dumps({"query": query})
headers = {"Content-Type": "application/json"}

conn.request("POST", "/stealth/bql?token=YOUR_API_TOKEN_HERE", payload, headers)
res = conn.getresponse()
data = res.read()
print(data.decode("utf-8"))
```
  
  
**Java:**

```java
HttpResponse response = Unirest.post("https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE")
  .header("Content-Type", "application/json")
  .body("{\\"query\\":\\"mutation ExtractJobsHTML {\\\\n  goto(url: \\\\\\"https://news.ycombinator.com\\\\\\", waitUntil: firstMeaningfulPaint) { status }\\\\n  click(selector: \\\\\\"span.pagetop > a[href='jobs']\\\\\\") { x y }\\\\n  html { html }\\\\n}\\",\\"variables\\":null,\\"operationName\\":\\"ExtractJobsHTML\\"}")
  .asString();

System.out.println(response.getBody());
```
  

### Parsing the API Response

After calling the API, the response contains raw HTML.
You can parse it using libraries like:

  
**JavaScript:**

```javascript
const { JSDOM } = require("jsdom");

async function ExtractJobsHTML() {
  const url = "https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE";
  const options = {
    method: "POST",
    headers: {
      'Content-Type': 'application/json'
    },
    body: '{"query":"mutation ExtractJobsHTML {\\\\n  goto(url: \\\\"https://news.ycombinator.com\\\\", waitUntil: firstMeaningfulPaint) { status }\\\\n  click(selector: \\\\"span.pagetop > a[href=\\'jobs\\']\\\\") { x y }\\\\n  html { html }\\\\n}","variables":null,"operationName":"ExtractJobsHTML"}'
  };

  try {
    const response = await fetch(url, options);
    const json = await response.json();
    const html = json.data.html.html;

    const dom = new JSDOM(html);
    const rows = dom.window.document.querySelectorAll("tr.athing");

    const results = [...rows].map((row) => {
      const anchor = row.querySelector("a");
      return anchor ? { title: anchor.textContent.trim(), link: anchor.href } : null;
    }).filter(Boolean);

    console.log(results);
  } catch (error) {
    console.error(error);
  }
}

ExtractJobsHTML();
```
  
  
**Python:**

```python
import http.client
from bs4 import BeautifulSoup
import json

query = """
mutation ExtractJobsHTML {
  goto(url: "https://news.ycombinator.com", waitUntil: firstMeaningfulPaint) {
    status
  }

  click(selector: "span.pagetop > a[href='jobs']") {
    x
    y
  }

  html {
    html
  }
}
"""

conn = http.client.HTTPSConnection("production-sfo.browserless.io")
payload = json.dumps({"query": query})
headers = {"Content-Type": "application/json"}

conn.request("POST", "/stealth/bql?token=YOUR_API_TOKEN_HERE", payload, headers)
res = conn.getresponse()
response = res.read()

response_json = json.loads(response.decode("utf-8"))
html = response_json["data"]["html"]["html"]

soup = BeautifulSoup(html, "html.parser")
results = [
    {"title": a.text.strip(), "link": a["href"]}
    for a in soup.select("tr.athing a")
]

print(results)
```
  
  
**Java:**

```java
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;

public class ExtractJobsHTML {
    public static void main(String[] args) throws Exception {
        HttpResponse response = Unirest.post("https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE")
            .header("Content-Type", "application/json")
            .body("{\\"query\\":\\"mutation ExtractJobsHTML {\\\\n  goto(url: \\\\\\"https://news.ycombinator.com\\\\\\", waitUntil: firstMeaningfulPaint) { status }\\\\n  click(selector: \\\\\\"span.pagetop > a[href='jobs']\\\\\\") { x y }\\\\n  html { html }\\\\n}\\",\\"variables\\":null,\\"operationName\\":\\"ExtractJobsHTML\\"}")
            .asString();

        JsonObject jsonResponse = JsonParser.parseString(response.getBody()).getAsJsonObject();
        String html = jsonResponse.getAsJsonObject("data").getAsJsonObject("html").get("html").getAsString();

        Document doc = Jsoup.parse(html);
        Elements jobs = doc.select("tr.athing a");

        for (Element job : jobs) {
            System.out.println("Title: " + job.text());
            System.out.println("Link: " + job.attr("href"));
        }
    }
}
```
  

## FAQ & Troubleshooting

<details>
<summary>My API calls return different results than the IDE</summary>

Use the Export as Code feature. It includes all settings from your IDE session (proxy, stealth, viewport), ensuring identical behavior between testing and production.

</details>

<details>
<summary>I'm getting a <code>401 Unauthorized</code> error</summary>

Your API token is invalid or expired. Copy a fresh token from the [dashboard](https://browserless.io/account/) and pass it as a `?token=` query parameter in the endpoint URL.

</details>

## Next steps

  <a href="/browserql/session-management/reconnect-to-browserless" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Session management</h3>
      reuse sessions across multiple queries
    
  </a>
  <a href="/browserql/writing-bql/language-basics" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Writing BQL</h3>
      BQL script structure, actions, and response fields
    
  </a>
  <a href="/browserql/bot-detection/overview" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Bot detection</h3>
      stealth, proxies, and CAPTCHA solving
    
  </a>

---

# Conditional Logic in BQL

URL: https://docs.browserless.io/browserql/writing-bql/conditional-logic

`if` and `ifnot` are BrowserQL mutations that execute nested operations based on a condition. `if` runs its body when the condition matches; `ifnot` runs its body when it does not.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Familiarity with [BQL language basics](/browserql/writing-bql/language-basics)

> **Conditions evaluate immediately, they don't wait**
> `if` and `ifnot` do not wait for a selector to appear or a response to arrive, so an element not already on the page evaluates as not present. Use [`waitForSelector`](/bql-schema/operations/mutations/wait-for-selector) or [`waitForResponse`](/bql-schema/operations/mutations/wait-for-response) before your conditional if timing matters.

## Arguments

| Argument | Type | Available on | Description |
|---|---|---|---|
| `selector` | `String` | `if`, `ifnot` | Triggers if the CSS selector matches an element on the page. |
| `visible` | `Boolean` | `if` only | When using `selector`, requires the element to be visible in the viewport. Defaults to `false`. |
| `response` | `ResponseInput` | `if`, `ifnot` | Triggers based on HTTP response status codes or URL pattern. |
| `request` | `RequestInput` | `if`, `ifnot` | Triggers based on a matching HTTP request. |

**`ResponseInput` fields:**

- `statuses: [Int]`: one or more HTTP status codes to match
- `url: String`: glob-style URL pattern to match the response URL

**`RequestInput` fields:**

- `method: Method`: HTTP verb to match (`GET`, `POST`, `PUT`, `DELETE`, `PATCH`, and others)
- `url: String`: glob-style URL pattern to match the request URL

If a condition is not met, the branch returns `null` and execution continues. A skipped condition is not an error.

For the full schema, see the [API reference](/bql-schema/operations/mutations/if).

## Element Presence Conditions

Check if a selector is present on the page and run different operations depending on the result. Because `if` and `ifnot` are evaluated independently and in sequence, you can chain them to cover multiple branches in a single mutation. A common pattern is checking for a CAPTCHA before proceeding: if one is found, solve it and continue; if not, extract content directly:

```graphql
mutation HandleCaptcha {
  goto(url: "https://www.google.com/recaptcha/api2/demo") {
    status
  }

  # No CAPTCHA - extract content directly
  ifnot(selector: ".g-recaptcha") {
    html {
      html
    }
  }

  # CAPTCHA detected - solve it before proceeding
  if(selector: ".g-recaptcha") {
    solve {
      time
      found
      solved
    }
    click(selector: "#recaptcha-demo-submit") {
      time
    }
    waitForNavigation {
      time
    }
    html {
      html
    }
  }
}
```

### Visible Elements

By default, `selector` matches any element in the DOM, including those hidden with CSS. Add `visible: true` to `if` to require the element to also be rendered and visible in the viewport. This is useful for elements like modals, error messages, or menus that exist in the DOM before they are shown:

```graphql
mutation VisibleElementCheck {
  goto(url: "https://practicetestautomation.com/practice-test-login/") {
    status
  }

  # Only fires if #error is in the DOM and visible — not just present but hidden
  if(selector: "#error", visible: true) {
    text(selector: "#error") {
      text
    }
  }
}
```

## Response Code Conditions

One of the most common uses is handling different HTTP response codes.

```graphql
mutation ResponseHandling {
  goto(url: "https://quotes.toscrape.com/doesntexist") {
    status
  }

  # 4xx means access was denied or the resource wasn't found — capture the text and redirect
  ifError4xx: if(response: {statuses: [400, 401, 403, 404]}) {
    text {
      text
    }
    goto(url: "https://quotes.toscrape.com") {
      status
    }
  }

  # Successful response — extract the main content
  ifnot(response: {statuses: [400, 401, 403, 404, 500, 502, 503]}) {
    html(selector: ".col-md-8") {
      html
    }
  }
}
```

## Request Conditions

`request` triggers based on HTTP requests the page has made. Use `url` to match a glob-style URL pattern, `method` to match an HTTP verb, or both together:

```graphql
mutation RequestCondition {
  goto(url: "https://jsonplaceholder.typicode.com/posts/1") {
    status
  }

  # Fires if a GET request to the posts endpoint was made
  if(request: {method: GET, url: "**/posts/**"}) {
    text(selector: "body") {
      text
    }
  }

  # No matching request detected — take a screenshot for debugging
  ifnot(request: {method: GET, url: "**/posts/**"}) {
    screenshot {
      base64
    }
  }
}
```

## FAQ & Troubleshooting

<details>
<summary>My <code>if</code> condition isn't matching even though the element is on the page</summary>

`if` evaluates immediately and does not wait for elements to appear. Add a `waitForSelector` mutation before your conditional to ensure the element is in the DOM when the condition runs.

</details>

<details>
<summary>Can I nest <code>if</code> inside <code>ifnot</code> or vice versa?</summary>

No. `if` and `ifnot` cannot be nested inside each other. Use sequential conditionals with compound CSS selectors for multi-branch logic instead.

</details>

## Next steps

  <a href="/browserql/writing-bql/waiting-for-things" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Wait conditions</h3>
      combine conditionals with precise waiting strategies
    
  </a>
  <a href="/browserql/using-the-ide/using-api-calls" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Exporting scripts</h3>
      convert conditional BQL queries to production code
    
  </a>
  <a href="/browserql/bot-detection/overview" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Bot detection</h3>
      use conditionals to handle detection scenarios and CAPTCHAs
    
  </a>

---

# Error Handling in BQL

URL: https://docs.browserless.io/browserql/writing-bql/error-handling

# Error handling in BQL

BrowserQL doesn't abort a mutation when one step fails. This guide explains what happens to the rest of your query when a step errors or times out, what the HTTP response looks like, and how to control failure behavior.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Familiarity with [writing BQL mutations](/browserql/writing-bql/language-basics)

## A failed step doesn't abort the rest

Once a valid mutation reaches execution, every top-level field runs, in the order you wrote it, even if an earlier field fails. When a step errors, including a `waitForSelector` that times out, BQL records the error and moves on to the next field. (Requests that never reach execution, such as an invalid token or a malformed query, are covered under [non-200 status](#when-you-get-a-non-200-status) below.)

```graphql
mutation Example {
  goto(url: "https://example.com") {
    status
  }
  missing: waitForSelector(selector: "#never-appears", timeout: 2000) {
    time
  }
  html {
    html
  }
}
```

Here `missing` times out, but `html` still runs and returns the page HTML. BQL runs each top-level field independently, so one field's failure never stops the fields after it.

What this means in practice:

- The failed field's entry in `data` is `null`. Fields that succeed still return their real values.
- Later steps run against whatever page state exists at that point. After a timed-out `goto`, a following `screenshot` may capture whatever the page reached rather than nothing, since a `goto` timeout only bounds the wait for the load condition and doesn't guarantee the navigation committed.
- A later step can still fail as a knock-on effect. If the element it needed never appeared it times out too, but it is attempted and reports its own error.

## The HTTP status is 200 with an errors array

Any mutation that runs to completion returns **HTTP 200**, even when steps inside it failed. Per-step failures and timeouts are reported in a GraphQL `errors` array in the response body, alongside whatever `data` succeeded:

```json
{
  "data": {
    "goto": { "status": 200 },
    "missing": null,
    "html": { "html": "<!doctype html>..." }
  },
  "errors": [
    {
      "message": "Timed out waiting for selector \"#never-appears\" (requestId: abc123)",
      "path": ["missing"]
    }
  ]
}
```

So a `200` doesn't mean everything worked. Check for an `errors` array in the body, not just the status code.

Validation errors, such as an unknown field or a bad argument, also come back as `200` with an `errors` array, with no execution at all.

### When you get a non-200 status

A handful of conditions fail before or around execution and return a non-200 status:

| Status | Cause |
|--------|-------|
| `400` / `401` / `404` / `429` | Rejected before execution: malformed request, invalid token, unknown route, or concurrency and rate limits. |
| `408` | The whole request exceeded the overall session timeout (your `timeout` query parameter or plan limit). This is the session ending, not a single step timing out. |
| `500` | Execution threw outside GraphQL, such as an unparseable query. The body is still `{ "errors": [ ... ] }`. |

## Reading the error object

Errors follow the standard GraphQL shape. Each has a `message`, and step failures also carry a `path` whose value is the top-level field name or its alias, so you can tell which step failed. Browserless appends a request ID to every message:

```text
Timed out waiting for selector "h1" (requestId: abc123)
```

Include that `requestId` when you contact support: it lets the team match your call to the server-side logs.

## Controlling failure behavior

BrowserQL has no per-step "best attempt" or "continue on error" flag, because top-level steps are already non-fatal: the mutation continues regardless. If you're coming from the [REST APIs](/rest-apis/intro), note that `bestAttempt` is a REST parameter and doesn't exist in BQL.

The two controls you do have:

- **Per-step timeouts.** Most mutations accept a `timeout` argument (milliseconds) to bound how long that step waits before it fails. Use it to stop a slow step from consuming the whole session.
- **[`if` / `ifnot` mutations](/browserql/writing-bql/conditional-logic).** Branch on page state so a step only runs when it makes sense, for example only `solve` a CAPTCHA when its selector is present.

See [Wait conditions](/browserql/writing-bql/waiting-for-things) for the full set of `waitFor*` mutations and their timeout arguments.

## FAQ & Troubleshooting

<details>
<summary>Does a timed-out <code>waitForSelector</code> stop the rest of my mutation?</summary>

No. It records an error and the remaining fields still run. The `waitForSelector` field is `null` in `data`, and later fields return their own results (or their own errors). To make a step conditional instead of just letting it fail, wrap it in an [`if` / `ifnot`](/browserql/writing-bql/conditional-logic) mutation.

</details>

<details>
<summary>My mutation returned <code>200</code> but the data is wrong or missing</summary>

A `200` only means the request completed, not that every step succeeded. Look for an `errors` array in the response body: each entry's `path` names the field that failed. A field listed there will be `null` in `data`.

</details>

<details>
<summary>A step that uses an exported variable fails with a strange error</summary>

Chained variables (`@export(as: "x")` then `${x}`) are substituted between steps as the mutation runs. If the exporting field failed, its value was never captured, so the `${x}` placeholder in a later field is substituted with the text `undefined` (or stays as the literal `${x}` if no earlier step has exported that name, such as a typo in the variable name). That usually surfaces as a confusing downstream error, like an invalid URL or selector containing the word "undefined", so check that the exporting step succeeded before relying on its variable. See [Variables and the @export directive](/browserql/writing-bql/exporting-variables).

</details>

## Next steps

  <a href="/browserql/writing-bql/conditional-logic" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Conditional logic</h3>
      branch on page state with if and ifnot
    
  </a>
  <a href="/browserql/writing-bql/waiting-for-things" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Wait conditions</h3>
      every waitFor mutation and its timeout argument
    
  </a>

---

# Variables and the @export Directive

URL: https://docs.browserless.io/browserql/writing-bql/exporting-variables

The `@export` directive saves a field's value into a named variable you can reference in later actions of the same query.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Familiarity with [BQL language basics](/browserql/writing-bql/language-basics)

## Export directive

Append `@export(as: "name")` to any field to capture its value into a named variable. The `as` argument sets the variable name. You can export as many variables as you need. Each one is available to all actions that follow it in the same mutation.

To use a variable in a later action, reference it with `${variableName}` syntax in any action parameter. BrowserQL substitutes the exported value at that point in the query. Use aliases to distinguish multiple calls to the same action at the same level.

```graphql
mutation ExportMultipleValues {
  goto(url: "https://news.ycombinator.com/") {
    status
  }
  getTopLink: evaluate(content: "document.querySelector('.athing.submission .title a').href") {
    value @export(as: "topStoryLink")
  }
  getTopTitle: evaluate(content: "document.querySelector('.athing.submission .title a').textContent") {
    value @export(as: "topStoryTitle")
  }
  type(selector: "input[type=text]", text: "${topStoryTitle}") {
    text
    time
  }
  gotoTopLink: goto(url: "${topStoryLink}") {
    url
    status
  }
}
```

Exports support only scalar values: strings and numbers. Complex objects and arrays cannot be exported. Exports are scoped to the mutation and are not available in other queries.

> **Undefined exports pass through as literal strings**
> If you reference a variable that hasn't been exported yet, BrowserQL passes the raw `${variableName}` text through unchanged. No error is raised. Always export a variable before referencing it.

## FAQ & Troubleshooting

<details>
<summary>My exported variable shows as a literal <code>{'${variableName}'}</code> string</summary>

The variable hasn't been exported before it's referenced. Exports are evaluated in order, so the `@export` directive must appear in a mutation that runs before the mutation that uses `${variableName}`.

</details>

<details>
<summary>Can I export arrays or objects?</summary>

No. Exports support only scalar values (strings and numbers). If you need to pass structured data between mutations, export individual fields and reference them separately.

</details>

## Next steps

  <a href="/browserql/writing-bql/language-basics" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Language basics</h3>
      BQL mutation structure, aliases, and available actions
    
  </a>
  <a href="/browserql/using-the-ide/using-api-calls" className="next-step-card-link">
    
      <h3 className="next-step-card-title">API calls</h3>
      run export-enabled queries from JavaScript, Python, or cURL
    
  </a>
  <a href="/browserql/session-management/reconnect-to-browserless" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Session reconnect</h3>
      reuse sessions across multiple queries
    
  </a>

---

# BQL Language Basics

URL: https://docs.browserless.io/browserql/writing-bql/language-basics

This guide covers BQL script structure, core actions, aliases, and response fields with working examples.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Familiarity with [GraphQL syntax](https://graphql.org/learn/)

## BQL script structure

A BQL script is one mutation block containing one or more actions. Actions run in order, top to bottom. Each action has a name, optional arguments that configure it, and response fields that define what data it returns.

```graphql
mutation ScriptName {
  goto(url: "https://example.com", waitUntil: domContentLoaded) {
    status
  }
  text(selector: "h1") {
    text
  }
  screenshot(type: png) {
    base64
  }
}
```

- `goto`, `text`, and `screenshot` are the action names.
- `url`, `waitUntil`, `selector`, and `type` are arguments. They configure how the action runs.
- `status`, `text`, and `base64` are response fields. They define what data gets returned.

Find all available actions, arguments, and response fields in the [BQL API Reference](/bql-schema/operations/mutations/back) or the [Built-in Documentation](/browserql/using-the-ide/ide-features#built-in-documentation) in the IDE.

### Aliases

Because a BQL script is a single mutation block, every action name must be unique. If you use the same action twice, BQL cannot distinguish them in the response.

This fails:

```graphql
mutation LoginForm {
  goto(url: "https://practicetestautomation.com/practice-test-login/") {
    status
  }
  type(selector: "#username", text: "student") {
    time
  }
  type(selector: "#password", text: "Password123") {  # ❌ duplicate action name
    time
  }
  click(selector: "#submit") {
    time
  }
}
```

Fix it by prefixing each duplicate action with an alias:

```graphql
mutation LoginForm {
  goto(url: "https://practicetestautomation.com/practice-test-login/") {
    status
  }
  typeUsername: type(selector: "#username", text: "student") {
    time
  }
  typePassword: type(selector: "#password", text: "Password123") {
    time
  }
  click(selector: "#submit") {
    time
  }
}
```

Aliases also become the keys in the JSON response, making the result easier to parse.

> **For GraphQL readers**
> In GraphQL terms, each action is a mutation field on the root mutation type. Aliases are required because GraphQL does not allow duplicate field names within the same selection set.

## Core Actions

| Action | What it does | Returns |
|--------|-------------|---------|
| [`goto`](/bql-schema/operations/mutations/goto) | Navigate to a URL | `status`, `time` |
| [`click`](/bql-schema/operations/mutations/click) | Click an element | `x`, `y`, `time` |
| [`type`](/bql-schema/operations/mutations/type) | Type text into an element | `time`, `selector` |
| [`select`](/bql-schema/operations/mutations/select) | Select an option from a dropdown | `value`, `selector` |
| [`text`](/bql-schema/operations/mutations/text) | Extract visible text from an element | `text` |
| [`html`](/bql-schema/operations/mutations/html) | Extract raw HTML from an element | `html` |
| [`screenshot`](/bql-schema/operations/mutations/screenshot) | Capture a screenshot | `base64` |
| [`solve`](/bql-schema/operations/mutations/solve) | Solve a CAPTCHA | `found`, `solved`, `time` |

For the full list of available actions and their arguments, see the [BQL API Reference](/bql-schema/operations/mutations/back).

## FAQ & Troubleshooting

<details>
<summary>What query language does BrowserQL use?</summary>

BrowserQL uses GraphQL. Queries are structured as mutations that navigate, click, type, wait, and extract data from web pages. Each mutation runs sequentially against a live browser session.

</details>

<details>
<summary>How do I navigate to a page in BQL?</summary>

Use the `goto` mutation with a `url` parameter and a `waitUntil` condition such as `load`, `domContentLoaded`, or `networkIdle`. This ensures the page is ready before running subsequent mutations.

</details>

<details>
<summary>My mutation returns <code>null</code> for response fields</summary>

You need to request the specific response fields you want. BQL only returns fields you explicitly include in your query. Add fields like `status`, `text`, or `html` inside the mutation's selection set.

</details>

## Next steps

  <a href="/browserql/writing-bql/conditional-logic" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Conditional logic</h3>
      create "do A or do B" workflows that adapt to page conditions
    
  </a>
  <a href="/browserql/writing-bql/waiting-for-things" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Wait conditions</h3>
      wait for elements, navigation, and network responses
    
  </a>
  <a href="/browserql/using-the-ide/using-api-calls" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Exporting scripts</h3>
      convert BQL queries to code in multiple languages
    
  </a>
  <a href="/bql-schema/operations/mutations/back" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BQL API reference</h3>
      full list of available mutations and arguments
    
  </a>

---

# Wait Conditions

URL: https://docs.browserless.io/browserql/writing-bql/waiting-for-things

Use these waiters to pause execution until a specific page condition is met:

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Familiarity with [BQL language basics](/browserql/writing-bql/language-basics)

- [`waitForSelector`](#waitforselector) → DOM element present
- [`waitForNavigation`](#waitfornavigation) → Page navigation complete
- [`waitForRequest`](#waitforrequest) → Outgoing network request
- [`waitForResponse`](#waitforresponse) → Network response received
- [`waitForTimeout`](#waitfortimeout) → Fixed delay

For all available return fields, see the [BQL schema reference](/bql-schema/schema).

## waitForNavigation

Waits for a navigation event to fire. Use this after triggering an action, such as a click, that causes a page load.

Parameters:

- `timeout`: Float, optional. Maximum milliseconds to wait. Default is 30000.
- `waitUntil`: [WaitUntilGoto enum](/bql-schema/types/enums/wait-until-goto), optional. When to consider the page fully loaded.

```graphql
mutation waitingForNavigation {
  goto(url: "https://example.com/") {
    status
  }

  waitForNavigation(waitUntil: load) {
    status
    time
  }
}
```

## waitForRequest

Waits for the browser to make a particular outgoing request.

Parameters:

- `method`: [Method](/bql-schema/types/enums/method), optional. The HTTP method of the request to wait for.
- `timeout`: Float, optional. Maximum milliseconds to wait before timing out. Default is 30000.
- `url`: String, optional. Glob-style pattern to match the request URL.

The example navigates to a page and waits for an outgoing POST request to any URL containing `api`. It returns the matched URL and how long the request took to appear.

```graphql
mutation waitingForARequest {
  goto(url: "https://www.cnn.com", waitUntil: domContentLoaded) {
    status
    time
  }

  waitForRequest(
    url: "**api**"
    method: POST
    timeout: 10000
  ) {
    time
    url
  }
}
```

## waitForResponse

Waits for a particular network response to be received by the browser.

Parameters:

- `statuses`: [Int] list, optional. HTTP status code(s) to wait for. Accepts a single code or a list.
- `url`: String, optional. Glob-style pattern to match the response URL.

The example navigates to a page and waits for a successful response from any URL containing `api`. It returns the status code, elapsed time, and the matched URL.

```graphql
mutation waitingForAResponse {
  goto(url: "https://www.cnn.com", waitUntil: domContentLoaded) {
    status
    time
  }

  waitForResponse(
    url: "**api**"
    statuses: [200]
  ) {
    status
    time
    url
  }
}
```

## waitForSelector

Waits for a CSS selector to appear in the DOM. If the selector already exists when this runs, it returns immediately. If the selector does not appear before the timeout, the mutation throws.

Parameters:

- `selector`: String, required. A valid CSS selector.
- `timeout`: Float, optional. Maximum milliseconds to wait before failing.
- `visible`: Boolean, optional. When `true`, also waits for the element to be visible (not `display: none` or `visibility: hidden`).

```graphql
mutation waitingForASelector {
  goto(url: "https://example.com/") {
    status
  }

  waitForSelector(selector: "h1", timeout: 5000) {
    selector
    time
  }
}
```

## waitForTimeout

Pauses execution for a fixed number of milliseconds. Use this for rate-limiting, pacing between actions, or throttling requests to avoid overloading a target. Prefer condition-based waiters when possible. They respond to actual page state rather than an arbitrary delay.

Parameters:

- `time`: Float, required. Milliseconds to wait.

```graphql
mutation waitingForAHardcodedTimeout {
  goto(url: "https://example.com/") {
    status
  }

  waitForTimeout(time: 1000) {
    time
  }
}
```

## FAQ & Troubleshooting

<details>
<summary>My <code>waitForSelector</code> times out even though the element exists</summary>

The element may be inside an iframe or shadow DOM that BQL can't reach with a top-level selector. Check the page structure in devtools. Also verify your CSS selector is correct and the element isn't dynamically generated after a user interaction.

</details>

<details>
<summary>What's the difference between <code>waitForSelector</code> and <code>waitForEvent</code>?</summary>

`waitForSelector` waits for a specific DOM element matching a CSS selector. `waitForEvent` waits for a browser-level event like `load` or `domcontentloaded`. Use `waitForSelector` when you need a specific element; use `waitForEvent` for page-level readiness.

</details>

## Next steps

  <a href="/browserql/writing-bql/exporting-variables" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Variables and exports</h3>
      save and reuse dynamic data across mutations
    
  </a>
  <a href="/browserql/using-the-ide/using-api-calls" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Exporting scripts</h3>
      convert BQL queries to production code
    
  </a>
  <a href="/browserql/use-cases/scrape-and-extract-data" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Data extraction</h3>
      extract structured data with mapSelector and querySelectorAll
    
  </a>

---

# Browserless CLI

URL: https://docs.browserless.io/cli

# Browserless CLI

The Browserless CLI (`@browserless.io/cli`) captures cookies, `localStorage`, and `IndexedDB` from a local Chromium-based browser and uploads them as reusable cloud profiles. It also provides an interactive agent chat and configuration management.

All extraction happens locally. Only the filtered result is sent to Browserless.

With the CLI, you can:

- Capture auth state from a browser you already use (Chrome, Edge, Brave, etc.) without writing code.
- Manage cloud profiles: list, rename, delete, and refresh them from the terminal.
- Filter sensitive domains locally before upload.
- Chat with the hosted Browserless agent in an interactive REPL.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Node.js >= 24
- A Chromium-based browser installed locally (Chrome, Edge, Brave, or Chromium)

> **When to use the CLI vs the CDP method**
> Use the CLI when you want to capture auth state from a browser you already use without writing code. Use the [CDP method](/baas/features/authenticated-profiles#creating-a-profile) when you need programmatic control over the login flow.

## Installation

```bash
npm install -g @browserless.io/cli
```

Requires **Node.js >= 24**.

After installing, run `browserless` with no arguments to see the full command tree.

## Quickstart

Capture your Chrome login state and upload it as a cloud profile in one command:

```bash
browserless auth login YOUR_API_TOKEN
browserless profile upload --browser chrome --profile Default --name my-session
```

That's it. Pass `?profile=my-session` on any Browserless connection URL to reuse that auth state.

## Further reading

- [Authentication](/cli/authentication) – store and manage your API token
- [Capture pipeline](/cli/capture-pipeline) – how the local extraction works
- [Profile commands](/cli/profile-commands) – upload, refresh, inspect, list, show, rename, delete
- [Browser sources](/cli/browser-sources) – discover local browsers and register custom ones
- [Domain filtering](/cli/domain-filtering) – exclude sensitive domains before upload
- [Agent](/cli/agent) – interactive chat with the hosted Browserless agent
- [Configuration](/cli/configuration) – settings, global flags, environment variables, exit codes

## FAQ & Troubleshooting

<details>
<summary>The CLI command isn't recognized</summary>

Verify the CLI is installed globally with `npm list -g @browserless.io/cli`. If missing, run `npm install -g @browserless.io/cli` to install it.

</details>

<details>
<summary>How do I authenticate the CLI?</summary>

Run `browserless auth` and enter your API token when prompted. The token is stored locally for subsequent commands.

</details>

## Next steps

  <a href="/cli" className="next-step-card-link">
    
      <h3 className="next-step-card-title">CLI overview</h3>
      all available CLI commands
    
  </a>
  <a href="/cli/authentication" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Authentication</h3>
      set up CLI credentials
    
  </a>

---

# Agent

URL: https://docs.browserless.io/cli/agent

# Agent

Interactive chat with the Browserless hosted agent. The LLM runs server-side and drives a browser for you.

Command: `browserless agent [prompt]`

The agent supports multi-turn conversations: type follow-up prompts at the `>` prompt. Previous prompts are recalled with the up arrow (history stored in `~/.browserless/agent_history`, capped at 200 entries).

If the first prompt has login intent, the CLI prompts for username and password (masked) and prepends a credentials block so the agent can authenticate on your behalf.

When the agent pauses for input (e.g. an MFA code), it prompts you inline. Press Enter on an empty line or Ctrl-C to exit.

- The Browserless CLI installed (`npm install -g @browserless.io/cli`)
- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Flags

- `--no-history`
  Disable reading and writing prompt history.
- `--json`
  Emit the final structured result as JSON to stdout. Streaming events still render during the turn, but only the `end` payload is written to stdout.

## Examples

Open the interactive REPL

```bash
browserless agent
```

Pass an initial prompt directly

```bash
browserless agent "Log in to GitHub and star browserless/browserless"
```

## FAQ & Troubleshooting

<details>
<summary>The CLI command isn't recognized</summary>

Verify the CLI is installed globally with `npm list -g @browserless.io/cli`. If missing, run `npm install -g @browserless.io/cli` to install it.

</details>

<details>
<summary>How do I authenticate the CLI?</summary>

Run `browserless auth` and enter your API token when prompted. The token is stored locally for subsequent commands.

</details>

## Next steps

  <a href="/cli" className="next-step-card-link">
    
      <h3 className="next-step-card-title">CLI overview</h3>
      all available CLI commands
    
  </a>
  <a href="/cli/authentication" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Authentication</h3>
      set up CLI credentials
    
  </a>

---

# Authentication

URL: https://docs.browserless.io/cli/authentication

# Authentication

Connect the CLI to your Browserless account by storing your API token locally.

The token is stored in your OS keychain (macOS Keychain, libsecret on Linux, DPAPI on Windows) and falls back to a mode-0600 file at `~/.browserless/config.json`.

Command: `browserless auth <subcommand>`

- The Browserless CLI installed (`npm install -g @browserless.io/cli`)
- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Subcommands

- `login [token]`
  Store your API token. Accepts a positional argument or stdin (`echo $TOKEN | browserless auth login`). Omitting the argument and piping from stdin keeps the token out of your shell history.
- `status`
  Print the current identity and where the token is stored.
- `logout`
  Wipe credentials from both keychain and config file.

## Examples

Log in with a token

```bash
browserless auth login bless_abc123def456
```

Check auth status

```bash
browserless auth status
```

```text
Authenticated
  Token stored in: keychain
  Server: https://production-sfo.browserless.io
```

## Token resolution

When a command needs authentication, the CLI checks these sources in order:

1. `--token` flag
2. `BROWSERLESS_TOKEN` environment variable
3. OS keychain (keyed by server URL, so credentials for multiple regions coexist)
4. `~/.browserless/config.json` (mode 0600 fallback)

If none resolve, the command exits with code 3.

## FAQ & Troubleshooting

<details>
<summary>The CLI command isn't recognized</summary>

Verify the CLI is installed globally with `npm list -g @browserless.io/cli`. If missing, run `npm install -g @browserless.io/cli` to install it.

</details>

<details>
<summary>How do I authenticate the CLI?</summary>

Run `browserless auth` and enter your API token when prompted. The token is stored locally for subsequent commands.

</details>

## Next steps

  <a href="/cli" className="next-step-card-link">
    
      <h3 className="next-step-card-title">CLI overview</h3>
      all available CLI commands
    
  </a>
  <a href="/cli/authentication" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Authentication</h3>
      set up CLI credentials
    
  </a>

---

# Browser sources

URL: https://docs.browserless.io/cli/browser-sources

# Browser sources

Discover which local browsers and profiles are available, or register custom Chromium-based browsers.

Command: `browserless profile sources <subcommand>`

- The Browserless CLI installed (`npm install -g @browserless.io/cli`)
- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Subcommands

- `list`
  List all detected local browsers with their available profiles.
- `add`
  Register a custom Chromium binary.
- `rm <name>`
  Remove a custom browser registration.

## Flags for sources add

- `--name <name>`
  Display name for the custom browser.
- `--path <path>`
  Path to the Chromium binary.
- `--user-data-dir <path>`
  Path to the browser's user data directory.

## Examples

List available browsers

```bash
browserless profile sources list
```

Register a custom browser

```bash
browserless profile sources add \
  --name "My Chromium" \
  --path /opt/chromium/chrome \
  --user-data-dir ~/.config/my-chromium
```

## Supported browsers

The CLI detects these browsers out of the box:

| Key | Display name |
|-----|-------------|
| `chrome` | Google Chrome |
| `chrome-beta` | Google Chrome Beta |
| `chrome-dev` | Google Chrome Dev |
| `chrome-canary` | Google Chrome Canary |
| `brave` | Brave |
| `edge` | Microsoft Edge |
| `vivaldi` | Vivaldi |
| `opera` | Opera |
| `arc` | Arc |
| `chromium` | Chromium (incl. Snap on Linux) |

The `--browser` flag is case-insensitive: `--browser CHROME`, `--browser Chrome`, and `--browser "google chrome"` all resolve to the same entry.

`--profile` accepts either the user-visible profile name from Chrome's `Local State` (e.g. `Personal`, `Work`) or the on-disk directory name (`Default`, `Profile 1`).

## FAQ & Troubleshooting

<details>
<summary>The CLI command isn't recognized</summary>

Verify the CLI is installed globally with `npm list -g @browserless.io/cli`. If missing, run `npm install -g @browserless.io/cli` to install it.

</details>

<details>
<summary>How do I authenticate the CLI?</summary>

Run `browserless auth` and enter your API token when prompted. The token is stored locally for subsequent commands.

</details>

## Next steps

  <a href="/cli" className="next-step-card-link">
    
      <h3 className="next-step-card-title">CLI overview</h3>
      all available CLI commands
    
  </a>
  <a href="/cli/authentication" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Authentication</h3>
      set up CLI credentials
    
  </a>

---

# Capture pipeline

URL: https://docs.browserless.io/cli/capture-pipeline

# Capture pipeline

When you run `profile upload`, `profile refresh`, or `profile inspect`, the CLI executes this pipeline:

1. **Copy** — A filtered snapshot of your local browser profile (skipping caches, GPU data, and lock files) is written to a temp directory.
2. **Launch** — Your real browser binary is spawned headless against the temp copy with sync, telemetry, and Safe Browsing disabled so nothing mutates your live profile.
3. **Extract** — The CLI connects via CDP (`Storage.getCookies` + per-origin `localStorage` / `IndexedDB`) and captures all auth state.
4. **Filter** — Domain exclusions (persistent list + per-run `--exclude-domain`) and the private-domain filter are applied. Excluded data never leaves your machine.
5. **Upload** — The filtered payload is sent to the Browserless server.
6. **Cleanup** — The temp directory and headless process are torn down.

- The Browserless CLI installed (`npm install -g @browserless.io/cli`)
- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Profile size limits

The server enforces these caps on the upload payload:

| Limit | Value |
|-------|-------|
| Serialized artifact size | 2 MB |
| Origins per capture | 50 |
| IndexedDB databases per origin | 5 |
| IndexedDB entries per store | 1,000 |
| Profile name length | 255 characters |
| Profile name pattern | No whitespace, `?`, or `#` |

If your capture exceeds 2 MB, use `--auto-fit` to drop the heaviest origins until it fits, or `--exclude-domain` / `--only-domain` to narrow scope.

## FAQ & Troubleshooting

<details>
<summary>The CLI command isn't recognized</summary>

Verify the CLI is installed globally with `npm list -g @browserless.io/cli`. If missing, run `npm install -g @browserless.io/cli` to install it.

</details>

<details>
<summary>How do I authenticate the CLI?</summary>

Run `browserless auth` and enter your API token when prompted. The token is stored locally for subsequent commands.

</details>

## Next steps

  <a href="/cli" className="next-step-card-link">
    
      <h3 className="next-step-card-title">CLI overview</h3>
      all available CLI commands
    
  </a>
  <a href="/cli/authentication" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Authentication</h3>
      set up CLI credentials
    
  </a>

---

# Configuration

URL: https://docs.browserless.io/cli/configuration

# Configuration

- The Browserless CLI installed (`npm install -g @browserless.io/cli`)
- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## config

Manage CLI settings. The configuration is stored in `~/.browserless/config.json` (mode 0600, created on first use).

Command: `browserless config <subcommand>`

### Subcommands

- `get <key>`
  Read a config value. Sensitive keys are redacted by default; pass `--show-sensitive` to print verbatim.
- `set <key> <value>`
  Write a user-managed config value.
- `unset <key>`
  Remove a user-managed config value.

### Configurable keys

| Key | Description |
|-----|-------------|
| `server` | Target server URL (mutually exclusive with `region`). |
| `region` | Production region shorthand: `sfo`, `lon`, or `ams`. |
| `terms_ttl_days` | Days between liability disclaimer re-prompts (default 7). |
| `telemetry` | `true` or `false` (default on). |
| `excluded_domains` | Managed via `profile exclude add/rm/list`. |

### Examples

Set a region

```bash
browserless config set region sfo
```

Point to a self-hosted server

```bash
browserless config set server https://my-browserless.example.com
```

Disable telemetry

```bash
browserless config set telemetry false
```

Read a config value

```bash
browserless config get region
```

## Global flags

Every command that hits the Browserless API accepts these flags.

Command: `browserless {command} [global flags]`

### Flags

- `--token <token>`
  Override token resolution (flag > env > keychain > config file).
- `--server <url>`
  Override the configured server URL.
- `--region <code>`
  `sfo` / `lon` / `ams` shorthand for `--server`. Passing both `--server` and `--region` is an error.
- `--json`
  Emit the server response as JSON (script-friendly).

## Environment variables

| Variable | Effect |
|----------|--------|
| `BROWSERLESS_TOKEN` | Use this token without a stored credential (overrides keychain + file). |
| `BROWSERLESS_ACCEPT_TERMS` | `1` to pre-accept the liability disclaimer. |
| `BROWSERLESS_CONFIG_DIR` | Override `~/.browserless/` location (useful for CI or multi-tenant setups). |
| `BROWSERLESS_DISABLE_KEYCHAIN` | `1` to force plaintext config-file storage even if the keychain works. |
| `BROWSERLESS_CDP_WAIT_MS` | Override the 15 s CDP-readiness timeout (raise on slow CI runners). |
| `BROWSERLESS_TELEMETRY_DISABLED` | `1` to disable usage analytics for the run. |
| `DO_NOT_TRACK` | `1` also disables usage analytics (cross-tool standard). |
| `CI` | When set, the disclaimer refuses to prompt interactively. Use `--accept-terms` instead. |

## Exit codes

| Code | Meaning |
|------|---------|
| 0 | Success |
| 1 | Internal bug (prints a stack trace) |
| 2 | Bad arguments (`UsageError`) |
| 3 | Precondition failed: no token, disclaimer declined, profile busy, corrupt config (`PreconditionError`) |
| 4 | Local capture pipeline failed: copy / browser launch / CDP (`CaptureError`) |
| 5 | Server returned a non-2xx response (`ApiError`) |

## Usage analytics

The CLI reports lightweight, pseudonymous usage data so the team knows which commands to invest in. It's on by default and easy to turn off.

**What is sent** (once per command, only when authenticated against a Browserless cloud region):

- The command that ran (e.g. `profile upload`) and the **names** of flags you passed (never their values).
- Outcome: `ok` / `error`, exit code, and error class on failure.
- Duration, CLI version, OS/architecture/Node major, whether it's running under CI.
- The production region (`sfo` / `lon` / `ams`). Never a raw server URL.
- A random per-install device ID stored in the Browserless config directory (default: `~/.browserless/telemetry-device-id`).

**Never sent:** your API token, profile names, captured URLs or domains, cookies, or any captured auth state. Commands targeting self-hosted servers aren't tracked at all.

**Opt out:**

```bash
browserless config set telemetry false
```

Or per-run via environment variable:

```bash
BROWSERLESS_TELEMETRY_DISABLED=1 browserless profile upload ...
DO_NOT_TRACK=1 browserless profile upload ...
```

## FAQ & Troubleshooting

<details>
<summary>The CLI command isn't recognized</summary>

Verify the CLI is installed globally with `npm list -g @browserless.io/cli`. If missing, run `npm install -g @browserless.io/cli` to install it.

</details>

<details>
<summary>How do I authenticate the CLI?</summary>

Run `browserless auth` and enter your API token when prompted. The token is stored locally for subsequent commands.

</details>

## Next steps

  <a href="/cli" className="next-step-card-link">
    
      <h3 className="next-step-card-title">CLI overview</h3>
      all available CLI commands
    
  </a>
  <a href="/cli/authentication" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Authentication</h3>
      set up CLI credentials
    
  </a>

---

# Domain filtering

URL: https://docs.browserless.io/cli/domain-filtering

# Domain filtering

Manage persistent domain exclusions. Exclude sensitive domains locally before upload. Browserless never sees filtered data.

Command: `browserless profile exclude <subcommand>`

- The Browserless CLI installed (`npm install -g @browserless.io/cli`)
- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Subcommands

- `add <domain>`
  Add a domain to the persistent exclusion list.
- `list`
  Print all persistently excluded domains.
- `rm <domain>`
  Remove a domain from the persistent exclusion list.

Suffix-match rules: `example.com` matches `example.com`, `.example.com`, `www.example.com`, and `sub.www.example.com`.

## Examples

Add a domain exclusion

```bash
browserless profile exclude add chase.com
```

List exclusions

```bash
browserless profile exclude list
```

Remove an exclusion

```bash
browserless profile exclude rm chase.com
```

> **Info**
> The CLI always filters `localhost`, `*.local`, `*.internal`, and IP-literal hosts regardless of your exclusion list. These protect against accidentally uploading intranet or local-network state that would never apply to a cloud session.

## Per-run filtering

In addition to the persistent exclusion list, you can filter domains at capture time:

- `--exclude-domain <domain>` — append a domain for this run only (repeatable).
- `--no-default-excludes` — skip the persistent exclusion list for this run.
- `--only-domain <domain>` — allowlist mode: capture only domains matching this pattern (repeatable, applies after `--exclude-domain`).

See [Profile commands](/cli/profile-commands) for the full flags reference.

## FAQ & Troubleshooting

<details>
<summary>The CLI command isn't recognized</summary>

Verify the CLI is installed globally with `npm list -g @browserless.io/cli`. If missing, run `npm install -g @browserless.io/cli` to install it.

</details>

<details>
<summary>How do I authenticate the CLI?</summary>

Run `browserless auth` and enter your API token when prompted. The token is stored locally for subsequent commands.

</details>

## Next steps

  <a href="/cli" className="next-step-card-link">
    
      <h3 className="next-step-card-title">CLI overview</h3>
      all available CLI commands
    
  </a>
  <a href="/cli/authentication" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Authentication</h3>
      set up CLI credentials
    
  </a>

---

# Profile commands

URL: https://docs.browserless.io/cli/profile-commands

# Profile commands

- The Browserless CLI installed (`npm install -g @browserless.io/cli`)
- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## profile upload

Capture local auth state and create a **new** cloud profile. Fails if the name already exists.

Command: `browserless profile upload [flags]`

### Flags

- `--browser <key>`
  Which local browser to capture from (e.g. `chrome`, `edge`, `brave`). Case-insensitive.
- `--profile <name>`
  The browser profile to capture (e.g. `Default`, `Personal`, `Profile 1`).
- `--name <name>`
  Name for the new cloud profile.
- `--exclude-domain <domain>`
  Append a domain to the exclusion list for this run (repeatable, suffix-match).
- `--no-default-excludes`
  Skip the persistent exclusion list for this run.
- `--only-domain <domain>`
  Allowlist: capture only domains matching this pattern (repeatable, applies after `--exclude-domain`).
- `--auto-fit`
  Drop the heaviest origins (largest first) until the artifact fits under the 2 MB server cap. Cookies are never dropped.
- `--keep-domain <domain>`
  Protect a domain from `--auto-fit` drops (repeatable, suffix-match).
- `--accept-terms`
  Pre-accept the liability disclaimer (useful in CI/scripts).
- `--password-store <type>`
  Linux only: pass `basic` / `gnome-libsecret` / `kwallet5` to the launched browser if auto-detection picks wrong.

### Examples

Basic upload from Chrome's default profile

```bash
browserless profile upload \
  --browser chrome --profile Default --name my-chrome
```

Upload with domain filtering

```bash
browserless profile upload \
  --browser chrome --profile Default --name demo \
  --exclude-domain chase.com --exclude-domain "*.okta.com"
```

Upload with auto-fit and protected domains

```bash
browserless profile upload \
  --browser chrome --profile Default --name demo \
  --auto-fit --keep-domain google.com --keep-domain github.com
```

Upload capturing only specific sites

```bash
browserless profile upload \
  --browser chrome --profile Default --name demo \
  --only-domain github.com --only-domain "*.atlassian.net"
```

## profile refresh

Capture local auth state and **overwrite** an existing cloud profile. Fails if the name doesn't exist.

Command: `browserless profile refresh [flags]`

Accepts all the same flags as `profile upload`.

### Examples

Refresh an existing profile

```bash
browserless profile refresh \
  --browser chrome --profile Default --name my-chrome
```

## profile inspect

Capture and print a summary without uploading. Useful for debugging what the CLI would send.

Command: `browserless profile inspect [flags]`

Accepts all the same capture flags as `profile upload`.

### Examples

```bash
browserless profile inspect --browser chrome --profile Default
```

## profile launch

Launch your real browser interactively against the local profile. Useful for fixing auth state (logging in, dismissing dialogs) before re-running `profile refresh`. The source browser must be closed first.

Command: `browserless profile launch [flags]`

### Flags

- `--browser <key>`
  Which local browser to launch.
- `--profile <name>`
  The browser profile to open.
- `--headless`
  Launch headless instead of visible (useful for scripted checks).

### Examples

```bash
browserless profile launch --browser chrome --profile Default
```

## profile list

List all cloud profiles for the current token.

Command: `browserless profile list [flags]`

### Flags

- `--limit <n>`
  Maximum number of profiles to return.
- `--offset <n>`
  Number of profiles to skip (for pagination).

### Examples

```bash
browserless profile list
```

```bash
browserless profile list --limit 5 --offset 10
```

## profile show

Print metadata for a cloud profile: creation date, last-refreshed timestamp, origin count, and artifact size.

Command: `browserless profile show <name>`

### Examples

```bash
browserless profile show my-chrome
```

## profile rename

Change the name of a cloud profile without re-uploading. Any connection URLs referencing the old name stop working immediately.

Command: `browserless profile rename <old-name> <new-name>`

### Examples

```bash
browserless profile rename my-chrome production-chrome
```

## profile delete

Permanently remove a cloud profile and its stored auth state. This is irreversible.

Command: `browserless profile delete <name>`

### Examples

```bash
browserless profile delete old-profile
```

## FAQ & Troubleshooting

<details>
<summary>The CLI command isn't recognized</summary>

Verify the CLI is installed globally with `npm list -g @browserless.io/cli`. If missing, run `npm install -g @browserless.io/cli` to install it.

</details>

<details>
<summary>How do I authenticate the CLI?</summary>

Run `browserless auth` and enter your API token when prompted. The token is stored locally for subsequent commands.

</details>

## Next steps

  <a href="/cli" className="next-step-card-link">
    
      <h3 className="next-step-card-title">CLI overview</h3>
      all available CLI commands
    
  </a>
  <a href="/cli/authentication" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Authentication</h3>
      set up CLI credentials
    
  </a>

---

# Enterprise Changelog

URL: https://docs.browserless.io/enterprise/changelog

> Note that these changes are only for Browserless' cloud, Enterprise and self-hosted Enterprise deployments. For information on the open-source container [please refer to this link](https://github.com/browserless/browserless/blob/main/CHANGELOG.md).

## Latest

- Dependency updates.

## v2.163.1

- Dependency updates.
- Improve agent checkout reliability so card payments complete instead of filling card details without charging.
- Improve agent sessions with redacted snapshots after secret fills, more accurate clicks, and single-tab handoff live links.
- Mask card numbers, email addresses, and passwords typed into form fields in session replays.
- Verify account ownership before allowing connections to an existing browser session.
- Limit Session API live links, reconnect windows, and keep-alive to the session's expiry time.
- Stop charging proxy usage for sessions that fell back to a direct connection.

## v2.163.0

- Dependency updates.
- Let `/agent/run` pause for human help, report missing recordings, and keep generated skills to visited sites.
- Restore the standard default viewport on `/screenshot`, `/pdf`, `/content`, and `/export`.
- Fix `blockConsentModals` request timeouts and cap `/export` wait times to the request timeout.
- Preserve proxy city targeting when reconnecting to a BrowserQL session.
- Redact credentials and request bodies from request logs.

## v2.162.0

- Dependency updates.
- Add support for completing one-time payments for agents.
- Improve CAPTCHA solving.
- Bug fixes.

## v2.161.0

- Dependency updates.
- Add optional final-page captures in multiple formats to managed agent runs.
- Add an optional DevTools panel to live sessions and improve live view resizing.
- Report a completion reason once per live URL, including links that are never opened.
- Improve CAPTCHA solving on checkout pages that render challenges immediately on load.

## v2.160.0

- Dependency updates.
- Add opt-in recording for `/agent/run` sessions and limit live-view links to the session's remaining lifetime.
- Fix reconnecting to persistent sessions right after a client disconnects.
- Improve CAPTCHA solving for slider puzzles and press-and-hold challenges.
- Block sensitive host-level browser commands by default, with `ALLOW_RESTRICTED_CDP_METHODS` opt-in for self-hosted deployments.
- Reject invalid proxy state values with a clear error.
- Fix BrowserQL deep selectors acting on the wrong page and incomplete screenshots of tall elements.

## v2.159.0

- Add optional task instructions to live sessions.
- Improve CAPTCHA solving reliability.
- Improve error messages returned to browser pages when a request is rejected.
- Fix browser launch flags with values being ignored when passed in `launch.args`.
- Fix browser reconnects on self-hosted deployments.
- Security hardening for outbound network requests.

## v2.158.0

- Dependency updates.
- Add a `closeLiveURL` BQL mutation to close live URLs and disconnect their viewers.
- Improve CAPTCHA solving, including challenges inside embedded cross-origin frames, with fewer false-positive solve results.
- Add on-demand recovery of saved profiles through automated login.
- Show clearer errors when connecting credential integrations with invalid tokens.
- Fix corrupted screenshots of tall elements.
- Fix session recordings losing their final events when sessions close immediately after stopping.

## v2.157.0

- Dependency updates.
- Smart Scrape can now run a sequence of page actions — waiting, clicking, typing, and scrolling — before extracting content, so you can reach content that only appears after interaction.
- Automated login now completes email one-time-code (OTP) verification when creating authenticated profiles, so sign-ins that email a code no longer stall.
- Improved CAPTCHA solving for slider challenges that run inside embedded frames, with more reliable solver selection.
- Search now tells genuinely empty results apart from sources that were unavailable or timed out, so retries happen only when they can help.
- Live URL sessions can now switch the viewer between browser tabs, and the Live URL stays valid when the original page closes.
- Improved proxy reliability — failed proxy providers recover on the next request.

## v2.156.0

- Dependency updates.
- Allow removing selected sites and their stored browser state from saved profiles.
- Add agent-assisted checkout support for authenticated browser sessions.
- Add secure credential saving and clearer guidance after secret entry.
- Improve session replay privacy and prevent replay from stalling browser actions.
- Preserve protected-site session settings and solve details in generated scripts.
- Make strict network-idle waits settle sooner for intentionally long-lived connections.

## v2.155.0

- Honor explicit `networkidle0` waits so slow-loading pages fully finish before screenshots, with a bounded maximum extra wait.

## v2.154.0

- Dependency updates.
- Fix intermittent CAPTCHA solve failures on pages that embed a standalone Turnstile widget.

## v2.153.0

- Updated dependencies and improved runtime stability.
- Improved usage and performance monitoring across browser automation, scraping, and live sessions.
- Improved compatibility and reliability for CAPTCHA solving.
- Improved session recording and live viewer reliability.
- Added safeguards for background navigation and screenshot requests.

## v2.152.0

- Dependency updates.
- Add optional task outcome reporting to the agent API, with verdicts shown in session completion logs.
- Add predefined scraper endpoints at `/scrapers`, enabled by setting `SCRAPE_BUNDLE_DIR`.
- Keep Windows device-profile assignment consistent for a session, including across reconnects.
- Fix BrowserQL network-idle `waitUntil` options so navigation waits for the page to finish loading.
- Improve Smart Scrape result quality by rejecting blocked or empty pages, and add an `onlyMainContent` option.

## v2.151.0

- Add capability discovery for agent sessions to check supported features before a browser is allocated.
- Brave stealth sessions now identify as Brave instead of masking as Chrome, improving fingerprint consistency.

## v2.150.0

- Dependency updates.
- Route a portion of agent runs and script generation to an alternate model configuration, keyed by account.
- Improve Akamai detection in captcha auto-detect, including site-branded deny pages.
- Fix PerimeterX solves reporting success before the page had actually loaded.
- Return the PerimeterX clearance token more reliably after a successful solve.
- Add DataDome interstitial solving to the `/unblock` endpoint.
- Fix captcha solves failing with a missing-proxy error on reused browser sessions.

## v2.149.0

- Dependency updates.
- Add OS, screen, and device-scale persona options to Agent routes.
- Expose content-shaping controls on `POST /smart-scrape`, add `rawText`, and return page metadata.
- Improve stealth session fingerprinting.
- Block denied BrowserQL requests.
- Fix injected script and style tags.

## v2.148.0

- Dependency updates.
- The `/pdf` endpoint now uses the request's granted timeout budget instead of a fixed limit.
- Hosted Cloud sessions created without an expiry are now bounded by the plan's retention limit.
- Fix residential proxy connections to fidelity.com failing to load.

## v2.147.1

- Dependency updates.
- Allow `/screenshot` captures to run for the full request timeout instead of stopping at 30 seconds.
- Fix `/export` returning the wrong document when a page navigates away or rewrites itself during waits.
- Block requests to disallowed URLs without ending the whole session over benign sub-resources.
- Improve captcha solving in stealth sessions by matching each persona's screen size and touch behavior.
- Make the spoofed GPU and browser identity consistent between pages and workers in stealth sessions.

## v2.147.0

- Dependency updates.
- Add session-end reporting for `/chromium/agent` sessions, including why a session timed out and how many commands ran.
- Fix `clearSecrets` in agent sessions not restoring screenshots, PDFs, and page captures after a secret-based login.
- Tighten reconnect authentication so tokenless reconnects are only allowed when no credentials or ownership are present.
- Fix deep crawls marking pages as failed when they have long titles, descriptions, or URLs.

## v2.146.0

- Add a `/profile/:name/download` endpoint to export saved authentication profiles (cloud only).
- Allow self-hosted deployments to reconnect to browser sessions without a token when ownership cannot be determined.

## v2.145.0

- Add session recording to the MCP agent, with start and stop controls.
- Add device, screen, and operating system emulation options to the MCP agent.
- Crawl results now expire according to your plan's retention period and are cleaned up automatically.
- Remove stale bot-protection cookies when restoring saved profiles, while preserving login cookies.
- The `/meta` endpoint now reports the Enterprise version, with the SDK version available separately.

## v2.144.0

- Add an opt-in `CAPTCHA_FAST_IDENTIFY` setting that skips detection polling for explicitly specified captcha types, cutting solve latency.
- Add an opt-in `CAPTCHA_SOLVE_TIMING` setting that reports additional captcha solve timing metrics.
- Honor caller-supplied timeouts above 30 seconds on `/pdf`, `/screenshot`, and `/content`, up to the request's allowed time.
- Treat a `timeout` of `0` on `/pdf`, `/screenshot`, and `/content` as the request's full allowed time.

## v2.143.1

- Dependency updates.
- Fix `/pdf`, `/screenshot`, `/export`, and `/content` requests failing when injected JavaScript contains escaped characters.

## v2.143.0

- Dependency updates.
- Improve font accuracy for Windows 10 stealth browser personas.
- Fix email one-time codes being missed when they arrive before the code field appears.
- Improve detection of alphanumeric email one-time codes.
- Fix smart-scrape sessions being terminated when running behind a managed proxy.

## v2.142.0

- Reconnect windows on hosted Cloud now match each plan's maximum session length.
- Reconnecting can no longer extend a session past its original time limit.

## v2.141.0

- Improve automated screenshots and click accuracy, including clicking by screen position.
- Improve element targeting on pages with nested or embedded content.
- Add browser and device emulation options such as operating system, device, and screen size.
- Match language, timezone, and regional settings to the session's location.
- Free up leftover resources when sessions end, reducing memory growth over time.
- Fix live session viewers lingering after a session or connection closes.
- Restrict reconnecting to a browser session to the account that owns it.

## v2.140.0

- Dependency updates.
- Detect an already-cleared captcha by default, removing an unnecessary wait on every check.
- Add support for storing session replays on your own storage service via `SESSION_REPLAY_CLOUD_ENDPOINT`.
- Stop rejecting session reattach and crawl status requests when the browser pool is busy.
- Prevent cloud-unit account balances from going below zero, and normalize existing negative balances.
- Fix session replays for persistent sessions not appearing in the dashboard.
- Preserve longer live-view timeouts on paid plans instead of rejecting the viewer link.
- Reduce delays when generating PDFs and screenshots.

## v2.139.0

- Dependency updates.
- Improve usage reporting for browser profiles so activity is attributed to its originating source.
- Clear stealth sessions from Active Browsers as soon as the browser closes, instead of waiting for a timeout.

## v2.138.0

- Add fleet-split labels to block metrics, enabling block-rate comparisons between worker groups.

## v2.137.0

- Dependency updates.
- Make Live URLs available to authenticated users on every hosted Cloud plan, including Free.
- Increase the maximum Free browser-session duration from one minute to two minutes.
- Preserve legacy routing when BrowserQL migration settings are missing or lost.
- Cap captcha auto-solve retries and bill each challenge only once.

## v2.136.0

- Dependency updates.
- Agent snapshots now include richer context about page elements and forms, improving automation accuracy.

## v2.135.0

- Dependency updates.
- Allow screenshots and other captures after a credentialed login in the same session.
- Support one-time passcodes for logins that require multi-factor authentication.
- Report the original requester IP in request logs, configurable with `TRUSTED_REQUESTER_IP_PROXIES`.
- Support custom ad-blocking rule sets when ad blocking is enabled.

## v2.134.0

- Updates to `@browserless.io/browserless` at `v2.56.0`.
- Update `puppeteer-core` to 25.7.0
- `/agent/run` now returns a standardized final result including the answer, success status, and reason.
- Improvements to `/smart-scrape` endpoint.

## v2.133.0

- Dependency updates.
- Add optional reporting when a page load is blocked by anti-bot protection.
- Extract text from PDF pages during scraping.
- Fix intermittent captcha solve failures when using residential proxies.
- Fix screenshot and PDF capture failing on sites that redirect immediately after loading.
- Fix missing console output from iframes in stealth sessions.

## v2.132.0

- Dependency updates.
- Proxy usage during autologin profile creation is now optional and chosen by the caller, avoiding unexpected metered usage.
- Invalid or plan-disallowed proxy settings are now rejected immediately with a clear error instead of a failed task.
- Solved captcha challenges are now properly applied to the page, so protected content loads instead of showing the challenge again.
- `/smart-scrape` now detects captcha walls and retries with a full browser instead of returning the challenge page as content.

## v2.131.1

- Fix an explicit captcha solve being interrupted by automatic solving on the same page.
- Fix video playback failures in the `chromium` engine for commonly used media formats.
- Session URLs from `/session` now carry a per-session credential instead of your account token.
- Fix the reported GPU details in stealth sessions so they match the spoofed profile.

## v2.131.0

- Skill capture results now report whether a skill was created or updated, with a clear rejection reason.
- Improve handling of malformed, unsafe, off-topic, or failed skill captures.
- Saved profiles now list only domains you actually logged into, with accurate login expiry dates.

## v2.130.0

- Dependency updates.
- Saved authentication profiles now track when each site's sign-in expires, surfaced through the profile API.
- Profiles created before this release report no expiry details until their next save.
- Stealth sessions now emulate the platform Browserless runs on by default, with other options still selectable.

## v2.129.0

- Dependency updates.
- Sessions blocked by an account's own spend limit now report a distinct error, separate from running out of credits.
- Live browsing keeps the viewer's cursor in sync with the page, including embedded content.
- Pressing Enter in a live session now activates links, buttons, and keyboard-driven widgets.
- Improve captcha solve reliability on protected sites, avoiding false failures and needless retries.
- Extend Windows browser emulation defaults to `/unblock` and `/smart-scrape`, with an option to override it.
- Allow individual accounts to keep the previous engine for `/pdf`, `/screenshot`, `/content`, and `/export` requests.

## v2.128.0

- Stealth sessions now default to a Windows browser identity on cloud deployments, greatly improving solve rates.
- Self-hosted deployments keep their existing platform identity for consistent browser fingerprints.
- Improve captcha solver selection so the chosen solver matches the session's operating system.

## v2.127.2

- Improve Akamai captcha success rates by preferring our own solver, with vendor fallback retained.

## v2.127.1

- Improve captcha solve reliability on sites protected by advanced bot detection.

## v2.127.0

- Add richer success and failure reporting for `/agent/run`, including run duration and outcome.
- Report completion for runs that end via cancellation, recovery, or queueing failure.
- Report rejected agent runs that previously went unrecorded.
- Track authenticated profile creation consistently, including profiles created by automatic login.
- Categorize automatic login failures so common causes are easier to identify.

## v2.126.1

- Improve reliability of DataDome captcha solving with automatic retries.

## v2.126.0

- Dependency updates.
- Improve request logs with clearer endpoint and source labels for agent and assistant traffic.
- Keep a stable exit IP for the whole session on stealth, BrowserQL, and `/unblock` requests.
- Fix pasted text being duplicated in live sessions.
- Fix pasted values not registering in some form fields during live sessions.

## v2.124.0

- Dependency updates.
- Browser profiles created with the CLI now remember their capture settings, so a refresh replays them without extra options.
- Add support for solving Akamai security challenges, including interstitials that no longer fail the request.
- Add support for rotating-disc CAPTCHAs and improve slider CAPTCHA handling, retries, and outcome checks.
- Stealth sessions emulating an OS now report a consistent, realistic hardware profile, with optional pinning to a stable device.
- Stealth sessions report screen orientation more naturally, reducing detectable fingerprinting traces.
- Stop temporary files from accumulating and filling disk space on long-running workers.
- Fix live session clicks landing in the wrong place after the viewer is resized.
- Improve residential proxy routing so certain sites are reachable instead of failing to connect.

## v2.123.1

- Updates to `@browserless.io/browserless` at `v2.55.2`.
- Fix session replays dropping page changes, so recordings now capture the full session.
- Keep recordings intact when an upload fails, so nothing is lost and can be retried.
- Improve anti-bot challenge detection and page classification, avoiding hangs on blocked pages.

## v2.123.0

- Dependency updates.
- Show a clearer connection message when an account reaches its own spend limit, pointing to account settings.
- Add failure and timing telemetry for authenticated profiles to speed up support diagnosis.
- `/unblock` now returns a retryable error instead of a success when a page is still blocked.
- Fix `/smart-scrape` wrongly reporting a challenge on pages that were actually cleared.
- Improve internal captcha diagnostic tooling checks.

## v2.122.0

- Dependency updates.
- Add request lifecycle markers to customer logs for clearer per-request visibility.
- Solve Amazon's account-verification image-grid captcha challenges.
- Improve browser identity consistency for stealth sessions.

## v2.121.0

- Dependency updates.
- Automated sign-in now handles security checks, CAPTCHAs, and multi-factor prompts more reliably.
- Improve CAPTCHA and anti-bot handling on the `/smart-scrape` endpoint.
- Capture-only scrape requests now fail clearly when no requested output is produced.
- Improve accuracy of proxy bandwidth accounting for scrapes.
- Automatically turn successful browser automation runs into reusable skills, with quality checks.
- Browser sessions now start with a consistent desktop-sized window.
- Make switching to the newest window in BrowserQL deterministic and more resilient.

## v2.120.0

- Improve log output.

## v2.119.1

- Adjust exported metrics.

## v2.119.0

- Dependency updates.
- Add a managed AI agent endpoint that runs a natural-language task in a real browser and returns structured results.
- Add an option to give Windows stealth sessions a coherent, consistent device fingerprint via `STEALTH_WINDOWS_ARCHETYPES`.
- Return a clear, actionable error from `/pdf` when generation fails, instead of a generic 500.

## v2.118.0

- Update the default hosted-agent model for better intelligence, speed, and larger context; still overridable via `OPENAI_MODEL` and `OPENAI_REASONING_EFFORT`.
- Clarify the live session viewer's error when a requested timeout exceeds your plan's maximum, showing the limit and how to fix it.

## v2.117.0

- Dependency updates.
- Add a `/scrape` endpoint to extract structured data from a page URL or raw HTML.
- Add a `/skills` endpoint for storing and reusing trusted automation skills.
- Add customer-visible structured request logs with a configurable log level.
- Keep stealth fingerprints consistent per session profile, including desktop screen size and scaling.
- Fix enterprise reCAPTCHA v3 being misclassified, improving solve reliability.
- Harden smart scraping against server-side request forgery.
- Fix popups and new tabs becoming invisible to clients during live sessions.
- Fix black cropping when capturing tall elements in screenshots.

## v2.116.1

- Dependency updates.
- Bug fixes.
- Improve `/crawl` endpoint reliability.

## v2.116.0

- Dependency updates.
- Updates to `@browserless.io/browserless` at `v2.55.0`.
- Fixes to browser profiles, crawls, and integrations.
- Improve error handling on the `/unblock` endpoint.
- Captcha auto-solve events reach client-created CDP sessions.
- Accept `blockAdsInclude` on connect and durable `/session` routes.

## v2.115.0

- Add real-time BrowserQL subscriptions for console and network events.
- Accept live session requests with or without a JSON content type.
- Return from captcha solving faster when the page is already clear.
- Raise the default file-transfer size limit to 50 MB.

## v2.114.0

- Dependency updates.
- Log the target URL for navigations that begin but never complete, making stuck sessions easier to debug.
- Add mobile Android device emulation for sessions.
- Fix live sessions freezing when a click opens a new browser tab.
- Revoked API keys now stop being accepted within seconds instead of minutes.
- Fix residential proxy errors when solving challenges after a redirect.
- Fix `/unblock` returning an unsolved challenge page as a successful response.

## v2.113.0

- Fix sticky datacenter proxy sessions so they persist reliably instead of being recreated.
- Fix a crash on `/smart-scrape` and `/search` that returned a 500 and consumed credits.

## v2.112.0

- Dependency updates.
- Upgrade the hosted agent to a stronger model with higher reasoning effort.
- Fix a black screen that could appear when starting a live session view.

## v2.111.0

- Dependency updates.
- Update stealth session markers to reduce detection.

## v2.110.0

- Dependency updates.
- Add per-session operating system emulation for stealth sessions via `emulationOs`.
- Improve stealth fingerprint consistency.
- Return the underlying error message from failed BQL mutations.
- Improve request logs, and include a request ID.
- Add an option to load only selected ad-blocking rulesets for faster session startup with `blockAdsInclude`.

## v2.109.0

- Multiple API keys under the same account now share a single cloud-unit credit balance.
- Fix automated-login profiles not being saved after a successful login.

## v2.108.2

- Dependency updates.
- Fix autologin agent failing to attach to its browser session in multi-worker deployments.

## v2.108.1

- Dependency updates.
- Fix BQL script generation to produce consistent output.
- Improve crawl-delete response time for crawls with many URLs.
- Improve liveURL connection reliability through VPNs and corporate proxies.
- Fix BQL Windows desktop identity not applying on cloud deployments.
- Support multi-line statement bodies in BQL `evaluate` mutations.

## v2.108.0

- Updates to `@browserless.io/browserless` at `v2.54.2`.
- Dependency updates.
- Add endpoint to list persistent sessions and their connection details.
- Add Script Generator endpoint with streaming output in multiple formats.
- Add opt-in Windows identity for stealth sessions to improve site compatibility.
- Add profile validation for agent connections.
- Fix credential autologin in certain session flows.
- Fix Turnstile solving on some sites.

## v2.107.0

- Dependency updates.
- Updates to `@browserless.io/browserless` at `v2.54.1`.
- Agent endpoint (`/chromium/agent`) gains `uploadFile` and `getDownloads` methods for file transfers.
- Add autologin profiles powered by 1Password.
- Improve stealth mode.

## v2.106.0

- Dependency updates.
- Updates to `@browserless.io/browserless` at `v2.54.0`.
- Additional per-route metric.
- Disable Chrome optimization-guide model downloads.
- Return `403` when a navigation is blocked.
- Drop Playwright `1.42` support and add Playwright `1.61`.

## v2.105.0

- Dependency updates.
- Add BQL mutations: `emulateMediaType`, `addScriptTag`, `addStyleTag`, `fulfill`, and `markdown`.
- Extend BQL mutations: `goto` accepts `referer`, `waitForSelector` supports `hidden`, `viewport` supports `hasTouch`/`landscape`, and `pdf` supports `fullPage`.
- Fix `acceptInsecureCerts` not taking effect in stealth mode.
- Fix proxied requests.

## v2.104.0

- Add CDP proxy commands `Browserless.uploadFile` and `Browserless.fileDownloaded`.
- Fix BQL stealth `reconnect()`.

## v2.103.0

- Dependency updates.
- Add 1Password credential support.
- Unblock API reconnection fix.
- Fix navigation guards.
- `/export` with `includeResources: true` now bundles cross-origin assets.
- Improve reCAPTCHA detection accuracy.

## v2.102.0

- Dependency updates.
- Updates to `@browserless.io/browserless` at `v2.52.2`.
- Add session-replay support to the `/unblock` endpoint.
- Fix extension loading failures.

## v2.101.0

- Dependency updates.
- Updates to `@browserless.io/browserless` at `v2.52.1`.
- Allow MCP agents to create, save, and reuse browser profiles across sessions.
- Log output re-leveled to surface warnings while reducing routine noise.
- Isolate WebSocket errors in live URL, VHS recording, and CDP proxy sessions.
- Add `WS_MAX_PAYLOAD` env var to raise the WebSocket payload limit for large CDP responses.
- Additional metrics for per-route latency, queue wait, browser acquire time, and capacity gauges.

## v2.100.0

- Dependency updates.
- Updates to `@browserless.io/browserless` at `v2.52.0`.
- Add `solve(type: audio)` BQL mutation for solving reCAPTCHA v2 audio challenges.
- Add datacenter proxy network alongside residential proxies for sessions, REST APIs, scraping, and BQL. Datacenter traffic is billed at a lower unit rate.
- Fix `if`/`ifnot` BQL blocks running mutations in parallel instead of sequentially.
- Fix stealth browser launch hanging when the `blockAds` or `record` is specified.

## v2.99.1

- Bug fixes.
- Dependency updates.

## v2.99.0

- Dependency updates.
- Add a hosted agent endpoint (`/chromium/cli`) where Browserless runs the LLM and drives the browser to complete a task, streaming progress to the CLI; LLM usage is billed as separate agent units.
- Move the `/unblock` endpoint onto the BrowserQL stack, adding configurable waits, an optional WebSocket endpoint, and screenshots that honor the requested image format.
- Add an Arkoselabs/FunCaptcha solver, detect captchas inside same-origin iframes, and improve rotate-captcha accuracy with honest solve reporting.
- Make screenshots and PDFs best-effort after captcha solving, so a post-solve page navigation no longer fails the whole request.
- Improve stealth realism so sessions more closely resemble a real-world browser (on by default).
- Honor the `operator` argument in the `waitForResponse`, `waitForRequest`, `if`, and `ifnot` BQL mutations.
- Return partial results from the BQL `response()` mutation when an individual response body is unavailable.
- Bypass the limiter for BQL reconnect requests so in-progress sessions aren't dropped when a worker is under load.
- Adjust consumption for requests that fail validation (for example a missing `url`).
- Add OpenTelemetry session metrics (`completed`/`rejected`/`active`) and per-endpoint `http.route` span labels.

## v2.98.1

- Updates to `@browserless.io/browserless` at `v2.51.1` — Node.js `24.16.0`, NPM `11.16.0`, and puppeteer-core `25.1.0`.
- New `authenticate` mutation.
- Bug fixes

## v2.98.0

- Dependency updates.
- Add a new `authenticate` BQL mutation for HTTP authentication.
- Improve press and hold captcha solving.
- Fix `snapshot` accessibility attribute type handling.
- Improve secret redaction in OpenTelemetry observability data.

## v2.97.1

- Dependency updates.

## v2.96.3

- Improve Cloudflare and Turnstile captcha solving.
- Improve stealth reliability against Cloudflare for Puppeteer and Playwright.
- Return `429` for new sessions when the worker is at capacity.
- Improve `429` observability in OpenTelemetry traces.
- Add rotate captcha solving.

## v2.96.2

- Improve scrape, map, and search reliability on slow sites.

## v2.96.1

- Improve `/smart-scrape` reliability against protected sites with target-aware proxy selection and improved captcha detection and solving.
- Improve `/unblock` reliability with a request-wide timeout deadline, more robust browser startup, and `waitUntil` support for finer navigation control.
- Fix copy-paste in LiveURL when the viewer is embedded in a cross-origin iframe (such as the Browserless account app).

## v2.96.0

- Improve adblock list build resilience by falling back to a bundled snapshot when the upstream source is unreachable.

## v2.95.2

- Fix in-page WebSocket connections for `/function` and `/download` endpoints being blocked on HTTPS deployments due to mixed-content restrictions.

## v2.95.1

- Dependency updates.
- Add `Browserless.refreshProfile` CDP method to update an existing profile's auth state in place without creating a duplicate.
- Updates to `@browserless.io/browserless` at `v2.49.0`.
- Reduce production Docker image size by removing development tooling from the final image.

## v2.95.0

- Dependency updates.
- Fix billing and `Browser Session Ended` tracking for persistent session and reconnect WebSocket connections (`/session/connect/:id` and `/reconnect/:id`); each WebSocket connection is now billed independently, restoring usage accounting for persistent-session traffic.
- Add `POST /profile/upload` and `POST /profile/refresh` endpoints to create a profile from a pre-captured auth state or replace an existing profile's state in place, with server-side sanitization of cookies, origins, and IndexedDB plus a diagnostics field describing what was filtered or truncated.
- Fix LiveURL OAuth login for authenticated profiles, and default the browser type to `stealth` when creating a profile without specifying one.

## v2.94.1

- Dependency updates.
- Cap live viewers at 5 per session ID on `/live/<id>` and return `429 Too Many Requests` once exceeded, freeing slots automatically when viewers disconnect.
- Include captcha detection and classification in snapshot captures, giving agents better visibility into security challenges encountered during automation.
- Add an optional `profile` parameter to crawl requests for authenticated scraping; crawls now end early on critical errors, truncate oversized error messages, and return `503` if the profile service is unavailable.

## v2.94.0

- Updates to `@browserless.io/browserless` at `v2.48.3`.
- Dependency updates.
- Fix agent snapshot generation when pages contain malformed or empty HTML attributes, so snapshots no longer fail on attribute encoding edge cases.
- Add an optional `profile` parameter to `/scrape` requests for server-side hydration of authenticated browser profiles; requests that use a profile now require an API token and return clearer validation errors.
- LiveURL sessions now show distinct end-of-session messages for timeouts versus manual closures, and the message is delivered before the socket disconnects so users actually see it.
- Credit limit error messages now link to `https://browserless.io/account/upgrade` instead of the generic account page, and have minor grammar fixes.

## v2.93.0

- Update `@browserless.io/browserless` to v2.48.2
- Improve proxy city targeting
- Add tab management methods for agents

## v2.92.3

- Add Akamai captcha solving support.

## v2.92.2

- Improve browser fingerprint consistency for CPU architecture and font rendering in Docker images.
- Dependency updates

## v2.92.1

- Enable agentic browsing for all accounts
- Dependency updates

## v2.92.0

- Add authenticated profiles: capture a logged-in browser's cookies, localStorage, and IndexedDB once via `Browserless.saveProfile`, then reuse on any session with `?profile=<name>`.
- Add HTTP API for profile lifecycle: `POST /profile`, `GET /profiles`, `GET|PUT|DELETE /profile/:name`.

## v2.91.0

- Add route for agentic browsing

## v2.90.6

- Set default `/crawl` scrape timeout to 150s.
- Add `customer.id` to OpenTelemetry resource attributes.
- Add `Content-Disposition` header for PDF API responses.
- Improve captcha handling by hiding the auto-challenge.
- Cache uBOL-home release metadata in the adblock build.

## v2.90.5

- Update search limits: Free 1→3, Prototyping 3→5, Starter 5→10, Scale 10→20.
- Dependency updates.

## v2.90.4

- Improve DataDome captcha solving reliability.

## v2.90.3

- Add captcha solving support for browser-use sessions.

## v2.90.2

- Add DataDome captcha solving support.
- Improve smart-scrape captcha detection and post-challenge navigation.
- Add captcha solving support for browser-use sessions.
- Improve `/crawl` API with enhanced orchestration and documentation.
- Fix timeout override for applicable routes.
- Fix session TTL display for browser sessions.
- Bug fixes and improvements.

## v2.90.1

- Bug fixes and improvements.

## v2.90.0

- New crawl API for scalable web crawling.
- Fix BQL `close()` overwriting reconnect `keepUntil` with zero TTL.

## v2.89.2

- Updates to `@browserless.io/browserless` at `v2.46.0`.
- Fix 400 errors caused by strict `Record<string,string>` types in API schemas.
- Fix Docker multi-heavy base image tagging.
- Dependency updates.

## v2.89.1

- Updates to `@browserless.io/browserless` at `v2.45.0`.
- Fix proxy SSL certificate errors (`ERR_CERT_AUTHORITY_INVALID`).
- Improve proxy vendor health checks and failover reliability.
- Harden URL validation against SSRF and DNS rebinding.
- Fix session TTL management.
- Fix `stopRecording` hanging in certain error conditions.
- Fix recording quality for VHS sessions.
- Unify scrape format types across REST API schemas.

## v2.89.0

- Add metadata enrichment for `/map` endpoint.
- Add option to transfer base64 instead of binary for recordings.
- Fix liveURL blank screen.
- Fix bypass CSP for virtual keyboard and emulated `<select>`s.
- Fix high CPU consumption for `/map`.
- Add `--disable-component-extensions-with-background-pages` to stealth args.

## v2.88.2

- Updates to `browserless.io/browserless` at `v2.43.0`.

## v2.88.1

- New `/map` API.
- Add `emulateComponents` option for liveURL.
- Dependency updates.

## v2.87.0

- Deprecate `codes` in `waitForResponse` mutation, favoring `statuses` instead.
- Fix `<select />` elements in liveURL view.
- Enable uploaded extensions cache.
- New `/search` API.

## v2.86.1

- Power-scrape endpoint is now smart-scrape.
- Improve Turnstile captcha detection and solving.
- Improve session replay in reconnect sessions.
- Improve stealth chromium for persistent sessions.

## v2.86.0

- Updates to `browserless.io/browserless` at `v2.42.0`.
- Updates NodeJS to `24.14.0`.
- Supports `puppeteer-core` version `24.37.5`.
- Dependency updates.
- Added support for loading extensions from session metadata.

## v2.85.1

- Supports HTTPs third party proxies in BQL
- Fixes mobile typing bug in liveURL

## v2.85.0

- Updates to `browserless.io/browserless` at `v2.41.0`.
- Supports `puppeteer-core` version `24.37.4`.
- OTEL support.

## v2.84.2

- Fix power scraper initialization bug

## v2.84.1

- Fix query param validation bug

## v2.84.0

- New power scrape endpoint for intelligent content extraction.
- Fix external proxies in BQL incorrectly charging proxy units.

## v2.83.0

- Fix proxy consumption bug
- Fix screencasting video dimensions
- Improve docker image size

## v2.82.1

- Bug fixes.

## v2.82.0

- Added new proxy vendor.
- New `processKeepAlive` flag.
- Fix screencasting video dimensions.
- `liveURL` connection issues.

## v2.81.0

- Updates to `browserless.io/browserless` at `v2.39.0`.
- Session replay improvements.
- Improve proxy mutations in BQL.
- Additional captchas support.
- Supports `playwright-core` versions `1.41.2`, `1.42.1`, `1.43.1`, `1.44.1`, `1.45.3`, `1.46.1`, `1.47.2`, `1.48.2`, `1.49.1`, `1.50.1`, `1.51.1`, `1.52.0`, `1.53.2`, `1.54.2`, `1.55.1`, `1.56.1`, `1.57.0` and `1.58.1`.
- Supports `puppeteer-core` version `24.36.1`.

## v2.80.1

- Improve CAPTCHA detection and solving.
- Support for external proxy server parameter.

## v2.80.0

- Updates to `browserless.io/browserless` at `v2.38.4`.
- Updates NodeJS to `24.13.0`.
- Improve captcha solving performance.
- Additional captchas support.
- Supports `playwright-core` versions `1.41.2`, `1.42.1`, `1.43.1`, `1.44.1`, `1.45.3`, `1.46.1`, `1.47.2`, `1.48.2`, `1.49.1`, `1.50.1`, `1.51.1`, `1.52.0`, `1.53.2`, `1.54.2`, `1.55.1`, `1.56.1` and `1.57.0`.
- Supports `puppeteer-core` version `24.36.0`.

## v2.79.2

- Improve CAPTCHA detection and solving.

## v2.79.1

- Improve liveURL rendering in high DPI displays.
- Bug fixes.

## v2.79.0

- Improve CAPTCHA detection mechanism.
- Fix bug where deeply nested elements weren't found in nested shadow DOMs

## v2.78.3

- Improved BQL proxy URL regeneration on reconnects
- Streamlined event logging

## v2.78.2

- Improved proxy connection reliability

## v2.78.1

- Upgraded browser tracking TTL

## v2.78.0

- Improved stealth and bot detection evasion capabilities

## v2.77.1

- Fix black screen issue with live urls

## v2.77.0

- New `MAX_RECONNECT_TIME` launch flag
- Ensure the WS connection is always closed when using the `/kill` endpoint
- Improve captcha detection
- Fix bug in live URL that added an offset to mouse events

## v2.76.0

- Improve proxy reliability and performance
- Fix third party proxies for BQL
- Improve captcha solving performance

## v2.75.0

- Improved proxy reliability and performance
- Enhanced captcha detection
- Better stealth mode compatibility
- Session replay improvements

## v2.74.4

- Stealth route enhancements
- Connection stability improvements

## v2.74.3

- Enhanced captcha handling

## v2.74.2

- Bug fixes and improvements

## v2.74.1

- Added proxy provider configuration options

## v2.74.0

- Dependency updates
- Improved captcha support for BQL
- Performance optimizations

## v2.73.2

- Improve Resdential Proxy Quaility

## v2.73.1

- Minor improvements in liveURL client

## v2.73.0

- Add `proxyLocaleMatch` parameter to set browser language based on proxy country
- Fix BQL session replay

## v2.72.1

- Deprecate `verify` mutation
- Improve captcha callback function caller
- Improve internal logs

## v2.72.0

- Added new proxy vendor
- New `solveImageCaptcha` mutation
- Better handle proxy requests that contains cities

## v2.71.1

- Fix bug in `/unblock` API where it errored if the `screenshot` parameter was not set
- Allow all paid accounts to use the screencasting API

## v2.71.0

- Implemented new proxy provider
- Improved Cloudflare CAPTCHA solver

## v2.70.2

- Updated proxy provider priority

## v2.70.1

- Improved recaptcha-v3 solver for speed and reliability improvements
- Added `/proxy/cities` API endpoint for getting a list of supported proxy cities
- Improved selectors for normal captcha, and Cloudflare captcha

## v2.70.0

- Improved recaptcha v3 support
- Add fallback between captcha solvers
- Improve event logging

## v2.69.1

- Dependency updates.
- Improved recaptcha v2 support in stealth routes

## v2.69.0

- Match the browsers to the stealth endpoints.
- Cloudflare captcha tracking improvements

## v2.68.5

- Added support for Turnstile Cloudflare captcha
- Fixed amplitude attempt events to track Cloudflare captchas correctly
- Added captcha subtype to amplitude events

## v2.68.4

- clean disk space before building docker images

## v2.68.3

- Restore automation of timezone and add proxy city validation changes

## v2.68.1

- Dependency updates.
- Bug fixes and improvements

## v2.68.0

- Dependency updates.
- Allow `--disable-bundled-ppapi-flash`, `--disable-dev-shm-usage`, `--disable-domain-reliability`, `--disable-gpu`, `--disable-speech-api`, `--disable-webgl`, and `--wm-window-animations-disabled` flags to be used.
- Improve CAPTCHA solvers.

## v2.67.1

- Bug fixes and improvements

## v2.67.0

- Updates to `browserless.io/browserless` at `v2.38.2`.
- Supports `playwright-core` versions `1.41.2`, `1.42.1`, `1.43.1`, `1.44.1`, `1.45.3`, `1.46.1`, `1.47.2`, `1.48.2`, `1.49.1`, `1.50.1`, `1.51.1`, `1.52.0`, `1.53.2`, `1.54.2`, and `1.55.1`.
- Supports `puppeteer-core` version `24.26.1`.
- Allow using Brave on BQL
- Captcha selector fixes

## v2.66.2

- Critical security bug fix

## v2.66.1

- Critical security bug fix

## v2.66.0

- Revert proxy city and timezone changes
- Upgrade Playwright, Puppeteer and Browserless
- Upgrade Chrome to 141.0
- Support for Capy challenges

## v2.65.2

- Improve CAPTCHA detection and solving.

## v2.65.1

- Add city database for proxy validation
- Validate cities against supported proxy providers
- Automate timezone based on proxy location

## v2.65.0

- Dependency updates
- New `@export` directive
- Fix `timeout` in solve captcha mutations
- Normalize allowed cities for proxies
- Fix Amplitude IDs for sessions

## v2.64.3

- Fix Amplitude logging error

## v2.64.1

- Allow using `solveCaptchas` on stealth routes.

## v2.64.0

- Dependency updates.
- New feature: auto solve captcha.
- Improve captcha resolution state polling.
- Fix captcha solving edge cases.

## v2.63.3

- Dependency updates.
- Log more event properties on `Captcha Solve Error` events.
- Allow sending `acceptInsecureCerts` as query param.
- Improve captcha-solving parameter sanitation.

## v2.63.2

- Dependency updates.
- Updates to @browserless.io/browserless 2.37.1.

## v2.63.1

- Dependency updates.
- Switches to `ioredis` for cloud-unit platform.

## v2.63.0

- Dependency updates.
- Major `/stealth` route enhancements.
- Many testing updates for internal stability.
- Fixes BrowserQL `goto` calls that return a chrome-related error.
- Other fixes and updates for stealth and captcha solving.

## v2.62.1

- Dependency updates.
- Better timeout messages on cloud platform.
- Other testing fixes.

## v2.62.0

- Dependency updates.
- Update to @browserless.io/browserless 2.37.0.
- Minor build updates and unit-test enhancements.
- BrowserQL `click` mutation fixes and reliability improvements.
- Fix /pdf, /screenshot and other REST APIs with requestInterceptors that contain base64 responses.
- Other minor fixes and improvements.

## v2.61.2

- Dependency updates.
- Cleanup of internal docs.
- Fixes and issue with /unblock API's `waitForFn` properties.

## v2.61.1

- Dependency updates.
- Fixes an issue where /unblock might orphan the browser process.
- Logging and analytics improvements in cloud.

## v2.61.0

- Dependency updates.
- Update to Ubuntu 24.04.
- Update @browserless.io/browserless to 2.36.0.
- New `switchToWindow` BrowserQL API.
- Stealth enhancements and updates.
- Prettier changes on source, logging cleanup.

## v2.60.1

- Dependency updates.
- BrowserQL documentation updates and enhancements.
- Analytical improvements on the cloud-unit platform.

## v2.60.0

- Dependency updates.
- Replay feature is now shipped for cloud-unit.
- Documentation cleanup and updates.
- Updated request parsing and parameter handling.
- Hybrid URL logging clean up.
- Stealth enhancements and improvements.

## v2.59.0

- Dependency updates.
- Stealth updates to all stealth enabled routes.
- Fixes and updates to our captcha-solving capabilities.
- Use `npm ci` for production builds (prevents unintentional package updates).
- Other fixes and updates.

## v2.57.0

- New fingerprint management and rotation for browserless' `/stealth` CDP routes!
- Updates to browserless.io/browserless@`2.34.1`
- Supports `playwright-core` versions `1.41.2`, `1.42.1`, `1.43.1`, `1.44.1`, `1.45.3`, `1.46.1`, `1.47.2`, `1.48.2`, `1.49.1`, `1.50.1`, `1.51.1`, `1.52.0`, `1.53.1`, and `1.54.2`.
- Supports `puppeteer-core` version `24.16.2`.
- Captcha solving updates and improvements.

## v2.56.0

- Updates to `browserless.io/browserless` at `v2.34.0`.
- Supports `playwright-core` versions `1.41.2`, `1.42.1`, `1.43.1`, `1.44.1`, `1.45.3`, `1.46.1`, `1.47.2`, `1.48.2`, `1.49.1`, `1.50.1`, `1.51.1`, `1.52.0`, `1.53.1`, and `1.54.2`.
- Supports `puppeteer-core` version `24.16.1`.
- Supports using of Chrome Extensions in cloud-unit products. Please refer to documentation on how to use this new feature.
- Sessions API now support interoperability with BrowserQL.
- Introduces a new vendor for Captcha Solving.
- New environment variables:
  - `EXTENSIONS_DIR` The directory to store extensions onto.
  - `LOAD_EXTENSIONS_FROM_CLOUD` Whether or not to load extensions from a S3 compatible URL.
  - `EXTENSIONS_CLOUD_BUCKET` The bucket for extensions to be loaded from.
  - `EXTENSIONS_CLOUD_REGION` The region to load extensions from.
  - `CAPSOLVER_API_KEY` An API key to use for when Capsolver is used for captcha solving.

## v2.55.0

- Dependency updates.
- Reverts some breaking stealth changes in favor of stability.
- Adds a "mode" option in BQL's `removeAttributes` parameter for better HTML parsing.

## v2.54.0

- Dependency updates.
- New LiveURL updates that include an optional `showBrowserInterface` for rendering all the tabs.
- Fixes some issues with stealth and headers that Chrome generates.

## v2.53.1

- Revert live-URL updates until they're backwards compatible.

## v2.53.0

- Dependency updates.g
- New `viewport` API for BQL.
- Consolidation of some internal utilities for consistency sake.
- Big improvements to the `/stealth` routes for CDP libraries.
- You can `solveCaptcha` without specifying the vendor in BQL.
- Support for proxying through residential proxy providers.
- Fixes an issue in BQL that can cause a browser to stay open when navigating.
- `LiveURL` can now support multi-tabs workflows.
- Testing fixes.

## v2.52.4

- Fixes an issue when deleting sessions

## v2.52.3

- Updates to proxying for future compatibility

## v2.52.2

- Fixes an issue where proxying can cause un-necessary latency

## v2.52.1

- Fix BQL navigation not filtering out iframe page events.

## v2.52.0

- Dependency updates.
- Fixes navigation (`goto`, `back`, `forward`) issues in BQL for goto, forward and backward actions.
- `blockConsentModal` is now default to `false` in BQL as it sometimes causes sites to hang indefinitely when loading.
- New `amazonWaf` for BQL captcha solving.
- New downloading handling forcing all downloads to be in a configurable path by setting a `DOWNLOAD_DIR`.
- Fix `page`'s created by playwright's `context` object hanging.
- Fix issues with CDP-based screen-recording.
- Other fixes, improvements, and reliability enhancements.
- Bumps `puppeteer-core` to `24.12.1`.
- Bumps `playwright-core` to `1.54.1`.
- Drops support for `playwright-core` at `1.49`.
- Supports:
  - puppeteer-core: `24.12.1`
  - playwright-core: `1.41.2`, `1.42.1`, `1.43.1`, `1.44.1`, `1.45.3`, `1.46.1`, `1.47.2`, `1.48.2`, `1.49.1`, `1.50.1`, `1.51.1`, `1.52.0`,
  - Chromium: `139.0.7258.5`
  - Firefox: `140.0.2`
  - Webkit: `26.0`
  - Chrome: `138.0.7204.101` (amd64 only)
  - Edge: `138.0.3351.83` (amd64 only)

---

# Production Best Practices

URL: https://docs.browserless.io/enterprise/docker/best-practices

Production best practices for deploying and managing Browserless Enterprise Docker containers at scale.

- A Browserless Enterprise license
- Docker installed on your infrastructure

## Security Hardening

### Use Secrets Management

Never store sensitive credentials in environment variables or code. Use Docker secrets or a secrets management system:

```yaml
# Docker Compose with secrets
services:
  browserless:
    image: registry.browserless.io/browserless/browserless/enterprise:latest
    secrets:
      - browserless_key
      - browserless_token
    environment:
      - KEY_FILE=/run/secrets/browserless_key
      - TOKEN_FILE=/run/secrets/browserless_token

secrets:
  browserless_key:
    file: ./secrets/license_key.txt
  browserless_token:
    file: ./secrets/api_token.txt
```

### Network Isolation

Isolate Browserless containers from external networks and use a reverse proxy for external access:

```yaml
services:
  browserless:
    networks:
      - internal
    # Don't expose ports externally; use reverse proxy

  nginx:
    networks:
      - internal
      - external
    ports:
      - "443:443"

networks:
  internal:
    internal: true
  external:
```

### Minimize Attack Surface

Disable unnecessary features to reduce potential security vulnerabilities:

```bash
# Disable unnecessary features
-e ALLOW_GET=false \
-e ALLOW_FILE_PROTOCOL=false \
-e ENABLE_CORS=false
```
## Performance Tuning

### Tune concurrency settings

Match your concurrency settings to your workload patterns and available resources. Key settings include `CONCURRENT` (active sessions), `QUEUED` (waiting requests, typically 1.5-2x concurrent), and `TIMEOUT` (session timeout in milliseconds). See the [Configuration Reference](/enterprise/docker/config#essential-configuration) for detailed guidance on these settings.

### Resource Limits

Define resource limits to prevent containers from consuming excessive resources:

```yaml
deploy:
  resources:
    limits:
      cpus: '8'
      memory: 16G
    reservations:
      cpus: '4'
      memory: 8G
```

**Sizing Guide:**
- **Light workload** (5-10 concurrent): 2 CPU, 4GB RAM
- **Medium workload** (10-20 concurrent): 4 CPU, 8GB RAM
- **Heavy workload** (20-50 concurrent): 8+ CPU, 16GB+ RAM

### Connection Pooling

Reuse browser contexts instead of creating new browsers for better performance:

```javascript
// Good: Reuse browser connection and create multiple pages
const browser = await puppeteer.connect({
  browserWSEndpoint: 'ws://localhost:3000?token=your-token-here'
});
const page1 = await browser.newPage();
const page2 = await browser.newPage();

// Avoid: Creating new browser connections repeatedly
// const browser1 = await puppeteer.connect(...);
// const browser2 = await puppeteer.connect(...);
```

### Queue Rejection Behavior

When `QUEUED` is set to `0`, any request that arrives while all `CONCURRENT` slots are occupied is immediately rejected with HTTP 429. Set `QUEUED` to a value greater than 0 (e.g., `10`, the default) to absorb traffic bursts.

### Health Check Threshold Behavior

`MAX_MEMORY_PERCENT` and `MAX_CPU_PERCENT` (when `HEALTH=true`) only reject new incoming sessions. Running sessions continue until they finish or time out. If memory exceeds the threshold from a running session, the OS OOM killer may terminate the container. Set thresholds conservatively (e.g., `80`) to leave headroom.

### Scaling Out vs Scaling Up

When you need more capacity, add more containers behind a load balancer rather than increasing `CONCURRENT` on a single instance. Each Chrome process competes for CPU and memory. See the [NGINX Load Balancing](/enterprise/docker/nginx-load-balancing) guide.

### Performance Tips
- Use `--shm-size=2g` (or `shm_size: '2g'` in Compose) to give Chrome enough shared memory in containers. Avoid the `--disable-dev-shm-usage` Chrome flag, which forces Chrome to write to `/tmp` instead of `/dev/shm` and can degrade performance.
- Enable `HEALTH` checks to prevent overload (reject requests when resources are high)
- Monitor `/pressure` endpoint to track system load
- Use headless mode (default) for better performance

## Monitoring & Observability

### Health Checks

Implement Docker health checks using the `/pressure` endpoint to monitor container availability and resource usage. Configure the `HEALTH` environment variable to enable pre-request health checks that reject requests when CPU or memory thresholds are exceeded. See the [Configuration Reference](/enterprise/docker/config#health--monitoring) for all health check options.

### Metrics Collection

Persist metrics for long-term analysis and alerting:

```bash
# Persist metrics
-e METRICS_JSON_PATH=/metrics/metrics.json \
-v ./metrics:/metrics

# Query metrics endpoint
curl http://localhost:3000/metrics
```

**Key Metrics to Monitor:**
- **Session metrics**: Total sessions, successful, failed, timeouts
- **Queue metrics**: Queue depth, queue wait time, rejections
- **Resource metrics**: CPU usage, memory usage, disk usage
- **Performance metrics**: Average session duration, p95/p99 latency

### Webhook Monitoring

Configure webhooks to receive real-time alerts for critical events like queuing, timeouts, errors, and health failures. See the [Webhooks guide](/enterprise/docker/webhooks) for complete configuration details and payload examples.

### Logging Best Practices

Configure appropriate logging levels for production:

```bash
# Production: Minimal logging
-e DEBUG=browserless:*

# Debugging: Verbose logging (temporary only)
-e DEBUG=*

# Turn off debug logs
-e DEBUG=-*
```

> **Warning**
> Avoid running `DEBUG=*` in production as it generates excessive logs and impacts performance.

## High Availability

### Multi-Instance Deployment

Deploy multiple Browserless containers behind a load balancer for redundancy and horizontal scale. This architecture provides:

- **High availability**: Automatic failover if an instance becomes unhealthy
- **Load distribution**: Balance traffic across instances based on connection count or other metrics
- **Zero downtime deployments**: Update instances one at a time without service interruption
- **Horizontal scaling**: Add capacity by deploying more instances

For complete setup instructions including Docker Compose configurations, NGINX setup, health checks, and SSL configuration, see the [NGINX Load Balancing](/enterprise/docker/nginx-load-balancing) guide.

### Auto-Scaling Strategies

**Horizontal Scaling:**
- Monitor queue depth via `/pressure` endpoint
- Scale up when queue is consistently high
- Scale down when instances are underutilized

**Vertical Scaling:**
- Increase `CONCURRENT` setting when adding resources
- Monitor CPU/memory to determine when to scale vertically

**Kubernetes Auto-Scaling:**
```yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: browserless-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: browserless
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
```

## Data Persistence

### Volume Management

Define volumes for persistent data:

```yaml
volumes:
  # User data (cookies, local storage)
  - browserless_data:/user_data

  # Downloaded files
  - browserless_downloads:/downloads

  # Metrics history
  - browserless_metrics:/metrics

  # Custom fonts
  - ./fonts:/usr/share/fonts/custom:ro

volumes:
  browserless_data:
    driver: local
  browserless_downloads:
    driver: local
  browserless_metrics:
    driver: local
```

### Backup Strategy

Implement regular backups for important data:

```bash
# Backup metrics
docker cp browserless:/metrics/metrics.json ./backups/metrics-$(date +%Y%m%d).json

# Backup user data
docker run --rm -v browserless_data:/data -v $(pwd)/backups:/backup \
  alpine tar czf /backup/user-data-$(date +%Y%m%d).tar.gz /data
```

**Backup Schedule Recommendations:**
- **Metrics**: Daily backups, retain 30 days
- **User data**: Weekly backups, retain 4 weeks
- **Configuration**: On every change, version controlled

### Disaster Recovery

Prepare for disaster recovery:

1. **Document your setup**: Keep docker-compose files and configurations in version control
2. **Test restores regularly**: Verify backups can be restored successfully
3. **Have fallback instances**: Keep standby instances in different regions/zones
4. **Monitor external dependencies**: Track docker registry availability

## CI/CD Integration

### Automated Testing

Test Browserless integration in your CI pipeline:

```yaml
# GitHub Actions example
jobs:
  test:
    runs-on: ubuntu-latest
    services:
      browserless:
        image: registry.browserless.io/browserless/browserless/enterprise:latest
        ports:
          - 3000:3000
        env:
          TOKEN: test-token
    steps:
      - name: Run browser tests
        run: npm test
        env:
          BROWSERLESS_URL: ws://localhost:3000
```

### Deployment Pipeline

Implement a safe deployment pipeline:

1. **Pull new image** in staging environment
2. **Run smoke tests** against staging
3. **Rolling update** in production (one instance at a time)
4. **Monitor metrics** after deployment
5. **Rollback** if issues detected

### Configuration Management

Use environment-specific configurations:

```bash
# Use different env files per environment
docker run --env-file .env.production ...   # Production
docker run --env-file .env.staging ...      # Staging
docker run --env-file .env.development ...  # Development
```

## Summary Checklist

Before going to production, ensure:

- [ ] Secrets are managed securely (not in plaintext)
- [ ] Network isolation is configured
- [ ] Resource limits are set appropriately
- [ ] Health checks are enabled
- [ ] Monitoring and alerting are configured
- [ ] Multiple instances are deployed (HA)
- [ ] Load balancer is configured with health checks
- [ ] Data volumes are backed up regularly
- [ ] Disaster recovery plan is documented
- [ ] CI/CD pipeline includes Browserless tests
- [ ] Documentation is updated for your team

## Related Guides

- [Docker Configuration Reference](/enterprise/docker/config) - All environment variables
- [NGINX Load Balancing](/enterprise/docker/nginx-load-balancing) - Load balancer setup
- [Webhooks](/enterprise/docker/webhooks) - Event notifications and alerts

## FAQ & Troubleshooting

<details>
<summary>How do I get an Enterprise license?</summary>

Contact [sales@browserless.io](mailto:sales@browserless.io) or visit the [pricing page](https://browserless.io/pricing) for Enterprise plan details.

</details>

<details>
<summary>Can I run Browserless on my own infrastructure?</summary>

Yes. Enterprise customers can self-host using the Docker image. See [Docker deployment](/enterprise/docker/enterprise-image) for setup instructions.

</details>

## Next steps

  <a href="/enterprise/docker/enterprise-image" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Docker deployment</h3>
      self-host Browserless with Docker
    
  </a>
  <a href="/enterprise/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure your Enterprise deployment
    
  </a>

---

# Migrating from Cloud to Self-Hosted

URL: https://docs.browserless.io/enterprise/docker/cloud-to-self-hosted

This guide covers the differences between Browserless Cloud and a self-hosted Enterprise deployment, and walks through the steps to migrate.

- An existing Browserless Cloud account to migrate from
- A Browserless Enterprise license for the self-hosted deployment
- Docker installed on your infrastructure

## What Changes

- **Connection URLs** change from `wss://production-sfo.browserless.io` (or other regional endpoints) to your self-hosted address.
- **Authentication**: Cloud uses a cloud API token tied to your account. Self-hosted uses the `TOKEN` env var you configure.
- **EXTERNAL env var**: Set `EXTERNAL` to your public-facing URL. Without it, reconnect and LiveURL URLs contain `localhost:3000`, which is unreachable by clients. This is the most common migration pitfall.
- **No residential proxies by default**: Cloud provides managed proxy vendors. Self-hosted does not include proxy credentials. Bring your own proxy server and configure it per-request via BQL `proxy()` mutation or launch args. Browserless also sells proxy access for self-hosted customers. [Contact sales](https://www.browserless.io/contact) for details.
- **Metrics are local**: Use the `/metrics` endpoint or configure [OpenTelemetry](/enterprise/docker/opentelemetry). There is no cloud dashboard for self-hosted.
- **Infrastructure is yours**: You manage scaling, updates, container orchestration, and infrastructure.

## What Stays the Same

- All API endpoints (REST, WebSocket, BQL) use identical paths and request formats.
- CDP commands (`Browserless.reconnect`, `Browserless.liveURL`, `Browserless.closeLiveURL`) work the same.
- BQL queries and mutations require no changes.
- Session management patterns (reconnect lifecycle, session persistence) are identical.
- Connection URL format is the same. Only the host portion changes.

## Migration Checklist

1. Pull the enterprise image and authenticate with the private registry (see [Deployment Guide](/enterprise/docker/enterprise-image)).
2. Set `KEY` (license key) and `TOKEN` (API auth) env vars.
3. Set up additional tokens with appropriate roles if multiple teams will access the instance (see [Token Roles](/enterprise/token-roles)).
4. Set `EXTERNAL` to your public-facing URL, including protocol and port if non-standard.
5. Add `--shm-size=2g` or `--ipc=host` to your Docker run command for Chrome shared memory.
6. Test all endpoints: health check (`/pressure`), WebSocket connection, REST APIs, BQL.
7. Update all client connection URLs from cloud endpoints to your self-hosted address.
8. If using proxies, configure your own proxy server in BQL or launch args.
9. Align reverse proxy/load balancer timeouts with the `TIMEOUT` env var (see [Timeouts](/enterprise/private-deployment/timeouts)).

## Common Pitfalls

**Missing `EXTERNAL`**: Reconnect URLs and LiveURL links contain `localhost:3000`, which is unreachable from client machines.

**Missing `--shm-size=2g`**: Chrome crashes with "out of memory" errors under load. Docker defaults shared memory to 64MB, which is not enough for Chrome.

**Reverse proxy idle timeout shorter than session timeout**: Connections drop silently with no error code. Set your proxy's read timeout above the `TIMEOUT` env var value (see [Timeouts](/enterprise/private-deployment/timeouts)).

## FAQ & Troubleshooting

<details>
<summary>How do I get an Enterprise license?</summary>

Contact [sales@browserless.io](mailto:sales@browserless.io) or visit the [pricing page](https://browserless.io/pricing) for Enterprise plan details.

</details>

<details>
<summary>Can I run Browserless on my own infrastructure?</summary>

Yes. Enterprise customers can self-host using the Docker image. See [Docker deployment](/enterprise/docker/enterprise-image) for setup instructions.

</details>

## Next steps

  <a href="/enterprise/docker/enterprise-image" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Docker deployment</h3>
      self-host Browserless with Docker
    
  </a>
  <a href="/enterprise/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure your Enterprise deployment
    
  </a>

---

# Docker Configuration Reference

URL: https://docs.browserless.io/enterprise/docker/config

This page is the complete reference for configuring Browserless Docker containers via environment variables, covering authentication, session limits, storage, security, and monitoring. It applies to both open-source and Enterprise deployments; for a step-by-step deployment walkthrough, see the [Enterprise Deployment Guide](/enterprise/docker/enterprise-image).

- Docker installed on your infrastructure
- A Browserless Enterprise license (for Enterprise-only variables like `KEY`)

> **Coming from v1?**
> Many v1 environment variables (`DEFAULT_HEADLESS`, `DEFAULT_LAUNCH_ARGS`, `DEFAULT_STEALTH`, `PREBOOT_CHROME`, and others) were removed in v2, and their per-request replacements live in your connect calls instead. The container logs deprecated names on startup. See the [v1 to v2 migration guide](/enterprise/migrate-from-v1) for the full mapping to their per-request replacements.

### Enterprise License & Authentication

| Variable | Description | Default | Required | Example |
|----------|-------------|---------|----------|---------|
| `KEY` | Enterprise license key (activates enterprise features) | - | Enterprise only | `ent_abc123...` |
| `TOKEN` | API authentication token for client requests. If not set, all endpoints are unauthenticated. Set TOKEN for any deployment accessible beyond localhost. | (none) | Recommended | `my-secure-token` |

> **KEY vs TOKEN**
> - **`KEY`**: Validates your enterprise license and unlocks enterprise features
> - **`TOKEN`**: Authenticates client API requests (optional for private networks, recommended for production)
> - **Never confuse these**: Using `TOKEN` instead of `KEY` will fail to activate enterprise features

**Example:**
```bash
docker run -p 3000:3000 \
  -e "KEY=ent_abc123..." \
  -e "TOKEN=my-secure-token" \
  registry.browserless.io/browserless/browserless/enterprise:latest
```

### Essential Configuration

| Variable | Description | Default | Example |
|----------|-------------|---------|---------|
| `CONCURRENT` / `MAX_CONCURRENT_SESSIONS` | Maximum concurrent browser sessions. Once reached, requests queue until workers are ready. Keep small initially and grow as needed | `10` | `20` |
| `QUEUED` / `QUEUE_LENGTH` | Maximum queued requests before issuing `429` responses. If `CONCURRENT=5` and `QUEUED=5`, then 10 total connections allowed (5 running + 5 pending) | `10` | `30` |
| `TIMEOUT` | Session timeout in milliseconds. Set to `-1` for no timeout (ensure proper cleanup in your scripts!) | `30000` | `300000` |
| `MAX_RECONNECT_TIME` | Maximum allowed reconnection timeout limit (in milliseconds) for browser sessions after disconnection. Controls how long browsers remain available for reconnection via BrowserQL GraphQL or CDP WebSocket reconnect handlers. If unset, defaults to `Infinity` for backward compatibility | `Infinity` | `300000` |
| `PORT` | Internal port for Browserless. Map externally via Docker's `-p` flag (e.g., `-p 8080:3000`) | `3000` | `8080` |
| `HOST` | IP or domain to bind to. The Docker image already sets this to `0.0.0.0`, which accepts connections on every interface inside the container. Reaching it from another container or host still needs a published port (Docker's `-p` flag) and a shared network or suitable routing/firewall rules. The underlying library itself defaults to `localhost`, but that only applies when running outside Docker. | `0.0.0.0` | `192.168.1.1` |

**Example:**
```bash
# Configure for 20 concurrent sessions, 30 queued, 5-minute timeout
docker run -p 3000:3000 \
  -e "CONCURRENT=20" \
  -e "QUEUED=30" \
  -e "TIMEOUT=300000" \
  registry.browserless.io/browserless/browserless/enterprise:latest

# Limit reconnection timeout to 5 minutes (300000ms)
docker run -p 3000:3000 \
  -e "MAX_RECONNECT_TIME=300000" \
  registry.browserless.io/browserless/browserless/enterprise:latest

# Map to external port 8080 and bind to specific host
docker run -p 8080:3000 \
  -e "HOST=192.168.1.1" \
  registry.browserless.io/browserless/browserless/enterprise:latest

# Disable session timeout (use with caution!)
docker run -p 3000:3000 -e "TIMEOUT=-1" registry.browserless.io/browserless/browserless/enterprise:latest
```

> **No Timeout Option**
> You can opt-out of timers by setting `TIMEOUT=-1` for no session timer. Be sure to close connections when not in use to prevent resource exhaustion!

> **Reconnection Timeout Control**
> The `MAX_RECONNECT_TIME` environment variable allows administrators to control how long browser sessions remain available for reconnection after disconnection. This helps manage resource usage in enterprise environments by preventing browsers from staying open indefinitely while waiting for reconnection.
> 
> - **When set**: Any reconnect request (via BrowserQL GraphQL or CDP WebSocket) with a timeout exceeding this limit will be rejected with a clear error message
> - **When unset**: Defaults to `Infinity`, allowing unlimited reconnection timeouts (backward compatible behavior)
> - **Validation**: The limit is enforced in both BrowserQL GraphQL reconnect mutations and CDP WebSocket reconnect handlers
> 
> This setting is separate from `TIMEOUT`, which controls the overall session duration. `MAX_RECONNECT_TIME` specifically limits the reconnection window after a client disconnects.

> **Concurrent Session Best Practice**
> Start with a low `CONCURRENT` value (5-10) and gradually increase it based on your hardware capacity. Running too many concurrent browser sessions can starve system resources. Monitor CPU and memory usage as you scale up.

### Storage & Persistence

| Variable | Description | Default | Example |
|----------|-------------|---------|---------|
| `DOWNLOAD_DIR` | Directory for file downloads. Defaults to `browserless-download-dirs` inside OS temp directory | OS temp dir | `/downloads` |
| `DATA_DIR` | User data directory for cookies, cache, and local storage. Forces all sessions to use same cache unless overridden per-session | OS temp dir | `/user_data` |
| `METRICS_JSON_PATH` | Persistent metrics storage path. Browserless will read on startup and write periodically. Accessible via `/metrics` endpoint | - | `/metrics/metrics.json` |

> **Volume Mounts Required**
> When setting custom directories like `DOWNLOAD_DIR` or `DATA_DIR`, ensure you mount corresponding volumes in your Docker configuration to persist data across container restarts.

**Example:**
```bash
# Set custom download directory with volume mount
docker run -p 3000:3000 \
  -e "DOWNLOAD_DIR=/downloads" \
  -v /host/downloads:/downloads \
  registry.browserless.io/browserless/browserless/enterprise:latest

# Persist user data (cookies, cache) and metrics
docker run -p 3000:3000 \
  -e "DATA_DIR=/user_data" \
  -e "METRICS_JSON_PATH=/metrics/metrics.json" \
  -v /host/user_data:/user_data \
  -v /host/metrics:/metrics \
  registry.browserless.io/browserless/browserless/enterprise:latest
```

### Security Settings

| Variable | Description | Default | Production Recommended |
|----------|-------------|---------|------------------------|
| `ENABLE_CORS` / `CORS` | Enable CORS headers for cross-origin requests | `false` | `false` |
| `ALLOW_GET` | Allow GET requests with URL-encoded JSON in `body` query parameter (vs POST only) | `false` | `false` |
| `ALLOW_FILE_PROTOCOL` | Allow `file://` URLs (disabled by default for security) | `false` | `false` |
| `CORS_ALLOW_ORIGIN` | Allowed CORS origins (when CORS enabled) | `*` | Specific domains |
| `CORS_ALLOW_METHODS` | Allowed HTTP methods (when CORS enabled) | All | `POST` |
| `CORS_MAX_AGE` | CORS preflight cache max age in seconds (when CORS enabled) | `2592000` | `300` |

**Example:**
```bash
# Enable CORS with specific origin and methods
docker run -p 3000:3000 \
  -e "CORS=true" \
  -e "CORS_ALLOW_ORIGIN=https://myapp.com" \
  -e "CORS_ALLOW_METHODS=POST,DELETE" \
  -e "CORS_MAX_AGE=300" \
  registry.browserless.io/browserless/browserless/enterprise:latest

# Enable GET requests (useful for simple testing, not recommended for production)
docker run -p 3000:3000 \
  -e "ALLOW_GET=true" \
  registry.browserless.io/browserless/browserless/enterprise:latest

# Allow file:// protocol (use with caution)
docker run -p 3000:3000 \
  -e "ALLOW_FILE_PROTOCOL=true" \
  registry.browserless.io/browserless/browserless/enterprise:latest
```

> **Production Security**
> For production deployments:
> - Keep `CORS=false` or restrict origins to specific domains
> - Keep `ALLOW_GET=false` (GET requests are less secure than POST)
> - Keep `ALLOW_FILE_PROTOCOL=false` unless absolutely required
> - Configure `TOKEN` for API authentication

### Health & Monitoring

| Variable | Description | Default | Example |
|----------|-------------|---------|---------|
| `HEALTH` / `PRE_REQUEST_HEALTH_CHECK` | Enable health checks before sessions. Returns `503` if CPU/memory thresholds exceeded | `false` | `true` |
| `MAX_MEMORY_PERCENT` | Max memory % threshold before rejecting requests (requires `HEALTH=true`) | `99` | `90` |
| `MAX_CPU_PERCENT` | Max CPU % threshold before rejecting requests (requires `HEALTH=true`) | `99` | `90` |

**Example:**
```bash
# Enable health checks with 80% CPU and memory thresholds
docker run -p 3000:3000 \
  -e "HEALTH=true" \
  -e "MAX_CPU_PERCENT=80" \
  -e "MAX_MEMORY_PERCENT=80" \
  registry.browserless.io/browserless/browserless/enterprise:latest
```

> **When to Use Health Checks**
> Enable health checks (`HEALTH=true`) in production to prevent overloading your instances. This is especially important when running multiple Browserless containers behind a load balancer, as it helps distribute load more effectively.

You can also query current system load at any time via the [`/pressure` endpoint](/enterprise/utility-functions/pressure), which returns CPU, memory, running sessions, and queue depth.

### Logging & Debugging

| Variable | Description | Default | Example |
|----------|-------------|---------|---------|
| `DEBUG` | Debug log patterns using [debug](https://www.npmjs.com/package/debug) module. Use `*` for all logs (very verbose), `-*` to disable, or patterns like `browserless:*,-verbose` | `browserless*` | `browserless:*,-verbose` |
| `TZ` | Container timezone for log timestamps | `UTC` | `America/New_York` |

**Example:**
```bash
# Disable all debug logging
docker run -p 3000:3000 -e "DEBUG=-*" registry.browserless.io/browserless/browserless/enterprise:latest

# Enable all logging (generates lots of output!)
docker run -p 3000:3000 -e "DEBUG=*" registry.browserless.io/browserless/browserless/enterprise:latest

# Set timezone for logs
docker run -p 3000:3000 -e "TZ=America/New_York" registry.browserless.io/browserless/browserless/enterprise:latest
```

> **Verbose Logging**
> Using `DEBUG=*` will generate extensive log output from Browserless and all npm packages. This can significantly impact performance and disk space. Use sparingly and only for troubleshooting.

Browserless uses two logging systems. The `DEBUG` environment variable controls namespace-based log filtering (logs prefixed with `browserless.io:`). The `LOG_LEVEL` environment variable controls severity-based filtering (logs prefixed with the component name like `server:trace:` or `enterprise-hooks:info:`).

`LOG_LEVEL` accepts: `trace` (default), `debug`, `info`, `warn`, `error`, `fatal`. Setting `LOG_LEVEL="info"` suppresses trace and debug messages while keeping info, warnings, and errors.

To reduce log noise in production, a common configuration is:

```bash
LOG_LEVEL=info DEBUG=browserless*,-**:verbose
```

### Logging & Observability

| Variable | Description | Default | Example |
|----------|-------------|---------|---------|
| `LOG_LEVEL` | Application log level. Controls the verbosity of Winston logger output. Values: `trace`, `debug`, `info`, `warn`, `error`, `fatal`, `silent`. | `trace` | `info` |
| `LOG_FORMAT` | Log output format. Set to `json` for structured JSON logs suitable for log aggregation pipelines. | `plain` | `json` |
| `HEARTBEAT_INTERVAL` | WebSocket heartbeat interval in milliseconds. The server sends a heartbeat ping to connected clients at this interval. Reduce this value when running behind load balancers with aggressive idle connection timeouts. | `30000` | `10000` |
| `OTEL_ENABLED` | Enable OpenTelemetry instrumentation. See [OpenTelemetry guide](/enterprise/docker/opentelemetry) for all OTel variables. | `false` | `true` |
| `DISABLE_BLOCKLIST` | Disable the built-in URL blocklist that prevents navigation to localhost, private IPs, and cloud metadata endpoints. | `false` | `true` |

**Example:**
```bash
# Production logging: structured JSON, info level, OTel enabled
docker run -p 3000:3000 \
  -e "LOG_LEVEL=info" \
  -e "LOG_FORMAT=json" \
  -e "OTEL_ENABLED=true" \
  -e "OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4318" \
  registry.browserless.io/browserless/browserless/enterprise:latest
```

### External Address Configuration

When using a load balancer or reverse proxy (like NGINX) in front of Browserless, you need to configure the external address so Browserless can generate correct external URLs (e.g., in the `/sessions` API).

| Variable | Description | Example |
|----------|-------------|---------|
| `EXTERNAL` | Fully-qualified external URL for link generation | `https://browserless.company.com` |

> **Legacy Support**
> The `PROXY_URL` environment variable is still supported for backward compatibility but is deprecated. Use `EXTERNAL` for new deployments.

See the [NGINX Load Balancing](/enterprise/docker/nginx-load-balancing) guide for complete proxy setup instructions.

**Example:**
```bash
# Configure external address for proper link generation in /sessions API
docker run -p 3000:3000 \
  -e "EXTERNAL=https://browserless.company.com" \
  registry.browserless.io/browserless/browserless/enterprise:latest
```

The `/sessions` API will return properly formatted URLs:
```json
[
  {
    "description": "",
    "devtoolsFrontendUrl": "/devtools/inspector.html?wss=browserless.company.com/devtools/page/C6962B3428FC8E42CDA6484AB5B57EAC",
    "id": "C6962B3428FC8E42CDA6484AB5B57EAC",
    "title": "Example Domain",
    "type": "page",
    "url": "https://www.example.com/",
    "webSocketDebuggerUrl": "wss://browserless.company.com/devtools/page/C6962B3428FC8E42CDA6484AB5B57EAC",
    "browserId": "b519351a-355e-47d5-82cc-7c240cfa40f3",
    "browserWSEndpoint": "wss://browserless.company.com/devtools/browser/b519351a-355e-47d5-82cc-7c240cfa40f3",
    "port": "42169",
    "trackingId": null
  }
]
```

### Webhook Alerts

Configure webhook URLs to receive alerts for various events:

| Variable | Trigger Event | Payload Includes |
|----------|---------------|------------------|
| `QUEUE_ALERT_URL` | When requests start queuing | Queue depth, timestamp |
| `REJECT_ALERT_URL` | When requests are rejected (429) | Rejection reason, metrics |
| `TIMEOUT_ALERT_URL` | When sessions timeout | Session info, duration |
| `ERROR_ALERT_URL` | When unhandled errors occur | Error message |
| `FAILED_HEALTH_URL` | When health check fails | CPU, memory, pressure |

See the [Webhooks](/enterprise/docker/webhooks) guide for payload details and examples.

## FAQ & Troubleshooting

<details>
<summary>My enterprise features aren't activating even though I set a token</summary>

You likely set <code>TOKEN</code> instead of <code>KEY</code>. <code>KEY</code> validates your enterprise license and unlocks enterprise features, while <code>TOKEN</code> only authenticates client API requests. Set the <code>KEY</code> variable to your license key to activate enterprise features.

</details>

<details>
<summary>Requests are returning <code>429</code> responses</summary>

The queue is full. Total allowed connections equals <code>CONCURRENT</code> (running) plus <code>QUEUED</code> (pending), so requests beyond that limit are rejected with <code>429</code>. Raise <code>CONCURRENT</code> and <code>QUEUED</code> to fit your hardware, or set a <code>REJECT_ALERT_URL</code> webhook to be notified when rejections start.

</details>

<details>
<summary>Sessions are being killed before my automation finishes</summary>

Sessions end after <code>TIMEOUT</code> milliseconds (default <code>30000</code>). Increase <code>TIMEOUT</code> for long-running jobs, or set <code>TIMEOUT=-1</code> to disable the timer entirely, being sure to close connections yourself to avoid resource exhaustion.

</details>

## Next steps

  <a href="/enterprise/docker/enterprise-image" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Docker deployment</h3>
      Self-host Browserless with Docker
    
  </a>
  <a href="/enterprise/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      Configure your Enterprise deployment
    
  </a>

---

# Adding Custom Routes

URL: https://docs.browserless.io/enterprise/docker/custom-routes

The Browserless SDK lets you register your own HTTP and WebSocket routes alongside the built-in ones, building on the same [extending pattern](/enterprise/docker/extending) used for adding npm modules. This guide walks through building a child Docker image that ships a compiled TypeScript route on top of the Enterprise image.

Common reasons to do this:

- Expose an **unauthenticated healthcheck** for Kubernetes liveness and readiness probes (the built-in `/pressure` endpoint requires a token).
- Add a **custom API** that wraps Browserless functionality with your own logic.
- **Override** a built-in route with one that behaves differently for your deployment.

- A Browserless Enterprise license or self-hosted deployment

## How route loading works

At startup, the SDK walks the directory pointed to by the `BLESS_ROUTES` environment variable. The Enterprise image sets this for you:

```bash
BLESS_ROUTES=${APP_DIR}/node_modules/@browserless.io/browserless/build/routes
```

For every immediate subdirectory, the loader registers anything found under `<group>/http/` and `<group>/ws/`. That means you can drop a new file into a new group (or an existing one) and the runtime will pick it up. No env-var or code changes required.

> **Don't use the `ROUTES` env var**
> Setting `ROUTES` **replaces** the routes directory entirely. It is not additive, so pointing it at your own folder removes every built-in route. Instead, layer your route file into the existing directory as shown below.

## Building a child image with a custom route

This example adds an unauthenticated `/healthz` endpoint that returns `204` when the instance has spare capacity and `503` when it is overloaded. This is suitable for Kubernetes probes that cannot embed an API token.

Create a project directory with a TypeScript route source, build config, and Dockerfile:

```text
custom-routes/
├── Dockerfile
├── package.json
├── tsconfig.json
└── src/
    └── healthz.get.ts
```

Routes extend `HTTPRoute` (or `BrowserHTTPRoute`, `WebSocketRoute`, `BrowserWebsocketRoute`) and export a default class. Setting `auth = false` disables token authentication for this route only.

```ts title="src/healthz.get.ts"

  APITags,
  HTTPRoute,
  Methods,
  Request,
  contentTypes,
  writeResponse,
} from '@browserless.io/browserless';

  name = 'HealthzGetRoute';
  accepts = [contentTypes.any];
  auth = false;
  browser = null;
  concurrency = false;
  contentTypes = [contentTypes.text];
  description = 'Unauthenticated liveness/readiness probe for Kubernetes.';
  method = Methods.get;
  path = '/healthz';
  tags = [APITags.management];

  async handler(_req: Request, res: ServerResponse): Promise<void> {
    const monitoring = this.monitoring();
    const limiter = this.limiter();
    const { cpuOverloaded, memoryOverloaded } = await monitoring.overloaded();
    const ready = limiter.hasCapacity && !cpuOverloaded && !memoryOverloaded;

    return writeResponse(res, ready ? 204 : 503, '');
  }
}
```

The naming convention is `{name}.{method}.ts`. The loader uses the file suffix (`.http.ts`, `.ws.ts`, or a method like `.get.ts` / `.post.ts`) to register the route correctly.

The SDK is only needed at compile time for type resolution; the runtime imports against the copy already installed in the Enterprise image.

```json title="package.json"
{
  "name": "browserless-custom-routes",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "build": "tsc"
  },
  "devDependencies": {
    "@browserless.io/browserless": "2.48.2",
    "@types/node": "^24.0.0",
    "typescript": "^5.5.0"
  }
}
```

```json title="tsconfig.json"
{
  "compilerOptions": {
    "rootDir": "src",
    "outDir": "build",
    "module": "es2022",
    "moduleResolution": "Bundler",
    "target": "es2022",
    "lib": ["dom", "es2022"],
    "types": ["node"],
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "resolveJsonModule": true
  },
  "include": ["src/**/*"]
}
```

Pin the SDK version to the one shipped in the Enterprise image you are extending so the types match.

A multi-stage build keeps `node_modules` and the TypeScript compiler out of the final image.

```dockerfile title="Dockerfile"
# syntax=docker/dockerfile:1.6

# Args used in a FROM must be declared before the first FROM.
ARG ENTERPRISE_IMAGE=registry.browserless.io/browserless/browserless/enterprise:latest

# ---- Stage 1: compile the TS route against the SDK types ----
FROM node:24-alpine AS build

WORKDIR /work

COPY package.json package-lock.json* ./
RUN npm install --no-audit --no-fund --ignore-scripts

COPY tsconfig.json ./
COPY src ./src

RUN npx tsc

# ---- Stage 2: layer the compiled route onto the Enterprise image ----
FROM ${ENTERPRISE_IMAGE}

USER root

# BLESS_ROUTES is set by the Enterprise image and points at the SDK's
# build/routes dir. Dropping a file under a new "custom" group means the
# runtime auto-discovers it without overriding the built-in route tree.
RUN mkdir -p ${BLESS_ROUTES}/custom/http \
 && chown -R blessuser:blessuser ${BLESS_ROUTES}/custom

COPY --from=build --chown=blessuser:blessuser \
  /work/build/healthz.get.js \
  ${BLESS_ROUTES}/custom/http/healthz.get.js

USER blessuser
```

Pin `ENTERPRISE_IMAGE` to a specific tag for reproducibility.

After authenticating to the Browserless registry, build the image:

```sh
docker login registry.browserless.io
docker build -t projects/browserless-healthz:latest .
```

Run the image with the same environment variables you use for the base Enterprise image:

```sh
docker run -p 3000:3000 \
  -e KEY=your-license-key \
  -e TOKEN=your-api-token \
  projects/browserless-healthz:latest
```

Verify the route responds without a token:

```sh
curl -v http://localhost:3000/healthz

# * Host localhost:3000 was resolved.
# ...
# > GET /healthz HTTP/1.1
# > Host: localhost:3000
# ...
# < HTTP/1.1 204 No Content
# < Content-Type: text/plain; charset=UTF-8
# ...
```

Existing built-in routes still require a token and continue to work unchanged.

## Consuming REST parameters

Routes can match dynamic path segments and read them inside the handler.

> **Use Globs for matching**
> The router uses [micromatch](https://github.com/micromatch/micromatch) glob syntax, not Express-style `:id` placeholders. A path like `/users/:id` is expressed as `/users/+([0-9a-zA-Z-_])?(/)`. The `+([...])` group matches one-or-more allowed characters, and the trailing `?(/)` makes the optional slash forgiving.

The SDK ships a `getFinalPathSegment` helper that pulls the last segment off the URL. This example proxies a request to [JSONPlaceholder](https://jsonplaceholder.typicode.com) using the ID from the URL:

```ts title="src/users.get.ts"

  APITags,
  BadRequest,
  HTTPRoute,
  Methods,
  NotFound,
  Request,
  contentTypes,
  getFinalPathSegment,
  jsonResponse,
} from '@browserless.io/browserless';

  name = 'UsersGetRoute';
  accepts = [contentTypes.any];
  auth = true;
  browser = null;
  concurrency = false;
  contentTypes = [contentTypes.json];
  description = 'Fetch a user by ID from the upstream JSONPlaceholder API.';
  method = Methods.get;
  path = '/users/+([0-9a-zA-Z-_])?(/)'; // This is NOT a RegEx, but a micromatch glob pattern.
  tags = [APITags.management];

  async handler(req: Request, res: ServerResponse): Promise<void> {
    const id = getFinalPathSegment(req.parsed.pathname);

    if (!id) {
      throw new BadRequest('Missing user id in path');
    }

    const upstream = await fetch(
      `https://jsonplaceholder.typicode.com/users/${id}`,
    );

    if (upstream.status === 404) {
      throw new NotFound(`No user found with id "${id}"`);
    }

    const user = await upstream.json();
    return jsonResponse(res, 200, user);
  }
}
```

> **Error classes throw the right HTTP code**
> `BadRequest`, `NotFound`, `Unauthorized`, `Timeout`, `TooManyRequests`, and `ServerError` are all exported from the SDK. Throwing one returns the matching status without manual `writeResponse` calls

Compile-and-drop is identical to the healthcheck route, just add `users.get.ts` next to `healthz.get.ts`, build the image, and the loader picks it up.

Test it against a running container:

```sh
curl "http://localhost:3000/users/1?token=your-api-token"

# {
#   "id": 1,
#   "name": "Leanne Graham",
#   "username": "Bret",
#   // ...
# }
```

## Adding more route types

The same pattern works for any route primitive exported from the SDK:

| Class                    | Use for                                        |
| ------------------------ | ---------------------------------------------- |
| `HTTPRoute`              | REST endpoints with no browser dependency      |
| `BrowserHTTPRoute`       | REST endpoints that launch a browser           |
| `WebSocketRoute`         | WebSocket endpoints with no browser dependency |
| `BrowserWebsocketRoute`  | WebSocket endpoints that connect to a browser  |

File suffix conventions: `*.http.ts` and `*.ws.ts`, or a more specific `{name}.{method}.ts` (e.g. `metrics.get.ts`). Drop the compiled output under `${BLESS_ROUTES}/<group>/http/` or `${BLESS_ROUTES}/<group>/ws/` to register it.

For configuration variables that affect the runtime environment, see the [Docker configuration reference](/enterprise/docker/config). For deployment best practices including health checks and scaling, see [best practices](/enterprise/docker/best-practices).

## FAQ & Troubleshooting

<details>
<summary>How do I get an Enterprise license?</summary>

Contact [sales@browserless.io](mailto:sales@browserless.io) or visit the [pricing page](https://browserless.io/pricing) for Enterprise plan details.

</details>

<details>
<summary>Can I run Browserless on my own infrastructure?</summary>

Yes. Enterprise customers can self-host using the Docker image. See [Docker deployment](/enterprise/docker/enterprise-image) for setup instructions.

</details>

## Next steps

  <a href="/enterprise/docker/enterprise-image" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Docker deployment</h3>
      self-host Browserless with Docker
    
  </a>
  <a href="/enterprise/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure your Enterprise deployment
    
  </a>

---

# Enterprise Deployment Guide

URL: https://docs.browserless.io/enterprise/docker/enterprise-image

The Browserless Enterprise Docker image provides production-grade browser automation with advanced session management, monitoring, and multi-browser support.

The Enterprise image includes:

- **Advanced Session Management**: Enhanced queueing, priority handling, and session persistence
- **Production Monitoring**: Built-in metrics, health checks, and performance tracking
- **Enterprise Support**: Direct access to our engineering team for deployment assistance
- **Custom Features**: Session recording, live debugging, and advanced webhook integrations
- **License-Based Deployment**: Flexible licensing for private deployments without usage-based billing
- **Multi-Browser Support**: Chrome, Chromium, Firefox, WebKit, and Edge in a single container

- Docker 20.10+, Podman, or Kubernetes
- An Enterprise license key from [sales@browserless.io](mailto:sales@browserless.io)
- Registry credentials from the Browserless team for accessing the private registry
- Minimum 2 CPU cores and 4GB RAM (see [Resource Requirements](#resource-requirements))

## Getting Started

Authenticate with the Browserless private registry using credentials provided by our team:

```bash
docker login registry.browserless.io
```

Enter your username and password when prompted.

> **Info**
> Your registry credentials are separate from your license key. The credentials grant access to pull the image, while the license key activates the enterprise features at runtime.

Pull the latest enterprise image (supports both ARM64 and AMD64 architectures):

```bash
docker pull registry.browserless.io/browserless/browserless/enterprise:latest
```

For production deployments, we recommend pinning to a specific version:

```bash
# Pin to a specific version for stability
docker pull registry.browserless.io/browserless/browserless/enterprise:2.3.0
```

Run the enterprise image with minimal configuration:

```bash
docker run \
  --rm \
  -p 3000:3000 \
  -e KEY=your-enterprise-license-key \
  registry.browserless.io/browserless/browserless/enterprise:latest
```

> **Shared memory (`/dev/shm`)**
> Chrome uses `/dev/shm` for shared memory. In Docker, `/dev/shm` defaults to 64MB, which can cause Chrome crashes or instability under load.
> For production workloads, increase shared memory with `--shm-size=2g`. In some environments, `--ipc=host` can also help, but it shares the host IPC namespace and may be less desirable from an isolation standpoint.

Once running, verify the deployment:
- **API Documentation**: [http://localhost:3000/docs](http://localhost:3000/docs)
- **Health Check**: [http://localhost:3000/pressure](http://localhost:3000/pressure)
- **Metrics**: [http://localhost:3000/metrics](http://localhost:3000/metrics)

### Setting Your License Key

Your Enterprise license key (`KEY`) and API token (`TOKEN`) are essential for running the Enterprise image. Below are examples showing different ways to configure these variables. For detailed information on these and other configuration options, see the [Configuration Reference](/enterprise/docker/config).

  
**Docker Run:**

```bash
docker run \
  -e KEY=your-enterprise-license-key \
  -e TOKEN=your-secure-api-token \
  -p 3000:3000 \
  registry.browserless.io/browserless/browserless/enterprise:latest
```
  
  
**Environment File:**

    Create a `.env` file:
```bash
KEY=your-enterprise-license-key
TOKEN=your-secure-api-token
CONCURRENT=20
```

    Run with the env file:
```bash
docker run --env-file .env -p 3000:3000 \
  registry.browserless.io/browserless/browserless/enterprise:latest
```
  
  
**Docker Compose:**

```yaml
services:
  browserless:
    image: registry.browserless.io/browserless/browserless/enterprise:latest
    environment:
      - KEY=${BROWSERLESS_LICENSE_KEY}
      - TOKEN=${BROWSERLESS_API_TOKEN}
    ports:
      - "3000:3000"
```
  

## Production Deployment
For production deployments, it's essential to configure the Enterprise image for performance, security, and reliability. Below are recommended configurations and best practices.

### Docker Compose (Recommended)

For production deployments, we recommend using Docker Compose with comprehensive configuration:

  
**Production Setup:**

```yaml
---
services:
  browserless:
    image: registry.browserless.io/browserless/browserless/enterprise:2.3.0
    container_name: browserless-enterprise
    restart: unless-stopped

    environment:
      # License & Authentication
      - KEY=${BROWSERLESS_LICENSE_KEY}
      - TOKEN=${BROWSERLESS_API_TOKEN}

      # Performance & Scaling
      - CONCURRENT=20
      - QUEUED=30
      - TIMEOUT=300000

      # Storage & Persistence
      - DOWNLOAD_DIR=/downloads
      - DATA_DIR=/user_data
      - METRICS_JSON_PATH=/metrics/metrics.json

      # Security
      - ENABLE_CORS=false
      - ALLOW_GET=false
      - ALLOW_FILE_PROTOCOL=false

      # Health & Monitoring
      - HEALTH=true
      - MAX_MEMORY_PERCENT=90
      - MAX_CPU_PERCENT=90

      # Logging
      - DEBUG=browserless*
      - TZ=America/New_York

    ports:
      - "3000:3000"

    volumes:
      - ./downloads:/downloads
      - ./user_data:/user_data
      - ./metrics:/metrics
      - ./fonts:/usr/share/fonts/custom

    # Increase shared memory from Docker's 64MB default to prevent Chrome crashes under load
    shm_size: '2g'
    # Cap container log size to prevent disk exhaustion
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "5"

    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/pressure"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

    deploy:
      resources:
        limits:
          cpus: '4'
          memory: 8G
        reservations:
          cpus: '2'
          memory: 4G
```
  
  
**High Availability:**

```yaml
---
services:
  browserless-1:
    image: registry.browserless.io/browserless/browserless/enterprise:2.3.0
    environment:
      - KEY=${BROWSERLESS_LICENSE_KEY}
      - TOKEN=${BROWSERLESS_API_TOKEN}
      - CONCURRENT=20
      - HEALTH=true
    ports:
      - "3001:3000"
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 8G
        reservations:
          memory: 4G

  browserless-2:
    image: registry.browserless.io/browserless/browserless/enterprise:2.3.0
    environment:
      - KEY=${BROWSERLESS_LICENSE_KEY}
      - TOKEN=${BROWSERLESS_API_TOKEN}
      - CONCURRENT=20
      - HEALTH=true
    ports:
      - "3002:3000"
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 8G
        reservations:
          memory: 4G

  # Load balancer (nginx)
  nginx:
    image: nginx:alpine
    ports:
      - "3000:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - browserless-1
      - browserless-2
    restart: unless-stopped
```
  
  
**With Webhooks:**

```yaml
---
services:
  browserless:
    image: registry.browserless.io/browserless/browserless/enterprise:2.3.0
    environment:
      - KEY=${BROWSERLESS_LICENSE_KEY}
      - TOKEN=${BROWSERLESS_API_TOKEN}
      - CONCURRENT=15

      # Webhook Alerts
      - QUEUE_ALERT_URL=https://your-monitoring.com/webhooks/queue
      - REJECT_ALERT_URL=https://your-monitoring.com/webhooks/reject
      - TIMEOUT_ALERT_URL=https://your-monitoring.com/webhooks/timeout
      - FAILED_HEALTH_URL=https://your-monitoring.com/webhooks/health

    ports:
      - "3000:3000"
    restart: unless-stopped
```
  

### Resource Requirements

Recommended specifications based on workload:

| Workload | CPU Cores | Memory | Concurrent Sessions | Storage |
|----------|-----------|--------|---------------------|---------|
| Light (Development) | 2 | 4GB | 5-10 | 10GB |
| Medium (Production) | 4 | 8GB | 10-20 | 50GB |
| Heavy (Enterprise) | 8+ | 16GB+ | 20-50 | 100GB+ |

## Connecting to Your Enterprise Instance
Once your Enterprise instance is running, connect using your preferred method. Below are examples for Puppeteer, Playwright, and the REST API.

### Local Network (No Token)

For applications on the same private network, you can connect without a token:

  
**Puppeteer:**

```javascript
const puppeteer = require('puppeteer-core');

let browser;
try {
  browser = await puppeteer.connect({
    browserWSEndpoint: 'ws://localhost:3000/chromium'
  });

  const page = await browser.newPage();
  await page.goto('https://example.com');
  const screenshot = await page.screenshot();

  // Use screenshot...
} catch (error) {
  console.error('Browser automation failed:', error);
} finally {
  if (browser) {
    await browser.close();
  }
}
```
  
  
**Playwright:**

```javascript
const { chromium } = require('playwright');

let browser;
try {
  browser = await chromium.connectOverCDP('ws://localhost:3000/chromium');
  const context = await browser.newContext();
  const page = await context.newPage();

  await page.goto('https://example.com');
  const screenshot = await page.screenshot();

  // Use screenshot...
} catch (error) {
  console.error('Browser automation failed:', error);
} finally {
  if (browser) {
    await browser.close();
  }
}
```
  
  
**REST API:**

```bash
# Screenshot API
curl -X POST http://localhost:3000/screenshot \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com"}' \
  --output screenshot.png

# Content API
curl -X POST http://localhost:3000/content \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com"}'
```
  

### External Access (With Token)

For production deployments with external access, always use token authentication:

  
**Puppeteer:**

```javascript
const puppeteer = require('puppeteer-core');

let browser;
try {
  browser = await puppeteer.connect({
    browserWSEndpoint: 'ws://browserless.company.com:3000/chromium?token=YOUR_API_TOKEN'
  });

  const page = await browser.newPage();
  await page.goto('https://example.com');
  const data = await page.screenshot();

  return data;
} catch (error) {
  console.error('Browser connection failed:', error);
  throw error;
} finally {
  if (browser) {
    await browser.close();
  }
}
```
  
  
**Playwright:**

```javascript
const { chromium } = require('playwright');

let browser;
try {
  browser = await chromium.connectOverCDP(
    'ws://browserless.company.com:3000/chromium?token=YOUR_API_TOKEN'
  );

  const context = await browser.newContext();
  const page = await context.newPage();
  await page.goto('https://example.com');

  const screenshot = await page.screenshot();
  return screenshot;
} catch (error) {
  console.error('Browser connection failed:', error);
  throw error;
} finally {
  if (browser) {
    await browser.close();
  }
}
```
  
  
**REST API:**

```bash
# Via query parameter
curl -X POST "http://browserless.company.com:3000/screenshot?token=YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com"}' \
  --output screenshot.png

# Via header (recommended)
curl -X POST http://browserless.company.com:3000/screenshot \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -d '{"url": "https://example.com"}' \
  --output screenshot.png
```
  

## Configuration Options

The Enterprise image supports extensive configuration through environment variables. For a comprehensive list of all available options including license settings, performance tuning, security, health monitoring, webhooks, and more, see the [Docker Configuration Reference](/enterprise/docker/config).

For multi-user deployments, see [Token Roles](/enterprise/token-roles) to set up role-based access control with admin, developer, and viewer permissions.

## Production Deployment Checklist

Before deploying to production, review these essential guides:

- **[Best Practices](/enterprise/docker/best-practices)** - Security hardening, performance tuning, monitoring, high availability, and data persistence strategies
- **[NGINX Load Balancing](/enterprise/docker/nginx-load-balancing)** - Set up load balancing for high availability deployments
- **[Webhooks](/enterprise/docker/webhooks)** - Configure event notifications and alerting

## FAQ & Troubleshooting

<details>
<summary>How do I get an Enterprise license?</summary>

Contact [sales@browserless.io](mailto:sales@browserless.io) or visit the [pricing page](https://browserless.io/pricing) for Enterprise plan details.

</details>

<details>
<summary>Can I run Browserless on my own infrastructure?</summary>

Yes. Enterprise customers can self-host using the Docker image. See [Docker deployment](/enterprise/docker/enterprise-image) for setup instructions.

</details>

## Next steps

  <a href="/enterprise/docker/enterprise-image" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Docker deployment</h3>
      Self-host Browserless with Docker
    
  </a>
  <a href="/enterprise/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      Configure your Enterprise deployment
    
  </a>

---

# Extending the Docker Image

URL: https://docs.browserless.io/enterprise/docker/extending

The Browserless Docker image includes Chrome/Chromium and common dependencies for browser automation. You'll need to extend the image when you require:

- **npm modules** for use in `/function` API calls or custom scripts
- **System packages** like fonts, libraries, or CLI tools
- **Custom configurations** or startup scripts

This guide shows you how to build a custom image with your own dependencies.

- A Browserless Enterprise license
- Docker installed on your infrastructure

> **Info**
> For hosted accounts, [contact us](https://www.browserless.io/contact) to use a third-party module.

Create a `Dockerfile`. This example adds the `aws-sdk` module.

```dockerfile
FROM registry.browserless.io/browserless/browserless/enterprise:latest

# Install the AWS SDK
RUN npm install aws-sdk
```

Pin the `FROM` value to a specific version for stability. For example, to use version 2.3.0:

```dockerfile
FROM registry.browserless.io/browserless/browserless/enterprise:2.3.0

# Install the AWS SDK
RUN npm install aws-sdk
```

Save your file and proceed to the build step.

Build the image with Docker:

```sh
docker build -t my/browserless:latest .
```

This tags the image as `my/browserless:latest`. Change the name and tag if you plan to push the image to a public or private registry.

The first build takes a few minutes to pull dependencies. Subsequent builds are faster due to the local cache.

Run the custom image with the same environment variables as the base image. Whitelist the new module with `FUNCTION_EXTERNALS` so Browserless can execute it.

```sh
docker run -p 3000:3000 -e FUNCTION_EXTERNALS='["aws-sdk"]' my/browserless:latest
```

Calls to `/function` now have access to the `aws-sdk` module:

```sh
curl -X POST \
  http://localhost:3000/function \
  -H 'Content-Type: application/javascript' \
  -d 'const aws = require('\''aws-sdk'\'');

module.exports = async () => {
  const data = Object.keys(aws);

  return {
    data,
    type: '\''text/plain'\'',
  };
};'
```
To add more modules, install them in the `Dockerfile`, rebuild the image, and run it.

For a full list of environment variables you can pass at runtime, see the [Docker configuration reference](/enterprise/docker/config). To add custom HTTP or WebSocket routes to your image, see [custom routes](/enterprise/docker/custom-routes).

## FAQ & Troubleshooting

<details>
<summary>How do I get an Enterprise license?</summary>

Contact [sales@browserless.io](mailto:sales@browserless.io) or visit the [pricing page](https://browserless.io/pricing) for Enterprise plan details.

</details>

<details>
<summary>Can I run Browserless on my own infrastructure?</summary>

Yes. Enterprise customers can self-host using the Docker image. See [Docker deployment](/enterprise/docker/enterprise-image) for setup instructions.

</details>

## Next steps

  <a href="/enterprise/docker/enterprise-image" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Docker deployment</h3>
      self-host Browserless with Docker
    
  </a>
  <a href="/enterprise/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure your Enterprise deployment
    
  </a>

---

# NGINX Load Balancing

URL: https://docs.browserless.io/enterprise/docker/nginx-load-balancing

NGINX acts as a reverse proxy and load balancer for Browserless Docker containers, providing SSL termination, traffic distribution across multiple instances, and automatic failover through health monitoring.

- A Browserless Enterprise license
- NGINX installed on your infrastructure
- Multiple Browserless Docker containers running (see the [Docker Configuration Reference](/enterprise/docker/config))

## How NGINX Works as a Load Balancer

NGINX acts as a reverse proxy that sits between your clients and Browserless instances. When a request arrives, NGINX:

1. **Receives the incoming connection** - Accepts HTTP/HTTPS requests and WebSocket connections from clients
2. **Routes to a backend** - Selects one of the healthy Browserless instances based on the configured load balancing algorithm (e.g., least connections, round-robin)
3. **Forwards the request** - Proxies the request to the selected Browserless container, maintaining WebSocket connections for browser automation
4. **Handles failures** - If a backend instance fails or returns an error, automatically retries the request on another healthy instance
5. **Returns the response** - Sends the backend's response back to the client

This architecture allows you to scale horizontally by adding more Browserless containers without changing your client code, while NGINX ensures traffic is distributed efficiently and failures are handled gracefully.

## Why Use NGINX with Browserless?

- **Load Balancing**: Distribute traffic across multiple Browserless containers
- **SSL Termination**: Handle HTTPS certificates at the proxy level
- **Health Checks**: Automatically retry failed requests to healthy instances
- **WebSocket Support**: Properly handle WebSocket connections for Puppeteer and Playwright
- **High Availability**: Prevent single points of failure

## Docker Compose Setup

Here's a complete example using Docker Compose with NGINX load balancing two Browserless instances:

### docker-compose.yml

```yaml
version: '3.8'

services:
  nginx:
    image: nginx:alpine
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
    ports:
      - "80:80"
    depends_on:
      - browserless_one
      - browserless_two
    restart: unless-stopped

  browserless_one:

image: registry.browserless.io/browserless/browserless/enterprise:latest
    environment:
      - TOKEN=your-token-here
      - CONCURRENT=10
    restart: unless-stopped

  browserless_two:
    image: registry.browserless.io/browserless/browserless/enterprise:latest
    environment:
      - TOKEN=your-token-here
      - CONCURRENT=10
    restart: unless-stopped
```

### nginx.conf

```nginx
upstream browserless {
  least_conn;
  server browserless_one:3000;
  server browserless_two:3000;
}

server {
  listen 80;

  location / {
    proxy_pass http://browserless;
    proxy_next_upstream error timeout http_500 http_503 http_429 non_idempotent;

    # WebSocket support
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection 'upgrade';
    proxy_set_header Host $host;
    proxy_cache_bypass $http_upgrade;

    # Extended timeouts for long-running browser operations
    proxy_connect_timeout 900;
    proxy_send_timeout 900;
    proxy_read_timeout 900;
    send_timeout 900;
  }
}
```

## Configuration Explanation

### Upstream Block

The `upstream` block defines the backend Browserless instances:

- `least_conn`: Routes requests to the server with the fewest active connections
- `server browserless_one:3000`: References the Docker service name and internal port
- Multiple servers provide redundancy and load distribution

### Server and Location Blocks

The `server` and `location` blocks handle incoming requests:

- `listen 80`: Listens for HTTP traffic on port 80
- `proxy_pass http://browserless`: Routes requests to the upstream backend pool
- `proxy_next_upstream`: Automatically retries failed requests (errors, timeouts, 500/503/429 responses) on other servers
- `proxy_http_version 1.1`: Required for WebSocket support
- `Upgrade` and `Connection` headers: Essential for WebSocket connections used by Puppeteer and Playwright
- Extended timeouts (900 seconds): Accommodate long-running browser operations like scraping or PDF generation

## Starting the Setup

1. Create the `nginx.conf` file with the configuration above
2. Create the `docker-compose.yml` file
3. Start the services:

```bash
docker-compose up
```

Your browserless instances will be available at `http://localhost` with automatic load balancing.

## Advanced Configuration

### Health Checks

Enable health checks in Browserless to make load balancing more intelligent. When CPU or memory usage is high, Browserless will reject requests, causing NGINX to automatically route traffic to healthier instances:

```yaml
# docker-compose.yml
browserless_one:
  image: registry.browserless.io/browserless/browserless/enterprise:latest
  environment:
    - TOKEN=your-token-here
    - CONCURRENT=10
    - HEALTH=true

browserless_two:
  image: registry.browserless.io/browserless/browserless/enterprise:latest
  environment:
    - TOKEN=your-token-here
    - CONCURRENT=10
    - HEALTH=true
```

### SSL/HTTPS Configuration

For production deployments, add SSL support. Update your `nginx.conf` to listen on port 443 and include SSL certificates:

```nginx
upstream browserless {
  least_conn;
  server browserless_one:3000;
  server browserless_two:3000;
}

server {
  listen 443 ssl;
  server_name your-domain.com;

  ssl_certificate /path/to/your/certificate.pem;
  ssl_certificate_key /path/to/your/private-key.pem;
  ssl_protocols TLSv1.2 TLSv1.3;

  location / {
    proxy_pass http://browserless;
    proxy_next_upstream error timeout http_500 http_503 http_429 non_idempotent;

    # WebSocket support
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection 'upgrade';
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_cache_bypass $http_upgrade;

    # Extended timeouts
    proxy_connect_timeout 900;
    proxy_send_timeout 900;
    proxy_read_timeout 900;
    send_timeout 900;
  }
}

# Optional: Redirect HTTP to HTTPS
server {
  listen 80;
  server_name your-domain.com;
  return 301 https://$server_name$request_uri;
}
```

Also update your `docker-compose.yml` to expose port 443:

```yaml
nginx:
  image: nginx:alpine
  volumes:
    - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
    - /path/to/certificates:/etc/nginx/certs:ro
  ports:
    - "80:80"
    - "443:443"
  depends_on:
    - browserless_one
    - browserless_two
  restart: unless-stopped
```

### Browserless Proxy Awareness

When using NGINX in front of Browserless, configure Browserless to be aware of the proxy for proper link generation:

```yaml
# docker-compose.yml
browserless_one:
  image: registry.browserless.io/browserless/browserless/enterprise:latest
  environment:
    - TOKEN=your-token-here
    - CONCURRENT=10
    - EXTERNAL=https://your-domain.com
```

The `EXTERNAL` variable should be set to the fully-qualified URL (including protocol and port if non-standard) where your Browserless instances are accessible externally. See the [Docker Configuration](/enterprise/docker/config#external-address-configuration) documentation for more details.

### Session Affinity for Reconnect and LiveURL

Browserless sessions are stateful. A browser running on Instance A cannot be reconnected from Instance B. Both `Browserless.reconnect` and `Browserless.liveURL` return URLs that contain the originating instance's address. Without the `EXTERNAL` env var set, these URLs contain `localhost:3000`, which is unreachable by end users or external clients. If `EXTERNAL` is set to the shared load balancer address, reconnect requests may route to the wrong instance.

Solutions:
- Set `EXTERNAL` to each instance's direct address (e.g., `EXTERNAL=http://browserless-1:3000` for instance 1, `EXTERNAL=http://browserless-2:3000` for instance 2). Reconnect and LiveURL links then bypass the load balancer and reach the correct instance.
- Use the returned URL as-is. It already points to the correct instance.

For initial connections, use round-robin or least-connections routing. For reconnections and LiveURL links, route the client directly to the URL returned by Browserless.

### /pressure Endpoint for Health-Aware Routing

The `/pressure` GET endpoint returns real-time capacity data for the instance:

```bash
curl http://localhost:3000/pressure?token=YOUR_TOKEN
```

Response:
```json
{
  "pressure": {
    "cpu": 45,
    "date": 1711468800000,
    "isAvailable": true,
    "maxConcurrent": 10,
    "maxQueued": 10,
    "memory": 62,
    "message": "",
    "queued": 0,
    "reason": "",
    "recentlyRejected": 0,
    "running": 3
  }
}
```

| Field | Type | Description |
|-------|------|-------------|
| `cpu` | number or null | CPU usage percentage |
| `memory` | number or null | Memory usage percentage |
| `isAvailable` | boolean | Whether the instance accepts new sessions |
| `maxConcurrent` | number | Maximum concurrent browser sessions |
| `maxQueued` | number | Maximum queued requests |
| `running` | number | Currently running sessions |
| `queued` | number | Currently queued requests |
| `reason` | string | Why the instance is unavailable: `full`, `cpu`, `memory`, or empty |
| `recentlyRejected` | number | Recent rejected connection count |

Use `isAvailable` in your load balancer health check to route traffic away from overloaded instances.

### Scaling to More Instances

To add more Browserless instances, add more services to your `docker-compose.yml` and update the nginx upstream block:

```yaml
# docker-compose.yml
services:
  nginx:
    # ... nginx config
    depends_on:
      - browserless_one
      - browserless_two
      - browserless_three
      - browserless_four

  browserless_one:
    # ... config

  browserless_two:
    # ... config

  browserless_three:
    # ... config

  browserless_four:
    # ... config
```

```nginx
# nginx.conf
upstream browserless {
  least_conn;
  server browserless_one:3000;
  server browserless_two:3000;
  server browserless_three:3000;
  server browserless_four:3000;
}
```

## Testing Your Setup

You can test your load-balanced setup by connecting with Puppeteer:

```javascript
const puppeteer = require('puppeteer');

const browser = await puppeteer.connect({
  browserWSEndpoint: 'ws://localhost:80?token=your-token-here'
});

const page = await browser.newPage();
await page.goto('https://example.com');
const title = await page.title();
console.log(title);

await browser.close();
```

The requests will be automatically distributed across your Browserless instances by NGINX.

## FAQ & Troubleshooting

<details>
<summary>How do I get an Enterprise license?</summary>

Contact [sales@browserless.io](mailto:sales@browserless.io) or visit the [pricing page](https://browserless.io/pricing) for Enterprise plan details.

</details>

<details>
<summary>Can I run Browserless on my own infrastructure?</summary>

Yes. Enterprise customers can self-host using the Docker image. See [Docker deployment](/enterprise/docker/enterprise-image) for setup instructions.

</details>

## Next steps

  <a href="/enterprise/docker/enterprise-image" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Docker deployment</h3>
      self-host Browserless with Docker
    
  </a>
  <a href="/enterprise/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure your Enterprise deployment
    
  </a>

---

# OpenTelemetry Integration

URL: https://docs.browserless.io/enterprise/docker/opentelemetry

Browserless Enterprise supports OpenTelemetry (OTel) for exporting distributed traces, metrics, and logs to any OTLP-compatible backend. OTel is disabled by default with zero runtime overhead. When enabled, the system uses dynamic imports so OTel packages only load if `OTEL_ENABLED=true`.

- A Browserless Enterprise license
- An OTLP-compatible observability backend to receive the exported data (e.g. Grafana Tempo, Jaeger, Datadog, Honeycomb)

## Environment Variables

| Variable | Description | Default | Notes |
|----------|-------------|---------|-------|
| `OTEL_ENABLED` | Master toggle. Set to `true` to enable OTel instrumentation. | `false` | Must be the string `true` |
| `OTEL_TRACES_ENABLED` | Export distributed traces. | `true` (when OTel enabled) | Disable with `false` to export only metrics/logs |
| `OTEL_METRICS_ENABLED` | Export metrics (CPU, memory, session counts). | `true` (when OTel enabled) | |
| `OTEL_LOGS_ENABLED` | Bridge application logs to OTel LogRecords via a Winston transport. | `false` | Set to `true` to export logs |
| `OTEL_SERVICE_NAME` | Service name reported in traces and metrics. | `browserless-enterprise` | Change to distinguish multiple instances |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP collector endpoint URL. | (none, required when enabled) | Example: `http://collector:4318` |
| `OTEL_EXPORTER_OTLP_PROTOCOL` | Exporter wire protocol. | `http/protobuf` | Only `http/protobuf` is supported. Other values log a warning and fall back to `http/protobuf`. |
| `OTEL_EXPORTER_OTLP_HEADERS` | Authentication headers as comma-separated `key=value` pairs. | (none) | Example: `Authorization=Bearer abc123` |
| `OTEL_RESOURCE_ATTRIBUTES` | Additional resource attributes as comma-separated `key=value` pairs. | (none) | Example: `deployment.environment=production,service.version=2.86.0` |
| `OTEL_METRIC_EXPORT_INTERVAL` | Metric export interval in milliseconds. | `60000` | Lower values increase collector load |

## Docker Compose Example

```yaml
services:
  browserless:
    image: registry.browserless.io/browserless/browserless/enterprise:latest
    environment:
      - KEY=your-license-key
      - TOKEN=your-api-token
      - OTEL_ENABLED=true
      - OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318
      - OTEL_SERVICE_NAME=browserless-prod
      - OTEL_LOGS_ENABLED=true
    ports:
      - "3000:3000"

  otel-collector:
    image: otel/opentelemetry-collector-contrib:latest
    ports:
      - "4318:4318"
    volumes:
      - ./otel-config.yaml:/etc/otelcol-contrib/config.yaml
```

## Ignored Endpoints

HTTP auto-instrumentation skips `/health`, `/metrics`, and `/ready` to reduce trace noise. These endpoints do not appear in your trace data.

## Log Integration

When `OTEL_ENABLED=true` and `OTEL_LOGS_ENABLED=true`, the enterprise logger adds an OpenTelemetry Winston transport that bridges application logs into OTel LogRecords. Control log verbosity with the `LOG_LEVEL` env var (see [Configuration Reference](/enterprise/docker/config)).

## Protocol Limitation

Only `http/protobuf` is supported as the exporter protocol. Setting `OTEL_EXPORTER_OTLP_PROTOCOL` to `grpc` or other values logs a warning and falls back to `http/protobuf`.

## FAQ & Troubleshooting

<details>
<summary>How do I get an Enterprise license?</summary>

Contact [sales@browserless.io](mailto:sales@browserless.io) or visit the [pricing page](https://browserless.io/pricing) for Enterprise plan details.

</details>

<details>
<summary>Can I run Browserless on my own infrastructure?</summary>

Yes. Enterprise customers can self-host using the Docker image. See [Docker deployment](/enterprise/docker/enterprise-image) for setup instructions.

</details>

## Next steps

  <a href="/enterprise/docker/enterprise-image" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Docker deployment</h3>
      self-host Browserless with Docker
    
  </a>
  <a href="/enterprise/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure your Enterprise deployment
    
  </a>

---

# Enterprise Docker Overview

URL: https://docs.browserless.io/enterprise/docker/overview

The Enterprise Docker image lets you run Browserless on your own infrastructure. You get full control over where your data lives, how your containers scale, and what network policies apply. The image supports the same APIs, WebSocket connections, and browser automation features as all other Browserless deployment options.

Self-hosting is a good fit when you need data sovereignty, air-gapped environments, or custom network configurations that a managed deployment cannot provide. If you prefer Browserless to manage infrastructure for you, see [Private Deployment](/enterprise/private-deployment/overview).

## Self-Hosting Features

Running your own containers means you control the full stack:

- **[Deployment Guide](/enterprise/docker/enterprise-image):** Pull the Enterprise image, configure your token, and start running browsers in minutes.
- **[Configuration Reference](/enterprise/docker/config):** Tune concurrency, timeouts, queue behavior, health checks, and security settings through environment variables.
- **[NGINX Load Balancing](/enterprise/docker/nginx-load-balancing):** Distribute traffic across multiple Browserless containers with sticky sessions and health-check routing.
- **[Extending the Image](/enterprise/docker/extending):** Install custom fonts, packages, or browser extensions by building on top of the base image.
- **[Webhooks](/enterprise/docker/webhooks):** Receive HTTP callbacks when sessions are queued, rejected, timed out, or hit errors.
- **[OpenTelemetry](/enterprise/docker/opentelemetry):** Export traces, metrics, and logs to any OTLP-compatible backend for observability.

## Getting started

Pull the Enterprise image and start a container:

```sh
docker run --rm -p 3000:3000 -e TOKEN=YOUR_API_TOKEN_HERE ghcr.io/browserless/chromium
```

Connect your automation code to `ws://localhost:3000/chromium?token=YOUR_API_TOKEN_HERE` and you're running. See the [Deployment Guide](/enterprise/docker/enterprise-image) for full setup and configuration details.

## FAQ & Troubleshooting

<details>
<summary>How do I get an Enterprise license?</summary>

Contact [sales@browserless.io](mailto:sales@browserless.io) or visit the [pricing page](https://browserless.io/pricing) for Enterprise plan details.

</details>

<details>
<summary>Can I run Browserless on my own infrastructure?</summary>

Yes. Enterprise customers can self-host using the Docker image. See [Docker deployment](/enterprise/docker/enterprise-image) for setup instructions.

</details>

## Next steps

  <a href="/enterprise/docker/enterprise-image" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Docker deployment</h3>
      self-host Browserless with Docker
    
  </a>
  <a href="/enterprise/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure your Enterprise deployment
    
  </a>

---

# Docker Webhooks

URL: https://docs.browserless.io/enterprise/docker/webhooks

Configure the Docker image to call a URL when specific events occur, such as queuing, rejection, or errors. These webhook URLs are set as environment variables alongside the other [Docker configuration options](/enterprise/docker/config).

- A Browserless Enterprise license
- Docker installed on your infrastructure

> **HTTP Method**
> All webhooks use the HTTP GET method with no request body. Browserless does not read the response. Respond quickly (HTTP 200/204) to close the connection.

## Queuing

`QUEUE_ALERT_URL` triggers a GET request to the specified URL when requests start queuing.

```bash
$ docker run -d \
  --restart always \
  --name browserless \
  -e "QUEUE_ALERT_URL=https://my.website.com/queued" \
  -p 3000:3000 \
  registry.browserless.io/browserless/browserless/enterprise:latest
```

## Rejections

`REJECT_ALERT_URL` triggers a GET request to the specified URL when requests are rejected.

```bash
$ docker run -d \
  --restart always \
  --name browserless \
  -e "REJECT_ALERT_URL=https://my.website.com/rejected" \
  -p 3000:3000 \
  registry.browserless.io/browserless/browserless/enterprise:latest
```

## Timeouts

`TIMEOUT_ALERT_URL` triggers a GET request to the specified URL when sessions time out.

```bash
$ docker run -d \
  --restart always \
  --name browserless \
  -e "TIMEOUT_ALERT_URL=https://my.website.com/time-out" \
  -p 3000:3000 \
  registry.browserless.io/browserless/browserless/enterprise:latest
```

## Errors

`ERROR_ALERT_URL` triggers a GET request to the specified URL when unhandled errors occur during request processing.

```bash
$ docker run -d \
  --restart always \
  --name browserless \
  -e "ERROR_ALERT_URL=https://my.website.com/error" \
  -p 3000:3000 \
  registry.browserless.io/browserless/browserless/enterprise:latest
```

When an error occurs, Browserless appends the error message as a query parameter:

```
GET https://my.website.com/error?error=Session%20timeout%20exceeded
```

No request body is sent. Parse the `error` query parameter to extract the message.

## Health Failure

`FAILED_HEALTH_URL` triggers a GET request to the specified URL when CPU and memory exceed 99%. Use this event to restart or replace the container.

```bash
$ docker run -d \
  --restart always \
  --name browserless \
  -e "FAILED_HEALTH_URL=https://my.website.com/failed-health" \
  -p 3000:3000 \
  registry.browserless.io/browserless/browserless/enterprise:latest
```

For webhooks for other events, [contact us](https://www.browserless.io/contact). You can also monitor container health programmatically using the [/pressure utility endpoint](/enterprise/utility-functions/pressure).

## FAQ & Troubleshooting

<details>
<summary>How do I get an Enterprise license?</summary>

Contact [sales@browserless.io](mailto:sales@browserless.io) or visit the [pricing page](https://browserless.io/pricing) for Enterprise plan details.

</details>

<details>
<summary>Can I run Browserless on my own infrastructure?</summary>

Yes. Enterprise customers can self-host using the Docker image. See [Docker deployment](/enterprise/docker/enterprise-image) for setup instructions.

</details>

## Next steps

  <a href="/enterprise/docker/enterprise-image" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Docker deployment</h3>
      self-host Browserless with Docker
    
  </a>
  <a href="/enterprise/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure your Enterprise deployment
    
  </a>

---

# Launch Parameters

URL: https://docs.browserless.io/enterprise/launch-options

Launch parameters configure how each browser session starts and behaves. You pass them as query parameters or as a JSON object on the WebSocket connection URL when connecting via Puppeteer, Playwright, or any CDP client.

These are per-session settings. For server-level configuration (concurrency limits, timeouts, health checks), see the [Docker Configuration Reference](/enterprise/docker/config).

- A Browserless Enterprise license or self-hosted deployment

## Configuration Methods

There are two ways to pass launch options:

- **Query parameters** go directly in the connection URL (e.g., `&headless=false&stealth=true`). Best for simple, standalone settings.
- **The `launch` object** is a JSON string passed as a single `launch` query parameter. Use it for browser-level options that contain arrays or nested values, like `args: [...]`, where brackets and quotes require encoding.

Both methods can be combined. Query parameters take precedence when the same option appears in both.

```js

const browser = await puppeteer.connect({
  browserWSEndpoint: `ws://localhost:3000/chromium?token=YOUR_API_TOKEN_HERE&stealth=true&blockAds=true&timeout=60000`,
});
```

## Quick Examples

These examples connect to an enterprise instance with a launch object that sets a custom window size via Chrome flags.

  
**Puppeteer:**

```js
import puppeteer from "puppeteer-core";

const launch = btoa(JSON.stringify({
  args: ["--window-size=1920,1080"],
}));

const browser = await puppeteer.connect({
  browserWSEndpoint: `ws://localhost:3000/chromium?token=YOUR_API_TOKEN_HERE&launch=${launch}`,
});

const page = await browser.newPage();
await page.goto("https://example.com");
await browser.close();
```
  
  
**Playwright CDP:**

```js
import { chromium } from "playwright-core";

const launch = btoa(JSON.stringify({
  args: ["--window-size=1920,1080"],
}));

const browser = await chromium.connectOverCDP(
  `ws://localhost:3000/chromium?token=YOUR_API_TOKEN_HERE&launch=${launch}`
);

const context = browser.contexts()[0] || (await browser.newContext());
const page = await context.newPage();
await page.goto("https://example.com");
await browser.close();
```
  
  
**Playwright Connect:**

```js
import { chromium } from "playwright-core";

const launch = btoa(JSON.stringify({
  args: ["--window-size=1920,1080"],
}));

const browser = await chromium.connect(
  `ws://localhost:3000/chromium/playwright?token=YOUR_API_TOKEN_HERE&launch=${launch}`
);

const page = await browser.newPage();
await page.goto("https://example.com");
await browser.close();
```
  
  
**Python:**

```python
import json
import base64
from playwright.sync_api import sync_playwright

launch = base64.b64encode(json.dumps({
    "args": ["--window-size=1920,1080"],
}).encode()).decode()

with sync_playwright() as p:
    browser = p.chromium.connect_over_cdp(
        f"ws://localhost:3000/chromium?token=YOUR_API_TOKEN_HERE&launch={launch}"
    )

    context = browser.contexts[0] if browser.contexts else browser.new_context()
    page = context.new_page()
    page.goto("https://example.com")
    browser.close()
```
  

## Launch Object Options

The `launch` parameter accepts a JSON object with these properties:

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `args` | `string[]` | `[]` | Chrome command-line flags passed to the browser process |
| `headless` | `boolean` | `true` | Run the browser in headless mode |
| `stealth` | `boolean` | `false` | Enable stealth mode to reduce automation detection |
| `slowMo` | `number` | `0` | Add a delay (in ms) between each operation |
| `ignoreDefaultArgs` | `boolean \| string[]` | `false` | Remove all or specific default Chrome flags |
| `acceptInsecureCerts` | `boolean` | `false` | Accept self-signed and expired TLS certificates |

> **Warning**
> `acceptInsecureCerts` replaces the older `ignoreHTTPSErrors` parameter, which has been [renamed and deprecated by the Puppeteer team](https://github.com/puppeteer/puppeteer/issues/12341). Both are accepted for backwards compatibility, but use `acceptInsecureCerts` in new code.

### Encoding the Launch Value

The `launch` value must be encoded before appending it to the URL. Base64 is the simplest approach because it avoids escaping brackets, quotes, and commas.

**Base64 encoding**

  
**JavaScript:**

```js
import puppeteer from "puppeteer-core";

const launch = btoa(JSON.stringify({
  args: ["--window-size=1280,720"],
  headless: true,
}));

const browser = await puppeteer.connect({
  browserWSEndpoint: `ws://localhost:3000/chromium?token=YOUR_API_TOKEN_HERE&launch=${launch}`,
});
```
  
  
**Python:**

```python
import json
import base64
from playwright.sync_api import sync_playwright

launch = base64.b64encode(json.dumps({
    "args": ["--window-size=1280,720"],
    "headless": True,
}).encode()).decode()

with sync_playwright() as p:
    browser = p.chromium.connect_over_cdp(
        f"ws://localhost:3000/chromium?token=YOUR_API_TOKEN_HERE&launch={launch}"
    )
```
  

**URL encoding**

  
**JavaScript:**

```js
import puppeteer from "puppeteer-core";

const launch = JSON.stringify({
  args: ["--window-size=1280,720"],
});

const browser = await puppeteer.connect({
  browserWSEndpoint: `ws://localhost:3000/chromium?token=YOUR_API_TOKEN_HERE&launch=${encodeURIComponent(launch)}`,
});
```
  
  
**Python:**

```python
import json
import urllib.parse
from playwright.sync_api import sync_playwright

launch = json.dumps({
    "args": ["--window-size=1280,720"],
})

with sync_playwright() as p:
    browser = p.chromium.connect_over_cdp(
        f"ws://localhost:3000/chromium?token=YOUR_API_TOKEN_HERE&launch={urllib.parse.quote(launch)}"
    )
```
  

### Chrome Flags

Chrome flags are passed via the `args` array inside the `launch` object. Enterprise deployments support the full set of [Chrome command-line switches](https://peter.sh/experiments/chromium-command-line-switches/), with no flag allowlist.

Before encoding:
```json
{"args": ["--window-size=1920,1080", "--lang=en-US", "--disable-web-security"]}
```

Common flags:

- `--window-size=WIDTH,HEIGHT` sets the initial browser window dimensions
- `--lang=LOCALE` sets the browser UI language
- `--proxy-server=HOST:PORT` routes traffic through a proxy
- `--proxy-bypass-list=HOSTS` skips the proxy for specific hosts
- `--disable-web-security` disables same-origin policy (useful for testing)
- `--user-data-dir=PATH` sets a persistent browser profile directory
- `--disable-gpu` disables GPU hardware acceleration
- `--disable-dev-shm-usage` writes shared memory files to `/tmp` instead of `/dev/shm`
- `--disable-features=FEATURE1,FEATURE2` disables specific Chromium features
- `--enable-features=FEATURE1,FEATURE2` enables specific Chromium features

> **Automatic WebRTC protection**
> The enterprise image adds WebRTC leak prevention flags to every session automatically. These prevent your server's real IP from leaking through WebRTC:
> 
> - `--webrtc-ip-handling-policy=disable_non_proxied_udp`
> - `--force-webrtc-ip-handling-policy`
> - `--disable-features=WebRtcHideLocalIpsWithMdns`
> - `--enforce-webrtc-ip-permission-check`
> 
> You do not need to add these yourself.

## Query Parameter Options

These parameters go directly in the connection URL as query strings.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `token` | `string` | — | API authentication token (matches the `TOKEN` env var) |
| `timeout` | `number` | `30000` | Session timeout in milliseconds. The server-level `TIMEOUT` env var sets the upper bound. |
| `headless` | `boolean` | `true` | Run the browser in headless mode |
| `stealth` | `boolean` | `false` | Enable stealth mode to reduce automation detection |
| `blockAds` | `boolean` | `false` | Block ads using the built-in uBlock Origin extension |
| `blockAdsInclude` | `string` | — | With `blockAds=true`, load only the named uBlock Origin Lite rulesets (comma-separated or JSON array) instead of all of them, lowering per-launch startup cost. Opt-in — omit for the full extension. See [BrowserQL launch parameters](/browserql/launch-parameters#restricting-ad-block-rulesets-blockadsinclude) for the ruleset list. |
| `blockConsentModals` | `boolean` | `false` | Block cookie consent modals and banners |
| `humanlike` | `boolean` | `false` | Enable humanlike browsing behavior (mouse movements, typing delays) |
| `record` | `boolean` | `false` | Record the session for later playback (requires license support) |
| `replay` | `boolean` | `false` | Enable session replay (requires license support) |
| `solveCaptchas` | `boolean` | `false` | Automatically solve captchas during the session (requires license support) |

Example with multiple query parameters:

```js

const browser = await puppeteer.connect({
  browserWSEndpoint: `ws://localhost:3000/chromium?token=YOUR_API_TOKEN_HERE&stealth=true&blockAds=true&timeout=120000`,
});
```

## Browser Context Settings

Some browser settings operate at the page or context level and cannot be configured through launch parameters or Chrome flags. These include viewport dimensions, locale, timezone, user agent, and geolocation.

Set these using CDP commands or library-level APIs after connecting, before your first navigation.

  
**Puppeteer:**

```js
import puppeteer from "puppeteer-core";

const browser = await puppeteer.connect({
  browserWSEndpoint: `ws://localhost:3000/chromium?token=YOUR_API_TOKEN_HERE`,
});

const page = await browser.newPage();

// Configure context-level settings before navigating
await page.setViewport({ width: 1920, height: 1080 });
await page.setUserAgent(
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
);

const cdp = await page.createCDPSession();
await cdp.send("Emulation.setTimezoneOverride", {
  timezoneId: "America/New_York",
});
await cdp.send("Emulation.setLocaleOverride", {
  locale: "en-US",
});
await cdp.send("Emulation.setGeolocationOverride", {
  latitude: 40.7128,
  longitude: -74.006,
  accuracy: 100,
});

// Now navigate
await page.goto("https://example.com");
await browser.close();
```
  
  
**Playwright:**

```js
import { chromium } from "playwright-core";

const browser = await chromium.connectOverCDP(
  `ws://localhost:3000/chromium?token=YOUR_API_TOKEN_HERE`
);

// Playwright sets context-level options when creating a new context
const context = await browser.newContext({
  viewport: { width: 1920, height: 1080 },
  userAgent:
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
  locale: "en-US",
  timezoneId: "America/New_York",
  geolocation: { latitude: 40.7128, longitude: -74.006 },
  permissions: ["geolocation"],
});

const page = await context.newPage();

// Now navigate
await page.goto("https://example.com");
await browser.close();
```
  

Issue these commands before your first `page.goto()` call. Any navigation that happens before the overrides are in place will use the browser's default settings.

> **Stealth mode and context settings**
> When stealth mode is active, the enterprise image auto-configures timezone and browser locale based on proxy location data. You only need to set these manually for non-stealth sessions or when you want to override the automatic values.

### Automatic CDP Behaviors

The enterprise image intercepts and modifies certain CDP commands behind the scenes:

- **Viewport normalization**: If a client sends an `Emulation.setDeviceMetricsOverride` with the default 800x600 dimensions, the image resets width and height to 0 (full viewport). This prevents detection via a hardcoded viewport size.
- **Target filtering**: `Target.setDiscoverTargets` responses filter out worker threads, reducing automation fingerprints visible to page scripts.
- **Browser close protection**: `Browser.close` and `Browser.crash` commands return a success response without terminating the browser process. This prevents client code from accidentally killing a shared session.
- **Download path enforcement**: `Browser.setDownloadBehavior` calls are redirected to the configured downloads directory.

These behaviors are transparent. You do not need to account for them in your code.

## Enterprise vs. BaaS Differences

### What enterprise adds

- **All Chrome flags**: Pass any [Chrome command-line switch](https://peter.sh/experiments/chromium-command-line-switches/) via `args`. BaaS restricts flags to an allowlist.
- **`--user-data-dir`**: Point to a persistent browser profile directory for cookies, cache, and local storage across sessions.
- **Full `ignoreDefaultArgs`**: Remove any default Chrome flags without restriction.
- **Custom extensions and fonts**: Mount extensions and font files directly into the container via Docker volumes.

### What stays the same

- **Launch JSON format**: Same JSON structure and encoding methods (base64, URL encoding).
- **Core query parameters**: `token`, `timeout`, `stealth`, `blockAds`, `headless`, and other standalone params work identically.
- **Library connection methods**: Puppeteer `connect()`, Playwright `connectOverCDP()`, and Playwright `connect()` all work the same way.

### What BaaS has that enterprise does not

- **Managed residential proxies**: The `proxy`, `proxyCountry`, `proxyCity`, `proxyState`, `proxySticky`, `proxyLocaleMatch`, and `proxyPreset` parameters are BaaS-only. For enterprise, configure proxies via the `--proxy-server` Chrome flag or an external proxy service.

## FAQ & Troubleshooting

<details>
<summary>How do I get an Enterprise license?</summary>

Contact [sales@browserless.io](mailto:sales@browserless.io) or visit the [pricing page](https://browserless.io/pricing) for Enterprise plan details.

</details>

<details>
<summary>Can I run Browserless on my own infrastructure?</summary>

Yes. Enterprise customers can self-host using the Docker image. See [Docker deployment](/enterprise/docker/enterprise-image) for setup instructions.

</details>

## Next steps

  <a href="/enterprise/docker/enterprise-image" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Deployment Guide</h3>
      Deploy and configure the Enterprise Docker image
    
  </a>

  <a href="/enterprise/docker/config" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Docker Configuration</h3>
      Environment variables for server-level settings
    
  </a>

  <a href="/enterprise/watching-sessions" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Watching Sessions</h3>
      Debug and monitor sessions in real time
    
  </a>

---

# Live Debugger

URL: https://docs.browserless.io/enterprise/live-debugger

This page covers the self-hosted Live Debugger that ships with Browserless Docker images, its key features, and how to access it. It also covers alternative debugging approaches using the Hybrid feature and the BrowserQL Editor.

- A self-hosted Browserless Docker deployment

> **Local and Hosted Live Debuggers function differently**
> The Live Debugger described on this page runs on self-hosted containers. The hosted Live Debugger (accessible from your account page) is a standalone sandbox that does not debug sessions from your code. For debugging your own scripts, use the [Hybrid feature in v2](/enterprise/watching-sessions) or the BrowserQL Editor.

The Live Debugger ships with all Chromium Docker images and provides an in-browser development environment for headless browser scripts. Key features include:

- Editor with syntax highlighting and auto-completion
- TypeScript support
- Visual screencast of the browser that you can interact with
- Browser DevTools integration
- File downloading for testing PDFs/PNGs
- Control over browser-specific parameters (headless, stealth, etc.)
- Tab-based layout with code-saving capabilities
- **Save your code to disk as a Node.js project**

In your Browserless account, the debugger link appears in the menu under the `Resources` section:

![Account Menu](/img/account-debugger.png)

The Live Debugger interface:

![Account Menu](/img/debugger-v1.png)

From here, connect to your worker's WebSocket endpoints, set custom launch flags, and configure session parameters.

## Alternative Debugging Approaches

### Hybrid Feature in v2

For Puppeteer and Playwright, use the [Hybrid automation feature](/enterprise/watching-sessions) to:

- Stream remote headless browsers to users via a shareable URL
- Detect when CAPTCHAs are found on the page
- Allow human intervention in automation workflows
- Continue automation after human interaction is complete

### BrowserQL Editor

For real-time session viewing and debugging, use the [BrowserQL Editor](https://browserless.io/account/bql), which provides:

- Live view of the browser session in the right panel
- Built-in documentation and debugging tools
- Ability to create and test queries before integrating them into your code
- Advanced session settings and configuration options

**To access the BrowserQL Editor:**

1. Log into your Browserless account at [browserless.io/account](https://browserless.io/account)
2. Navigate to the BQL section in the sidebar menu, or
3. Go directly to [https://browserless.io/account/bql](https://browserless.io/account/bql)

For more information about BrowserQL and its features, see our [BrowserQL documentation](/browserql/start).

## FAQ & Troubleshooting

<details>
<summary>How to get a self-hosted Browserless Docker deployment</summary>

Pull any [Browserless Docker image](/enterprise/open-source) and run it on your own infrastructure. The Live Debugger ships with every Chromium image, so no Enterprise license is required.

</details>

<details>
<summary>Can I run Browserless on my own infrastructure?</summary>

Yes. Enterprise customers can self-host using the Docker image. See [Docker deployment](/enterprise/docker/enterprise-image) for setup instructions.

</details>

## Next steps

  <a href="/enterprise/docker/enterprise-image" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Docker deployment</h3>
      self-host Browserless with Docker
    
  </a>
  <a href="/enterprise/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure your Enterprise deployment
    
  </a>

---

# Built-in Queueing System

URL: https://docs.browserless.io/enterprise/long-queues

Browserless includes a built-in queueing system that manages browser concurrency and request flow. The system prevents your infrastructure from being overwhelmed by too many simultaneous browser instances.

- A Browserless Enterprise license (self-hosted Docker or Private Deployment)

## How the Queueing System Works

Browserless implements a three-tier system to manage browser requests:

### Concurrency Limit

When you start a browser session, the system checks if you are within your concurrency limit. If you are, the browser starts immediately.

### Queueing

Once you exceed your concurrency limit, new requests enter a queue instead of being rejected. The system processes queued requests in order as browser slots become available.

### Request Rejection

If your queue is full, new requests are rejected with an error message. This prevents system overload and allows existing requests to complete.

## Why Proper Browser Cleanup Matters

The queueing system relies on browsers being properly closed to free up slots for queued requests. When browsers aren't closed correctly, they continue consuming resources until they hit the default timeout, which can cause unnecessary delays for other requests.

### The Problem with Hanging Browsers

When browsers are left open without proper cleanup:

- **Resource Waste**: Each hanging browser continues consuming CPU and memory
- **Queue Delays**: Queued requests must wait longer for slots to become available
- **Timeout Issues**: Browsers may hit navigation timeouts due to resource constraints
- **Reduced Throughput**: Fewer requests can be processed efficiently

### Best Practices for Browser Management

To ensure optimal performance and prevent hanging browsers:

- **Always close browsers when done**: Call `browser.close()` when your task completes
- **Handle errors gracefully**: Close browsers even when errors occur to free up resources
- **Set reasonable timeouts**: Configure appropriate timeout values for your use case (defaults may be too high)
- **Monitor your usage**: Keep track of your concurrency and queue limits

## Concurrency and Worker Health

### Health Check Options

Enable health check options in your worker settings to verify machine health before starting a new browser. When resources are insufficient, the system rejects the request instead of starting a browser that would time out.

### Managing Concurrency for High Resource Usage

If you experience frequent timeouts when running many requests, reduce your max concurrency in worker settings. This limits how many browsers run simultaneously per machine.

**For high CPU/memory workloads**, run fewer browsers per machine by reducing concurrency or using larger machines. Lower concurrency gives each browser more CPU and memory, reduces timeouts, and improves stability.

Start with a conservative concurrency setting and adjust based on your performance metrics.

## Configuring Queue Behavior

### Self-Hosted Docker

Set concurrency and queue limits via environment variables:

- `CONCURRENT`: Maximum concurrent browser sessions (default: `10`)
- `QUEUED`: Maximum queued requests before returning `429` (default: `10`)
- `TIMEOUT`: Session timeout in milliseconds (default: `30000`)

See the [Configuration Reference](/enterprise/docker/config) for all options.

### Private Deployment (Managed)

Adjust concurrency and queue limits from your account dashboard under **Settings**. See [Worker Settings](/enterprise/private-deployment/worker-settings#concurrency-and-queue) for details.

## FAQ & Troubleshooting

<details>
<summary>How do I get an Enterprise license?</summary>

Contact [sales@browserless.io](mailto:sales@browserless.io) or visit the [pricing page](https://browserless.io/pricing) for Enterprise plan details.

</details>

<details>
<summary>Can I run Browserless on my own infrastructure?</summary>

Yes. Enterprise customers can self-host using the Docker image. See [Docker deployment](/enterprise/docker/enterprise-image) for setup instructions.

</details>

## Next steps

  <a href="/enterprise/docker/enterprise-image" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Docker deployment</h3>
      self-host Browserless with Docker
    
  </a>
  <a href="/enterprise/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure your Enterprise deployment
    
  </a>

---

# Migrate from V1

URL: https://docs.browserless.io/enterprise/migrate-from-v1

This guide covers everything you need to update when migrating an Enterprise deployment from a V1 image to a V2 image. For self-hosted Docker migrations, see also the [BaaS Docker migration reference](/baas/migrate#docker).

- A Browserless Enterprise license (self-hosted Docker or Private Deployment)
- An existing V1 deployment to migrate from

## Connection URL Paths

V2 requires the browser type in the URL path. Requests to V1-style paths on a V2 image return **404 Not Found**.

| Use case | V1 | V2 |
|---|---|---|
| Playwright | `wss://chrome.browserless.io/playwright` | `wss://chrome.browserless.io/chrome/playwright` |
| Puppeteer (Chrome) | `wss://chrome.browserless.io` | `wss://chrome.browserless.io/chrome` |
| Puppeteer (Chromium) | `wss://chrome.browserless.io` | `wss://chrome.browserless.io/chromium` |

## Launch Arguments Format

V2 consolidates all launch flags into a single JSON `launch` query parameter instead of individual query parameters. The value must be URL-encoded. See [Encoding the launch value](/enterprise/launch-options#encoding-the-launch-value) for details.

**V1**
```
?token=YOUR_API_TOKEN_HERE&--use-gl=angle&--use-angle=gl
```

**V2 (URL-encoded)**
```
?token=YOUR_API_TOKEN_HERE&launch=%7B%22args%22%3A%5B%22--use-gl%3Dangle%22%2C%22--use-angle%3Dgl%22%5D%7D
```

Other launch flag examples:

**Window size**

```
# V1
ws://localhost:3000?token=ABCD&--window-size=1920,1080

# V2
ws://localhost:3000?token=ABCD&launch={"args":["--window-size=1920,1080"]}
```

**Headless mode**

```
# V1
ws://localhost:3000?token=ABCD&headless=shell

# V2
ws://localhost:3000?token=ABCD&launch={"headless":"shell"}
```

Unknown query parameters or JSON POST parameters now return a **4xx error** instead of being silently ignored.

## User Data Directory

V2 with Playwright does not support `--user-data-dir` as a launch argument. Puppeteer on V2 still supports it.

## Available Browsers

V2 adds Firefox and WebKit alongside Chrome and Chromium:

- `/chrome/playwright`
- `/chromium/playwright`
- `/firefox/playwright`
- `/webkit/playwright`

## Docker Environment Variables

Multiple environment variables have changed in V2 for simplicity and clarity. Browserless logs deprecated variable names on startup, so check your logs during migration.

### Removed Variables (No Replacements)

These variables have been removed. Use the equivalent per-request option in your API or library connect calls instead:

| Removed variable | Alternative |
|---|---|
| `CHROME_REFRESH_TIME` | Pre-booted Chrome is no longer supported |
| `DEFAULT_BLOCK_ADS` | Use `blockAds` in API or library connect calls |
| `DEFAULT_DUMPIO` | Use `dumpio` in launch arguments |
| `DEFAULT_HEADLESS` | Use `headless: false` in launch arguments |
| `DEFAULT_IGNORE_DEFAULT_ARGS` | Use `ignoreDefaultArgs` in launch arguments |
| `DEFAULT_IGNORE_HTTPS_ERRORS` | Use `acceptInsecureCerts` in connect calls |
| `DEFAULT_LAUNCH_ARGS` | Use the `args` option in launch arguments |
| `DEFAULT_STEALTH` | Use the `stealth` option in connect calls |
| `DISABLED_FEATURES` | Fully deprecated |
| `ENABLE_HEAP_DUMP` | No longer supported |
| `FUNCTION_BUILT_INS` | No longer supported (new function implementation) |
| `FUNCTION_ENABLE_INCOGNITO_MODE` | No longer supported |
| `FUNCTION_ENV_VARS` | No longer supported |
| `FUNCTION_EXTERNALS` | No longer supported (new function implementation) |
| `KEEP_ALIVE` | Pre-booting and keep-alive are no longer supported |
| `PREBOOT_CHROME` | Pre-booting and keep-alive are no longer supported |
| `PRINT_GET_STARTED_LINKS` | Ignored |
| `WORKSPACE_DELETE_EXPIRED` | No longer needed (libraries handle this now) |
| `WORKSPACE_DIR` | No longer needed (libraries handle this now) |
| `WORKSPACE_EXPIRE_DAYS` | No longer needed (libraries handle this now) |

### Renamed Variables

Browserless will log these and handle them internally, but you should update them to prevent deprecation warnings:

| V1 | V2 |
|---|---|
| `CONNECTION_TIMEOUT` | `TIMEOUT` |
| `DEFAULT_USER_DATA_DIR` | `DATA_DIR` |
| `ENABLE_API_GET` | `ALLOW_GET` |
| `ENABLE_CORS` | `CORS` |
| `MAX_CONCURRENT_SESSIONS` | `CONCURRENT` |
| `PRE_REQUEST_HEALTH_CHECK` | `HEALTH` |
| `PROXY_URL` | `EXTERNAL` |
| `QUEUE_LENGTH` | `QUEUED` |
| `TOKEN` | Name unchanged. If unset, every endpoint is unauthenticated |

### Docker Registry Change

Docker images have moved from Docker Hub to GitHub's container registry. Use `ghcr.io/browserless` or visit the [Packages page](https://github.com/orgs/browserless/packages).

## Other Breaking Changes

- **Selenium and WebDriver**: No longer supported in V2.
- **`/function` API**: Now runs inside the browser's JavaScript runtime instead of Node.js. Uses ECMAScript modules (`export default`) instead of CommonJS (`module.exports`). Test all `/function` calls thoroughly before deploying.
- **`blockAds`**: Now uses uBlock Origin instead of request interception.
- **`/stats` API**: Moved to `/performance`.
- **`/screencast` API**: Removed in favor of a library-based embedded API approach.
- **`waitFor` parameter**: Removed from `/pdf`, `/screenshot`, and `/scrape` APIs. Use Puppeteer's `waitForEvent`, `waitForFunction`, `waitForSelector`, or `waitForTimeout` instead.
- **`TOKEN`**: If unset, every endpoint is unauthenticated, including `/function`, which runs arbitrary Puppeteer code from the request body. Browserless does not generate one for you.

## Upgrade Process

Coordinate upgrades with Browserless support:

1. **Request a test environment** — Browserless provides a separate API token pointing to a worker running the newer image (same machine profile as production).
2. **Run your integration tests** — Verify connection URLs, launch parameters, and that screenshots and PDFs render correctly.
3. **Coordinate production upgrade** — The production fleet is updated via a Relaunch onto new workers. IP addresses will change, so update any firewall allowlists afterward.

## FAQ & Troubleshooting

<details>
<summary>How do I get an Enterprise license?</summary>

Contact [sales@browserless.io](mailto:sales@browserless.io) or visit the [pricing page](https://browserless.io/pricing) for Enterprise plan details.

</details>

<details>
<summary>Can I run Browserless on my own infrastructure?</summary>

Yes. Enterprise customers can self-host using the Docker image. See [Docker deployment](/enterprise/docker/enterprise-image) for setup instructions.

</details>

## Next steps

  <a href="/enterprise/docker/enterprise-image" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Docker deployment</h3>
      self-host Browserless with Docker
    
  </a>
  <a href="/enterprise/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure your Enterprise deployment
    
  </a>

---

# Open Source Docker Deployment

URL: https://docs.browserless.io/enterprise/open-source

The [open-source Browserless image](https://github.com/browserless/browserless) runs headless browsers inside Docker with built-in support for **Puppeteer**, **Playwright**, and **REST APIs**. It's free, self-hosted, and available on [GitHub Container Registry](https://github.com/browserless/browserless/pkgs/container/chromium).

**What you get:**

- Browser images for Chromium, Chrome, Firefox, WebKit, and Edge
- Native Puppeteer and Playwright connectivity over WebSocket
- REST APIs for screenshots, PDFs, scraping, and more
- Session management, health checks, and a built-in debugger UI

> **Looking for Enterprise?**
> The open-source image covers core browser automation. If you need BrowserQL, stealth/CAPTCHA solving, session recording, live debugging, webhooks, or OpenTelemetry, see [Enterprise Docker](/enterprise/docker/overview).

## Available images

All images are published to `ghcr.io/browserless/` and support both `linux/amd64` and `linux/arm64` architectures.

| Image | Browsers Included | Pull Command |
|-------|-------------------|--------------|
| `chromium` | Chromium | `docker pull ghcr.io/browserless/chromium` |
| `chrome` | Chrome | `docker pull ghcr.io/browserless/chrome` |
| `firefox` | Firefox | `docker pull ghcr.io/browserless/firefox` |
| `webkit` | WebKit | `docker pull ghcr.io/browserless/webkit` |
| `edge` | Microsoft Edge | `docker pull ghcr.io/browserless/edge` |
| `multi` | All of the above | `docker pull ghcr.io/browserless/multi` |

> **ARM limitations**
> Chrome and Edge are only available on `linux/amd64`. The `multi` image on ARM includes Chromium, Firefox, and WebKit only.

## Quickstart

Start a Browserless container with a token and concurrency limit:

```bash
docker run \
  --rm \
  -p 3000:3000 \
  -e "CONCURRENT=10" \
  -e "TOKEN=6R0W53R135510" \
  ghcr.io/browserless/chromium
```

> **Warning**
> If you don't set `TOKEN`, Browserless does **not** generate one for you. Every endpoint is left unauthenticated, including `/function`, which runs arbitrary Puppeteer code from the request body. Set `TOKEN` for any deployment reachable beyond localhost.

Point your automation library at the running container:

  
**Puppeteer:**

```javascript
  const puppeteer = require("puppeteer-core");

  const browser = await puppeteer.connect({
browserWSEndpoint: "ws://localhost:3000?token=6R0W53R135510",
  });
  const page = await browser.newPage();
  await page.goto("https://example.com");
  const screenshot = await page.screenshot();
  await browser.close();
```
  
  
**Playwright:**

    
      
**JavaScript:**

```javascript
const { chromium } = require("playwright");

const browser = await chromium.connectOverCDP(
  "ws://localhost:3000?token=6R0W53R135510"
);
const context = await browser.newContext();
const page = await context.newPage();
await page.goto("https://example.com");
const screenshot = await page.screenshot({ type: "png" });
await browser.close();
```
      
      
**Python:**

```python
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.connect_over_cdp(
        "ws://localhost:3000?token=6R0W53R135510"
    )
    context = browser.new_context()
    page = context.new_page()
    page.goto("https://example.com")
    screenshot = page.screenshot(type="png")
    browser.close()
```
      
      
**Java:**

```java
import com.microsoft.playwright.*;

try (Playwright playwright = Playwright.create()) {
    Browser browser = playwright.chromium()
        .connectOverCDP("ws://localhost:3000?token=6R0W53R135510");
    BrowserContext context = browser.newContext();
    Page page = context.newPage();
    page.navigate("https://example.com");
    byte[] screenshot = page.screenshot(
        new Page.ScreenshotOptions().setType(ScreenshotType.PNG)
    );
    browser.close();
}
```
      
      
**C#:**

```csharp
using Microsoft.Playwright;

var playwright = await Playwright.CreateAsync();
var browser = await playwright.Chromium
    .ConnectOverCDPAsync("ws://localhost:3000?token=6R0W53R135510");
var context = await browser.NewContextAsync();
var page = await context.NewPageAsync();
await page.GotoAsync("https://example.com");
var screenshot = await page.ScreenshotAsync(
    new PageScreenshotOptions { Type = ScreenshotType.Png }
);
await browser.CloseAsync();
```
      
    
  

Confirm the instance is running with a REST API call:

```bash
curl "http://localhost:3000/chromium/content?token=6R0W53R135510&url=https://example.com"
```

You should get back the rendered HTML of `example.com`:

```html
<!doctype html>
<html>
<head>
    <title>Example Domain</title>
...
```

Or open `http://localhost:3000` in your browser to see the built-in docs and debugger UI.

### Docker compose

For repeatable deployments, use a `docker-compose.yml`:

```yaml
services:
  browserless:
    image: ghcr.io/browserless/chromium
    ports:
      - "3000:3000"
    environment:
      - TOKEN=6R0W53R135510
      - CONCURRENT=10
      - QUEUED=10
      - TIMEOUT=30000
    shm_size: "2g"
    restart: unless-stopped
```

```bash
docker compose up -d
```

> **Shared memory**
> Always set `shm_size: "2g"` (or `--shm-size=2g` on the CLI). Docker defaults to 64 MB of shared memory, which causes Chrome to crash under load.

## Endpoints

The open-source image exposes REST APIs, WebSocket endpoints, and management routes.

### REST APIs

Each browser has its own path prefix. For Chromium:

| Endpoint | Method | Description |
|----------|--------|-------------|
| `/chromium/content` | POST | Returns rendered HTML content of a page |
| `/chromium/pdf` | POST | Generates a PDF from a page |
| `/chromium/screenshot` | POST | Captures a viewport screenshot as PNG or JPEG |
| `/chromium/scrape` | POST | Scrapes structured data from a page |
| `/chromium/download` | POST | Downloads a file triggered by page interaction |
| `/chromium/function` | POST | Runs a custom function with a browser context |
| `/chromium/performance` | POST | Returns performance metrics for a page |

Replace `/chromium` with `/chrome`, `/firefox`, `/webkit`, or `/edge` when using those browser images.

All REST endpoints accept a `?token=` query parameter for authentication.

### WebSocket endpoints

Connect automation libraries over WebSocket:

| Endpoint | Description |
|----------|-------------|
| `/chromium/playwright` | Playwright WebSocket connection |
| `/?token=` (root) | Puppeteer CDP WebSocket connection |
| `/chromium` | CDP connection for Chromium |

### Management endpoints

| Endpoint | Method | Description |
|----------|--------|-------------|
| `/active` | GET | Liveliness probe (returns 204) |
| `/config` | GET | Current server configuration |
| `/metrics` | GET | Session and performance metrics |
| `/metrics/total` | GET | Aggregate metrics |
| `/pressure` | GET | Current CPU, memory, and queue pressure |
| `/sessions` | GET | Active browser sessions |

## Configuration reference

Configure the container with environment variables. Pass them with `-e` on the CLI or in the `environment` block of your Compose file.

### Core settings

| Variable | Description | Default |
|----------|-------------|---------|
| `TOKEN` | API token for authenticating requests. If not set, all endpoints are unauthenticated, including `/function`. | (none) |
| `CONCURRENT` | Maximum concurrent browser sessions. Additional requests queue. | `10` |
| `QUEUED` | Maximum queued requests. Once full, new requests get a `429` response. | `10` |
| `TIMEOUT` | Session timeout in milliseconds. Set to `-1` to disable (make sure your code closes browsers). | `30000` |
| `PORT` | Internal HTTP port. Map externally with Docker's `-p` flag. | `3000` |
| `HOST` | Address to bind to. The image already sets this to `0.0.0.0`, which accepts connections on every interface inside the container. Reaching it from another container still needs a shared Docker network and a published port. The underlying library defaults to `localhost` only when run outside Docker. | `0.0.0.0` |

### Storage

| Variable | Description | Default |
|----------|-------------|---------|
| `DATA_DIR` | User data directory for cookies, cache, and local storage. Mount a volume for persistence. | OS temp dir |
| `DOWNLOAD_DIR` | Directory for file downloads. Mount a volume to access downloaded files. | OS temp dir |
| `METRICS_JSON_PATH` | Path to persist metrics across restarts. | OS temp dir |

### Health checks

| Variable | Description | Default |
|----------|-------------|---------|
| `HEALTH` | Enable pre-request health checks. Returns `503` when CPU or memory usage exceeds the threshold. | `false` |
| `MAX_CPU_PERCENT` | CPU usage threshold (requires `HEALTH=true`). | `99` |
| `MAX_MEMORY_PERCENT` | Memory usage threshold (requires `HEALTH=true`). | `99` |

### Networking & security

| Variable | Description | Default |
|----------|-------------|---------|
| `CORS` | Enable CORS headers. | `false` |
| `CORS_ALLOW_ORIGIN` | Allowed CORS origins. | `*` |
| `CORS_ALLOW_METHODS` | Allowed HTTP methods for CORS. | `OPTIONS, POST, GET` |
| `CORS_MAX_AGE` | CORS preflight cache max age in seconds. | `2592000` |
| `ALLOW_GET` | Allow GET requests with URL-encoded JSON body. | `false` |
| `ALLOW_FILE_PROTOCOL` | Allow `file://` URLs. | `false` |
| `EXTERNAL` | Public-facing URL for link generation (when behind a reverse proxy). | (none) |

### Debugging

| Variable | Description | Default |
|----------|-------------|---------|
| `DEBUG` | Debug log namespaces ([debug](https://www.npmjs.com/package/debug) module). `*` for all, `-*` for none. | `browserless*,-**:verbose` |
| `TZ` | Container timezone. | `UTC` |
| `ENABLE_DEBUGGER` | Show the built-in debugger UI at the root URL. | `true` |

### Webhook alerts

| Variable | Trigger |
|----------|---------|
| `QUEUE_ALERT_URL` | Requests start queuing |
| `REJECT_ALERT_URL` | Requests rejected (429) |
| `TIMEOUT_ALERT_URL` | Sessions timeout |
| `ERROR_ALERT_URL` | Unhandled errors |
| `FAILED_HEALTH_URL` | Health check failures |

## Common recipes

### Set the timezone

```bash
docker run --rm -p 3000:3000 \
  -e "TOKEN=6R0W53R135510" \
  -e "TZ=America/New_York" \
  ghcr.io/browserless/chromium
```

### Tune concurrency and queueing

Allow 20 concurrent sessions with a queue of 30, and a 5-minute session timeout:

```bash
docker run --rm -p 3000:3000 \
  -e "TOKEN=6R0W53R135510" \
  -e "CONCURRENT=20" \
  -e "QUEUED=30" \
  -e "TIMEOUT=300000" \
  ghcr.io/browserless/chromium
```

### Use a proxy

Pass proxy settings per-session through launch arguments. Browserless doesn't bundle a proxy server, so you'll need to bring your own.

  
**Puppeteer:**

```javascript
const browser = await puppeteer.connect({
  browserWSEndpoint:
    "ws://localhost:3000?token=6R0W53R135510&--proxy-server=http://proxy.example.com:8080",
});
```
  
  
**Playwright:**

```javascript
const browser = await chromium.connectOverCDP(
  "ws://localhost:3000?token=6R0W53R135510&--proxy-server=http://proxy.example.com:8080"
);
```
  

### Persist user data (sessions, cookies, cache)

Mount a volume to `DATA_DIR` so browser data survives container restarts:

```bash
docker run --rm -p 3000:3000 \
  -e "TOKEN=6R0W53R135510" \
  -e "DATA_DIR=/data" \
  -v /path/on/host:/data \
  ghcr.io/browserless/chromium
```

This forces all sessions to share the same user data directory. To use different profiles per session, override the user data directory in your automation code via launch arguments instead.

### Enable health checks

Reject new sessions when the container is under heavy load:

```bash
docker run --rm -p 3000:3000 \
  -e "TOKEN=6R0W53R135510" \
  -e "HEALTH=true" \
  -e "MAX_CPU_PERCENT=80" \
  -e "MAX_MEMORY_PERCENT=80" \
  ghcr.io/browserless/chromium
```

### Run behind a reverse proxy

When Browserless sits behind NGINX or another proxy, set `EXTERNAL` so generated URLs (like those in the `/sessions` response) point to the correct address:

```bash
docker run --rm -p 3000:3000 \
  -e "TOKEN=6R0W53R135510" \
  -e "EXTERNAL=https://browserless.yourcompany.com" \
  ghcr.io/browserless/chromium
```

For full load-balancing setups, see [NGINX Load Balancing](/enterprise/docker/nginx-load-balancing).

## Upgrading

Images on `ghcr.io/browserless/` use a `latest` tag and versioned tags (e.g., `ghcr.io/browserless/chromium:2.26.1`).

**To upgrade:**

1. Pull the new image:
```bash
docker pull ghcr.io/browserless/chromium:latest
```
2. Stop and remove the running container.
3. Start a new container with the same configuration.

For Compose deployments:

```bash
docker compose pull
docker compose up -d
```

> **Pin versions in production**
> Use a specific version tag instead of `latest` so upgrades are intentional:
> ```yaml
> image: ghcr.io/browserless/chromium:2.26.1
> ```

Check the [GitHub releases](https://github.com/browserless/browserless/releases) for changelogs and breaking changes before upgrading.

## Open Source vs. Cloud vs. Enterprise

| Feature | Open Source | Cloud | Enterprise Docker |
|---------|-----------|-------|-------------------|
| **Puppeteer / Playwright** | Yes | Yes | Yes |
| **Core REST APIs** (`/screenshot`, `/pdf`, `/content`, `/scrape`, `/function`, `/download`, `/performance`) | Yes | Yes | Yes |
| **Advanced REST APIs** (`/unblock`, `/smart-scrape`, `/search`, `/map`, `/crawl`, `/agent/run`) | No | Yes | No |
| **Session management** | Basic | Managed | Advanced (priority, persistence) |
| **BrowserQL** | No | Yes | Yes |
| **Stealth / CAPTCHA solving** | No | Yes | Yes |
| **`blockAds` parameter** | Yes | Yes | Yes |
| **`blockConsentModals` parameter** | No | Yes | Yes |
| **Session recording** | No | Yes | Yes |
| **Live debugger** | No | Yes | Yes |
| **Webhooks** | Alerts only | N/A | Full lifecycle events |
| **OpenTelemetry** | No | N/A | Yes |
| **Token roles** | No | N/A | Yes |
| **Managed proxies** | No | Yes | No (bring your own) |
| **Support** | Community (GitHub Issues) | Email | Priority engineering support |
| **Pricing** | Free | Usage-based | License-based |

Ready to upgrade? See the [Enterprise Deployment Guide](/enterprise/docker/enterprise-image) or [contact sales](https://www.browserless.io/contact).

## FAQ & Troubleshooting

<details>
<summary>Is Smart Scrape (<code>/smart-scrape</code>) available in the open-source image?</summary>

No. `/smart-scrape` is a Cloud-only endpoint, along with `/unblock`, `/search`, `/map`, `/crawl`, and `/agent/run`. The closest open-source equivalents are [`/scrape`](/rest-apis/scrape) for structured extraction and [`/content`](/rest-apis/content) for rendered HTML.

</details>

<details>
<summary>Can I use stealth or <code>/unblock</code> in the open-source image?</summary>

No. Stealth routes and CAPTCHA solving require Cloud or an [Enterprise license](/enterprise/docker/enterprise-image), and `/unblock` is Cloud-only. The open-source image runs standard browsers; you can still pass your own launch flags and proxies.

</details>

<details>
<summary>Do <code>blockAds</code> and <code>blockConsentModals</code> work in the open-source image?</summary>

`blockAds` works: the image ships a built-in ad blocker you can enable per request or connect call. `blockConsentModals` is not in the open-source image; it's available on Cloud and Enterprise.

</details>

<details>
<summary>Chrome crashes with "out of memory"</summary>

Docker defaults shared memory (`/dev/shm`) to 64 MB. Chrome needs more.

**Fix:** Add `--shm-size=2g` to your `docker run` command, or `shm_size: "2g"` in Compose.

</details>

<details>
<summary>"Connection refused" when connecting from another container</summary>

The image already binds to `0.0.0.0` by default, so this is usually something else: a firewall, the two containers not sharing a Docker network, or an explicit `HOST` override to something more restrictive like `127.0.0.1`. Confirm both containers are on the same network and that you haven't set `HOST` yourself.

</details>

<details>
<summary>Do I need to set a token?</summary>

Yes, unless the container is only reachable by you. Browserless doesn't generate one for you: leaving `TOKEN` unset leaves every endpoint unauthenticated, including `/function`, which executes arbitrary Puppeteer code from the request body. Set your own value with `-e "TOKEN=your-secret-value"`.

</details>

<details>
<summary>Getting 429 responses</summary>

Your request queue is full. Either increase `CONCURRENT` and `QUEUED`, or slow down your request rate. Check `/pressure` to see current load.

</details>

<details>
<summary>Sessions hang or don't close</summary>

Set a reasonable `TIMEOUT` (default is 30 seconds). If you set `TIMEOUT=-1`, make sure your code calls `browser.close()`. Otherwise sessions stay open indefinitely and consume resources.

</details>

<details>
<summary>Fonts or characters render incorrectly</summary>

The Docker images include common fonts for Latin, CJK, Arabic, Thai, and Indic scripts. If you need additional fonts, [extend the image](/enterprise/docker/extending) with your own Dockerfile.

</details>

<details>
<summary>How do I use Firefox or WebKit instead of Chromium?</summary>

Use the corresponding image and WebSocket path:

```bash
# Firefox
docker run --rm -p 3000:3000 -e "TOKEN=6R0W53R135510" ghcr.io/browserless/firefox

# WebKit
docker run --rm -p 3000:3000 -e "TOKEN=6R0W53R135510" ghcr.io/browserless/webkit
```

Connect with Playwright (Firefox and WebKit don't support CDP, so Puppeteer won't work):

```javascript
const browser = await playwright.firefox.connect("ws://localhost:3000/firefox/playwright?token=6R0W53R135510");
```

</details>

<details>
<summary>Can I run multiple browsers in one container?</summary>

Yes, use the `multi` image. It includes all supported browsers and exposes each on its own path (`/chromium`, `/chrome`, `/firefox`, `/webkit`, `/edge`).

</details>

<details>
<summary>Where do I report bugs or request features?</summary>

Open an issue on [GitHub](https://github.com/browserless/browserless/issues).

</details>

## Next steps

  <a href="/enterprise/docker/enterprise-image" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Docker deployment</h3>
      self-host Browserless with Docker
    
  </a>
  <a href="/enterprise/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure your Enterprise deployment
    
  </a>

---

# Enterprise Overview

URL: https://docs.browserless.io/enterprise/overview

Browserless Enterprise is a browser automation platform for organizations that need full infrastructure control, network isolation, and dedicated resources beyond what shared cloud plans provide.

## How It Works

1. Choose a deployment model: self-host with Docker, or have Browserless manage a private deployment across major cloud providers.
2. Connect your applications via WebSocket (Puppeteer/Playwright) or REST APIs.
3. Monitor performance and debug sessions in real time with built-in tools.

## Enterprise Features

- **[User Data Directory](/enterprise/user-data-directory)**: Persist browser sessions and cookies between runs
- **[Watching Sessions](/enterprise/watching-sessions)**: Monitor and debug browser sessions in real time
- **[Live Debugger](/enterprise/live-debugger)**: Debug your browser automation scripts interactively
- **[Utility Functions](/enterprise/utility-functions/intro)**: Manage sessions, configuration, and metrics
- **[Queue Management](/enterprise/long-queues)**: Handle high-volume workloads with queue management and priority processing

## Deployment Options

  <a href="/enterprise/open-source" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Open Source (Self-Hosted)</h3>
      Free, self-hosted solution with core browser automation capabilities. Best for testing, development, and small projects.
    
  </a>

  <a href="/enterprise/docker/enterprise-image" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Enterprise Docker (Self-Hosted)</h3>
      Full Enterprise features in a self-hosted container. Ideal for production workloads requiring data sovereignty.
    
  </a>

  <a href="https://www.browserless.io/contact" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Private Deployment</h3>
      Custom Enterprise infrastructure across major cloud providers. Best for large-scale enterprise deployments.
    
  </a>

For any questions or assistance, [contact our support team](https://www.browserless.io/contact).

## FAQ & Troubleshooting

<details>
<summary>How do I get an Enterprise license?</summary>

Contact [sales@browserless.io](mailto:sales@browserless.io) or visit the [pricing page](https://browserless.io/pricing) for Enterprise plan details.

</details>

<details>
<summary>Can I run Browserless on my own infrastructure?</summary>

Yes. Enterprise customers can self-host using the Docker image. See [Docker deployment](/enterprise/docker/enterprise-image) for setup instructions.

</details>

## Next steps

  <a href="/enterprise/docker/enterprise-image" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Docker deployment</h3>
      Self-host Browserless with Docker
    
  </a>
  <a href="/enterprise/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      Configure your Enterprise deployment
    
  </a>

---

# Fleet Operations

URL: https://docs.browserless.io/enterprise/private-deployment/fleet-operations

Your fleet dashboard exposes three operations for managing your workers: Restart Worker, Force Restart, and Relaunch. For monitoring worker health before and after these operations, see [observability](/enterprise/private-deployment/observability).

- A Browserless Private Deployment fleet

## Restart Worker

Restarts the Browserless service on a single worker (same VM, same hardware). Use this when one worker is behaving unexpectedly while the rest of the fleet is healthy.

- Terminates active sessions on that worker only
- Other workers continue serving traffic
- Does not affect IP addresses
- Completes in seconds

Access this from the **Production Workers** table in your dashboard by selecting an individual worker.

## Force Restart

Restarts the Browserless service on all workers simultaneously (same VMs, same hardware). Use this for fleet-wide service issues when you want a quick reset without provisioning new infrastructure.

- May interrupt active sessions across all workers
- Does not affect IP addresses
- Faster than Relaunch

> **Warning**
> If CPU or memory are consistently high and Force Restart doesn't resolve the issue, use **Relaunch** instead. Restarting the service on an overloaded machine will not clear the underlying resource pressure.

## Relaunch

Provisions entirely new VMs and containers using a blue-green deployment. Your current workers continue serving traffic while new ones come online. Once the new fleet is healthy, traffic switches over and the old VMs are decommissioned.

Use Relaunch when:
- Session times have been gradually increasing over days or weeks
- Force Restart did not resolve the issue
- A node has gone offline and traffic is not routing correctly

Image version upgrades are also executed as a Relaunch. Browserless coordinates the rollout with you.

### What changes during a Relaunch

| | Changes |
|---|---|
| API token | No, stays the same |
| Worker IP addresses | Yes, new VMs get new IPs |
| Active sessions | Not interrupted (traffic continues on old workers during transition) |
| Dashboard metrics | Reset on the new workers |
| `estimatedMonthlyUnits` | Unreliable for several hours (insufficient history on new workers) |

**Duration:** Approximately 5 to 10 minutes.

### Update firewall allowlists after a Relaunch

Each worker has a fixed public IP shown beneath its title in the **Workers** section of your cluster page. When you Relaunch, these IPs change. If you or any third-party service uses IP-based allowlisting, update those lists after the new workers are online.

The new IPs appear in the Workers section once the Relaunch completes.

For details on matching client library versions after an upgrade, see [image versions](/enterprise/private-deployment/image-versions). For load balancing configuration across your fleet, see [load balancing](/enterprise/private-deployment/load-balancing).

## FAQ & Troubleshooting

<details>
<summary>How do I get a Browserless Private Deployment fleet?</summary>

Contact [sales@browserless.io](mailto:sales@browserless.io) or visit the [pricing page](https://browserless.io/pricing) for Enterprise plan details.

</details>

<details>
<summary>Can I run Browserless on my own infrastructure?</summary>

Yes. Enterprise customers can self-host using the Docker image. See [Docker deployment](/enterprise/docker/enterprise-image) for setup instructions.

</details>

## Next steps

  <a href="/enterprise/docker/enterprise-image" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Docker deployment</h3>
      self-host Browserless with Docker
    
  </a>
  <a href="/enterprise/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure your Enterprise deployment
    
  </a>

---

# Browserless GraphQL API

URL: https://docs.browserless.io/enterprise/private-deployment/graphql-api

The Browserless GraphQL API exposes queries for monitoring fleet health, session activity, and unit consumption on your dedicated fleet.

- A Browserless Private Deployment fleet

> **Note**
> This GraphQL API is specifically for monitoring and managing private Browserless deployments (checking session pressure, metrics, etc). If you are looking for browser automation capabilities, you should use [BrowserQL](/browserql/start) instead, which is our dedicated browser automation API.

## Pressure

The `pressure` query shows how much load your instance(s) are under, and whether they can accept more traffic or not. This is real-time, so if you need to check if your instance can take more traffic you can do a request to check prior to running your `puppeteer.connect` call.

> **Warning**
> The pressure queries are still in BETA and may experience breaking schema changes.

**GraphQL Example:**

```graphql
{
  pressure(apiToken: "YOUR_API_TOKEN_HERE") {
    running
    recentlyRejected
    queued
    isAvailable
    date
  }
}
```

**cURL Example:**

```bash
curl --location 'https://api.browserless.io/graphql' \
--header 'Content-Type: application/json' \
--data '{"query":"{\n  pressure(apiToken: \"YOUR_API_TOKEN_HERE\") {\n    running\n    recentlyRejected\n    queued\n    isAvailable\n    date\n  }\n}","variables":{}}'
```

This request will return a JSON object with the following payload:

```json
{
  "data": {
    "pressure": {
      "running": 0,
      "recentlyRejected": 0,
      "queued": 0,
      "isAvailable": true,
      "date": 1524762532204
    }
  }
}
```

Use the `isAvailable` boolean to check if the instance can handle more connections before running your `puppeteer.connect` call. This is useful for pre-session checks to keep workers healthy and prevent rejections or queuing.

## Metrics

The `metrics` query returns performance data for your workers, including successful, rejected, and timed-out session counts.

> **Warning**
> The metrics queries are still in BETA and may experience breaking schema changes.

**GraphQL Example:**

```graphql
{
  metrics(apiToken: "YOUR_API_TOKEN_HERE") {
    successful
    rejected
    timedout
    queued
    cpu
    memory
    date
  }
}
```

**cURL Example:**

```bash
curl --location 'https://api.browserless.io/graphql' \
--header 'Content-Type: application/json' \
--data '{"query":"{\n  metrics(apiToken: \"YOUR_API_TOKEN_HERE\") {\n    successful\n    rejected\n    timedout\n    queued\n    cpu\n    memory\n    date\n  }\n}","variables":{}}'
```

This request returns an array of objects with metrics for your instances. When multiple instances exist, stats are aggregated in 5-minute intervals. CPU and memory are averaged across instances.

```json
{
  "data": {
    "metrics": [
      {
        "successful": 0,
        "rejected": 0,
        "timedout": 0,
        "queued": 0,
        "cpu": 0.002734375,
        "memory": 0.9055320561641963,
        "date": 1524227700000
      },
      //...
    ]
  }
}
```

## Sessions

> **Not for Session Management**
> This GraphQL sessions query is for monitoring currently running browser sessions. It should not be confused with the [REST sessions endpoint](/enterprise/utility-functions/sessions), which is used to create and manage persistent browser session endpoints.

The `sessions` query returns currently running browser instances. This query requires a dedicated fleet for security.

**GraphQL Example:**

```graphql
{
  sessions(apiToken: "YOUR_API_TOKEN_HERE") {
    description
    devtoolsFrontendUrl
    live
    kill
    title
    type
    url
    trackingId
    browserId
    browserWSEndpoint
    browserWSEndpointClient
  }
}
```

**cURL Example:**

```bash
curl --location 'https://api.browserless.io/graphql' \
--header 'Content-Type: application/json' \
--data '{
  "query": "{ sessions(apiToken: \"YOUR_API_TOKEN_HERE\") { description devtoolsFrontendUrl live kill title type url trackingId browserId browserWSEndpoint browserWSEndpointClient } }",
  "variables": {}
}'
```

> **Warning**
> For security purposes, we limit the number of failed GraphQL request attempts. If you encounter rate limiting errors, you'll need to wait until the top of the hour before making additional requests.

## Export Metrics

Use the `exportMetrics` query to retrieve unit consumption data. This is more accurate than the REST `/metrics/total` endpoint because it queries aggregated billing data rather than per-worker stats. Stats from `/metrics/total` reset after a Relaunch; `exportMetrics` does not.

### Authentication

The `exportMetrics` query requires an auth token obtained via the `login` mutation:

```graphql
mutation Login($email: String!, $password: String!) {
  login(email: $email, password: $password) {
    authToken
    needsSecondFactor
  }
}
```

If `needsSecondFactor` is `true`, complete authentication with `loginSecondFactor`. Pass the `authToken` from the `login` response and the OTP code from your authenticator app:

```graphql
mutation LoginSecondFactor($authToken: String!, $otp: String!) {
  loginSecondFactor(authToken: $authToken, otp: $otp) {
    authToken
  }
}
```

Use the `authToken` returned by `loginSecondFactor` (or by `login` if 2FA is not enabled) in the query below.

### Query

```graphql
query ExportMetrics($apiToken: String!, $timeslot: timeslot, $authToken: String) {
  exportMetrics(apiToken: $apiToken, timeslot: $timeslot, authToken: $authToken) {
    date
    totalUnitsUsed
    timeUnits
    proxyUnits
    captchaUnits
  }
}
```

The `timeslot` parameter accepts `"year"`, `"month"`, or `"week"`.

## FAQ & Troubleshooting

<details>
<summary>How do I get an Enterprise license?</summary>

Contact [sales@browserless.io](mailto:sales@browserless.io) or visit the [pricing page](https://browserless.io/pricing) for Enterprise plan details.

</details>

<details>
<summary>Can I run Browserless on my own infrastructure?</summary>

Yes. Enterprise customers can self-host using the Docker image. See [Docker deployment](/enterprise/docker/enterprise-image) for setup instructions.

</details>

## Next steps

  <a href="/enterprise/docker/enterprise-image" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Docker deployment</h3>
      self-host Browserless with Docker
    
  </a>
  <a href="/enterprise/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure your Enterprise deployment
    
  </a>

---

# Image Versions and Upgrades

URL: https://docs.browserless.io/enterprise/private-deployment/image-versions

This page covers how to check your current image version, match client library versions, and coordinate upgrades. For the operational steps involved in applying an upgrade, see [fleet operations](/enterprise/private-deployment/fleet-operations).

- A Browserless Private Deployment fleet

## Checking Your Current Version

Your current image version is shown in the dashboard under your cluster settings. Hover over the version label for full details. If a newer version is available, the dashboard displays a notification.

## Why Versions Matter

The image version determines:

- The bundled **Chromium binary** (used by both Puppeteer and Playwright)
- **Continuous improvements**: each version brings performance gains, stability fixes, and expanded dashboard visibility

## Matching Client Library Versions

Both Puppeteer and Playwright connect to the same Chromium binary bundled with your image. For best compatibility, match your client-side library version to what the image bundles.

The dashboard shows the Puppeteer version for your current image. For example, an image tagged `enterprise:2.5.0_chromium-124.0.6367.29_firefox-124.0_webkit-17.4` displays as Puppeteer `v22.6.4` in the dashboard. Use that version in your project: `npm install puppeteer@22.6.4`. Mismatched versions can cause connection failures, unexpected behavior, or protocol errors.

Contact Browserless support if you need the exact library version for your current image.

## Upgrade Process

Coordinate upgrades with Browserless support. The general process:

1. **Request a test environment** from Browserless, with a separate API token pointing to a worker running the newer image (same machine profile as your production fleet).
2. **Run your integration tests** against the test environment. Verify that connection URLs work, that your launch parameters are compatible, and that screenshots and PDFs render correctly.
3. **Coordinate production upgrade** with Browserless. The production fleet is updated via a [Relaunch](/enterprise/private-deployment/fleet-operations) onto new workers running the newer image. IP addresses will change. Update any firewall allowlists after the upgrade.

## FAQ & Troubleshooting

<details>
<summary>How do I get a Browserless Private Deployment fleet?</summary>

Contact [sales@browserless.io](mailto:sales@browserless.io) or visit the [pricing page](https://browserless.io/pricing) for Enterprise plan details.

</details>

<details>
<summary>Can I run Browserless on my own infrastructure?</summary>

Yes. Enterprise customers can self-host using the Docker image. See [Docker deployment](/enterprise/docker/enterprise-image) for setup instructions.

</details>

## Next steps

  <a href="/enterprise/docker/enterprise-image" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Docker deployment</h3>
      self-host Browserless with Docker
    
  </a>
  <a href="/enterprise/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure your Enterprise deployment
    
  </a>

---

# Load Balancing

URL: https://docs.browserless.io/enterprise/private-deployment/load-balancing

This page covers how regional load balancers route traffic across your Private Deployment fleet, dedicated worker IP addresses, and how concurrency and queue settings interact with multiple workers.

- A Browserless Private Deployment fleet

## Regional Load Balancers

Your workers run in one of four locations: San Francisco, New York, Amsterdam, or London. Requests to `https://chrome.browserless.io` route to one of the four regions automatically.

To reduce latency, connect through the load balancer nearest to your servers:

1. West load balancer (SFO): `https://chrome-us-west.browserless.io`
2. East load balancer (NYC): `https://chrome-us-east.browserless.io`
3. UK load balancer (LON): `https://chrome-eu-uk.browserless.io`
4. AMS load balancer (AMS): `https://chrome-eu-ams.browserless.io`

## Dedicated Worker IP Addresses

Each dedicated worker's public IP is shown beneath its title in the **Workers** section of your cluster page. Click the IP address to copy it. Use these IPs if you need to allowlist traffic from your dedicated workers. For shared fleet IPs, see [Whitelisting IP Addresses](/baas/troubleshooting/whitelisting-ips).

> **IP addresses change on Relaunch**
> When you [Relaunch](/enterprise/private-deployment/fleet-operations#relaunch) your fleet, new VMs are provisioned and worker IP addresses change. Update any IP-based allowlists after the Relaunch completes. The new IPs appear in the Workers section once the fleet is online.

## Concurrency and Queue Settings

Configure concurrency and queue limits in **Settings** in your [account dashboard](https://browserless.io/account/). When a worker reaches its concurrency limit, new requests enter the queue. If the queue is full, the worker returns 429. For tuning guidance, see [Worker Settings](/enterprise/private-deployment/worker-settings#concurrency-and-queue).

## Multiple Worker Load Balancing

With multiple workers, Browserless load-balances across them using least-connections routing. To manage load-balancing yourself, find each worker's individual endpoint directly beneath its title in the **Workers** section of your cluster page. Click the IP address to copy it to your clipboard.

## FAQ & Troubleshooting

<details>
<summary>How do I get a Browserless Private Deployment fleet?</summary>

Contact [sales@browserless.io](mailto:sales@browserless.io) or visit the [pricing page](https://browserless.io/pricing) for Enterprise plan details.

</details>

<details>
<summary>Can I run Browserless on my own infrastructure?</summary>

Yes. Enterprise customers can self-host using the Docker image. See [Docker deployment](/enterprise/docker/enterprise-image) for setup instructions.

</details>

## Next steps

  <a href="/enterprise/docker/enterprise-image" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Docker deployment</h3>
      self-host Browserless with Docker
    
  </a>
  <a href="/enterprise/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure your Enterprise deployment
    
  </a>

---

# Observability

URL: https://docs.browserless.io/enterprise/private-deployment/observability

This page covers unit consumption tracking, metrics endpoints, fleet health queries, dashboard limitations, and client-side monitoring recommendations for private fleets.

- A Browserless Private Deployment fleet

## GraphQL Unit Consumption Tracking

For reliable unit consumption data, use the `exportMetrics` query via the Browserless account API. This is more accurate than `/metrics/total` because it queries aggregated billing data rather than per-worker stats. It also survives Relaunches, unlike per-worker REST stats.

For the full authentication flow (`login`, `loginSecondFactor`) and the `exportMetrics` query, see [GraphQL API](/enterprise/private-deployment/graphql-api#export-metrics).

## REST Metrics API

The `/metrics` and `/metrics/total` endpoints return session statistics for your fleet. These are useful for operational monitoring but less reliable than `exportMetrics` for billing data: stats are per-worker and reset after a Relaunch.

:::important
Private fleet customers must call these endpoints on their **dedicated fleet URL**, not on a shared fleet URL. Use `chrome.browserless.io` (or your regional variant), not `production-sfo.browserless.io`.
:::

```sh
# Get per-interval metrics (last ~7 days, in 5-minute buckets)
curl "https://chrome.browserless.io/metrics?token=YOUR_TOKEN"

# Get aggregate totals
curl "https://chrome.browserless.io/metrics/total?token=YOUR_TOKEN"
```

Calling `/metrics` does not consume units.

See [Metrics API](/enterprise/utility-functions/metrics) for the full response schema and examples.

> **Note**
> Do not include a `Content-Type: application/json` header on GET requests to the metrics REST endpoint. This returns HTTP 400. GET requests do not require a content type header.

### Known limitations

- **Metrics reset after a Relaunch.** New VMs have no history. `estimatedMonthlyUnits` is unreliable for several hours until sufficient data accumulates.
- **Request counts can appear inflated.** When a health-rejected request is forwarded to another worker in the fleet, both the rejection and the forwarded request may be counted separately. Compare against your client-side request counts if you see unexpected discrepancies.

## GraphQL API for Fleet Health

The Browserless GraphQL API provides real-time visibility into fleet health and session activity. Use your dedicated fleet API token for all queries.

- **`pressure()`**: current load and availability. Useful for pre-session checks before connecting.
- **`metrics()`**: per-interval CPU, memory, and session success/failure counts.
- **`sessions()`**: currently running browser sessions.
- **`exportMetrics()`**: aggregated unit consumption data by time slot. More reliable than REST metrics for billing purposes.

See [GraphQL API](/enterprise/private-deployment/graphql-api) for query examples and the full schema.

## Dashboard Limitations

The dashboard provides a useful operational view, but has limitations:

- **Health-check rejections** (HTTP 500 when CPU/memory exceeds 90%) do not appear in dashboard session counts. Because the request is rejected before a browser is created, no session is ever logged.
- **Browser-side errors** (JavaScript exceptions thrown on your target page) are not recorded by Browserless. Implement your own error logging within browser sessions if you need this visibility.
- **Server-side container logs** are not accessible to customers. Contact Browserless support for server-side log investigation.

## Client-Side Monitoring

Because dashboard metrics have limitations and may not reflect gradual performance degradation, instrument your own client-side timing. Track the following for each session:

- WebSocket connection time (time to establish the browser connection)
- Page navigation time (`page.goto()` duration)
- Total session duration

**Alert on upward trends over days**, not only on absolute error thresholds. A gradual increase in session times over one to two weeks is the typical early indicator that a [Relaunch](/enterprise/private-deployment/fleet-operations#relaunch) would restore performance.

## FAQ & Troubleshooting

<details>
<summary>How do I get a Browserless Private Deployment fleet?</summary>

Contact [sales@browserless.io](mailto:sales@browserless.io) or visit the [pricing page](https://browserless.io/pricing) for Enterprise plan details.

</details>

<details>
<summary>Can I run Browserless on my own infrastructure?</summary>

Yes. Enterprise customers can self-host using the Docker image. See [Docker deployment](/enterprise/docker/enterprise-image) for setup instructions.

</details>

## Next steps

  <a href="/enterprise/docker/enterprise-image" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Docker deployment</h3>
      self-host Browserless with Docker
    
  </a>
  <a href="/enterprise/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure your Enterprise deployment
    
  </a>

---

# Private Deployment Overview

URL: https://docs.browserless.io/enterprise/private-deployment/overview

Private Deployment is a fully managed hosting option where Browserless runs dedicated infrastructure in the cloud on your behalf. Your workers run on isolated VMs, not shared with other customers. You connect through similar Browserless-hosted endpoints and the same APIs as the shared cloud fleet, but with dedicated resources, configurable concurrency, fleet management controls, and premium support.

Unlike [self-hosted Docker](/enterprise/docker/enterprise-image), where you manage infrastructure, scaling, updates, and monitoring yourself, Private Deployment handles all of that for you. Both options use the same enterprise image and support the same APIs.

## Private Deployment Features

With a private fleet, Browserless takes care of the operational overhead so you can focus on your automation code:

- **[Fleet Operations](/enterprise/private-deployment/fleet-operations):** Restart, force restart, or relaunch your workers from the dashboard without touching infrastructure.
- **[Worker Settings](/enterprise/private-deployment/worker-settings):** Adjust concurrency, timeouts, and health checks per worker from your account page.
- **[Load Balancing](/enterprise/private-deployment/load-balancing):** Browserless distributes traffic across your workers automatically with regional load balancers. You can also address individual workers directly for session persistence.
- **[GraphQL API](/enterprise/private-deployment/graphql-api) and [Observability](/enterprise/private-deployment/observability):** Track fleet health, unit consumption, and session metrics through the API and the dashboard.
- **[Performance and Capacity](/enterprise/private-deployment/performance):** Browserless manages capacity planning and health checks for your fleet. You configure the limits, the platform enforces them.
- **[Image Versions](/enterprise/private-deployment/image-versions):** Choose which image runs on your workers and coordinate upgrades on your schedule.
- **[Timeouts](/enterprise/private-deployment/timeouts):** Understand how timeouts layer from your client code through the load balancer to the worker process.

## Getting started

Access your fleet from your [Browserless account dashboard](https://browserless.io/account/). Connection URLs use the same format as the shared fleet. Only the host changes. For questions or to provision a private deployment, [contact us](https://www.browserless.io/contact).

## FAQ & Troubleshooting

<details>
<summary>How do I get an Enterprise license?</summary>

Contact [sales@browserless.io](mailto:sales@browserless.io) or visit the [pricing page](https://browserless.io/pricing) for Enterprise plan details.

</details>

<details>
<summary>Can I run Browserless on my own infrastructure?</summary>

Yes. Enterprise customers can self-host using the Docker image. See [Docker deployment](/enterprise/docker/enterprise-image) for setup instructions.

</details>

## Next steps

  <a href="/enterprise/docker/enterprise-image" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Docker deployment</h3>
      self-host Browserless with Docker
    
  </a>
  <a href="/enterprise/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure your Enterprise deployment
    
  </a>

---

# Performance and Capacity

URL: https://docs.browserless.io/enterprise/private-deployment/performance

This page covers capacity planning, health check behavior, burst traffic patterns, and performance best practices for your private fleet.

- A Browserless Private Deployment fleet

## Fleet Capacity

Your fleet's total concurrency is the product of the number of workers and the per-worker concurrency limit.

```
Total capacity = workers × per-worker concurrency
Example: 3 workers × 6 sessions = 18 concurrent sessions
```

Set your client-side concurrency limit below total fleet capacity to leave headroom for burst variance. For example, with 18 total slots, limiting client concurrency to 15–16 prevents the queue from filling during minor load spikes.

For concurrency and queue configuration, see [Worker Settings](/enterprise/private-deployment/worker-settings).

## Health Checks and Rejection Thresholds

Workers monitor CPU and memory utilization before accepting new sessions. When a worker's CPU or memory exceeds **90%**, it rejects incoming requests with:

```
HTTP 500: "Health checks have failed, rejecting"
```

This is expected behavior under sustained overload. The worker is protecting itself from becoming unresponsive. Reduce burst load, stagger requests (see below), or add workers to the fleet if this occurs regularly.

> **Note**
> Health check rejections may not appear in standard dashboard metrics. If you see unexplained HTTP 500 errors with this message, your workers are hitting resource limits.

## Burst Traffic

When many requests arrive simultaneously (for example, from a scheduled job), all requests compete for the same workers at the same instant. Browser launch is the most CPU-intensive phase, so a burst of launches can overload a worker even when the fleet has available capacity overall.

**Stagger your initial requests by 5–10 seconds.** Once the first batch of browsers is running, subsequent requests queue naturally and don't spike CPU in the same way. Even spreading the first few requests provides significant relief.

## Performance Degradation Over Time

Workers can exhibit gradual performance degradation over weeks of continuous operation: session connection times increase, page load times lengthen, and screenshot or PDF generation slows. This degradation is visible in your own client-side timing metrics but may not appear in the dashboard.

When you observe an upward trend in session times over days:

1. Perform a **Relaunch** from the dashboard. This provisions new VMs and typically restores baseline performance.
2. If Relaunch from the dashboard does not improve performance, contact Browserless support to request a relaunch onto new hardware.

See [Fleet Operations](/enterprise/private-deployment/fleet-operations) for the distinction between Restart and Relaunch.

## Client-Side Best Practices

These practices reduce unnecessary resource consumption on your workers:

- **Always close browser connections** in `finally` or `catch` blocks. Unclosed sessions occupy a concurrency slot until the global timeout fires.
- **Use shorter global timeouts.** A 5-minute timeout means a crashed session occupies a slot for up to 5 minutes. A 1–2 minute timeout cleans up zombie sessions faster. See [Worker Settings](/enterprise/private-deployment/worker-settings#global-timeout) to configure the global timeout.
- **Avoid serializing large objects** inside browser sessions. Logging or serializing large DOM structures or data objects causes memory spikes on the worker.

## Special Instance Types

Standard instances cover the requirements of 99% of private deployments. For specific workloads, Browserless can provision special instances. A few examples:

- **GPU-enabled instances**: suited for rendering-heavy workloads such as maps, complex data visualizations, or WebGL content.
- **Apple M1 instances**: suited for workloads that benefit from ARM-native performance.

To request a special instance type, contact Browserless support. Changing instance type requires a Relaunch.

## FAQ & Troubleshooting

<details>
<summary>How do I get a Browserless Private Deployment fleet?</summary>

Contact [sales@browserless.io](mailto:sales@browserless.io) or visit the [pricing page](https://browserless.io/pricing) for Enterprise plan details.

</details>

<details>
<summary>Can I run Browserless on my own infrastructure?</summary>

Yes. Enterprise customers can self-host using the Docker image. See [Docker deployment](/enterprise/docker/enterprise-image) for setup instructions.

</details>

## Next steps

  <a href="/enterprise/docker/enterprise-image" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Docker deployment</h3>
      self-host Browserless with Docker
    
  </a>
  <a href="/enterprise/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure your Enterprise deployment
    
  </a>

---

# Timeouts

URL: https://docs.browserless.io/enterprise/private-deployment/timeouts

This page walks through the full timeout chain for a Private Deployment fleet, self-hosted timeout considerations, and common timeout errors and their fixes.

- A Browserless Private Deployment fleet

## The Timeout Chain

Timeouts operate from innermost (your code) to outermost (infrastructure):

### Code-level timeouts

These are timeouts you set in your Puppeteer or Playwright code:

- `page.goto()` navigation timeout (default 30,000ms in Puppeteer/Playwright)
- `page.waitForSelector()` timeout
- `browserType.connect()` connection timeout (how long to wait to establish the WebSocket connection)

These are controlled entirely by your code and your library version. Increase them via `gotoOptions.timeout` in REST API calls, or `page.setDefaultNavigationTimeout()` in library code.

### Session timeout

The maximum lifetime of a browser session. When a session reaches this limit, Browserless terminates it regardless of what it is doing.

**Configure it two ways:**

- **Per-request:** Add `&timeout=<milliseconds>` to the connection URL. Example: `wss://chrome.browserless.io?token=YOUR_TOKEN&timeout=120000`
- **Global default:** Set the default for all sessions in your cluster dashboard under **Settings → Timeout**.

The default is 300,000ms (5 minutes). This acts as a safety net for sessions that hang or are not properly closed by your code. See [Worker Settings](/enterprise/private-deployment/worker-settings#global-timeout).

### Queue timeout

When all concurrency slots are full, new connection requests wait in a queue. If no slot opens in time, Browserless rejects the queued request.

Configure queue length in the dashboard under **Settings → Queue**. If you want immediate 429 errors instead of queuing, set queue length to 0. See [Worker Settings](/enterprise/private-deployment/worker-settings#concurrency-and-queue).

### Infrastructure timeout

The load balancer closes connections that hold open without sending or receiving data for an extended period. This timeout is not configurable. Your client receives a connection reset or generic timeout error, not a specific error code.

This is usually a symptom of concurrency pressure: requests waiting in the queue with no data moving. Address it by reducing concurrent load or adding workers.

---

## Self-Hosted Timeout Considerations

### WebSocket Heartbeat Interval

The enterprise image sends WebSocket heartbeat pings every 30 seconds by default (`HEARTBEAT_INTERVAL` env var). Load balancers with idle connection timeouts shorter than the heartbeat interval close connections silently. Set `HEARTBEAT_INTERVAL` below your load balancer's idle timeout. Common defaults:

- AWS ALB: 60 seconds
- NGINX `proxy_read_timeout`: 60 seconds
- Azure Container Apps: 240 seconds

### Per-Connection Timeout Override

Add `?timeout=<milliseconds>` to the WebSocket connection URL to override the global `TIMEOUT` for a single session:

```
ws://host:3000/chromium?token=TOKEN&timeout=300000
```

### Timeout Independence

These timeouts are all separate and must each be configured explicitly:

| Timeout | Controls | Configuration |
|---------|----------|---------------|
| Session timeout | How long the entire session lives | `TIMEOUT` env var or `?timeout=` query param |
| Reconnect timeout | How long the browser waits for a reconnection after disconnect | `timeout` parameter in `Browserless.reconnect` CDP command |
| LiveURL timeout | How long the LiveURL stream stays active | `timeout` parameter in `Browserless.liveURL` CDP command |
| MAX_RECONNECT_TIME | Caps the maximum reconnect timeout any client can request | `MAX_RECONNECT_TIME` env var. Default: `Infinity` |

### Reverse Proxy Alignment

Your reverse proxy's read/idle timeout must exceed the `TIMEOUT` env var. If NGINX closes a connection at 60 seconds but `TIMEOUT` is 300 seconds, the client receives a connection reset with no Browserless error code. Example NGINX alignment:

```nginx
proxy_read_timeout 600s;  # Must exceed TIMEOUT (300000ms = 300s, add buffer)
proxy_send_timeout 600s;
```

---

## Common Timeout Errors

| Error | Likely cause | Fix |
|---|---|---|
| `browserType.connect: Timeout Xms exceeded` | Fleet at capacity, wrong URL, or network issue | Verify URL, check fleet concurrency, increase connection timeout |
| `Navigation timeout of 30000 ms exceeded` | Target page loaded too slowly | Increase `gotoOptions.timeout` |
| `waiting for fonts to load` in screenshot timeout | Web fonts loading slowly during capture | Ensure fonts load before calling screenshot; check for slow font CDNs |
| HTTP 408 | `waitForEvent` event never fired before timeout | Verify the event name and that your page actually dispatches it |
| HTTP 429 | Queue is full | Implement backoff, reduce concurrency, or increase queue length |
| HTTP 500 `Health checks have failed, rejecting` | Worker CPU/memory above 90% | Stagger burst requests, reduce concurrency, or add workers |
| `Target page, context or browser has been closed` | Session timed out or crashed while an operation was in progress | Add error handling, verify timeout settings |
| `Protocol error (Network.getResponseBody): No data found` | Session crashed or page navigated away during a network operation | Add error handling; if persistent, contact support (may indicate memory pressure on the worker) |

## FAQ & Troubleshooting

<details>
<summary>How do I get a Browserless Private Deployment fleet?</summary>

Contact [sales@browserless.io](mailto:sales@browserless.io) or visit the [pricing page](https://browserless.io/pricing) for Enterprise plan details.

</details>

<details>
<summary>Can I run Browserless on my own infrastructure?</summary>

Yes. Enterprise customers can self-host using the Docker image. See [Docker deployment](/enterprise/docker/enterprise-image) for setup instructions.

</details>

## Next steps

  <a href="/enterprise/docker/enterprise-image" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Docker deployment</h3>
      self-host Browserless with Docker
    
  </a>
  <a href="/enterprise/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure your Enterprise deployment
    
  </a>

---

# Worker settings

URL: https://docs.browserless.io/enterprise/private-deployment/worker-settings

Worker settings control how your Private Deployment fleet behaves: the Chrome/Puppeteer version, concurrency and queue limits, session health checks, stealth mode, and experimental features. This page covers what each setting does and how to configure it.

- A Browserless Private Deployment fleet

## Opening worker settings

To open worker settings, log in to your [browserless account](https://browserless.io/account/), click the Production cluster icon, then click **Settings**.

![Worker Settings](/img/worker_settings.png)

## Puppeteer version and global timeout

Use the first two dropdowns to select the Chrome/Puppeteer version and global timeout.

### Supported chrome/puppeteer versions

Your workers run the latest image version by default. Browserless notifies you by email when a newer version is released. Workers update automatically on the next restart or relaunch. To pin to a specific version, select it from the dropdown.

For guidance on upgrading versions and V1→V2 breaking changes, see [Image Versions](/enterprise/private-deployment/image-versions).

### Global timeout

When a session reaches the global timeout, Browserless terminates it.

If your script throws an unhandled exception, the browser process may not exit. It hangs and consumes a concurrency slot until the global timeout fires. [Implement exception handling](https://hackernoon.com/7-tips-to-making-your-puppeteer-scripts-more-bulletproof) to close browsers reliably.

## Concurrency and Queue

Adjust concurrency and queue limits with the slider.

### Concurrency tips

If you see many timeouts or errors but few queued sessions, your worker may be resource-constrained. Lower the concurrency limit so sessions queue before the worker runs out of resources.

### Queue tips

If you use an external queue, set queue length to 0. Browserless returns an error immediately when the concurrency limit is reached. Due to async request handling, occasional requests may still enter the queue when the limit is 0; this is expected behavior.

## Session health checks

When session health checks are enabled, Browserless checks worker health before accepting each connection. If the worker cannot handle another request, it returns a 5xx response. Disabling this routes all requests to the worker regardless of its current health.

Workers on dedicated fleets are configured with a **90% CPU and memory threshold**. When utilization exceeds this, health checks fail and new requests receive HTTP 500 with `"Health checks have failed, rejecting"`. See [Performance and Capacity](/enterprise/private-deployment/performance#health-checks-and-rejection-thresholds) for guidance.

## Stealth mode

Stealth mode applies [puppeteer-extra-plugin-stealth](https://github.com/berstend/puppeteer-extra/tree/master/packages/puppeteer-extra-plugin-stealth) to your sessions, injecting simulated user behavior to help avoid basic bot detection.

## Experimental features

These features are experimental. Test them carefully before enabling in production.

### Pre-boot Chrome

Pre-boot Chrome launches browsers before they're needed, shaving a few milliseconds off session start time. For most workloads, the extra open browsers increase memory pressure and slow sessions. This option helps only when concurrency is set to 3 or fewer.

### Enable GET APIs

Enable GET API handlers to call REST endpoints via HTTP GET. Use this when routing Browserless requests through a CDN or cache layer. See [GET APIs](https://www.browserless.io/docs/api-get).

> **Danger**
> Do not call the hosted service from the browser. This exposes your API token.

### Automatic health restarts

When CPU or memory stays above 90% for 5 minutes, Browserless restarts the worker automatically. See [exiting on health failure](https://www.browserless.io/docs/docker.html#exit-on-health-failure).

## Fleet operations

The dashboard provides three operations for managing your fleet. See [Fleet Operations](/enterprise/private-deployment/fleet-operations) for full details on when to use each.

- **Restart Worker**: restarts the Browserless service on a single worker (same hardware)
- **Force Restart**: restarts all workers simultaneously (same hardware)
- **Relaunch**: provisions new VMs using blue-green deployment. IP addresses change. Takes 5–10 min.

## FAQ & Troubleshooting

<details>
<summary>How do I get a Browserless Private Deployment fleet?</summary>

Contact [sales@browserless.io](mailto:sales@browserless.io) or visit the [pricing page](https://browserless.io/pricing) for Enterprise plan details.

</details>

<details>
<summary>Can I run Browserless on my own infrastructure?</summary>

Yes. Enterprise customers can self-host using the Docker image. See [Docker deployment](/enterprise/docker/enterprise-image) for setup instructions.

</details>

## Next steps

  <a href="/enterprise/docker/enterprise-image" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Docker deployment</h3>
      self-host Browserless with Docker
    
  </a>
  <a href="/enterprise/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure your Enterprise deployment
    
  </a>

---

# Terminology

URL: https://docs.browserless.io/enterprise/terminology

Common terms used across the Browserless documentation and main site.

## WebSocket Endpoint
The WebSocket endpoint (wsEndpoint()) is the internal browser's native WebSocket URL that the browser process exposes when launched. This is a local address `(e.g., ws://127.0.0.1:9222/devtools/browser/...)` that points directly to the browser's debugging port.

For CDP browsers, this endpoint is obtained after launching the browser and represents the raw connection point to Chrome DevTools Protocol. For Playwright browsers, it's similarly obtained from the browser server's wsEndpoint() method.

## Connection URL
The connection URL (publicWSEndpoint()) is the externally-accessible URL that clients use to connect to the browser through Browserless. This URL combines the internal WebSocket endpoint's path with the configured external address. Example: `wss://chrome.browserless.io/chromium?token=YOUR-API-TOKEN`

## Worker

A worker is a Browserless container instance running on a virtual machine. Cloud-Unit plans use a distributed fleet of workers to serve multiple users simultaneously, while Enterprise customers can work with us to provision appropriately sized workers and scale them to meet their specific traffic requirements.

## Session

A session represents an active, connected Chrome instance. Browserless automatically queues up to twice your concurrency limit, allowing traffic bursts without code changes. Continue using `puppeteer.connect()` and requests queue when capacity is reached.

To handle higher session volumes, you can scale up your worker size or deploy additional worker instances.

## Concurrency

Concurrency is the maximum number of browser sessions that can run simultaneously on a Browserless instance. This controls how many parallel browser automations execute at once.

For self-hosted instances, configure concurrency via the `CONCURRENT` or `MAX_CONCURRENT_SESSIONS` environment variables (default: 10). See our [pricing plans](https://www.browserless.io/pricing) for plan-specific concurrency limits.

There's also a queue system configured via `QUEUED` or `QUEUE_LENGTH` environment variables (default 10) that allows requests to wait when concurrency is at capacity.

You can check current concurrency usage via the `/pressure` API endpoint, which returns:

- `running`: Current number of active sessions
- `queued`: Current number of queued sessions
- `maxConcurrent`: Maximum allowed concurrent sessions
- `maxQueued`: Maximum allowed queued sessions

## Pressure

Pressure indicates how much load your workers are under at any given time. Use the `/pressure` REST endpoint (see [Configuration Reference](/enterprise/docker/config)) or the GraphQL `pressure()` query (see [GraphQL API](/enterprise/private-deployment/graphql-api#pressure)) to check whether your instance can handle another connection before starting a new session.

## FAQ & Troubleshooting

<details>
<summary>How do I get an Enterprise license?</summary>

Contact [sales@browserless.io](mailto:sales@browserless.io) or visit the [pricing page](https://browserless.io/pricing) for Enterprise plan details.

</details>

<details>
<summary>Can I run Browserless on my own infrastructure?</summary>

Yes. Enterprise customers can self-host using the Docker image. See [Docker deployment](/enterprise/docker/enterprise-image) for setup instructions.

</details>

## Next steps

  <a href="/enterprise/docker/enterprise-image" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Docker deployment</h3>
      self-host Browserless with Docker
    
  </a>
  <a href="/enterprise/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure your Enterprise deployment
    
  </a>

---

# Token Roles and Permissions

URL: https://docs.browserless.io/enterprise/token-roles

Enterprise Browserless uses a token-based permission system with four roles. Each token carries a single role that determines what the holder can do. This page covers what each role permits, how the default `admin` token is created, and how to list, create, and delete tokens through the Token Management API.

- A Browserless Enterprise license (self-hosted Docker or Private Deployment)
- An `admin` token, required to manage tokens via the self-hosted API (Private Deployment users contact support instead)

> **Self-hosted only**
> Multi-token roles and the Token Management API below are only available on self-hosted Docker containers. [Private Deployment](/enterprise/private-deployment/overview) users who need multiple tokens for their account can contact [Browserless support](mailto:support@browserless.io) directly to have them provisioned.

## Roles

| Role | Description |
|---|---|
| `admin` | Full access. Can run browser traffic, view metrics, manage configuration, and create or delete other tokens. |
| `developer` | Can run browser traffic (connect browsers, create sessions, call REST APIs) and view metrics. Cannot create, view, or delete other tokens. |
| `viewer` | Can view metrics, load configuration, and see active sessions. Cannot connect browsers or call REST APIs. |
| `public` | URL-based access for resources explicitly marked as public. |

## Default Behavior

The root `TOKEN` environment variable receives the `admin` role automatically on first startup. You set this variable in your Docker configuration or through the dashboard. All other tokens are created through the management API below.

Tokens persist to disk and survive container restarts. Each token stores a `createdAt` Unix timestamp (milliseconds) and a `createdBy` field containing the full token string of the admin that created it.

## Token Management API

Create and delete operations require an `admin` token. Non-admin tokens receive `401 Unauthorized`.

### List All Tokens

Any authenticated token can list tokens.

```bash
curl -X GET "https://chrome.browserless.io/tokens?token=YOUR_API_TOKEN_HERE"
```

Returns an array of token objects:

```json
[
  {
    "token": "abc123-your-admin-token",
    "permissions": "admin",
    "createdAt": 1736935800000,
    "createdBy": "abc123-your-admin-token"
  },
  {
    "token": "def456-your-developer-token",
    "permissions": "developer",
    "createdAt": 1740063600000,
    "createdBy": "abc123-your-admin-token"
  }
]
```

Each object contains a `token` field (the token string), `permissions` (the role), `createdAt` (Unix timestamp in milliseconds), and `createdBy` (the full token string of the admin that created it).

### Create a Token

```bash
curl -X POST "https://chrome.browserless.io/token?token=YOUR_API_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{"permission": "developer", "label": "scraping-team"}'
```

The `permission` field (singular) accepts `admin`, `developer`, `viewer`, or `public`. The optional `label` field attaches a human-readable name to the token. The response returns the full token object:

```json
{
  "token": "ghi789-new-developer-token",
  "permissions": "developer",
  "label": "scraping-team",
  "createdAt": 1743015600000,
  "createdBy": "abc123-your-admin-token"
}
```

### Delete a Token

```bash
curl -X DELETE "https://chrome.browserless.io/token/def456-your-developer-token?token=YOUR_API_TOKEN_HERE"
```

Replace `def456-your-developer-token` with the `token` value from the list endpoint. Returns `204 No Content` on success.

## FAQ & Troubleshooting

<details>
<summary>How do I get an Enterprise license?</summary>

Contact [sales@browserless.io](mailto:sales@browserless.io) or visit the [pricing page](https://browserless.io/pricing) for Enterprise plan details.

</details>

<details>
<summary>Can I run Browserless on my own infrastructure?</summary>

Yes. Enterprise customers can self-host using the Docker image. See [Docker deployment](/enterprise/docker/enterprise-image) for setup instructions.

</details>

## Next steps

  <a href="/enterprise/docker/enterprise-image" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Docker deployment</h3>
      self-host Browserless with Docker
    
  </a>
  <a href="/enterprise/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure your Enterprise deployment
    
  </a>

---

# User Data Directory

URL: https://docs.browserless.io/enterprise/user-data-directory

The `--user-data-dir` flag persists browser data (cookies, localStorage, cache, and login sessions) across multiple browser sessions. Use it to maintain authenticated states and user preferences without re-authenticating each time.

- A Browserless Enterprise license (self-hosted Docker or Private Deployment)

## Deployment-Specific Setup

### Self-Hosted Docker

When running the enterprise Docker image, `--user-data-dir` works out of the box. Pass it as a Chrome flag in the `launch` parameter or via the `args` array. The directory path is local to the container, so mount a Docker volume if you need data to persist across container restarts.

Example connection (the `launch` value is the Base64-encoding of `{"args":["--user-data-dir=~/u/1"]}`):
```bash
ws://localhost:3000/chromium?token=YOUR_API_TOKEN_HERE&launch=eyJhcmdzIjpbIi0tdXNlci1kYXRhLWRpcj1+L3UvMSJdfQ==
```

The `launch` parameter must be Base64-encoded before sending. See [Launch parameters](/enterprise/launch-options#encoding-the-launch-value) for encoding details.

### Private Deployment (Dedicated Fleet)

On dedicated fleets, user data directories are stored locally on each worker's file system. You must connect to the specific worker where the data was created. See the section below on [pointing to a specific dedicated worker](#pointing-to-a-specific-dedicated-worker-on-enterprise-plans) for details.

## Implementation Details

Browserless creates the directory if it does not exist. Use a unique directory path for each browser profile. Only one browser can use a given `--user-data-dir` path at a time. You cannot share the same path across multiple concurrent browsers.

### Pointing to a specific dedicated worker on Enterprise plans

This section applies to any deployment with multiple workers in the fleet, whether Private Deployment or self-hosted.

User data directories are stored locally on each worker's file system and do not sync across your fleet. For example, a browser instance using `--user-data-dir=~/u/1` on Worker #1 will not be accessible from Worker #2. To handle this, point your connection at a specific worker. In your account page, open your production cluster and go to the workers section. Click the IP address of a worker to copy its direct endpoint. Keep track of which worker holds each browser profile to ensure consistent access.

- Your BQL endpoints would change from `https://chrome.browserless.io/bql` to something like `https://chrome.browserless.io/p/53616c7465645f5ff8cc738d5eecb3032823d67e37578fe4531b0f9a83dc80856c66d0fe36aba4d2f4bc5f01c18bdfab/bql?token=YOUR_API_TOKEN_HERE&--user-data-dir=~/custompath/123`
- Your websocket connections would change from `wss://chrome.browserless.io/chromium` to something like `wss://chrome.browserless.io/p/53616c7465645f5ff8cc738d5eecb3032823d67e37578fe4531b0f9a83dc80856c66d0fe36aba4d2f4bc5f01c18bdfab/chromium?token=YOUR_API_TOKEN_HERE&--user-data-dir=~/custompath/456`

## Using User Data Directory with Puppeteer and Playwright

These examples persist a login session to browserless.io. After logging in and closing the tab, running the script again shows you already logged in.

**Puppeteer:**

```javascript

const launchArgs = {
  headless: false,
  args: ['--user-data-dir=~/u/1']
};

const queryParams = new URLSearchParams({
  token: "YOUR_API_TOKEN_HERE", //this script using userdatadir only works for dedicated machines
  timeout: 300000,
  launch: JSON.stringify(launchArgs)
}).toString();

(async () => {
  let browser;
  try {
    browser = await puppeteer.connect({
      browserWSEndpoint: `wss://chrome.browserless.io/chromium?${queryParams}`,
      defaultViewport: null
    });

    console.log('Connected');

    const page = await browser.newPage();
    await page.goto('https://browserless.io/account/', {
      waitUntil: 'domcontentloaded'
    });

    console.log('Navigated');

    const client = await page.target().createCDPSession();
    const { liveURL } = await client.send('Browserless.liveURL', {
      timeout: 300000
    });

    console.log('Click for live experience:', liveURL);

    await new Promise((resolve) => {
      client.on('Browserless.liveComplete', resolve);
    });

    console.log(`Live URL completed on page: ${page.url()}`);

    await browser.close();
  } catch (error) {
    console.error('An error occurred:', error);
    if (browser) {
      await browser.close().catch(console.error);
    }
    throw error;
  }
})().catch(error => {
  console.error('Fatal error:', error);
});
```

**Playwright:**

```javascript

const launchArgs = {
  headless: false,
  stealth: true,
  args: ['--user-data-dir=~/u/1']
};
const queryParams = new URLSearchParams({
  token: "YOUR_API_TOKEN_HERE", //this script using userdatadir only works for dedicated machines
  timeout: 300000,
  launch: JSON.stringify(launchArgs)
}).toString();

(async () => {
  let browser;
  try {
    browser = await chromium.connectOverCDP(
      `wss://chrome.browserless.io/chromium/stealth?${queryParams}`
    );
    console.log('Connected');
    const [context] = await browser.contexts();
    const page = await context.newPage();
    await page.goto('https://browserless.io/account/', {
      waitUntil: 'domcontentloaded'
    });
    console.log('Navigated');
    const cdpSession = await context.newCDPSession(page);
    const { liveURL } = await cdpSession.send('Browserless.liveURL', {
      timeout: 300000
    });
    console.log('Click for live experience:', liveURL);
    await new Promise((resolve) => {
      cdpSession.on('Browserless.liveComplete', resolve);
    });

    console.log(`Live URL completed on page: ${page.url()}`);

    await browser.close();
  } catch (error) {
    console.error('An error occurred:', error);
    if (browser) {
      await browser.close().catch(console.error);
    }
    throw error;
  }
})().catch(error => {
  console.error('Fatal error:', error);
});
```

## Using with BrowserQL

The following example demonstrates how to use the `--user-data-dir` flag with BrowserQL to persist browser state. This example shows how to toggle dark mode on w3schools.com - each time you run it, the initial state will be different since it's persisting the toggle state from previous runs.

```javascript

const url = 'https://www.browserless.io/';
const token = 'YOUR_API_TOKEN_HERE'; //this script using userdatadir only works for dedicated machines
const timeout = 5 * 60 * 1000;
const launchArgs = {
  args: ['--user-data-dir=~/id-togle-test-123']
};
const queryParams = new URLSearchParams({
  timeout,
  token,
  launch: JSON.stringify(launchArgs)
}).toString();

const query = `
  mutation DarkModeToggle {
    goto(url: "https://www.w3schools.com/", waitUntil: domContentLoaded) {
      status
    }
    DarkModeClassBefore:evaluate(content: "document.body.className") {
      value
    }
    click(selector:"#tnb-dark-mode-toggle-btn"){
      time
    }
    DarkModeClassAfter:evaluate(content: "document.body.className") {
      value
    }
  }
`;

const variables = { url };

const endpoint =
  `https://chrome.browserless.io/chromium/bql?${queryParams}`;

const options = {
  method: 'POST',
  headers: {
    'content-type': 'application/json'
  },
  body: JSON.stringify({
    query,
  }),
};

try {
  console.log(`Running BQL Query: ${url}`);

  const response = await fetch(endpoint, options);

  if (!response.ok) {
    throw new Error(`Got non-ok response:\n` + (await response.text()));
  }

  const { data } = await response.json();

  console.log("Full response data:", JSON.stringify(data, null, 2));

  if (data && data.DarkModeClassBefore && data.DarkModeClassAfter) {
    console.log("Dark mode toggle results:");
    console.log("Before:", data.DarkModeClassBefore.value);
    console.log("After:", data.DarkModeClassAfter.value);
    console.log("Click time:", data.click.time, "ms");
  } else {
    console.log("Unexpected response structure:", data);
  }
} catch (error) {
  console.error(error);
}
```

## FAQ & Troubleshooting

<details>
<summary>How do I get an Enterprise license?</summary>

Contact [sales@browserless.io](mailto:sales@browserless.io) or visit the [pricing page](https://browserless.io/pricing) for Enterprise plan details.

</details>

<details>
<summary>Can I run Browserless on my own infrastructure?</summary>

Yes. Enterprise customers can self-host using the Docker image. See [Docker deployment](/enterprise/docker/enterprise-image) for setup instructions.

</details>

## Next steps

  <a href="/enterprise/docker/enterprise-image" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Docker deployment</h3>
      self-host Browserless with Docker
    
  </a>
  <a href="/enterprise/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure your Enterprise deployment
    
  </a>

---

# /config API

URL: https://docs.browserless.io/enterprise/utility-functions/config

The `/config` API returns your worker's current configuration. Available on Private Deployment and Enterprise Docker plans only. For setting these values, see the [Docker configuration reference](/enterprise/docker/config).

- A Browserless Enterprise license (self-hosted Docker or Private Deployment)

## Get Worker Configuration

Issue a `GET` request to `/config`:

  
**cURL:**

```sh
curl -X GET \
  https://production-sfo.browserless.io/config?token=YOUR_API_TOKEN_HERE
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/config?token=${TOKEN}`;

const fetchConfig = async () => {
  const response = await fetch(url, {
    method: 'GET'
  });

  const result = await response.json();
  console.log(result);
};

fetchConfig();
```

  
  
**Python:**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/config?token={TOKEN}"

response = requests.get(url)
result = response.json()

print(result)
```
  
  
**Java:**

```java
import java.io.*;
import java.net.URI;
import java.net.http.*;

public class FetchConfig {
    public static void main(String[] args) {
        String TOKEN = "YOUR_API_TOKEN_HERE";
        String url = "https://production-sfo.browserless.io/config?token=" + TOKEN;

        HttpClient client = HttpClient.newHttpClient();

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(url))
            .GET()
            .build();

        try {
            HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
            System.out.println("Response: " + response.body());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
```

  
  
**C#:**

```csharp
using System;
using System.Net.Http;
using System.Threading.Tasks;

class Program {
    static async Task Main(string[] args) {
        string TOKEN = "YOUR_API_TOKEN_HERE";
        string url = $"https://production-sfo.browserless.io/config?token={TOKEN}";

        using var client = new HttpClient();

        try {
            var response = await client.GetAsync(url);
            response.EnsureSuccessStatusCode();

            var result = await response.Content.ReadAsStringAsync();
            Console.WriteLine("Response: " + result);
        } catch (Exception ex) {
            Console.WriteLine($"Error: {ex.Message}");
        }
    }
}
```

  

The response returns a JSON object like:

```json
{
  "allowCORS": true,
  "allowFileProtocol": false,
  "allowGetCalls": false,
  "concurrent": 10,
  "data": "/tmp/browserless-data-dirs",
  "debug": "browserless*,-**:verbose",
  "errorAlertURL": null,
  "healthFailureURL": null,
  "host": "localhost",
  "maxCPU": 99,
  "maxMemory": 99,
  "metricsJSONPath": "/tmp/browserless-metrics.json",
  "port": 3001,
  "queued": 10,
  "queuedAlertURL": null,
  "rejectAlertURL": null,
  "retries": 5,
  "timeout": 30000,
  "timeoutAlertURL": null,
  "token": "6R0W53R135510"
}
```

To monitor real-time load and capacity, use the [/pressure endpoint](/enterprise/utility-functions/pressure).

## FAQ & Troubleshooting

<details>
<summary>How do I get an Enterprise license?</summary>

Contact [sales@browserless.io](mailto:sales@browserless.io) or visit the [pricing page](https://browserless.io/pricing) for Enterprise plan details.

</details>

<details>
<summary>Can I run Browserless on my own infrastructure?</summary>

Yes. Enterprise customers can self-host using the Docker image. See [Docker deployment](/enterprise/docker/enterprise-image) for setup instructions.

</details>

## Next steps

  <a href="/enterprise/docker/enterprise-image" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Docker deployment</h3>
      self-host Browserless with Docker
    
  </a>
  <a href="/enterprise/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure your Enterprise deployment
    
  </a>

---

# Utility Functions Overview

URL: https://docs.browserless.io/enterprise/utility-functions/intro

REST endpoints for managing and monitoring Private Deployment and Enterprise Docker instances.

- A Browserless Enterprise license (self-hosted Docker or Private Deployment)

## Available Endpoints

- [/sessions](/enterprise/utility-functions/sessions): List running browser sessions
- [/metrics](/enterprise/utility-functions/metrics): Retrieve usage metrics and performance data
- [/config](/enterprise/utility-functions/config): View worker configuration
- [/pressure](/enterprise/utility-functions/pressure): Query real-time system load, capacity, and availability
- [/json/version](/enterprise/utility-functions/json-version): Get browser and DevTools protocol version information

> **Note**
> Utility Functions are available on Private Deployment and Enterprise Docker plans. Some may not be available on shared plans.

## FAQ & Troubleshooting

<details>
<summary>How do I get an Enterprise license?</summary>

Contact [sales@browserless.io](mailto:sales@browserless.io) or visit the [pricing page](https://browserless.io/pricing) for Enterprise plan details.

</details>

<details>
<summary>Can I run Browserless on my own infrastructure?</summary>

Yes. Enterprise customers can self-host using the Docker image. See [Docker deployment](/enterprise/docker/enterprise-image) for setup instructions.

</details>

## Next steps

  <a href="/enterprise/docker/enterprise-image" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Docker deployment</h3>
      self-host Browserless with Docker
    
  </a>
  <a href="/enterprise/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure your Enterprise deployment
    
  </a>

---

# /json/version API

URL: https://docs.browserless.io/enterprise/utility-functions/json-version

The `/json/version` API returns version information about the browser and DevTools protocol running on your Browserless instance. This is part of the standard Chrome DevTools Protocol. Available on both Private Deployment and Enterprise Docker plans.

- A Browserless Enterprise license (self-hosted Docker or Private Deployment)

## Querying browser version

Issue a `GET` request to `/json/version`:

  
**cURL:**

```sh
curl -X GET \
  https://production-sfo.browserless.io/json/version?token=YOUR_API_TOKEN_HERE
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/json/version?token=${TOKEN}`;

const fetchVersion = async () => {
  const response = await fetch(url, {
    method: 'GET'
  });

  const result = await response.json();
  console.log(result);
};

fetchVersion();
```

  
  
**Python:**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/json/version?token={TOKEN}"

response = requests.get(url)
result = response.json()

print(result)
```
  
  
**Java:**

```java
import java.io.*;
import java.net.URI;
import java.net.http.*;

public class FetchVersion {
    public static void main(String[] args) {
        String TOKEN = "YOUR_API_TOKEN_HERE";
        String url = "https://production-sfo.browserless.io/json/version?token=" + TOKEN;

        HttpClient client = HttpClient.newHttpClient();

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(url))
            .GET()
            .build();

        try {
            HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
            System.out.println("Response: " + response.body());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
```

  
  
**C#:**

```csharp
using System;
using System.Net.Http;
using System.Threading.Tasks;

class Program {
    static async Task Main(string[] args) {
        string TOKEN = "YOUR_API_TOKEN_HERE";
        string url = $"https://production-sfo.browserless.io/json/version?token={TOKEN}";

        using var client = new HttpClient();

        try {
            var response = await client.GetAsync(url);
            response.EnsureSuccessStatusCode();

            var result = await response.Content.ReadAsStringAsync();
            Console.WriteLine("Response: " + result);
        } catch (Exception ex) {
            Console.WriteLine($"Error: {ex.Message}");
        }
    }
}
```

  

Running this request will result in a JSON output like:

```json
{
  "Browser": "Chrome/145.0.7632.6",
  "Protocol-Version": "1.3",
  "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/145.0.0.0 Safari/537.36",
  "V8-Version": "14.5.201.2",
  "WebKit-Version": "537.36 (@47e20adcc15fc15f01825aa17e570c8f5492ac0f)",
  "webSocketDebuggerUrl": "ws://chrome.browserless.io",
  "Debugger-Version": "47e20adcc15fc15f01825aa17e570c8f5492ac0f"
}
```

### Response fields

| Field | Type | Description |
|-------|------|-------------|
| `Browser` | string | Browser name and version |
| `Protocol-Version` | string | Chrome DevTools Protocol version |
| `User-Agent` | string | Full user-agent string of the bundled browser |
| `V8-Version` | string | V8 JavaScript engine version |
| `WebKit-Version` | string | WebKit rendering engine version and commit hash |
| `webSocketDebuggerUrl` | string | Base WebSocket URL for CDP connections |
| `Debugger-Version` | string | DevTools debugger commit hash |

### Common use cases

- **Version matching**: Verify the bundled Chromium version matches your client library (see [Image Versions](/enterprise/private-deployment/image-versions#matching-client-library-versions))
- **Debugging**: Confirm the protocol version when troubleshooting CDP compatibility issues
- **Monitoring**: Track browser versions across your fleet to ensure consistency after upgrades

## FAQ & Troubleshooting

<details>
<summary>How do I get an Enterprise license?</summary>

Contact [sales@browserless.io](mailto:sales@browserless.io) or visit the [pricing page](https://browserless.io/pricing) for Enterprise plan details.

</details>

<details>
<summary>Can I run Browserless on my own infrastructure?</summary>

Yes. Enterprise customers can self-host using the Docker image. See [Docker deployment](/enterprise/docker/enterprise-image) for setup instructions.

</details>

## Next steps

  <a href="/enterprise/docker/enterprise-image" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Docker deployment</h3>
      self-host Browserless with Docker
    
  </a>
  <a href="/enterprise/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure your Enterprise deployment
    
  </a>

---

# /metrics API

URL: https://docs.browserless.io/enterprise/utility-functions/metrics

The metrics API returns an array of session statistics. Data covers a maximum of 1 week.

> **Private fleet customers**
> Call `/metrics` on your **dedicated fleet URL** (e.g., `chrome.browserless.io`), not on `production-sfo.browserless.io`. Using the shared fleet URL with a private fleet token will not return your fleet's data.
> 
> Metrics reset after a [Relaunch](/enterprise/private-deployment/fleet-operations#relaunch). `estimatedMonthlyUnits` will be unreliable for several hours on a freshly launched fleet. For reliable unit consumption tracking, use the [GraphQL exportMetrics query](/enterprise/private-deployment/observability#graphql-unit-consumption-tracking).

- A Browserless Enterprise license (self-hosted Docker or Private Deployment)

## Gathering metrics from your workers

Issue a `GET` request to `/metrics` to retrieve session statistics:

  
**cURL:**

```sh
curl -X GET \
  https://production-sfo.browserless.io/metrics?token=YOUR_API_TOKEN_HERE
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/metrics?token=${TOKEN}`;

const fetchMetrics = async () => {
  const response = await fetch(url, {
    method: 'GET'
  });

  const result = await response.json();
  console.log(result);
};

fetchMetrics();
```

  
  
**Python:**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/metrics?token={TOKEN}"

response = requests.get(url)
result = response.json()

print(result)
```

  
  
**Java:**

```java
import java.io.*;
import java.net.URI;
import java.net.http.*;

public class FetchMetrics {
    public static void main(String[] args) {
        String TOKEN = "YOUR_API_TOKEN_HERE";
        String url = "https://production-sfo.browserless.io/metrics?token=" + TOKEN;

        HttpClient client = HttpClient.newHttpClient();

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(url))
            .GET()
            .build();

        try {
            HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
            System.out.println("Response: " + response.body());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
```

  
  
**C#:**

```csharp
using System;
using System.Net.Http;
using System.Threading.Tasks;

class Program {
    static async Task Main(string[] args) {
        string TOKEN = "YOUR_API_TOKEN_HERE";
        string url = $"https://production-sfo.browserless.io/metrics?token={TOKEN}";

        using var client = new HttpClient();

        try {
            var response = await client.GetAsync(url);
            response.EnsureSuccessStatusCode();

            var result = await response.Content.ReadAsStringAsync();
            Console.WriteLine("Response: " + result);
        } catch (Exception ex) {
            Console.WriteLine($"Error: {ex.Message}");
        }
    }
}
```

  

> **Warning**
> Running this in the browser exposes your API token.

Running this request will result in an output like:

```json
[
  {
    "error": 3,
    "maxConcurrent": 4,
    "queued": 0,
    "rejected": 0,
    "running": 3,
    "sessionTimes": [992, 1041, 802],
    "successful": 0,
    "timedout": 0,
    "unauthorized": 0,
    "unhealthy": 0,
    "maxTime": 1041,
    "meanTime": 945,
    "minTime": 802,
    "totalTime": 2835,
    "units": 3,
    "date": 1709134233732,
    "cpu": 0.024877870173220466,
    "memory": 0.7784158564199399
  },
  // ...
]
```

### /metrics/total

This route will output a similar payload, but with the total stats of all sessions.

## FAQ & Troubleshooting

<details>
<summary>How do I get an Enterprise license?</summary>

Contact [sales@browserless.io](mailto:sales@browserless.io) or visit the [pricing page](https://browserless.io/pricing) for Enterprise plan details.

</details>

<details>
<summary>Can I run Browserless on my own infrastructure?</summary>

Yes. Enterprise customers can self-host using the Docker image. See [Docker deployment](/enterprise/docker/enterprise-image) for setup instructions.

</details>

## Next steps

  <a href="/enterprise/docker/enterprise-image" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Docker deployment</h3>
      self-host Browserless with Docker
    
  </a>
  <a href="/enterprise/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure your Enterprise deployment
    
  </a>

---

# /pressure API

URL: https://docs.browserless.io/enterprise/utility-functions/pressure

The `/pressure` API returns real-time system load information for your Browserless instance. Available on both Private Deployment and Enterprise Docker plans.

- A Browserless Enterprise license (self-hosted Docker or Private Deployment)

## Querying system pressure

Issue a `GET` request to `/pressure` to check current load:

  
**cURL:**

```sh
curl -X GET \
  https://production-sfo.browserless.io/pressure?token=YOUR_API_TOKEN_HERE
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/pressure?token=${TOKEN}`;

const fetchPressure = async () => {
  const response = await fetch(url, {
    method: 'GET'
  });

  const result = await response.json();
  console.log(result);
};

fetchPressure();
```

  
  
**Python:**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/pressure?token={TOKEN}"

response = requests.get(url)
result = response.json()

print(result)
```
  
  
**Java:**

```java
import java.io.*;
import java.net.URI;
import java.net.http.*;

public class FetchPressure {
    public static void main(String[] args) {
        String TOKEN = "YOUR_API_TOKEN_HERE";
        String url = "https://production-sfo.browserless.io/pressure?token=" + TOKEN;

        HttpClient client = HttpClient.newHttpClient();

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(url))
            .GET()
            .build();

        try {
            HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
            System.out.println("Response: " + response.body());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
```

  
  
**C#:**

```csharp
using System;
using System.Net.Http;
using System.Threading.Tasks;

class Program {
    static async Task Main(string[] args) {
        string TOKEN = "YOUR_API_TOKEN_HERE";
        string url = $"https://production-sfo.browserless.io/pressure?token={TOKEN}";

        using var client = new HttpClient();

        try {
            var response = await client.GetAsync(url);
            response.EnsureSuccessStatusCode();

            var result = await response.Content.ReadAsStringAsync();
            Console.WriteLine("Response: " + result);
        } catch (Exception ex) {
            Console.WriteLine($"Error: {ex.Message}");
        }
    }
}
```

  

Running this request will result in a JSON output like:

```json
{
  "pressure": {
    "cpu": 45,
    "memory": 62,
    "isAvailable": true,
    "maxConcurrent": 10,
    "maxQueued": 10,
    "running": 3,
    "queued": 0,
    "reason": "",
    "recentlyRejected": 0,
    "date": 1711468800000,
    "message": ""
  }
}
```

### Response fields

| Field | Type | Description |
|-------|------|-------------|
| `cpu` | number or null | CPU usage percentage |
| `memory` | number or null | Memory usage percentage |
| `isAvailable` | boolean | Whether the instance accepts new sessions |
| `maxConcurrent` | number | Maximum concurrent browser sessions |
| `maxQueued` | number | Maximum queued requests |
| `running` | number | Currently running sessions |
| `queued` | number | Currently queued requests |
| `reason` | string | Why the instance is unavailable: `full`, `cpu`, `memory`, or empty |
| `recentlyRejected` | number | Recent rejected connection count |
| `date` | number | Timestamp of the response in milliseconds |
| `message` | string | Additional status message |

### Common use cases

Use `/pressure` to check whether your instance can handle another connection before starting a new session. This is especially useful for:

- **Load balancing**: Route traffic to instances with available capacity (see [NGINX Load Balancing](/enterprise/docker/nginx-load-balancing))
- **Auto-scaling**: Trigger scale-up when `isAvailable` is `false` or `running` approaches `maxConcurrent`
- **Health monitoring**: Integrate with external monitoring systems to track resource usage over time
- **Queue management**: Check `queued` count to detect when requests are backing up

## FAQ & Troubleshooting

<details>
<summary>How do I get an Enterprise license?</summary>

Contact [sales@browserless.io](mailto:sales@browserless.io) or visit the [pricing page](https://browserless.io/pricing) for Enterprise plan details.

</details>

<details>
<summary>Can I run Browserless on my own infrastructure?</summary>

Yes. Enterprise customers can self-host using the Docker image. See [Docker deployment](/enterprise/docker/enterprise-image) for setup instructions.

</details>

## Next steps

  <a href="/enterprise/docker/enterprise-image" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Docker deployment</h3>
      self-host Browserless with Docker
    
  </a>
  <a href="/enterprise/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure your Enterprise deployment
    
  </a>

---

# /sessions API

URL: https://docs.browserless.io/enterprise/utility-functions/sessions

The `/sessions` API returns a list of currently running browser sessions. Available on Private Deployment and Enterprise Docker plans only.

To create and manage persistent browser sessions, see the [Session Management documentation](/baas/session-management/persisting-state).

- A Browserless Enterprise license (self-hosted Docker or Private Deployment)

## Listing Running Sessions

Issue a `GET` request to `/sessions` to list running sessions:

  
**cURL:**

```sh
curl -X GET \
  https://production-sfo.browserless.io/sessions?token=YOUR_API_TOKEN_HERE
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/sessions?token=${TOKEN}`;

const fetchSessions = async () => {
  const response = await fetch(url, {
    method: 'GET'
  });

  const result = await response.json();
  console.log(result);
};

fetchSessions();
```

  
  
**Python:**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/sessions?token={TOKEN}"

response = requests.get(url)
result = response.json()

print(result)
```

  
  
**Java:**

```java
import java.io.*;
import java.net.URI;
import java.net.http.*;

public class FetchSessions {
    public static void main(String[] args) {
        String TOKEN = "YOUR_API_TOKEN_HERE";
        String url = "https://production-sfo.browserless.io/sessions?token=" + TOKEN;

        HttpClient client = HttpClient.newHttpClient();

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(url))
            .GET()
            .build();

        try {
            HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
            System.out.println("Response: " + response.body());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
```

  
  
**C#:**

```csharp
using System;
using System.Net.Http;
using System.Threading.Tasks;

class Program {
    static async Task Main(string[] args) {
        string TOKEN = "YOUR_API_TOKEN_HERE";
        string url = $"https://production-sfo.browserless.io/sessions?token={TOKEN}";

        using var client = new HttpClient();

        try {
            var response = await client.GetAsync(url);
            response.EnsureSuccessStatusCode();

            var result = await response.Content.ReadAsStringAsync();
            Console.WriteLine("Response: " + result);
        } catch (Exception ex) {
            Console.WriteLine($"Error: {ex.Message}");
        }
    }
}
```

  

> **Warning**
> Running this in the browser exposes your API token.

Running this request will result in an output like:

```json
[{
  "id": null,
  "initialConnectURL": "wss://production-sfo.browserless.io/firefox/playwright/?token=YOUR_API_TOKEN_HERE",
  "isTempDataDir": true,
  "launchOptions": {},
  "numbConnected": 1,
  "routePath": ["/firefox/playwright", "/firefox/playwright"],
  "startedOn": 1709584439748,
  "ttl": 0,
  "userDataDir": null,
  "browser": "FirefoxPlaywright",
  "browserId": "d9a8570a73666d79d79ac23f07cf8966",
  "killURL": null,
  "running": true,
  "timeAliveMs": 10118,
  "type": "browser"
}]
```

## FAQ & Troubleshooting

<details>
<summary>How do I get an Enterprise license?</summary>

Contact [sales@browserless.io](mailto:sales@browserless.io) or visit the [pricing page](https://browserless.io/pricing) for Enterprise plan details.

</details>

<details>
<summary>Can I run Browserless on my own infrastructure?</summary>

Yes. Enterprise customers can self-host using the Docker image. See [Docker deployment](/enterprise/docker/enterprise-image) for setup instructions.

</details>

## Next steps

  <a href="/enterprise/docker/enterprise-image" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Docker deployment</h3>
      self-host Browserless with Docker
    
  </a>
  <a href="/enterprise/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure your Enterprise deployment
    
  </a>

---

# Watching Sessions

URL: https://docs.browserless.io/enterprise/watching-sessions

The Browserless Docker image and Dedicated hosted plans support real-time session watching for debugging and monitoring. For an overview of session lifecycle and reconnection, see [session management](/baas/session-management).

This guide covers three methods for watching sessions:
- Using the Docker image session viewer
- Using the Sessions tab in Dedicated plans
- Programmatically watching sessions via GraphQL API

- A Browserless Enterprise license (self-hosted Docker or Private Deployment)

## Watching sessions on the Docker image

To use the session viewer on the Docker image, you don't need to do anything special in your code or how you connect to Browserless. Optionally, you can add an `id` parameter to your connect call to sort or filter sessions to the one you care about.

Start the container as usual:

```sh
$ docker run --rm -p 3000:3000 ghcr.io/browserless/chrome
```

Then, in your application or script connect to it.

  
**Puppeteer:**

```js
import puppeteer from "puppeteer";

const browser = await puppeteer.connect({
  browserWSEndpoint: `wss://YOUR_CONTAINER_URL_HERE?token=YOUR_API_TOKEN_HERE`,
});
const page = await browser.newPage();
await page.goto("https://example.com/");
await browser.close();
```
  
  
**Playwright:**

    
      
**Javascript:**

```js
import { chromium } from "playwright-core";

(async () => {
  const browser = await chromium.connectOverCDP(
    `wss://YOUR_CONTAINER_URL_HERE?token=YOUR_API_TOKEN_HERE`
  );

  // Create a new context and page
  const context = browser.contexts()[0] || (await browser.newContext());
  const page = await context.newPage();

  // Navigate to the URL
  await page.goto("https://example.com/");
  console.log("Page loaded successfully");

  // Close the browser
  await browser.close();
})();
```
      
      
**Python:**

```python
import asyncio
from playwright.async_api import async_playwright

WS_ENDPOINT = "wss://YOUR_CONTAINER_URL_HERE?token=YOUR_API_TOKEN_HERE"

async def main():
    async with async_playwright() as p:
        browser = await p.chromium.connect_over_cdp(WS_ENDPOINT)

        # Use existing context or create a new one
        context = browser.contexts[0] if browser.contexts else await browser.new_context()
        page = await context.new_page()

        await page.goto("https://example.com/")
        print("Page loaded successfully")

        await browser.close()

asyncio.run(main())
```
      
      
**Java:**

```java
import com.microsoft.playwright.*;

public class PlaywrightRemoteBrowser {
    public static void main(String[] args) {
        String wsEndpoint = "wss://YOUR_CONTAINER_URL_HERE?token=YOUR_API_TOKEN_HERE";

        try (Playwright playwright = Playwright.create()) {
            Browser browser = playwright.chromium().connectOverCDP(wsEndpoint);

            // Use existing context or create a new one
            BrowserContext context = browser.contexts().isEmpty()
                ? browser.newContext()
                : browser.contexts().get(0);

            Page page = context.newPage();

            page.navigate("https://example.com/");
            System.out.println("Page loaded successfully");

            browser.close();
        }
    }
}
```
      
      
**C#:**

```csharp
using System;
using System.Threading.Tasks;
using Microsoft.Playwright;

class Program
{
    static async Task Main(string[] args)
    {
        string wsEndpoint = "wss://YOUR_CONTAINER_URL_HERE?token=YOUR_API_TOKEN_HERE";

        var playwright = await Playwright.CreateAsync();
        var browser = await playwright.Chromium.ConnectOverCDPAsync(wsEndpoint);

        // Use existing context or create a new one
        var context = browser.Contexts.Count > 0
            ? browser.Contexts[0]
            : await browser.NewContextAsync();

        var page = await context.NewPageAsync();

        await page.GotoAsync("https://example.com/");
        Console.WriteLine("Page loaded successfully");

        await browser.CloseAsync();
    }
}
```
      
    
  

In your browser, you can go to [http://localhost:3000/sessions](http://localhost:3000/sessions) to get a JSON of the running sessions.

> **Info**
> If your session is immediately closing, then you can keep it open by _not_ calling `browser.close` at the end (just remember to terminate your script with CTRL+C or similar).

## Using the Sessions tab in Dedicated plans

In Dedicated plans, you can see your active sessions at any time by going to the Sessions portion of the account page.

**To view running sessions:**
1. Click the "Fetch Running Sessions" button to retrieve actively running sessions across all your workers
2. Click the "Debugger" link to open and view specific sessions

Sessions appear in the list immediately after connecting. Refresh by clicking "Fetch Running Sessions".

## Programmatically Watching Sessions on Dedicated Plans

The GraphQL API lets you retrieve running sessions programmatically. Common use cases:
- End-users can watch their automation running in real time
- Engineers can debug live production issues
- Teams can pinpoint problems in scripts faster

**Security note**: When setting this up, ensure you don't accidentally expose non-relevant sessions among your users. Use a unique `id` parameter to filter and distinguish between running sessions.

Here's a complete example showing how to connect a browser, fetch the session via GraphQL, and get a live debugging URL.

  
**Puppeteer:**

```js
import puppeteer from "puppeteer";

const apiToken = "YOUR_API_TOKEN_HERE";
const id = "some-unique-id";

const getSessionsById = (id) => {
  return fetch("https://api.browserless.io/graphql", {
    method: "POST",
    headers: {
      "content-type": "application/json",
    },
    body: JSON.stringify({
      query: `
        query getSessions($id: String!, $apiToken: String!) {
          sessions(apiToken: $apiToken, id: $id) {
            description
            devtoolsFrontendUrl
            id
            title
            url
            browserId
            browserWSEndpoint
          }
        }
      `,
      variables: {
        id,
        apiToken,
      },
    }),
  })
    .then((res) => res.json())
    .then((res) => res.data.sessions)
    .catch((error) => {
      console.log(`Error retrieving sessions ${error.message}`);
      return null;
    });
};

const run = async () => {
  const sleep = (ms) => new Promise((res) => setTimeout(res, ms));

  let browser = null;

  try {
    browser = await puppeteer.connect({
      browserWSEndpoint: `wss://YOUR_CONTAINER_URL_HERE?token=${apiToken}&id=${id}`,
    });

    const page = await browser.newPage();
    await page.goto("https://example.com");

    const [session] = await getSessionsById(id);

    if (!session) {
      throw new Error(`Error retrieving session!`);
    }

    console.log(`Open: ${session.devtoolsFrontendUrl} in your browser!`);

    // Let the page stay open for 30 seconds so we can debug!
    await sleep(30000);
  } catch (error) {
    console.error(`Saw error when running: ${error.message}`);
  } finally {
    if (browser) {
      console.log(`Shutting down the browser.`);
      browser.close();
    }
  }
};

run();
```
  
  
**Playwright:**

    
      
**Javascript:**

```js
import { chromium } from "playwright-core";

const apiToken = "YOUR_API_TOKEN_HERE";
const id = "some-unique-id";

const getSessionsById = async (id) => {
  try {
    const response = await fetch("https://api.browserless.io/graphql", {
      method: "POST",
      headers: {
        "content-type": "application/json",
      },
      body: JSON.stringify({
        query: `
          query getSessions($id: String!, $apiToken: String!) {
            sessions(apiToken: $apiToken, id: $id) {
              description
              devtoolsFrontendUrl
              id
              title
              url
              id
              browserId
              browserWSEndpoint
            }
          }
        `,
        variables: {
          id,
          apiToken,
        },
      }),
    });
    const result = await response.json();
    return result.data.sessions;
  } catch (error) {
    console.error(`Error retrieving sessions: ${error.message}`);
    return null;
  }
};

const run = async () => {
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

  let browser = null;

  try {
    browser = await chromium.connectOverCDP(
      `wss://YOUR_CONTAINER_URL_HERE?token=${apiToken}&id=${id}`
    );

    const context = browser.contexts()[0] || (await browser.newContext());
    const page = await context.newPage();
    await page.goto("https://example.com");

    const [session] = await getSessionsById(id);

    if (!session) {
      throw new Error(`Error retrieving session!`);
    }

    console.log(`Open: ${session.devtoolsFrontendUrl} in your browser!`);

    // Let the page stay open for 30 seconds so we can debug!
    await sleep(30000);
  } catch (error) {
    console.error(`Saw error when running: ${error.message}`);
  } finally {
    if (browser) {
      console.log(`Shutting down the browser.`);
      await browser.close();
    }
  }
};

run();
```
      
      
**Python:**

```python
import asyncio
import requests
from playwright.async_api import async_playwright

API_TOKEN = "YOUR_API_TOKEN_HERE"
ID = "some-unique-id"

def get_sessions_by_id(session_id):
    try:
        response = requests.post(
            "https://api.browserless.io/graphql",
            json={
                "query": """
                    query getSessions($id: String!, $apiToken: String!) {
                        sessions(apiToken: $apiToken, id: $id) {
                            description
                            devtoolsFrontendUrl
                            id
                            title
                            url
                            browserId
                            browserWSEndpoint
                        }
                    }
                """,
                "variables": {
                    "id": session_id,
                    "apiToken": API_TOKEN,
                },
            },
            headers={"Content-Type": "application/json"},
        )
        response.raise_for_status()
        return response.json()["data"]["sessions"]
    except Exception as e:
        print(f"Error retrieving sessions: {e}")
        return None

async def run():
    async with async_playwright() as p:
        browser = None
        try:
            browser = await p.chromium.connect_over_cdp(
                f"wss://YOUR_CONTAINER_URL_HERE?token={API_TOKEN}&id={ID}"
            )

            context = browser.contexts[0] if browser.contexts else await browser.new_context()
            page = await context.new_page()
            await page.goto("https://example.com")

            sessions = get_sessions_by_id(ID)
            if not sessions:
                raise Exception("Error retrieving session!")

            session = sessions[0]
            print(f"Open: {session['devtoolsFrontendUrl']} in your browser!")

            # Let the page stay open for 30 seconds
            await asyncio.sleep(30)
        except Exception as e:
            print(f"Saw error when running: {e}")
        finally:
            if browser:
                print("Shutting down the browser.")
                await browser.close()

asyncio.run(run())
```
      
      
**Java:**

```java
import com.microsoft.playwright.*;
import java.net.http.*;
import java.net.URI;
import java.net.http.HttpClient;
import java.util.*;

import com.google.gson.Gson;

public class PlaywrightRemote {
    private static final String API_TOKEN = "YOUR_API_TOKEN_HERE";
    private static final String ID = "some-unique-id";

    private static List> getSessionsById(String id) throws Exception {
        HttpClient client = HttpClient.newHttpClient();
        String query = """
            query getSessions($id: String!, $apiToken: String!) {
                sessions(apiToken: $apiToken, id: $id) {
                    description
                    devtoolsFrontendUrl
                    id
                    title
                    url
                    browserId
                    browserWSEndpoint
                }
            }
        """;

        Map payload = Map.of(
            "query", query,
            "variables", Map.of("id", id, "apiToken", API_TOKEN)
        );

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://api.browserless.io/graphql"))
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(new Gson().toJson(payload)))
            .build();

        HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
        if (response.statusCode() != 200) {
            throw new Exception("Error retrieving sessions: " + response.body());
        }

        Map responseData = new Gson().fromJson(response.body(), Map.class);
        return (List>) ((Map) responseData.get("data")).get("sessions");
    }

    public static void main(String[] args) {
        try (Playwright playwright = Playwright.create()) {
            Browser browser = playwright.chromium().connectOverCDP(
                "wss://YOUR_CONTAINER_URL_HERE?token=" + API_TOKEN + "&id=" + ID
            );

            BrowserContext context = browser.contexts().isEmpty() ? browser.newContext() : browser.contexts().get(0);
            Page page = context.newPage();
            page.navigate("https://example.com");

            List> sessions = getSessionsById(ID);
            if (sessions.isEmpty()) {
                throw new Exception("Error retrieving session!");
            }

            Map session = sessions.get(0);
            System.out.println("Open: " + session.get("devtoolsFrontendUrl") + " in your browser!");

            Thread.sleep(30000);
            browser.close();
        } catch (Exception e) {
            System.err.println("Saw error when running: " + e.getMessage());
        }
    }
}
```
      
      
**C#:**

```csharp
using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Playwright;

class Program
{
    private const string ApiToken = "YOUR_API_TOKEN_HERE";
    private const string Id = "some-unique-id";

    static async Task Main(string[] args)
    {
        var httpClient = new HttpClient();
        var playwright = await Playwright.CreateAsync();
        IBrowser browser = null;

        try
        {
            browser = await playwright.Chromium.ConnectOverCDPAsync(
                $"wss://YOUR_CONTAINER_URL_HERE?token={ApiToken}&id={Id}"
            );

            var context = browser.Contexts.Count > 0 ? browser.Contexts[0] : await browser.NewContextAsync();
            var page = await context.NewPageAsync();
            await page.GotoAsync("https://example.com");

            var payload = new
            {
                query = @"
                    query getSessions($id: String!, $apiToken: String!) {
                        sessions(apiToken: $apiToken, id: $id) {
                            description
                            devtoolsFrontendUrl
                            id
                            title
                            url
                            browserId
                            browserWSEndpoint
                        }
                    }
                ",
                variables = new { id = Id, apiToken = ApiToken }
            };

            var response = await httpClient.PostAsync(
                "https://api.browserless.io/graphql",
                new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json")
            );

            response.EnsureSuccessStatusCode();
            var responseData = JsonSerializer.Deserialize(await response.Content.ReadAsStringAsync());
            var sessions = responseData.GetProperty("data").GetProperty("sessions").EnumerateArray();

            if (!sessions.MoveNext())
            {
                throw new Exception("Error retrieving session!");
            }

            var session = sessions.Current;
            Console.WriteLine($"Open: {session.GetProperty("devtoolsFrontendUrl").GetString()} in your browser!");

            await Task.Delay(30000);
        }
        catch (Exception e)
        {
            Console.WriteLine($"Saw error when running: {e.Message}");
        }
        finally
        {
            if (browser != null)
            {
                Console.WriteLine("Shutting down the browser.");
                await browser.CloseAsync();
            }
        }
    }
}

```
      
    
  

The examples include a `page.waitFor` call to give you time to visit the debugging page. In production, page interactions happen quickly and you may miss the session. To watch the script from the start, add the `&pause` query-string parameter to your `puppeteer.connect` call.

### Selecting Modules

The examples use `puppeteer` or `playwright`. Any CDP-based library works against the Chromium and Chrome endpoints, though Puppeteer and Playwright have the best support. Firefox and WebKit sessions need Playwright's native `connect` method, since CDP is Chromium-only.

The underlying GraphQL query retrieves session properties. Only `devtoolsFrontendUrl` is used in these examples. The other fields are included for reference.

```graphql
query getSessions($id: String!, $apiToken: String!) {
  sessions(apiToken: $apiToken, id: $id) {
    description
    devtoolsFrontendUrl
    id
    title
    url
    id
    browserId
    browserWSEndpoint
  }
}
```

This query retrieves sessions by API token and id. The response looks like:

```js
{
  "data": {
    "sessions": [
      {
        "id": null,
        "initialConnectURL": "wss://YOUR_CONTAINER_URL_HERE/firefox/playwright/?token=YOUR_API_TOKEN_HERE",
        "isTempDataDir": true,
        "launchOptions": {},
        "numbConnected": 1,
        "routePath": ["/firefox/playwright", "/firefox/playwright"],
        // ...
      }
    ]
  }
}
```

> In all of our returned links we _omit_ your API token, so you'll need to add it to the link manually to visit it. This is done for security purposes.

For questions or help with setup, [contact us](https://www.browserless.io/contact). The [Docker configuration reference](/enterprise/docker/config) covers environment variables that affect session behavior.

## FAQ & Troubleshooting

<details>
<summary>How do I get an Enterprise license?</summary>

Contact [sales@browserless.io](mailto:sales@browserless.io) or visit the [pricing page](https://browserless.io/pricing) for Enterprise plan details.

</details>

<details>
<summary>Can I run Browserless on my own infrastructure?</summary>

Yes. Enterprise customers can self-host using the Docker image. See [Docker deployment](/enterprise/docker/enterprise-image) for setup instructions.

</details>

## Next steps

  <a href="/enterprise/docker/enterprise-image" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Docker deployment</h3>
      self-host Browserless with Docker
    
  </a>
  <a href="/enterprise/launch-options" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      configure your Enterprise deployment
    
  </a>

---

# Examples

URL: https://docs.browserless.io/examples

# Examples

Ready-to-run examples for using Browserless with your AI agents, frameworks, and tools.

---

# Log in with an AI agent

URL: https://docs.browserless.io/examples/agent-login

# Log in with an AI agent

Use the `browserless_agent` MCP tool to log in to a website and interact with authenticated content in a single conversation.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- The [Browserless MCP server](https://github.com/browserless/browserless-mcp) connected to your AI agent

## Steps

  
**AI Agent:**

  Use the [Browserless MCP server](https://github.com/browserless/browserless-mcp) to log in to sites from any MCP-compatible AI agent (Claude Desktop, Cursor, Windsurf, ChatGPT, etc.).

  

  **1. Connect the MCP server**

  Send this prompt to your AI agent to install the Browserless MCP server:

```text
Go to https://github.com/browserless/browserless-mcp/blob/main/install.md
and follow the instructions to install the Browserless MCP server
for my client.
```

  **2. Log in to a site**

  Use `browserless_agent`. It keeps a live browser session so it can fill forms, click buttons, and wait for redirects.

```text
Use the browserless_agent tool to go to https://example.com/login,
enter my email user@example.com and password, click the login
button, and confirm I'm on the dashboard
```

  **3. Use an authenticated profile**

  If you already have a saved profile, use `browserless_agent` with that profile to skip the login step entirely. The profile loads your saved cookies and session data so the agent starts already logged in.

  Set up the authenticated profile first through the [Profiles page](https://browserless.io/account/profiles/) in your dashboard or using the REST or framework APIs. See [Save Logins to Authenticated Profiles](/examples/authenticated-profiles) for REST and framework options.

```text
Use the browserless_agent tool with my authenticated profile
"my-site" to go to https://example.com/dashboard and confirm
I'm logged in
```

  :::note
  `browserless_agent` can perform login interactions within a single conversation but cannot save profiles. To create reusable authenticated profiles, use the [Profiles page](https://browserless.io/account/profiles/) in your dashboard or the [REST or framework APIs](/examples/authenticated-profiles).
  :::

  
  
**Other Approaches:**

  For login automation using BQL, Puppeteer, Playwright, or direct REST calls, see [Log In with BQL and Browser Automation](/examples/browser-agent-login).

  For saving and reusing login state across sessions, see [Save Logins to Authenticated Profiles](/examples/authenticated-profiles).

---

# Save logins to authenticated profiles

URL: https://docs.browserless.io/examples/authenticated-profiles

# Save logins to authenticated profiles

Capture a logged-in browser state once and reuse it across parallel sessions without re-entering credentials.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

Saving an authenticated profile works in two phases: first you **create and save** the profile with a live login session, then you **reuse** that profile in any future request.

  
**REST API:**

  Use the REST API to save and reuse authenticated browser profiles.

  
    
**cURL:**

    

    **1. Start a profile session**

    Create a named profile session. The response includes a WebSocket URL to connect a CDP client for the login flow:

```bash
curl -X POST \
  "https://production-sfo.browserless.io/profile?token=YOUR_API_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{ "name": "my-profile" }'
```

    The response:

```json
{
  "id": "<session-id>",
  "name": "my-profile",
  "connect": "wss://production-sfo.browserless.io/session/connect/<id>?token=...",
  "stop": "https://production-sfo.browserless.io/session/<id>?token=..."
}
```

    **2. Log in and save**

    Connect a CDP client (see the Puppeteer tab), complete the login, and call `Browserless.saveProfile` to persist the session.

    **3. Reuse the profile**

    Append `?profile=my-profile` to any Browserless endpoint to start pre-authenticated:

```bash
curl -X POST \
  "https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE&profile=my-profile" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://app.example.com/dashboard" }' \
  --output dashboard.png
```

    
    
**JavaScript:**

    

    **1. Create a profile session**

```js
const TOKEN = 'YOUR_API_TOKEN_HERE';
const ORIGIN = 'https://production-sfo.browserless.io';

const session = await fetch(`${ORIGIN}/profile?token=${TOKEN}`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'my-profile' }),
}).then((r) => r.json());

console.log(session.connect);
// Use session.connect with the Frameworks tab to log in and save.
```

    **2. Reuse the saved profile**

```js
import fs from 'fs';

const response = await fetch(
  `${ORIGIN}/screenshot?token=${TOKEN}&profile=my-profile`,
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ url: 'https://app.example.com/dashboard' }),
  }
);

const buffer = await response.arrayBuffer();
await fs.promises.writeFile('dashboard.png', Buffer.from(buffer));
console.log('Screenshot saved.');
```

    
    
**Python:**

    

    **1. Install dependencies**

```bash
pip install requests playwright
```

    **2. Create a profile session**

```python
import requests

TOKEN = 'YOUR_API_TOKEN_HERE'
ORIGIN = 'https://production-sfo.browserless.io'

session = requests.post(
    f'{ORIGIN}/profile?token={TOKEN}',
    json={'name': 'my-profile'},
).json()

print(session['connect'])
# Use session['connect'] with the Frameworks → Playwright → Python tab to log in and save.
```

    Profile creation requires a live browser session. This tab covers only the HTTP steps. To complete the login and persist the session, switch to the **Frameworks → Playwright → Python** tab. That example connects to `session['connect']` with `playwright.chromium.connect_over_cdp`, logs in, then calls `page.context.new_cdp_session(page)` to open a CDP channel and `cdp.send('Browserless.saveProfile', {'name': 'my-profile'})` to save it.

    **3. Reuse the saved profile**

```python
import requests

TOKEN = 'YOUR_API_TOKEN_HERE'
ORIGIN = 'https://production-sfo.browserless.io'

response = requests.post(
    f'{ORIGIN}/screenshot?token={TOKEN}&profile=my-profile',
    json={'url': 'https://app.example.com/dashboard'},
)
with open('dashboard.png', 'wb') as f:
    f.write(response.content)
print('Screenshot saved.')
```

    
    
**Java:**

    

    **1. Add dependencies**

```xml
<dependency>
  <groupId>com.microsoft.playwright</groupId>
  <artifactId>playwright</artifactId>
  <version>1.60.0</version>
</dependency>
```

    **2. Create a profile session**

```java
import java.net.http.*;
import java.net.URI;
import java.nio.file.*;

HttpClient client = HttpClient.newHttpClient();

HttpRequest sessionRequest = HttpRequest.newBuilder()
    .uri(URI.create("https://production-sfo.browserless.io/profile?token=YOUR_API_TOKEN_HERE"))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString("{\"name\": \"my-profile\"}"))
    .build();
HttpResponse sessionResponse = client.send(sessionRequest, HttpResponse.BodyHandlers.ofString());
System.out.println(sessionResponse.body());
// Parse session.connect and use the Playwright Java tab to log in and save.
```

    **3. Reuse the saved profile**

```java
HttpRequest screenshotRequest = HttpRequest.newBuilder()
    .uri(URI.create("https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE&profile=my-profile"))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString("{\"url\": \"https://app.example.com/dashboard\"}"))
    .build();
HttpResponse<byte[]> screenshotResponse = client.send(screenshotRequest, HttpResponse.BodyHandlers.ofByteArray());
Files.write(Path.of("dashboard.png"), screenshotResponse.body());
System.out.println("Screenshot saved.");
```

    
    
**C#:**

    

    **1. Create a profile session**

```csharp
using System.Net.Http;
using System.Text;

using var client = new HttpClient();

var sessionBody = new StringContent("{\"name\": \"my-profile\"}", Encoding.UTF8, "application/json");
var sessionResponse = await client.PostAsync(
    "https://production-sfo.browserless.io/profile?token=YOUR_API_TOKEN_HERE",
    sessionBody
);
Console.WriteLine(await sessionResponse.Content.ReadAsStringAsync());
// Parse session.connect and use the Playwright C# tab to log in and save.
```

    **2. Reuse the saved profile**

```csharp
var screenshotBody = new StringContent("{\"url\": \"https://app.example.com/dashboard\"}", Encoding.UTF8, "application/json");
var screenshotResponse = await client.PostAsync(
    "https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE&profile=my-profile",
    screenshotBody
);
var bytes = await screenshotResponse.Content.ReadAsByteArrayAsync();
await File.WriteAllBytesAsync("dashboard.png", bytes);
Console.WriteLine("Screenshot saved.");
```

    
    
**Go:**

    

    **1. Create a profile session**

```go
package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
)

func main() {
    client := &http.Client{}

    // Create profile session.
    sessionBody := bytes.NewBufferString(`{"name": "my-profile"}`)
    sessionReq, _ := http.NewRequest("POST",
        "https://production-sfo.browserless.io/profile?token=YOUR_API_TOKEN_HERE",
        sessionBody,
    )
    sessionReq.Header.Set("Content-Type", "application/json")
    sessionResp, _ := client.Do(sessionReq)
    defer sessionResp.Body.Close()

    var session map[string]string
    json.NewDecoder(sessionResp.Body).Decode(&session)
    fmt.Println(session["connect"])
    // Use session["connect"] with the Frameworks tab to log in and save.
```

    **2. Reuse the saved profile**

```go
    // Reuse the saved profile.
    screenshotBody := bytes.NewBufferString(`{"url": "https://app.example.com/dashboard"}`)
    screenshotReq, _ := http.NewRequest("POST",
        "https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE&profile=my-profile",
        screenshotBody,
    )
    screenshotReq.Header.Set("Content-Type", "application/json")
    screenshotResp, _ := client.Do(screenshotReq)
    defer screenshotResp.Body.Close()
    data, _ := io.ReadAll(screenshotResp.Body)
    os.WriteFile("dashboard.png", data, 0644)
    fmt.Println("Screenshot saved.")
}
```

    
    
**PHP:**

    

    **1. Create a profile session**

```php
<?php
$ch = curl_init('https://production-sfo.browserless.io/profile?token=YOUR_API_TOKEN_HERE');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => '{"name": "my-profile"}',
    CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
    CURLOPT_RETURNTRANSFER => true,
]);
$session = json_decode(curl_exec($ch), true);
curl_close($ch);
// Use $session['connect'] with the Frameworks tab to log in and save.
```

    **2. Reuse the saved profile**

```php
$ch2 = curl_init('https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE&profile=my-profile');
curl_setopt_array($ch2, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => '{"url": "https://app.example.com/dashboard"}',
    CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
    CURLOPT_RETURNTRANSFER => true,
]);
file_put_contents('dashboard.png', curl_exec($ch2));
curl_close($ch2);
echo "Screenshot saved.\n";
```

    
    
**Ruby:**

    

    **1. Create a profile session**

```ruby
require 'net/http'
require 'json'
require 'uri'

# Create profile session.
uri = URI('https://production-sfo.browserless.io/profile?token=YOUR_API_TOKEN_HERE')
response = Net::HTTP.post(uri, '{"name": "my-profile"}', 'Content-Type' => 'application/json')
session = JSON.parse(response.body)
puts session['connect']
# Use session['connect'] with the Frameworks tab to log in and save.
```

    **2. Reuse the saved profile**

```ruby
# Reuse the saved profile.
uri2 = URI('https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE&profile=my-profile')
response2 = Net::HTTP.post(uri2, '{"url": "https://app.example.com/dashboard"}', 'Content-Type' => 'application/json')
File.binwrite('dashboard.png', response2.body)
puts 'Screenshot saved.'
```

    
  

  
  
**Frameworks:**

  Use a browser connection to log in, save a profile via CDP, and reconnect with the saved profile.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Log in and save the profile**

```js
import puppeteer from 'puppeteer-core';

const TOKEN = 'YOUR_API_TOKEN_HERE';
const ORIGIN = 'https://production-sfo.browserless.io';

const session = await fetch(`${ORIGIN}/profile?token=${TOKEN}`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'my-profile' }),
}).then((r) => r.json());

const browser = await puppeteer.connect({ browserWSEndpoint: session.connect });

try {
  const page = await browser.newPage();

  await page.goto('https://app.example.com/login');
  await page.type('#email', 'user@example.com');
  await page.type('#password', process.env.PASSWORD);
  await page.click("button[type='submit']");
  await page.waitForNavigation();

  const cdp = await page.createCDPSession();
  const result = await cdp.send('Browserless.saveProfile', { name: 'my-profile' });
  console.log(result);
  // { ok: true, profileId: '<id>', name: 'my-profile', cookieCount: 12, originCount: 1 }.
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Reuse the saved profile in parallel sessions**

```js
import puppeteer from 'puppeteer-core';

const WS = 'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&profile=my-profile';

// Each connect() call is a separate browser process — profiles let them all start authenticated.
const results = await Promise.all(
  Array.from({ length: 5 }, async (_, i) => {
    const browser = await puppeteer.connect({ browserWSEndpoint: WS });
    try {
      const page = await browser.newPage();
      await page.goto(`https://app.example.com/item/${i}`);
      return await page.title();
    } finally {
      // Always close to release the session even on error.
      await browser.close();
    }
  })
);

console.log(results);
```

    
    
**Playwright:**

    
      
**JavaScript:**

      

      **1. Install dependencies**

```bash
npm install playwright-core
```

      **2. Log in and save the profile**

```js
import { chromium } from 'playwright-core';

const TOKEN = 'YOUR_API_TOKEN_HERE';
const ORIGIN = 'https://production-sfo.browserless.io';

const session = await fetch(`${ORIGIN}/profile?token=${TOKEN}`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'my-profile' }),
}).then((r) => r.json());

const browser = await chromium.connectOverCDP(session.connect);

try {
  const context = browser.contexts()[0];
  const page = await context.newPage();

  await page.goto('https://app.example.com/login');
  await page.fill('#email', 'user@example.com');
  await page.fill('#password', process.env.PASSWORD);
  await page.click("button[type='submit']");
  await page.waitForURL('**/dashboard');

  const cdpSession = await context.newCDPSession(page);
  const result = await cdpSession.send('Browserless.saveProfile', { name: 'my-profile' });
  console.log(result);
  // { ok: true, profileId: '<id>', name: 'my-profile', cookieCount: 12, originCount: 1 }.
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

      **3. Reuse the saved profile**

```js
import { chromium } from 'playwright-core';

const browser = await chromium.connect(
  `wss://production-sfo.browserless.io/chromium/playwright?token=YOUR_API_TOKEN_HERE&profile=my-profile`
);

try {
  const context = browser.contexts()[0];
  const page = await context.newPage();
  await page.goto('https://app.example.com/dashboard'); // already logged in.
  console.log('Title:', await page.title());
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

      
      
**Python:**

      

      **1. Install dependencies**

```bash
pip install requests playwright
```

      **2. Log in and save the profile**

```python
import requests
from playwright.sync_api import sync_playwright

TOKEN = 'YOUR_API_TOKEN_HERE'
ORIGIN = 'https://production-sfo.browserless.io'

session = requests.post(
    f'{ORIGIN}/profile?token={TOKEN}',
    json={'name': 'my-profile'},
).json()

with sync_playwright() as playwright:
    # connect_over_cdp gives access to CDP sessions needed for Browserless.saveProfile.
    browser = playwright.chromium.connect_over_cdp(session['connect'])
    try:
        context = browser.contexts[0]
        page = context.new_page()

        page.goto('https://app.example.com/login')
        page.fill('#email', 'user@example.com')
        page.fill('#password', 'YOUR_PASSWORD')
        page.click("button[type='submit']")
        page.wait_for_url('**/dashboard')

        cdp = page.context.new_cdp_session(page)
        result = cdp.send('Browserless.saveProfile', {'name': 'my-profile'})
        print(result)
        # {'ok': True, 'profileId': '<id>', 'name': 'my-profile', 'cookieCount': 12, 'originCount': 1}
    finally:
        # Always close to release the session even on error.
        browser.close()
```

      **3. Reuse the saved profile**

```python
WS_REUSE = (
    'wss://production-sfo.browserless.io/chromium/playwright'
    '?token=YOUR_API_TOKEN_HERE&profile=my-profile'
)

with sync_playwright() as playwright:
    browser = playwright.chromium.connect_over_cdp(WS_REUSE)
    try:
        context = browser.contexts[0]
        page = context.pages[0]
        page.goto('https://app.example.com/dashboard')  # already logged in.
        print('Title:', page.title())
    finally:
        # Always close to release the session even on error.
        browser.close()
```

      
      
**Java:**

      

      **1. Add dependencies**

```xml
<dependency>
  <groupId>com.microsoft.playwright</groupId>
  <artifactId>playwright</artifactId>
  <version>1.60.0</version>
</dependency>
<!-- Add Gson or Jackson for JSON parsing -->
<dependency>
  <groupId>com.google.code.gson</groupId>
  <artifactId>gson</artifactId>
  <version>2.10.1</version>
</dependency>
```

      **2. Log in and save the profile**

```java
import com.microsoft.playwright.*;
import com.google.gson.*;
import java.net.http.*;
import java.net.URI;

public class SaveProfile {
    public static void main(String[] args) throws Exception {
        String token = "YOUR_API_TOKEN_HERE";
        String origin = "https://production-sfo.browserless.io";

        HttpClient http = HttpClient.newHttpClient();
        HttpRequest req = HttpRequest.newBuilder()
            .uri(URI.create(origin + "/profile?token=" + token))
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString("{\"name\": \"my-profile\"}"))
            .build();
        String body = http.send(req, HttpResponse.BodyHandlers.ofString()).body();
        String wsEndpoint = JsonParser.parseString(body)
            .getAsJsonObject().get("connect").getAsString();

        try (Playwright playwright = Playwright.create()) {
            Browser browser = playwright.chromium().connectOverCDP(wsEndpoint);
            try {
                BrowserContext context = browser.contexts().get(0);
                Page page = context.newPage();

                page.navigate("https://app.example.com/login");
                page.fill("#email", "user@example.com");
                page.fill("#password", System.getenv("PASSWORD"));
                page.click("button[type='submit']");
                page.waitForURL("**/dashboard");

                // connectOverCDP gives access to CDP sessions needed for Browserless.saveProfile.
                CDPSession cdp = page.context().newCDPSession(page);
                JsonElement result = cdp.send("Browserless.saveProfile", Map.of("name", "my-profile"));
                System.out.println(result);
                // {"ok":true,"profileId":"<id>","name":"my-profile","cookieCount":12,"originCount":1}
            } finally {
                // Always close to release the session even on error.
                browser.close();
            }
        }
    }
}
```

      **3. Reuse the saved profile**

```java
String wsReuse = "wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&profile=my-profile";

try (Playwright playwright = Playwright.create()) {
    Browser browser = playwright.chromium().connectOverCDP(wsReuse);
    try {
        BrowserContext context = browser.contexts().get(0);
        Page page = context.pages().get(0);
        page.navigate("https://app.example.com/dashboard"); // already logged in.
        System.out.println("Title: " + page.title());
    } finally {
        // Always close to release the session even on error.
        browser.close();
    }
}
```

      
      
**C#:**

      

      **1. Add dependencies**

```bash
dotnet add package Microsoft.Playwright
```

      **2. Log in and save the profile**

```csharp
using Microsoft.Playwright;
using System.Net.Http;
using System.Text;
using System.Text.Json;

const string Token = "YOUR_API_TOKEN_HERE";
const string Origin = "https://production-sfo.browserless.io";

using var http = new HttpClient();
var sessionBody = new StringContent("{\"name\": \"my-profile\"}", Encoding.UTF8, "application/json");
var sessionResp = await http.PostAsync($"{Origin}/profile?token={Token}", sessionBody);
var sessionJson = JsonDocument.Parse(await sessionResp.Content.ReadAsStringAsync());
var wsEndpoint = sessionJson.RootElement.GetProperty("connect").GetString();

using var playwright = await Playwright.CreateAsync();
var browser = await playwright.Chromium.ConnectOverCDPAsync(wsEndpoint);

try
{
    var context = browser.Contexts[0];
    var page = await context.NewPageAsync();

    await page.GotoAsync("https://app.example.com/login");
    await page.FillAsync("#email", "user@example.com");
    await page.FillAsync("#password", Environment.GetEnvironmentVariable("PASSWORD"));
    await page.ClickAsync("button[type='submit']");
    await page.WaitForURLAsync("**/dashboard");

    // ConnectOverCDP gives access to CDP sessions needed for Browserless.saveProfile.
    var cdp = await page.Context.NewCDPSessionAsync(page);
    var result = await cdp.SendAsync(
        "Browserless.saveProfile",
        new Dictionary<string, object> { ["name"] = "my-profile" }
    );
    Console.WriteLine(result);
    // { ok: true, profileId: '<id>', name: 'my-profile', cookieCount: 12, originCount: 1 }
}
finally
{
    // Always close to release the session even on error.
    await browser.CloseAsync();
}
```

      **3. Reuse the saved profile**

```csharp
using var playwright = await Playwright.CreateAsync();
var browser = await playwright.Chromium.ConnectOverCDPAsync(
    "wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&profile=my-profile"
);

try
{
    var context = browser.Contexts[0];
    var page = context.Pages[0];
    await page.GotoAsync("https://app.example.com/dashboard"); // already logged in.
    Console.WriteLine($"Title: {await page.TitleAsync()}");
}
finally
{
    // Always close to release the session even on error.
    await browser.CloseAsync();
}
```

      
    

    
  

  

## Next steps

  <a href="/examples/sessions" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Log In and Reuse Sessions</h3>
      full walkthrough of the session reuse pattern
    
  </a>
  <a href="/examples/concurrent-sessions" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Run Concurrent Browser Sessions</h3>
      launch many sessions in parallel using the saved profile
    
  </a>
  <a href="/examples/scrape" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape Structured Data</h3>
      extract data from authenticated pages
    
  </a>

---

# Batch DOM queries in a single browser call

URL: https://docs.browserless.io/examples/batch-dom-queries

# Batch DOM queries in a single browser call

When you connect to Browserless over WebSocket, every `page.$eval()` call is a separate network round-trip between your Node process and a remote Chrome instance. Bundle all DOM reads into one `page.evaluate()` call and you pay that cost once, no matter how many selectors you need. For more performance tips with remote browsers, see [BQL Best Practices](/browserql/best-practices).

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

  
**REST API:**

  The `/function` endpoint runs the entire script server-side inside the same browser process, so there's naturally only one round-trip per request. No special batching needed.

  
    
**cURL:**

    

    **1. Send the request**

```bash
curl -X POST \
  "https://production-sfo.browserless.io/function?token=YOUR_API_TOKEN_HERE" \
  -H "Content-Type: application/javascript" \
  --data-raw 'module.exports = async ({ page }) => {
    await page.goto("https://scraping-sandbox.netlify.app/products");

    const data = await page.evaluate(() => ({
      title: document.title,
      heading: document.querySelector("h1")?.textContent?.trim() ?? null,
      links: Array.from(document.querySelectorAll("a[href]")).map(a => ({
        text: a.textContent.trim(),
        href: a.href,
      })),
      productCount: document.querySelectorAll(".product-title").length,
    }));

    return data;
  };'
```

    **2. Check the output**

```json
{
  "title": "Product Catalog | Scraping Sandbox",
  "heading": "Product Catalog",
  "links": [
    { "text": "Contact Us", "href": "https://scraping-sandbox.netlify.app/contact-us" }
  ],
  "productCount": 8
}
```

    
    
**JavaScript:**

    

    **1. Send the request**

```js
const code = `module.exports = async ({ page }) => {
  await page.goto('https://scraping-sandbox.netlify.app/products');

  const data = await page.evaluate(() => ({
    title: document.title,
    heading: document.querySelector('h1')?.textContent?.trim() ?? null,
    links: Array.from(document.querySelectorAll('a[href]')).map(a => ({
      text: a.textContent.trim(),
      href: a.href,
    })),
    productCount: document.querySelectorAll('.product-title').length,
  }));

  return data;
};`;

const response = await fetch(
  'https://production-sfo.browserless.io/function?token=YOUR_API_TOKEN_HERE',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/javascript' },
    body: code,
  }
);

const data = await response.json();
console.log(data);
```

    **2. Check the output**

    Run with `node batch-dom-queries.mjs`. The response is a flat JSON object with `title`, `heading`, `links[]`, and `productCount`.

    
    
**Python:**

    

    **1. Install dependencies**

```bash
pip install requests
```

    **2. Send the request**

```python
import requests

code = """
module.exports = async ({ page }) => {
  await page.goto('https://scraping-sandbox.netlify.app/products');

  const data = await page.evaluate(() => ({
    title: document.title,
    heading: document.querySelector('h1')?.textContent?.trim() ?? null,
    links: Array.from(document.querySelectorAll('a[href]')).map(a => ({
      text: a.textContent.trim(),
      href: a.href,
    })),
    productCount: document.querySelectorAll('.product-title').length,
  }));

  return data;
};
"""

response = requests.post(
    'https://production-sfo.browserless.io/function?token=YOUR_API_TOKEN_HERE',
    headers={'Content-Type': 'application/javascript'},
    data=code.encode('utf-8'),
)

print(response.json())
```

    **3. Check the output**

    Run with `python batch_dom_queries.py`. The printed dict has `title`, `heading`, `links[]`, and `productCount`, the same shape as the JSON output above.

    
    
**Java:**

    

    **1. Send the request**

```java
import java.net.URI;
import java.net.http.*;

String token = "YOUR_API_TOKEN_HERE";
String endpoint = "https://production-sfo.browserless.io/function?token=" + token;

String code = """
    module.exports = async ({ page }) => {
      await page.goto('https://scraping-sandbox.netlify.app/products');

      const data = await page.evaluate(() => ({
        title: document.title,
        heading: document.querySelector('h1')?.textContent?.trim() ?? null,
        links: Array.from(document.querySelectorAll('a[href]')).map(a => ({
          text: a.textContent.trim(),
          href: a.href,
        })),
        productCount: document.querySelectorAll('.product-title').length,
      }));

      return data;
    };
    """;

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(endpoint))
    .header("Content-Type", "application/javascript")
    .POST(HttpRequest.BodyPublishers.ofString(code))
    .build();

HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
```

    **2. Check the output**

    Run the class. The response body is JSON with `title`, `heading`, `links`, and `productCount`.

    
    
**C#:**

    

    **1. Send the request**

```csharp
using System.Net.Http;
using System.Text;

string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"https://production-sfo.browserless.io/function?token={token}";

string code = @"module.exports = async ({ page }) => {
  await page.goto('https://scraping-sandbox.netlify.app/products');

  const data = await page.evaluate(() => ({
    title: document.title,
    heading: document.querySelector('h1')?.textContent?.trim() ?? null,
    links: Array.from(document.querySelectorAll('a[href]')).map(a => ({
      text: a.textContent.trim(),
      href: a.href,
    })),
    productCount: document.querySelectorAll('.product-title').length,
  }));

  return data;
};";

using (HttpClient httpClient = new HttpClient())
{
    var content = new StringContent(code, Encoding.UTF8, "application/javascript");
    var response = await httpClient.PostAsync(endpoint, content);
    string result = await response.Content.ReadAsStringAsync();
    Console.WriteLine(result);
}
```

    **2. Check the output**

    Run the program. The response body is JSON with `title`, `heading`, `links`, and `productCount`.

    
  

  
  
**Frameworks:**

  With a remote WebSocket connection, each `page.$eval()` is a separate network call. Your Node process serializes the selector, sends it over the wire, waits for Chrome to run it, then gets the result back. Do that four times in sequence and you've added four round-trips of latency. One `page.evaluate()` with all four queries inside cuts that to a single trip.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Connect and query**

```js
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint: 'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE',
});

try {
  const page = await browser.newPage();
  await page.goto('https://scraping-sandbox.netlify.app/products', { waitUntil: 'networkidle2' });

  // Don't do this — four separate round-trips between Node and Chrome:
  // const title = await page.title();
  // const heading = await page.$eval('h1', el => el.textContent.trim());
  // const links = await page.$$eval('a[href]', els => els.map(a => a.href));
  // const productCount = await page.$$eval('.product-title', els => els.length);

  // Do this instead — one round-trip, all data comes back together:
  const data = await page.evaluate(() => ({
    title: document.title,
    heading: document.querySelector('h1')?.textContent?.trim() ?? null,
    links: Array.from(document.querySelectorAll('a[href]')).map(a => ({
      text: a.textContent.trim(),
      href: a.href,
    })),
    productCount: document.querySelectorAll('.product-title').length,
  }));

  console.log(data);
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Run with `node batch-dom-queries.mjs`. You get a single object with a string `title`, a nullable string `heading`, an array of `{text, href}` link objects, and an integer `productCount`.

```json
{
  "title": "Product Catalog | Scraping Sandbox",
  "heading": "Product Catalog",
  "links": [
    { "text": "Contact Us", "href": "https://scraping-sandbox.netlify.app/contact-us" }
  ],
  "productCount": 8
}
```

    
    
**Playwright:**

    

    **1. Install dependencies**

```bash
npm install playwright-core
```

    **2. Connect and query**

    Playwright (like Puppeteer) stringifies the `evaluate()` callback before sending it to Chrome. That means closures don't work. Any variable from Node-land that the function references will be `undefined` inside Chrome. Pass Node-land values as a second argument to `evaluate()` instead.

```js
import { chromium } from 'playwright-core';

const browser = await chromium.connectOverCDP(
  'wss://production-sfo.browserless.io/chromium/playwright?token=YOUR_API_TOKEN_HERE'
);

try {
  const context = browser.contexts()[0];
  const page = await context.newPage();
  await page.goto('https://scraping-sandbox.netlify.app/products', { waitUntil: 'networkidle' });

  // One evaluate call returns all the data you need:
  const data = await page.evaluate(() => ({
    title: document.title,
    heading: document.querySelector('h1')?.textContent?.trim() ?? null,
    links: Array.from(document.querySelectorAll('a[href]')).map(a => ({
      text: a.textContent.trim(),
      href: a.href,
    })),
    productCount: document.querySelectorAll('.product-title').length,
  }));

  console.log(data);
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Run with `node batch-dom-queries.mjs`. Same shape as the Puppeteer output: a flat object with `title`, `heading`, `links[]`, and `productCount`.

```json
{
  "title": "Product Catalog | Scraping Sandbox",
  "heading": "Product Catalog",
  "links": [
    { "text": "Contact Us", "href": "https://scraping-sandbox.netlify.app/contact-us" }
  ],
  "productCount": 8
}
```

    
    
**BAP:**

    **1. Install dependencies**

```bash
npm install @browserless.io/bap-ts
```

    **2. Connect and batch the queries**

```typescript
import Browserless from '@browserless.io/bap-ts';

const browser = Browserless.connect({
  browserWSEndpoint: 'wss://production-sfo.browserless.io/chromium/bql',
  token: 'YOUR_API_TOKEN_HERE',
});

try {
  const page = await browser.newPage();
  await page.goto('https://scraping-sandbox.netlify.app/products', { waitUntil: 'domContentLoaded' });

  // One evaluate() keeps this to a single round trip, matching the BQL tab.
  // mapSelector can't be used here: it returns innerText/innerHTML/id/class
  // only, with no way to read an href.
  const batched = await page.evaluate(`JSON.stringify({
    heading: document.querySelector('h1')?.innerText ?? null,
    links: [...document.querySelectorAll('a[href]')].map((a) => ({
      innerText: a.innerText,
      href: a.href,
    })),
  })`);

  console.log(JSON.parse(batched ?? '{}'));
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Each call is one BrowserQL operation over the same WebSocket, so the whole batch costs a single connection.

    
  

  
  
**BQL:**

  

  **1. Write the mutation**

  BQL runs entirely server-side. See [Writing BrowserQL](/browserql/writing-bql/language-basics) for full details. There's no client process making sequential calls over a WebSocket. The whole query executes in one browser pass, so the round-trip problem doesn't exist and there's nothing to batch.

```graphql
mutation BatchDOMQueries {
  goto(url: "https://scraping-sandbox.netlify.app/products", waitUntil: domContentLoaded) {
    status
  }
  heading: text(selector: "h1")
  links: mapSelector(selector: "a[href]") {
    innerText
    href: attribute(name: "href")
  }
}
```

  **2. Run it**

  Paste into the [BQL IDE](https://account.browserless.io/bql) and click Run.

  **3. Check the output**

  The response wraps everything in a `data` object. `goto` returns the HTTP status, `heading` is a string, and `links` is an array of `{innerText, href}` objects. Note that BQL uses `innerText` rather than `text`. It reflects the computed text content, including nested elements.

```json
{
  "data": {
    "goto": { "status": 200 },
    "heading": "Product Catalog",
    "links": [
      { "innerText": "Contact Us", "href": "https://scraping-sandbox.netlify.app/contact-us" }
    ]
  }
}
```

---

# Log in with BQL and browser automation

URL: https://docs.browserless.io/examples/browser-agent-login

# Log in with BQL and browser automation

Automate a login flow (navigating to the login page, filling credentials, solving CAPTCHAs, and handling post-login redirects) using BQL or a Puppeteer/Playwright connection.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

  
**REST API:**

  Use the REST API to automate a login flow without a persistent browser connection.

  
    
**cURL:**

    

    **1. Build the BQL mutation**

    Chain navigation and interaction steps. The agent resolves selectors dynamically from a snapshot of the page:

```bash
https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE
```

    **2. Send the request**

```bash
curl -X POST \
  "https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "mutation AgentLogin { goto(url: \"https://app.example.com/login\") { status } waitForSelector(selector: \"input[type=email], input[name=email], #email\") { selector } typeEmail: type(selector: \"input[type=email], input[name=email], #email\", text: \"user@example.com\") { time } typePass: type(selector: \"input[type=password]\", text: \"YOUR_PASSWORD\") { time } solve { found solved } submit: click(selector: \"button[type=submit]\") { time } waitForNavigation { status } }"
  }'
```

    **3. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "selector": "input[name=email]" },
    "typeEmail": { "time": 134 },
    "typePass": { "time": 98 },
    "solve": { "found": false, "solved": false },
    "submit": { "time": 76 },
    "waitForNavigation": { "status": 200 }
  }
}
```

    
    
**JavaScript:**

    

    **1. Send the request**

```js
const response = await fetch(
  'https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      query: `mutation AgentLogin {
        goto(url: "https://app.example.com/login") {
          status
        }
        waitForSelector(selector: "input[type=email], input[name=email], #email") {
          selector
        }
        typeEmail: type(
          selector: "input[type=email], input[name=email], #email"
          text: "user@example.com"
        ) {
          time
        }
        typePass: type(selector: "input[type=password]", text: "YOUR_PASSWORD") {
          time
        }
        solve {
          found
          solved
        }
        submit: click(selector: "button[type=submit]") {
          time
        }
        waitForNavigation {
          status
        }
      }`,
      operationName: 'AgentLogin',
    }),
  }
);

const result = await response.json();
console.log('Login complete:', result.data);
```

    **2. Check the output**

    Run with `node agent-login.mjs`. The mutation chain handles any CAPTCHA and waits for the post-login navigation.

    
    
**Python:**

    

    **1. Install dependencies**

```bash
pip install requests
```

    **2. Send the request**

```python
import requests

query = """
mutation AgentLogin {
  goto(url: "https://app.example.com/login") {
    status
  }
  waitForSelector(selector: "input[type=email], input[name=email], #email") {
    selector
  }
  typeEmail: type(
    selector: "input[type=email], input[name=email], #email"
    text: "user@example.com"
  ) {
    time
  }
  typePass: type(selector: "input[type=password]", text: "YOUR_PASSWORD") {
    time
  }
  solve {
    found
    solved
  }
  submit: click(selector: "button[type=submit]") {
    time
  }
  waitForNavigation {
    status
  }
}
"""

response = requests.post(
    'https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE',
    json={'query': query, 'operationName': 'AgentLogin'},
)

print(response.json()['data'])
```

    **3. Check the output**

    Run with `python agent-login.py`. The mutation chain completes the login flow autonomously.

    
  

  
  
**Frameworks:**

  Use a browser connection to fill in credentials, solve CAPTCHAs, and submit the login form.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Connect and log in**

    Puppeteer gives you full control for adaptive login flows. Try multiple selectors, handle unexpected prompts, and recover from errors:

```js
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint:
    'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&solveCaptchas=true',
});

try {
  const page = await browser.newPage();
  await page.goto('https://app.example.com/login', { waitUntil: 'networkidle2' });

  // Try multiple selectors — login pages don't use a consistent attribute for the email field.
  const emailSelectors = ['input[type="email"]', 'input[name="email"]', '#email'];
  for (const sel of emailSelectors) {
    if (await page.$(sel)) {
      await page.type(sel, 'user@example.com');
      break;
    }
  }

  await page.type('input[type="password"]', process.env.PASSWORD);
  await page.click('button[type="submit"]');
  await page.waitForNavigation({ waitUntil: 'networkidle2' });

  console.log('Logged in. Current URL:', page.url());
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Run with `node agent-login.mjs`. The `solveCaptchas=true` parameter enables automatic CAPTCHA solving if one appears during login.

    
    
**Playwright:**

    

    **1. Install dependencies**

```bash
npm install playwright-core
```

    **2. Connect and log in**

```js
import { chromium } from 'playwright-core';

const browser = await chromium.connectOverCDP(
  'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&solveCaptchas=true'
);

try {
  // Use the default context — browser.newPage() creates a new context that
  // doesn't inherit proxy, profile, or launch settings.
  const context = browser.contexts()[0];
  const page = await context.newPage();
  await page.goto('https://app.example.com/login', { waitUntil: 'networkidle' });

  const emailSelectors = ['input[type="email"]', 'input[name="email"]', '#email'];
  for (const sel of emailSelectors) {
    if (await page.locator(sel).count() > 0) {
      await page.fill(sel, 'user@example.com');
      break;
    }
  }

  await page.fill('input[type="password"]', process.env.PASSWORD);
  await page.click('button[type="submit"]');
  await page.waitForLoadState('networkidle');

  console.log('Logged in. Current URL:', page.url());
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Run with `node agent-login.mjs`. The `solveCaptchas=true` parameter enables automatic CAPTCHA solving during the login flow.

    
    
**BAP:**

    **1. Install dependencies**

```bash
npm install @browserless.io/bap-ts
```

    **2. Connect and log in**

```typescript
import Browserless from '@browserless.io/bap-ts';

const browser = Browserless.connect({
  browserWSEndpoint: 'wss://production-sfo.browserless.io/chromium/bql',
  token: 'YOUR_API_TOKEN_HERE',
});

try {
  const page = await browser.newPage();
  await page.goto('https://app.example.com/login');

  const emailSelector = 'input[type=email], input[name=email], #email';
  await page.waitForSelector(emailSelector);

  await page.type(emailSelector, 'user@example.com');
  await page.type('input[type=password]', 'YOUR_PASSWORD');

  // Clears a CAPTCHA if the login form presents one.
  await page.solve();

  await page.click('button[type=submit]');
  await page.waitForNavigation();

  console.log(await page.url());
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    The final URL confirms the post-login redirect landed on the authenticated page.

    
  

  
  
**BQL:**

  

  **1. Write the mutation**

  Chain navigation, field detection, typing, CAPTCHA solving, and submission in a single request:

```graphql
mutation AgentLogin {
  goto(url: "https://app.example.com/login") {
    status
  }

  waitForSelector(selector: "input[type=email], input[name=email], #email") {
    selector
  }

  typeEmail: type(
    selector: "input[type=email], input[name=email], #email"
    text: "user@example.com"
  ) {
    time
  }

  typePass: type(selector: "input[type=password]", text: "YOUR_PASSWORD") {
    time
  }

  solve {
    found
    solved
  }

  submit: click(selector: "button[type=submit]") {
    time
  }

  waitForNavigation {
    status
  }
}
```

  **2. Run it**

  Paste into the [BQL IDE](https://account.browserless.io/bql) and click Run, or send via HTTP (see the cURL tab).

  **3. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "selector": "input[name=email]" },
    "typeEmail": { "time": 134 },
    "typePass": { "time": 98 },
    "solve": { "found": false, "solved": false },
    "submit": { "time": 76 },
    "waitForNavigation": { "status": 200 }
  }
}
```

  

## Next steps

  <a href="/examples/authenticated-profiles" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Save Logins to Authenticated Profiles</h3>
      persist the logged-in state for reuse across sessions
    
  </a>
  <a href="/examples/email-otp" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Log In via Email OTP</h3>
      handle login flows that require a one-time code sent by email
    
  </a>
  <a href="/examples/captcha" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Solving reCAPTCHAs</h3>
      automatically solve CAPTCHA challenges during login
    
  </a>

---

# Bulk invoice download

URL: https://docs.browserless.io/examples/bulk-invoice-download

# Bulk invoice download

Log into a sandbox vendor portal, navigate orders, and download invoices in bulk using browser automation.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

This example uses a [Browserless sandbox site](https://scraping-sandbox.netlify.app/harvest-direct/vendor-portal) that simulates a vendor portal behind a login screen. You can safely test login and download automation without affecting any real systems.

The workflow logs into the portal with demo credentials, navigates to the orders page, and extracts order data including links to each order's detail page where invoices can be downloaded.

> **Demo credentials:** `demo@example.com` / `helloworld`

  
**AI Agent:**

  Use the [Browserless MCP server](https://github.com/browserless/browserless-mcp) to download invoices from any MCP-compatible AI agent (Claude Desktop, Cursor, Windsurf, ChatGPT, etc.).

  **1. Connect the MCP server**

  Send this prompt to your AI agent to install the Browserless MCP server:

```text
Go to https://github.com/browserless/browserless-mcp/blob/main/install.md
and follow the instructions to install the Browserless MCP server
for my client.
```

  **2. Download invoices**

  Use `browserless_agent`. The task requires interaction: logging in, navigating orders, and clicking download buttons.

```text
Use the browserless_agent tool to go to https://scraping-sandbox.netlify.app/harvest-direct/vendor-portal,
log in with email demo@example.com and password helloworld,
then navigate to each order and download the invoice
```

  
  
**REST API:**

  Send the BQL mutation over HTTP to log in, list orders, and extract invoice data. This is a sandbox site, so no stealth mode is needed.

  
    
**cURL:**

    

    **1. Send the request**

```bash
curl -X POST \
  "https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "mutation BulkInvoiceDownload { goto(url: \"https://scraping-sandbox.netlify.app/harvest-direct/vendor-portal\", waitUntil: networkIdle) { status } waitForForm: waitForSelector(selector: \"form\", timeout: 10000) { time } typeEmail: type(selector: \"input[name=email]\", text: \"demo@example.com\") { time } typePassword: type(selector: \"input[name=password]\", text: \"helloworld\") { time } submitLogin: click(selector: \"button[type=submit]\") { time } waitForOrders: waitForSelector(selector: \"table tbody tr\", timeout: 10000) { time } orders: mapSelector(selector: \"table tbody tr\") { orderId: mapSelector(selector: \"td:nth-child(1)\") { innerText } date: mapSelector(selector: \"td:nth-child(2)\") { innerText } items: mapSelector(selector: \"td:nth-child(3)\") { innerText } total: mapSelector(selector: \"td:nth-child(4)\") { innerText } status: mapSelector(selector: \"td:nth-child(5)\") { innerText } viewLink: mapSelector(selector: \"td a\") { href: attribute(name: \"href\") { value } } } }",
    "variables": {},
    "operationName": "BulkInvoiceDownload"
  }'
```

    **2. Check the output**

    The response contains order metadata and links to each order's detail page where invoices can be downloaded:

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForForm": { "time": 512 },
    "typeEmail": { "time": 85 },
    "typePassword": { "time": 62 },
    "submitLogin": { "time": 15 },
    "waitForOrders": { "time": 1230 },
    "orders": [
      {
        "orderId": [{ "innerText": "ORD-001" }],
        "date": [{ "innerText": "2025-01-15" }],
        "items": [{ "innerText": "3" }],
        "total": [{ "innerText": "$1,250.00" }],
        "status": [{ "innerText": "Delivered" }],
        "viewLink": [{ "href": { "value": "/harvest-direct/vendor-portal/orders/ORD-001" } }]
      }
    ]
  }
}
```

    
    
**JavaScript:**

    

    **1. Send the request**

```js
const query = `mutation BulkInvoiceDownload {
  goto(url: "https://scraping-sandbox.netlify.app/harvest-direct/vendor-portal", waitUntil: networkIdle) {
    status
  }
  waitForForm: waitForSelector(selector: "form", timeout: 10000) {
    time
  }
  typeEmail: type(selector: "input[name=email]", text: "demo@example.com") {
    time
  }
  typePassword: type(selector: "input[name=password]", text: "helloworld") {
    time
  }
  submitLogin: click(selector: "button[type=submit]") {
    time
  }
  waitForOrders: waitForSelector(selector: "table tbody tr", timeout: 10000) {
    time
  }
  orders: mapSelector(selector: "table tbody tr") {
    orderId: mapSelector(selector: "td:nth-child(1)") { innerText }
    date: mapSelector(selector: "td:nth-child(2)") { innerText }
    items: mapSelector(selector: "td:nth-child(3)") { innerText }
    total: mapSelector(selector: "td:nth-child(4)") { innerText }
    status: mapSelector(selector: "td:nth-child(5)") { innerText }
    viewLink: mapSelector(selector: "td a") {
      href: attribute(name: "href") { value }
    }
  }
}`;

const response = await fetch(
  'https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ query, variables: {}, operationName: 'BulkInvoiceDownload' }),
  }
);

const { data } = await response.json();
const orders = data.orders.map((o) => ({
  orderId: o.orderId?.[0]?.innerText ?? '',
  date: o.date?.[0]?.innerText ?? '',
  total: o.total?.[0]?.innerText ?? '',
  status: o.status?.[0]?.innerText ?? '',
  viewLink: o.viewLink?.[0]?.href?.value ?? '',
}));
console.log(JSON.stringify(orders, null, 2));
```

    **2. Check the output**

```json
[
  {
    "orderId": "ORD-001",
    "date": "2025-01-15",
    "total": "$1,250.00",
    "status": "Delivered",
    "viewLink": "/harvest-direct/vendor-portal/orders/ORD-001"
  }
]
```

    
    
**Python:**

    

    **1. Install dependencies**

```bash
pip install requests
```

    **2. Send the request**

```python
import requests

query = """
mutation BulkInvoiceDownload {
  goto(url: "https://scraping-sandbox.netlify.app/harvest-direct/vendor-portal", waitUntil: networkIdle) {
    status
  }
  waitForForm: waitForSelector(selector: "form", timeout: 10000) {
    time
  }
  typeEmail: type(selector: "input[name=email]", text: "demo@example.com") {
    time
  }
  typePassword: type(selector: "input[name=password]", text: "helloworld") {
    time
  }
  submitLogin: click(selector: "button[type=submit]") {
    time
  }
  waitForOrders: waitForSelector(selector: "table tbody tr", timeout: 10000) {
    time
  }
  orders: mapSelector(selector: "table tbody tr") {
    orderId: mapSelector(selector: "td:nth-child(1)") { innerText }
    date: mapSelector(selector: "td:nth-child(2)") { innerText }
    items: mapSelector(selector: "td:nth-child(3)") { innerText }
    total: mapSelector(selector: "td:nth-child(4)") { innerText }
    status: mapSelector(selector: "td:nth-child(5)") { innerText }
    viewLink: mapSelector(selector: "td a") {
      href: attribute(name: "href") { value }
    }
  }
}
"""

response = requests.post(
    'https://production-sfo.browserless.io/chromium/bql',
    params={'token': 'YOUR_API_TOKEN_HERE'},
    json={'query': query, 'variables': {}, 'operationName': 'BulkInvoiceDownload'},
)

data = response.json()['data']
for order in data['orders']:
    oid = order['orderId'][0]['innerText']
    date = order['date'][0]['innerText']
    total = order['total'][0]['innerText']
    status = order['status'][0]['innerText']
    print(f'{oid} | {date} | {total} | {status}')
```

    **3. Check the output**

```text
ORD-001 | 2025-01-15 | $1,250.00 | Delivered
ORD-002 | 2025-02-15 | $980.00 | Delivered
```

    
    
**Java:**

    

    **1. Send the request**

```java
import java.net.URI;
import java.net.http.*;

String token = "YOUR_API_TOKEN_HERE";
String endpoint = "https://production-sfo.browserless.io/chromium/bql?token=" + token;

String query = "mutation BulkInvoiceDownload {"
    + " goto(url: \\\"https://scraping-sandbox.netlify.app/harvest-direct/vendor-portal\\\", waitUntil: networkIdle) { status }"
    + " waitForForm: waitForSelector(selector: \\\"form\\\", timeout: 10000) { time }"
    + " typeEmail: type(selector: \\\"input[name=email]\\\", text: \\\"demo@example.com\\\") { time }"
    + " typePassword: type(selector: \\\"input[name=password]\\\", text: \\\"helloworld\\\") { time }"
    + " submitLogin: click(selector: \\\"button[type=submit]\\\") { time }"
    + " waitForOrders: waitForSelector(selector: \\\"table tbody tr\\\", timeout: 10000) { time }"
    + " orders: mapSelector(selector: \\\"table tbody tr\\\") {"
    + "   orderId: mapSelector(selector: \\\"td:nth-child(1)\\\") { innerText }"
    + "   date: mapSelector(selector: \\\"td:nth-child(2)\\\") { innerText }"
    + "   total: mapSelector(selector: \\\"td:nth-child(4)\\\") { innerText }"
    + " }"
    + " }";

String payload = "{\"query\": \"" + query + "\", \"variables\": {}, \"operationName\": \"BulkInvoiceDownload\"}";

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(endpoint))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(payload))
    .build();

HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "orders": [
      {
        "orderId": [{ "innerText": "ORD-001" }],
        "date": [{ "innerText": "2025-01-15" }],
        "total": [{ "innerText": "$1,250.00" }]
      }
    ]
  }
}
```

    
    
**C#:**

    

    **1. Send the request**

```csharp
using System.Net.Http;
using System.Text;
using System.Text.Json;

string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"https://production-sfo.browserless.io/chromium/bql?token={token}";

var payload = new
{
    query = @"mutation BulkInvoiceDownload {
      goto(url: ""https://scraping-sandbox.netlify.app/harvest-direct/vendor-portal"", waitUntil: networkIdle) { status }
      waitForForm: waitForSelector(selector: ""form"", timeout: 10000) { time }
      typeEmail: type(selector: ""input[name=email]"", text: ""demo@example.com"") { time }
      typePassword: type(selector: ""input[name=password]"", text: ""helloworld"") { time }
      submitLogin: click(selector: ""button[type=submit]"") { time }
      waitForOrders: waitForSelector(selector: ""table tbody tr"", timeout: 10000) { time }
      orders: mapSelector(selector: ""table tbody tr"") {
        orderId: mapSelector(selector: ""td:nth-child(1)"") { innerText }
        date: mapSelector(selector: ""td:nth-child(2)"") { innerText }
        total: mapSelector(selector: ""td:nth-child(4)"") { innerText }
      }
    }",
    variables = new { },
    operationName = "BulkInvoiceDownload",
};

using (HttpClient httpClient = new HttpClient())
{
    var content = new StringContent(
        JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
    var response = await httpClient.PostAsync(endpoint, content);
    string body = await response.Content.ReadAsStringAsync();
    Console.WriteLine(body);
}
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "orders": [
      {
        "orderId": [{ "innerText": "ORD-001" }],
        "date": [{ "innerText": "2025-01-15" }],
        "total": [{ "innerText": "$1,250.00" }]
      }
    ]
  }
}
```

    
  

  
  
**Frameworks:**

  Connect a headless browser to the sandbox portal, log in, navigate to each order's detail page, and download the invoice.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Connect and download**

```js
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint: 'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE',
});

try {
  const page = await browser.newPage();

  const cdpSession = await page.createCDPSession();
  await cdpSession.send('Page.setDownloadBehavior', {
    behavior: 'allow',
    downloadPath: '/tmp/invoices',
  });

  await page.goto('https://scraping-sandbox.netlify.app/harvest-direct/vendor-portal', {
    waitUntil: 'networkidle2',
  });

  await page.waitForSelector('form');
  await page.type('input[name="email"]', 'demo@example.com');
  await page.type('input[name="password"]', 'helloworld');
  await page.click('button[type="submit"]');
  await page.waitForNavigation({ waitUntil: 'networkidle2' });

  const orderLinks = await page.$$eval('table tbody tr td a', (links) =>
    links.map((a) => a.href)
  );

  for (const link of orderLinks) {
    await page.goto(link, { waitUntil: 'networkidle2' });
    const downloadBtn = await page.$('a[download]');
    if (downloadBtn) {
      await downloadBtn.click();
      await new Promise((resolve) => setTimeout(resolve, 1000));
    }
  }

  console.log(`Downloaded invoices from ${orderLinks.length} orders`);
} finally {
  await browser.close();
}
```

    **3. Check the output**

    Run with `node bulk-invoice-download.mjs`. The script logs in, visits each order's detail page, and clicks the download button.

    
    
**Playwright:**

    

    **1. Install dependencies**

```bash
npm install playwright-core
```

    **2. Connect and download**

```js
import { chromium } from 'playwright-core';

const browser = await chromium.connectOverCDP(
  'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE'
);

try {
  const context = browser.contexts()[0];
  const page = await context.newPage();

  await page.goto('https://scraping-sandbox.netlify.app/harvest-direct/vendor-portal', {
    waitUntil: 'networkidle',
  });

  await page.waitForSelector('form');
  await page.fill('input[name="email"]', 'demo@example.com');
  await page.fill('input[name="password"]', 'helloworld');
  await page.click('button[type="submit"]');
  await page.waitForLoadState('networkidle');

  const orderLinks = await page.$$eval('table tbody tr td a', (links) =>
    links.map((a) => a.href)
  );

  for (const link of orderLinks) {
    await page.goto(link, { waitUntil: 'networkidle' });
    const downloadBtn = await page.$('a[download]');
    if (downloadBtn) {
      const [download] = await Promise.all([
        page.waitForEvent('download'),
        downloadBtn.click(),
      ]);
      const path = await download.path();
      console.log(`Downloaded: ${download.suggestedFilename()} -> ${path}`);
    }
  }

  console.log(`Downloaded invoices from ${orderLinks.length} orders`);
} finally {
  await browser.close();
}
```

    **3. Check the output**

    Run with `node bulk-invoice-download.mjs`. Each download logs the filename and local path.

    
  

  
  
**BQL:**

  

  **1. Write the mutation**

  Log into the sandbox vendor portal and extract order data. This is a sandbox site, so no stealth mode is needed.

```graphql
mutation BulkInvoiceDownload {
  goto(url: "https://scraping-sandbox.netlify.app/harvest-direct/vendor-portal", waitUntil: networkIdle) {
    status
  }
  waitForForm: waitForSelector(selector: "form", timeout: 10000) {
    time
  }
  typeEmail: type(selector: "input[name=email]", text: "demo@example.com") {
    time
  }
  typePassword: type(selector: "input[name=password]", text: "helloworld") {
    time
  }
  submitLogin: click(selector: "button[type=submit]") {
    time
  }
  waitForOrders: waitForSelector(selector: "table tbody tr", timeout: 10000) {
    time
  }
  orders: mapSelector(selector: "table tbody tr") {
    orderId: mapSelector(selector: "td:nth-child(1)") { innerText }
    date: mapSelector(selector: "td:nth-child(2)") { innerText }
    items: mapSelector(selector: "td:nth-child(3)") { innerText }
    total: mapSelector(selector: "td:nth-child(4)") { innerText }
    status: mapSelector(selector: "td:nth-child(5)") { innerText }
    viewLink: mapSelector(selector: "td a") {
      href: attribute(name: "href") { value }
    }
  }
}
```

  **2. Run it**

  Paste into the [BQL IDE](https://account.browserless.io/bql) and click Run.

  **3. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForForm": { "time": 512 },
    "typeEmail": { "time": 85 },
    "typePassword": { "time": 62 },
    "submitLogin": { "time": 15 },
    "waitForOrders": { "time": 1230 },
    "orders": [
      {
        "orderId": [{ "innerText": "ORD-001" }],
        "date": [{ "innerText": "2025-01-15" }],
        "items": [{ "innerText": "3" }],
        "total": [{ "innerText": "$1,250.00" }],
        "status": [{ "innerText": "Delivered" }],
        "viewLink": [{ "href": { "value": "/harvest-direct/vendor-portal/orders/ORD-001" } }]
      },
      {
        "orderId": [{ "innerText": "ORD-002" }],
        "date": [{ "innerText": "2025-02-15" }],
        "items": [{ "innerText": "5" }],
        "total": [{ "innerText": "$980.00" }],
        "status": [{ "innerText": "Delivered" }],
        "viewLink": [{ "href": { "value": "/harvest-direct/vendor-portal/orders/ORD-002" } }]
      }
    ]
  }
}
```

  

## Next steps

  <a href="/examples/forms" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Fill and Submit a Form</h3>
      automate form interactions with BQL
    
  </a>
  <a href="/examples/screenshot" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Take a Screenshot</h3>
      capture the portal state after downloading
    
  </a>
  <a href="/examples/sessions" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Authenticated Sessions</h3>
      persist login state for portals that require authentication
    
  </a>

---

# Solving reCAPTCHAs

URL: https://docs.browserless.io/examples/captcha

# Solving reCAPTCHAs

Automatically detect and solve reCAPTCHA v2, v3, invisible, and other CAPTCHA challenges during browser automation.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

  
**BQL via HTTP:**

  Use BrowserQL over HTTP to navigate a CAPTCHA page and solve it in a single mutation.

  
    
**cURL:**

    

    **1. Build the BrowserQL mutation**

    Use the BQL `solve` mutation, which auto-detects and solves any CAPTCHA on the current page:

```bash
https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE
```

    **2. Send the request**

```bash
curl -X POST \
  "https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "mutation SolveCaptcha { goto(url: \"https://www.google.com/recaptcha/api2/demo\") { status } solve { found solved time token } submit: click(selector: \"#recaptcha-demo-submit\") { time } }"
  }'
```

    **3. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "solve": { "found": true, "solved": true, "time": 4800, "token": "03AGdBq..." },
    "submit": { "time": 95 }
  }
}
```

    
    
**JavaScript:**

    

    **1. Send the request**

```js
const response = await fetch(
  'https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      query: `mutation SolveCaptcha {
        goto(url: "https://www.google.com/recaptcha/api2/demo") {
          status
        }
        solve {
          found
          solved
          time
          token
        }
        submit: click(selector: "#recaptcha-demo-submit") {
          time
        }
      }`,
      operationName: 'SolveCaptcha',
    }),
  }
);

const result = await response.json();
console.log(result.data.solve);
// { found: true, solved: true, time: 4800, token: '03AGdBq...' }
```

    **2. Check the output**

    Run with `node captcha.mjs`. The `solve` field confirms whether the CAPTCHA was found and solved.

    
    
**Python:**

    

    **1. Install dependencies**

```bash
pip install requests
```

    **2. Send the request**

```python
import requests

query = """
mutation SolveCaptcha {
  goto(url: "https://www.google.com/recaptcha/api2/demo") {
    status
  }
  solve {
    found
    solved
    time
    token
  }
  submit: click(selector: "#recaptcha-demo-submit") {
    time
  }
}
"""

response = requests.post(
    'https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE',
    json={'query': query, 'operationName': 'SolveCaptcha'},
)

result = response.json()
print(result['data']['solve'])
# {'found': True, 'solved': True, 'time': 4800, 'token': '03AGdBq...'}
```

    **3. Check the output**

    Run with `python captcha.py`. The `solve` key confirms whether the CAPTCHA was found and solved.

    
  

  
  
**Frameworks:**

  Use a browser connection with `solveCaptchas=true` in the WebSocket URL to solve CAPTCHAs during automation.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Connect with CAPTCHA solving enabled**

    Add `solveCaptchas=true` to the WebSocket URL to enable automatic background CAPTCHA solving:

```js
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint:
    'wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us&solveCaptchas=true&timeout=300000',
});

try {
  const page = await browser.newPage();
  const cdp = await page.createCDPSession();

  // Register before navigation so the event isn't missed if the CAPTCHA solves
  // immediately after the page loads.
  const captchaSolved = new Promise((resolve) =>
    cdp.on('Browserless.captchaAutoSolved', resolve)
  );

  await page.goto('https://www.google.com/recaptcha/api2/demo', {
    waitUntil: 'networkidle2',
  });

  // Await the CDP event rather than a fixed timeout — fires when Browserless
  // finishes solving the CAPTCHA, not after an arbitrary number of seconds.
  // Race against a timeout so the script doesn't hang on pages with no CAPTCHA.
  await Promise.race([
    captchaSolved,
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error('CAPTCHA timeout')), 30000)
    ),
  ]);

  await page.click('#recaptcha-demo-submit');
  await page.waitForNavigation({ waitUntil: 'networkidle2' });

  console.log('Done. Final URL:', page.url());
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Run with `node captcha.mjs`. Browserless detects and solves the reCAPTCHA in the background — you don't need to do anything else.

    
    
**Playwright:**

    

    **1. Install dependencies**

```bash
npm install playwright-core
```

    **2. Connect with CAPTCHA solving enabled**

```js
import { chromium } from 'playwright-core';

const browser = await chromium.connectOverCDP(
  'wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us&solveCaptchas=true&timeout=300000'
);

try {
  // Reuse the existing page from the default context so Browserless CDP events
  // (including captchaAutoSolved) are visible on this page object.
  const context = browser.contexts()[0];
  const page = context.pages()[0];
  const cdp = await page.context().newCDPSession(page);

  // Register before navigation so the event isn't missed if the CAPTCHA solves
  // immediately after the page loads.
  const captchaSolved = new Promise((resolve) =>
    cdp.on('Browserless.captchaAutoSolved', resolve)
  );

  await page.goto('https://www.google.com/recaptcha/api2/demo', {
    waitUntil: 'networkidle',
  });

  // Await the CDP event rather than a fixed timeout — fires when Browserless
  // finishes solving the CAPTCHA, not after an arbitrary number of seconds.
  // Race against a timeout so the script doesn't hang on pages with no CAPTCHA.
  await Promise.race([
    captchaSolved,
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error('CAPTCHA timeout')), 30000)
    ),
  ]);

  await page.click('#recaptcha-demo-submit');
  await page.waitForLoadState('networkidle');

  console.log('Done. Final URL:', page.url());
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Run with `node captcha.mjs`. Browserless automatically detects and solves the reCAPTCHA before you click submit.

    
    
**Python:**

    

    **1. Install dependencies**

```bash
pip install playwright
```

    **2. Connect with CAPTCHA solving enabled**

```python
import asyncio
from playwright.async_api import async_playwright

WS_ENDPOINT = (
    'wss://production-sfo.browserless.io/stealth'
    '?token=YOUR_API_TOKEN_HERE'
    '&proxy=residential&proxyCountry=us'
    '&solveCaptchas=true&timeout=300000'
)

async def main():
    async with async_playwright() as playwright:
        # connect_over_cdp gives a stable CDP session on the remote browser.
        browser = await playwright.chromium.connect_over_cdp(WS_ENDPOINT)
        try:
            # Reuse the existing page from the default context so Browserless CDP
            # events (including captchaAutoSolved) are visible on this page object.
            context = browser.contexts[0]
            page = context.pages[0]
            cdp = await page.context.new_cdp_session(page)

            # Register before navigation so the event isn't missed if the CAPTCHA
            # solves immediately after the page loads.
            captcha_solved: asyncio.Future = asyncio.get_event_loop().create_future()
            # Guard against set_result being called twice if the event fires more than once.
            cdp.on('Browserless.captchaAutoSolved',
                lambda event: captcha_solved.set_result(event) if not captcha_solved.done() else None
            )

            await page.goto(
                'https://www.google.com/recaptcha/api2/demo',
                wait_until='networkidle',
            )

            # Await the CDP event rather than a fixed timeout — fires when Browserless
            # finishes solving the CAPTCHA, not after an arbitrary number of seconds.
            # Race against a timeout so the script doesn't hang on pages with no CAPTCHA.
            await asyncio.wait_for(captcha_solved, timeout=30)

            await page.click('#recaptcha-demo-submit')
            await page.wait_for_load_state('networkidle')

            print('Done. Final URL:', page.url)
        finally:
            # Always close to release the session even on error.
            await browser.close()

asyncio.run(main())
```

    **3. Check the output**

    Run with `python captcha.py`. Browserless detects and solves the reCAPTCHA in the background.

    
    
**Java:**

    

    **1. Add dependencies**

```xml
<dependency>
  <groupId>com.microsoft.playwright</groupId>
  <artifactId>playwright</artifactId>
  <version>1.44.0</version>
</dependency>
```

    **2. Connect with CAPTCHA solving enabled**

```java
import com.microsoft.playwright.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

public class CaptchaExample {
    public static void main(String[] args) throws Exception {
        String wsEndpoint =
            "wss://production-sfo.browserless.io/stealth"
            + "?token=YOUR_API_TOKEN_HERE"
            + "&proxy=residential&proxyCountry=us"
            + "&solveCaptchas=true&timeout=300000";

        try (Playwright playwright = Playwright.create()) {
            Browser browser = playwright.chromium().connectOverCDP(wsEndpoint);
            try {
                // Reuse the existing page from the default context so Browserless CDP
                // events (including captchaAutoSolved) are visible on this page object.
                BrowserContext context = browser.contexts().get(0);
                Page page = context.pages().get(0);
                CDPSession cdp = page.context().newCDPSession(page);

                // Register before navigation so the event isn't missed if the CAPTCHA
                // solves immediately after the page loads.
                CompletableFuture captchaSolved = new CompletableFuture<>();
                cdp.on("Browserless.captchaAutoSolved", event -> captchaSolved.complete(null));

                page.navigate("https://www.google.com/recaptcha/api2/demo",
                    new Page.NavigateOptions().setWaitUntil(WaitUntilState.NETWORKIDLE));

                // Await the CDP event rather than a fixed timeout — fires when Browserless
                // finishes solving the CAPTCHA, not after an arbitrary number of seconds.
                captchaSolved.get(30, TimeUnit.SECONDS);

                page.click("#recaptcha-demo-submit");
                page.waitForLoadState(LoadState.NETWORKIDLE);

                System.out.println("Done. Final URL: " + page.url());
            } finally {
                // Always close to release the session even on error.
                browser.close();
            }
        }
    }
}
```

    **3. Check the output**

    Run with `mvn exec:java`. Browserless detects and solves the reCAPTCHA in the background.

    
    
**C#:**

    

    **1. Add dependencies**

```bash
dotnet add package Microsoft.Playwright
```

    **2. Connect with CAPTCHA solving enabled**

```csharp
using Microsoft.Playwright;

const string WsEndpoint =
    "wss://production-sfo.browserless.io/stealth"
    + "?token=YOUR_API_TOKEN_HERE"
    + "&proxy=residential&proxyCountry=us"
    + "&solveCaptchas=true&timeout=300000";

using var playwright = await Playwright.CreateAsync();
var browser = await playwright.Chromium.ConnectOverCDPAsync(WsEndpoint);

try
{
    // Reuse the existing page from the default context so Browserless CDP
    // events (including captchaAutoSolved) are visible on this page object.
    var context = browser.Contexts[0];
    var page = context.Pages[0];
    var cdp = await page.Context.NewCDPSessionAsync(page);

    // Register before navigation so the event isn't missed if the CAPTCHA
    // solves immediately after the page loads.
    var captchaSolved = new TaskCompletionSource<bool>();
    cdp.On("Browserless.captchaAutoSolved", e => captchaSolved.TrySetResult(e));

    await page.GotoAsync(
        "https://www.google.com/recaptcha/api2/demo",
        new PageGotoOptions { WaitUntil = WaitUntilState.NetworkIdle }
    );

    // Await the CDP event rather than a fixed timeout — fires when Browserless
    // finishes solving the CAPTCHA, not after an arbitrary number of seconds.
    // Compare completed task to timeoutTask so the exception is observed and thrown.
    var timeoutTask = Task.Delay(TimeSpan.FromSeconds(30));
    var completed = await Task.WhenAny(captchaSolved.Task, timeoutTask);
    if (completed == timeoutTask)
        throw new TimeoutException("CAPTCHA timeout");

    await page.ClickAsync("#recaptcha-demo-submit");
    await page.WaitForLoadStateAsync(LoadState.NetworkIdle);

    Console.WriteLine($"Done. Final URL: {page.Url}");
}
finally
{
    // Always close to release the session even on error.
    await browser.CloseAsync();
}
```

    **3. Check the output**

    Run with `dotnet run`. Browserless detects and solves the reCAPTCHA in the background.

    
    
**BAP:**

    **1. Install dependencies**

```bash
npm install @browserless.io/bap-ts
```

    **2. Connect and solve**

```typescript
import Browserless from '@browserless.io/bap-ts';

const browser = Browserless.connect({
  browserWSEndpoint: 'wss://production-sfo.browserless.io/chromium/bql',
  token: 'YOUR_API_TOKEN_HERE',
});

try {
  const page = await browser.newPage();
  await page.goto('https://www.google.com/recaptcha/api2/demo');

  const { found, solved, token } = await page.solve({ type: 'recaptcha' });
  console.log({ found, solved, token });

  await page.click('#recaptcha-demo-submit');
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Browserless supports reCAPTCHA v2, v3, invisible, Cloudflare Turnstile, GeeTest, and more via the `type` option.

    
  

  
  
**BQL:**

  

  **1. Write the mutation**

  The `solve` mutation auto-detects any CAPTCHA type. Pass `type: recaptcha` to target reCAPTCHA specifically, or omit it for auto-detection:

```graphql
mutation SolveCaptcha {
  goto(url: "https://www.google.com/recaptcha/api2/demo") {
    status
  }

  solve(type: recaptcha) {
    found
    solved
    time
    token
  }

  submit: click(selector: "#recaptcha-demo-submit") {
    time
  }
}
```

  Browserless supports reCAPTCHA v2, v3, invisible, Cloudflare Turnstile, GeeTest, and more via the `type` argument.

  **2. Run it**

  Paste into the [BQL IDE](https://account.browserless.io/bql) and click Run, or send via HTTP (see the cURL tab).

  **3. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "solve": { "found": true, "solved": true, "time": 4800, "token": "03AGdBq..." },
    "submit": { "time": 95 }
  }
}
```

  

## Next steps

  <a href="/examples/cloudflare" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Solving Cloudflare Challenges</h3>
      bypass Cloudflare Turnstile and JS challenges
    
  </a>
  <a href="/examples/forms" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Fill and Submit a Form</h3>
      automate complete form flows including CAPTCHA steps
    
  </a>

---

# Close a browser session

URL: https://docs.browserless.io/examples/close-session

# Close a browser session

Fully terminate a remote browser session and release all resources by calling `browser.close()` or sending a DELETE request to the session stop endpoint.

> **Disconnect without closing**
> Want to detach from a browser without terminating it so you can reconnect later? See [Disconnect and Reconnect to a Browser](/examples/reconnect) for the graceful disconnect pattern.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

Closing a session shuts down the browser process and frees compute resources. Use `browser.close()` in framework code, or `DELETE /session/{id}` for REST-managed sessions.

  
**REST API:**

  Use the Session API to create a session, then explicitly terminate it with a DELETE request when finished.

  
    
**cURL:**

    

    **1. Create a session**

```bash
curl -X POST "https://production-sfo.browserless.io/session?token=YOUR_API_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "ttl": 60000,
    "stealth": true
  }'
```

    The response includes a `stop` field with the DELETE URL:

```json
{
  "id": "d2f5c3a1-8b4e-4c7a-9e6d-1a2b3c4d5e6f",
  "browserQL": "https://production-sfo.browserless.io/chromium/bql?session=d2f5c3a1...",
  "browserWSEndpoint": "wss://production-sfo.browserless.io/chromium?session=d2f5c3a1...",
  "stop": "https://production-sfo.browserless.io/session/d2f5c3a1...?token=YOUR_API_TOKEN_HERE"
}
```

    **2. Close the session**

```bash
curl -X DELETE "STOP_URL_FROM_STEP_1&force=true"
```

    The `force=true` parameter ensures the session is terminated immediately, even if a browser is still connected.

    
    
**JavaScript:**

    

    **1. Create a session and do work**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";

const sessionResponse = await fetch(
  `https://production-sfo.browserless.io/session?token=${TOKEN}`,
  {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ ttl: 60000, stealth: true }),
  }
);
const session = await sessionResponse.json();
console.log("Session created:", session.id);
```

    **2. Close the session**

```js
const closeResponse = await fetch(`${session.stop}&force=true`, {
  method: "DELETE",
});
console.log("Session closed:", closeResponse.status === 200 ? "success" : "failed");
```

    **3. Check the output**

```text
Session created: d2f5c3a1-8b4e-4c7a-9e6d-1a2b3c4d5e6f
Session closed: success
```

    
    
**Python:**

    

    **1. Install dependencies**

```bash
pip install requests
```

    **2. Create a session and do work**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"

session_response = requests.post(
    f"https://production-sfo.browserless.io/session?token={TOKEN}",
    json={"ttl": 60000, "stealth": True},
)
session = session_response.json()
print("Session created:", session["id"])
```

    **3. Close the session**

```python
close_response = requests.delete(f"{session['stop']}&force=true")
print("Session closed:", "success" if close_response.ok else "failed")
```

    **4. Check the output**

```text
Session created: d2f5c3a1-8b4e-4c7a-9e6d-1a2b3c4d5e6f
Session closed: success
```

    
    
**Java:**

    

    **1. Create a session**

```java
import java.net.URI;
import java.net.http.*;

String token = "YOUR_API_TOKEN_HERE";
String endpoint = "https://production-sfo.browserless.io/session?token=" + token;

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(endpoint))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(
        "{\"ttl\": 60000, \"stealth\": true}"
    ))
    .build();

HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
String body = response.body();
System.out.println("Session created: " + body);

// Parse stop URL from response
int stopStart = body.indexOf("\"stop\":\"") + 8;
int stopEnd = body.indexOf("\"", stopStart);
String stopUrl = body.substring(stopStart, stopEnd);
```

    **2. Close the session**

```java
HttpRequest deleteRequest = HttpRequest.newBuilder()
    .uri(URI.create(stopUrl + "&force=true"))
    .DELETE()
    .build();

HttpResponse deleteResponse = client.send(
    deleteRequest, HttpResponse.BodyHandlers.ofString()
);
System.out.println("Session closed: " + deleteResponse.statusCode());
```

    **3. Check the output**

```text
Session created: {"id":"d2f5c3a1-...","stop":"https://...",...}
Session closed: 200
```

    
    
**C#:**

    

    **1. Create a session**

```csharp
using System.Net.Http;
using System.Text;
using System.Text.Json;

var token = "YOUR_API_TOKEN_HERE";
var client = new HttpClient();

var content = new StringContent(
    JsonSerializer.Serialize(new { ttl = 60000, stealth = true }),
    Encoding.UTF8,
    "application/json"
);

var response = await client.PostAsync(
    $"https://production-sfo.browserless.io/session?token={token}",
    content
);
var body = await response.Content.ReadAsStringAsync();
var session = JsonSerializer.Deserialize(body);

var stopUrl = session.GetProperty("stop").GetString();
Console.WriteLine($"Session created: {session.GetProperty("id")}");
```

    **2. Close the session**

```csharp
var closeResponse = await client.DeleteAsync($"{stopUrl}&force=true");
Console.WriteLine($"Session closed: {closeResponse.StatusCode}");
```

    **3. Check the output**

```text
Session created: d2f5c3a1-8b4e-4c7a-9e6d-1a2b3c4d5e6f
Session closed: OK
```

    
  

  
  
**Frameworks:**

  Call `browser.close()` to fully terminate the remote browser process and release the Browserless session.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Connect, do work, and close**

```js
import puppeteer from "puppeteer-core";

const TOKEN = "YOUR_API_TOKEN_HERE";
const browser = await puppeteer.connect({
  browserWSEndpoint: `wss://production-sfo.browserless.io?token=${TOKEN}`,
});

try {
  const page = await browser.newPage();
  await page.goto("https://scraping-sandbox.netlify.app/products", { waitUntil: "networkidle2" });

  const title = await page.title();
  console.log("Page title:", title);
} finally {
  await browser.close();
  console.log("Browser closed — session resources released.");
}
```

    **3. Check the output**

```text
Page title: Product Catalog | Scraping Sandbox
Browser closed — session resources released.
```

    `browser.close()` terminates the remote browser process. After calling it, the WebSocket connection is severed and the session cannot be reconnected.

    
    
**Playwright:**

    

    **1. Install dependencies**

```bash
npm install playwright-core
```

    **2. Connect, do work, and close**

```js
import { chromium } from "playwright-core";

const TOKEN = "YOUR_API_TOKEN_HERE";
const browser = await chromium.connectOverCDP(
  `wss://production-sfo.browserless.io?token=${TOKEN}&stealth`
);

try {
  const context = browser.contexts()[0] ?? await browser.newContext();
  const page = await context.newPage();
  await page.goto("https://scraping-sandbox.netlify.app/products", { waitUntil: "networkidle" });

  const title = await page.title();
  console.log("Page title:", title);
} finally {
  await browser.close();
  console.log("Browser closed — session resources released.");
}
```

    **3. Check the output**

```text
Page title: Product Catalog | Scraping Sandbox
Browser closed — session resources released.
```

    `browser.close()` shuts down the remote Chromium process. Use this instead of `browser.disconnect()` when you are done and do not need to reconnect.

    
    
**BAP:**

    **1. Install dependencies**

```bash
npm install @browserless.io/bap-ts
```

    **2. Connect and extract**

```typescript
import Browserless from '@browserless.io/bap-ts';

const browser = Browserless.connect({
  browserWSEndpoint: 'wss://production-sfo.browserless.io/chromium/bql',
  token: 'YOUR_API_TOKEN_HERE',
});

try {
  const page = await browser.newPage();
  await page.goto('https://scraping-sandbox.netlify.app/products', { waitUntil: 'networkIdle' });

  const { text } = await page.text({ selector: 'h1' });
  console.log(text);
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    `browser.close()` in the `finally` block ends the session, so there is no stop URL to call separately.

    
  

  
  
**BQL:**

  

  BQL sessions are automatically closed when the mutation completes. For Session API sessions using BQL, terminate them with a DELETE request to the stop URL.

  **1. Create a session with a BQL endpoint**

```bash
curl -X POST "https://production-sfo.browserless.io/session?token=YOUR_API_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{"ttl": 60000, "stealth": true}'
```

  Save the `browserQL` and `stop` fields from the response.

  **2. Run a BQL mutation against the session**

```graphql
mutation CloseExample {
  goto(url: "https://scraping-sandbox.netlify.app/products", waitUntil: networkIdle) {
    status
  }
  text(selector: "h1") {
    text
  }
}
```

  **3. Send it**

  Paste into the [BQL IDE](https://account.browserless.io/bql) and click Run.

  **4. Close the session**

```bash
curl -X DELETE "STOP_URL_FROM_STEP_1&force=true"
```

  Without this explicit DELETE, the session remains alive until its TTL expires and continues consuming resources.

  **5. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "text": { "text": "Product Catalog" }
  }
}
```

---

# Solving Cloudflare challenges

URL: https://docs.browserless.io/examples/cloudflare

# Solving Cloudflare challenges

Bypass Cloudflare Turnstile and JS challenges to access protected pages using the Browserless `/unblock` endpoint or BQL's `solve(type: cloudflare)` mutation.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

  
**REST API:**

  Use the `/unblock` REST endpoint to bypass Cloudflare challenges and retrieve page content. No WebSocket connection needed.

  
    
**cURL:**

    

    **1. Build the request**

    Use the `/unblock` endpoint with `proxy=residential`. Cloudflare's bot detection is significantly harder to bypass from datacenter IPs, so residential proxies have a much higher success rate:

```bash
https://production-sfo.browserless.io/unblock?token=YOUR_API_TOKEN_HERE&proxy=residential
```

    **2. Send the request**

```bash
curl -X POST \
  "https://production-sfo.browserless.io/unblock?token=YOUR_API_TOKEN_HERE&proxy=residential" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example-cloudflare-protected.com",
    "content": true,
    "cookies": false,
    "screenshot": false,
    "browserWSEndpoint": false
  }'
```

    **3. Check the output**

    The response contains the fully unblocked page HTML:

```json
{
  "content": "<!DOCTYPE html><html>...</html>",
  "cookies": [],
  "screenshot": null,
  "browserWSEndpoint": null
}
```

    Set `"screenshot": true` or `"cookies": true` to include those in the response as well.

    
    
**JavaScript:**

    

    **1. Send the request**

```js
const response = await fetch(
  'https://production-sfo.browserless.io/unblock?token=YOUR_API_TOKEN_HERE&proxy=residential',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      url: 'https://example-cloudflare-protected.com',
      content: true,
      cookies: false,
      screenshot: false,
      browserWSEndpoint: false,
    }),
  }
);

const { content } = await response.json();
console.log(content.slice(0, 500));
```

    **2. Check the output**

    Run with `node unblock.mjs`. The `content` field contains the fully rendered page HTML.

    
    
**Python:**

    

    **1. Install dependencies**

```bash
pip install requests
```

    **2. Send the request**

```python
import requests

response = requests.post(
    'https://production-sfo.browserless.io/unblock?token=YOUR_API_TOKEN_HERE&proxy=residential',
    json={
        'url': 'https://example-cloudflare-protected.com',
        'content': True,
        'cookies': False,
        'screenshot': False,
        'browserWSEndpoint': False,
    },
)

data = response.json()
print(data['content'][:500])
```

    **3. Check the output**

    Run with `python unblock.py`. The `content` field contains the fully rendered page HTML.

    
    
**Java:**

    

    **1. Install dependencies**

    Add Gson to your `pom.xml` for JSON parsing (`java.net.http.HttpClient` ships with the JDK):

```xml
<dependency>
  <groupId>com.google.code.gson</groupId>
  <artifactId>gson</artifactId>
  <version>2.10.1</version>
</dependency>
```

    **2. Send the request**

```java
import com.google.gson.JsonParser;
import java.net.URI;
import java.net.http.*;

public class CloudflareUnblock {
    public static void main(String[] args) throws Exception {
        String token = "YOUR_API_TOKEN_HERE";
        HttpClient client = HttpClient.newHttpClient();

        String body = "{\"url\":\"https://example-cloudflare-protected.com\","
            + "\"content\":true,\"cookies\":false,\"screenshot\":false,\"browserWSEndpoint\":false}";

        HttpRequest req = HttpRequest.newBuilder()
            .uri(URI.create("https://production-sfo.browserless.io/unblock?token=" + token + "&proxy=residential"))
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();

        HttpResponse res = client.send(req, HttpResponse.BodyHandlers.ofString());
        String content = JsonParser.parseString(res.body()).getAsJsonObject()
            .get("content").getAsString();
        System.out.println(content.substring(0, Math.min(500, content.length())));
    }
}
```

    **3. Check the output**

    Compile and run with `mvn exec:java`. The `content` field contains the fully rendered page HTML.

    
    
**C#:**

    

    **1. Dependencies**

    `System.Net.Http.HttpClient` and `System.Text.Json` are part of the .NET standard library.

    **2. Send the request**

```csharp
using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

class CloudflareUnblock
{
    static async Task Main()
    {
        const string token = "YOUR_API_TOKEN_HERE";
        using var client = new HttpClient();

        var body = new StringContent(
            "{\"url\":\"https://example-cloudflare-protected.com\","
            + "\"content\":true,\"cookies\":false,\"screenshot\":false,\"browserWSEndpoint\":false}",
            Encoding.UTF8, "application/json");

        var res = await client.PostAsync(
            $"https://production-sfo.browserless.io/unblock?token={token}&proxy=residential",
            body);

        using var json = JsonDocument.Parse(await res.Content.ReadAsStringAsync());
        string content = json.RootElement.GetProperty("content").GetString();
        Console.WriteLine(content[..Math.Min(500, content.Length)]);
    }
}
```

    **3. Check the output**

    Run with `dotnet run`. The `content` field contains the fully rendered page HTML.

    
    
**Go:**

    

    **1. Dependencies**

    `encoding/json` and `net/http` are part of Go's standard library.

    **2. Send the request**

```go
package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
)

type UnblockResponse struct {
    Content string `json:"content"`
}

func main() {
    token := "YOUR_API_TOKEN_HERE"
    body := `{"url":"https://example-cloudflare-protected.com","content":true,"cookies":false,"screenshot":false,"browserWSEndpoint":false}`

    req, _ := http.NewRequest("POST",
        "https://production-sfo.browserless.io/unblock?token="+token+"&proxy=residential",
        bytes.NewBufferString(body),
    )
    req.Header.Set("Content-Type", "application/json")

    resp, err := http.DefaultClient.Do(req)
    if err != nil { panic(err) }
    defer resp.Body.Close()

    data, _ := io.ReadAll(resp.Body)
    var result UnblockResponse
    json.Unmarshal(data, &result)

    end := len(result.Content)
    if end > 500 { end = 500 }
    fmt.Println(result.Content[:end])
}
```

    **3. Check the output**

    Run with `go run main.go`. The `content` field contains the fully rendered page HTML.

    
    
**PHP:**

    

    **1. Dependencies**

    This example uses PHP's built-in `curl`. No Composer packages needed.

    **2. Send the request**

```php
<?php

$token = 'YOUR_API_TOKEN_HERE';

$ch = curl_init("https://production-sfo.browserless.io/unblock?token={$token}&proxy=residential");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
    CURLOPT_POSTFIELDS => json_encode([
        'url' => 'https://example-cloudflare-protected.com',
        'content' => true,
        'cookies' => false,
        'screenshot' => false,
        'browserWSEndpoint' => false,
    ]),
    CURLOPT_RETURNTRANSFER => true,
]);
$body = curl_exec($ch);
curl_close($ch);

$data = json_decode($body, true);
echo substr($data['content'], 0, 500) . "\n";
```

    **3. Check the output**

    Run with `php unblock.php`. The `content` field contains the fully rendered page HTML.

    
    
**Ruby:**

    

    **1. Dependencies**

    `net/http` and `json` are part of Ruby's standard library.

    **2. Send the request**

```ruby
require 'net/http'
require 'json'
require 'uri'

TOKEN = 'YOUR_API_TOKEN_HERE'

uri = URI("https://production-sfo.browserless.io/unblock?token=#{TOKEN}&proxy=residential")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true

request = Net::HTTP::Post.new(uri)
request['Content-Type'] = 'application/json'
request.body = JSON.generate({
  url: 'https://example-cloudflare-protected.com',
  content: true,
  cookies: false,
  screenshot: false,
  browserWSEndpoint: false,
})

response = http.request(request)
data = JSON.parse(response.body)
puts data['content'][0, 500]
```

    **3. Check the output**

    Run with `ruby unblock.rb`. The `content` field contains the fully rendered page HTML.

    
  

  
  
**Frameworks:**

  Use a browser connection: request a `browserWSEndpoint` from `/unblock`, then connect to the already-unblocked session. The `ttl` parameter keeps the browser alive long enough for your client to connect.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Unblock and connect**

    Request a `browserWSEndpoint` from `/unblock`, then connect Puppeteer to the already-unblocked session:

```js
import puppeteer from 'puppeteer-core';

const TOKEN = 'YOUR_API_TOKEN_HERE';

const { browserWSEndpoint } = await fetch(
  `https://production-sfo.browserless.io/unblock?token=${TOKEN}&proxy=residential`,
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      url: 'https://example-cloudflare-protected.com',
      browserWSEndpoint: true,
      ttl: 30000,
    }),
  }
).then((r) => r.json());

// The /unblock response returns a raw WebSocket URL — append the token before connecting.
const browser = await puppeteer.connect({
  browserWSEndpoint: `${browserWSEndpoint}?token=${TOKEN}`,
});

try {
  const [page] = await browser.pages();
  console.log('Title:', await page.title());
  console.log('URL:', page.url());
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Run with `node unblock.mjs`. The browser is already past the Cloudflare check when Puppeteer connects.

    
    
**Playwright:**

    
      
**JavaScript:**

      

      **1. Install dependencies**

```bash
npm install playwright-core
```

      **2. Unblock and connect**

```js
import { chromium } from 'playwright-core';

const TOKEN = 'YOUR_API_TOKEN_HERE';

const { browserWSEndpoint } = await fetch(
  `https://production-sfo.browserless.io/unblock?token=${TOKEN}&proxy=residential`,
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      url: 'https://example-cloudflare-protected.com',
      browserWSEndpoint: true,
      ttl: 30000,
    }),
  }
).then((r) => r.json());

// The /unblock response returns a raw WebSocket URL — append the token before connecting.
const browser = await chromium.connectOverCDP(`${browserWSEndpoint}?token=${TOKEN}`);

try {
  const context = browser.contexts()[0];
  const page = context.pages()[0];
  console.log('Title:', await page.title());
  console.log('URL:', page.url());
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

      **3. Check the output**

      Run with `node unblock.mjs`. The browser is already past the Cloudflare check when Playwright connects.

      
      
**Python:**

      

      **1. Install dependencies**

```bash
pip install requests playwright
```

      **2. Unblock and connect**

```python
import requests
from playwright.sync_api import sync_playwright

TOKEN = 'YOUR_API_TOKEN_HERE'

res = requests.post(
    f'https://production-sfo.browserless.io/unblock?token={TOKEN}&proxy=residential',
    json={
        'url': 'https://example-cloudflare-protected.com',
        'browserWSEndpoint': True,
        'ttl': 30000,
    },
)
# The /unblock response returns a raw WebSocket URL — append the token before connecting.
ws_endpoint = res.json()['browserWSEndpoint']

with sync_playwright() as p:
    # connect_over_cdp exposes the pre-existing browser context with the already-unblocked page.
    browser = p.chromium.connect_over_cdp(f"{ws_endpoint}?token={TOKEN}")
    try:
        context = browser.contexts[0]
        page = context.pages[0]
        print('Title:', page.title())
        print('URL:', page.url)
    finally:
        # Always close to release the session even on error.
        browser.close()
```

      **3. Check the output**

      Run with `python unblock.py`. The browser is already past the Cloudflare check when Playwright connects.

      
      
**Java:**

      

      **1. Install dependencies**

      Add Playwright and Gson to your `pom.xml`:

```xml
<dependency>
  <groupId>com.microsoft.playwright</groupId>
  <artifactId>playwright</artifactId>
  <version>1.44.0</version>
</dependency>
<dependency>
  <groupId>com.google.code.gson</groupId>
  <artifactId>gson</artifactId>
  <version>2.10.1</version>
</dependency>
```

      **2. Unblock and connect**

```java
import com.google.gson.JsonParser;
import com.microsoft.playwright.*;
import java.net.URI;
import java.net.http.*;

public class CloudflareUnblock {
    public static void main(String[] args) throws Exception {
        String TOKEN = "YOUR_API_TOKEN_HERE";
        HttpClient client = HttpClient.newHttpClient();

        String body = "{\"url\":\"https://example-cloudflare-protected.com\","
            + "\"browserWSEndpoint\":true,\"ttl\":30000}";
        HttpRequest req = HttpRequest.newBuilder()
            .uri(URI.create("https://production-sfo.browserless.io/unblock?token=" + TOKEN + "&proxy=residential"))
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();
        HttpResponse httpRes = client.send(req, HttpResponse.BodyHandlers.ofString());

        // The /unblock response returns a raw WebSocket URL — append the token before connecting.
        String wsEndpoint = JsonParser.parseString(httpRes.body()).getAsJsonObject()
            .get("browserWSEndpoint").getAsString() + "?token=" + TOKEN;

        try (Playwright playwright = Playwright.create()) {
            Browser browser = playwright.chromium().connectOverCDP(wsEndpoint);
            try {
                BrowserContext context = browser.contexts().get(0);
                Page page = context.pages().get(0);
                System.out.println("Title: " + page.title());
                System.out.println("URL: " + page.url());
            } finally {
                // Always close to release the session even on error.
                browser.close();
            }
        }
    }
}
```

      **3. Check the output**

      Compile and run with `mvn exec:java`. The browser is already past the Cloudflare check when Playwright connects.

      
      
**C#:**

      

      **1. Install dependencies**

```bash
dotnet add package Microsoft.Playwright
```

      **2. Unblock and connect**

```csharp
using Microsoft.Playwright;
using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

class CloudflareUnblock
{
    static async Task Main()
    {
        const string TOKEN = "YOUR_API_TOKEN_HERE";
        using var httpClient = new HttpClient();

        var body = new StringContent(
            "{\"url\":\"https://example-cloudflare-protected.com\",\"browserWSEndpoint\":true,\"ttl\":30000}",
            Encoding.UTF8, "application/json");
        var res = await httpClient.PostAsync(
            $"https://production-sfo.browserless.io/unblock?token={TOKEN}&proxy=residential",
            body);

        using var json = JsonDocument.Parse(await res.Content.ReadAsStringAsync());
        // The /unblock response returns a raw WebSocket URL — append the token before connecting.
        string wsEndpoint = json.RootElement.GetProperty("browserWSEndpoint").GetString()
            + $"?token={TOKEN}";

        using var playwright = await Playwright.CreateAsync();
        var browser = await playwright.Chromium.ConnectOverCDPAsync(wsEndpoint);
        try
        {
            var context = browser.Contexts[0];
            var page = context.Pages[0];
            Console.WriteLine($"Title: {await page.TitleAsync()}");
            Console.WriteLine($"URL: {page.Url}");
        }
        finally
        {
            // Always close to release the session even on error.
            await browser.CloseAsync();
        }
    }
}
```

      **3. Check the output**

      Run with `dotnet run`. The browser is already past the Cloudflare check when Playwright connects.

      
    

    
    
**BAP:**

    **1. Install dependencies**

```bash
npm install @browserless.io/bap-ts
```

    **2. Connect and solve**

```typescript
import Browserless from '@browserless.io/bap-ts';

const browser = Browserless.connect({
  browserWSEndpoint: 'wss://production-sfo.browserless.io/stealth/bql',
  token: 'YOUR_API_TOKEN_HERE',
});

try {
  const page = await browser.newPage();
  await page.goto('https://example-cloudflare-protected.com', { waitUntil: 'networkIdle' });

  const { found, solved } = await page.solve({ type: 'cloudflare' });
  console.log({ found, solved, title: await page.title() });
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Note the `/stealth/bql` endpoint — the managed stealth browser is what clears the challenge.

    
  

  
  
**BQL:**

  

  **1. Write the mutation**

  Use the `stealth` BQL endpoint and chain `goto` with `solve(type: cloudflare)`:

```graphql
mutation UnblockCloudflare {
  goto(url: "https://example-cloudflare-protected.com", waitUntil: networkIdle) {
    status
  }

  solve(type: cloudflare) {
    found
    solved
    time
  }

  title {
    title
  }
}
```

  Send this to the `/stealth/bql` endpoint:

```http
POST https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE
```

  **2. Run it**

  Paste into the [BQL IDE](https://account.browserless.io/bql) (switch to the `stealth` browser), or send via HTTP (see the cURL tab).

  **3. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "solve": { "found": true, "solved": true, "time": 12340 },
    "title": { "title": "Welcome to Example Site" }
  }
}
```

  

## Next steps

  <a href="/examples/captcha" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Solving reCAPTCHAs</h3>
      solve CAPTCHA challenges on non-Cloudflare sites
    
  </a>
  <a href="/examples/cloudflare-access" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Browse Cloudflare Access-Protected Pages</h3>
      authenticate through Cloudflare Access zero-trust policies
    
  </a>
  <a href="/examples/scrape" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape Structured Data</h3>
      extract data once you're past the challenge
    
  </a>

---

# Browse Cloudflare Access-protected pages

URL: https://docs.browserless.io/examples/cloudflare-access

# Browse Cloudflare Access-protected pages

Access pages protected by Cloudflare Access zero-trust policies by passing Service Token credentials or reusing a saved authenticated profile.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- Cloudflare Access Service Token credentials (`CF-Access-Client-Id` and `CF-Access-Client-Secret`) **or** a valid Browserless authenticated profile for the protected site

## Steps

There are two approaches depending on your Cloudflare Access configuration:

- **Service Tokens** — machine-to-machine credentials issued by your Cloudflare team. Inject them as HTTP headers on every request.
- **User sessions** — save a logged-in browser state to a Browserless profile and reuse it. See [Save Logins to Authenticated Profiles](/examples/authenticated-profiles).

  
**REST API:**

  Use the REST API with Cloudflare Access service token headers to reach protected pages.

  
    
**cURL:**

    

    **1. Use a saved authenticated profile**

    If you have a Browserless profile saved after logging in through Cloudflare Access, append `profile=` to any request:

```bash
curl -X POST \
  "https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE&profile=cf-access-profile" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://internal.example.com/dashboard" }' \
  --output dashboard.png
```

    **2. Inject Service Token headers**

    For machine-to-machine access, use the `/content` endpoint with `setExtraHTTPHeaders` to pass the token on every navigation:

```bash
curl -X POST \
  "https://production-sfo.browserless.io/content?token=YOUR_API_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://internal.example.com/dashboard",
    "setExtraHTTPHeaders": {
      "CF-Access-Client-Id": "YOUR_CF_CLIENT_ID.access",
      "CF-Access-Client-Secret": "YOUR_CF_CLIENT_SECRET"
    }
  }'
```

    
    
**JavaScript:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Inject Service Token headers**

```js
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint: 'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE',
});

try {
  const page = await browser.newPage();

  // Headers must be set before navigation — each request to a CF Access-protected origin requires them.
  await page.setExtraHTTPHeaders({
    'CF-Access-Client-Id': 'YOUR_CF_CLIENT_ID.access',
    'CF-Access-Client-Secret': 'YOUR_CF_CLIENT_SECRET',
  });

  await page.goto('https://internal.example.com/dashboard', {
    waitUntil: 'networkidle2',
  });

  console.log('Title:', await page.title());
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Or reuse a saved profile**

```js
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint:
    'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&profile=cf-access-profile',
});

try {
  const page = await browser.newPage();
  await page.goto('https://internal.example.com/dashboard');
  console.log('Title:', await page.title());
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    
    
**Python:**

    

    **1. Install dependencies**

```bash
pip install playwright
```

    **2. Inject Service Token headers**

```python
from playwright.sync_api import sync_playwright

TOKEN = 'YOUR_API_TOKEN_HERE'
WS_ENDPOINT = f'wss://production-sfo.browserless.io/chromium/playwright?token={TOKEN}'

with sync_playwright() as playwright:
    browser = playwright.chromium.connect(WS_ENDPOINT)
    try:
        context = browser.new_context(
            extra_http_headers={
                'CF-Access-Client-Id': 'YOUR_CF_CLIENT_ID.access',
                'CF-Access-Client-Secret': 'YOUR_CF_CLIENT_SECRET',
            }
        )
        page = context.new_page()
        page.goto('https://internal.example.com/dashboard')
        print('Title:', page.title())
    finally:
        # Always close to release the session even on error.
        browser.close()
```

    **3. Or reuse a saved profile**

```python
from playwright.sync_api import sync_playwright

TOKEN = 'YOUR_API_TOKEN_HERE'
WS_ENDPOINT = f'wss://production-sfo.browserless.io?token={TOKEN}&profile=cf-access-profile'

with sync_playwright() as playwright:
    # connect_over_cdp preserves the saved profile's context and cookies.
    browser = playwright.chromium.connect_over_cdp(WS_ENDPOINT)
    try:
        context = browser.contexts[0]
        page = context.pages[0]
        page.goto('https://internal.example.com/dashboard')
        print('Title:', page.title())
    finally:
        # Always close to release the session even on error.
        browser.close()
```

    
    
**Java:**

    

    **1. Dependencies**

    `java.net.http.HttpClient` ships with the JDK. No extra packages needed. Use Jackson or Gson in production to serialize the nested `setExtraHTTPHeaders` object safely.

    **2. Use a saved authenticated profile**

```java
import java.net.URI;
import java.net.http.*;
import java.nio.file.*;

public class CloudflareAccess {
    public static void main(String[] args) throws Exception {
        String token = "YOUR_API_TOKEN_HERE";
        HttpClient client = HttpClient.newHttpClient();

        HttpRequest req = HttpRequest.newBuilder()
            .uri(URI.create("https://production-sfo.browserless.io/screenshot"
                + "?token=" + token + "&profile=cf-access-profile"))
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(
                "{\"url\":\"https://internal.example.com/dashboard\"}"))
            .build();

        HttpResponse<byte[]> res = client.send(req, HttpResponse.BodyHandlers.ofByteArray());
        Files.write(Path.of("dashboard.png"), res.body());
        System.out.println("Saved dashboard.png");
    }
}
```

    **3. Inject Service Token headers**

```java
import java.net.URI;
import java.net.http.*;

public class CloudflareAccessToken {
    public static void main(String[] args) throws Exception {
        String token = "YOUR_API_TOKEN_HERE";
        HttpClient client = HttpClient.newHttpClient();

        // Use Jackson or Gson for production — inline JSON is fragile for values with special characters.
        String body = "{\"url\":\"https://internal.example.com/dashboard\","
            + "\"setExtraHTTPHeaders\":{"
            + "\"CF-Access-Client-Id\":\"YOUR_CF_CLIENT_ID.access\","
            + "\"CF-Access-Client-Secret\":\"YOUR_CF_CLIENT_SECRET\"}}";

        HttpRequest req = HttpRequest.newBuilder()
            .uri(URI.create("https://production-sfo.browserless.io/content?token=" + token))
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();

        HttpResponse res = client.send(req, HttpResponse.BodyHandlers.ofString());
        System.out.println(res.body().substring(0, Math.min(200, res.body().length())));
    }
}
```

    
    
**C#:**

    

    **1. Dependencies**

    `System.Net.Http.HttpClient` and `System.Text.Json` are part of the .NET standard library.

    **2. Use a saved authenticated profile**

```csharp
using System;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;

class CloudflareAccess
{
    static async Task Main()
    {
        const string token = "YOUR_API_TOKEN_HERE";
        using var client = new HttpClient();

        var body = new StringContent(
            "{\"url\":\"https://internal.example.com/dashboard\"}",
            Encoding.UTF8, "application/json");
        var res = await client.PostAsync(
            $"https://production-sfo.browserless.io/screenshot?token={token}&profile=cf-access-profile",
            body);
        await File.WriteAllBytesAsync("dashboard.png", await res.Content.ReadAsByteArrayAsync());
        Console.WriteLine("Saved dashboard.png");
    }
}
```

    **3. Inject Service Token headers**

```csharp
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

class CloudflareAccessToken
{
    static async Task Main()
    {
        const string token = "YOUR_API_TOKEN_HERE";
        using var client = new HttpClient();

        var payload = JsonSerializer.Serialize(new
        {
            url = "https://internal.example.com/dashboard",
            setExtraHTTPHeaders = new Dictionary<string, string>
            {
                ["CF-Access-Client-Id"] = "YOUR_CF_CLIENT_ID.access",
                ["CF-Access-Client-Secret"] = "YOUR_CF_CLIENT_SECRET",
            }
        });
        var body = new StringContent(payload, Encoding.UTF8, "application/json");
        var res = await client.PostAsync(
            $"https://production-sfo.browserless.io/content?token={token}",
            body);
        string content = await res.Content.ReadAsStringAsync();
        Console.WriteLine(content[..Math.Min(200, content.Length)]);
    }
}
```

    
    
**Go:**

    

    **1. Dependencies**

    `encoding/json` and `net/http` are part of Go's standard library.

    **2. Use a saved authenticated profile**

```go
package main

import (
    "bytes"
    "fmt"
    "io"
    "net/http"
    "os"
)

func main() {
    token := "YOUR_API_TOKEN_HERE"

    req, _ := http.NewRequest("POST",
        "https://production-sfo.browserless.io/screenshot?token="+token+"&profile=cf-access-profile",
        bytes.NewBufferString(`{"url":"https://internal.example.com/dashboard"}`),
    )
    req.Header.Set("Content-Type", "application/json")

    res, err := http.DefaultClient.Do(req)
    if err != nil { panic(err) }
    defer res.Body.Close()

    data, _ := io.ReadAll(res.Body)
    os.WriteFile("dashboard.png", data, 0644)
    fmt.Println("Saved dashboard.png")
}
```

    **3. Inject Service Token headers**

```go
package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
)

func main() {
    token := "YOUR_API_TOKEN_HERE"

    payload, _ := json.Marshal(map[string]interface{}{
        "url": "https://internal.example.com/dashboard",
        "setExtraHTTPHeaders": map[string]string{
            "CF-Access-Client-Id":     "YOUR_CF_CLIENT_ID.access",
            "CF-Access-Client-Secret": "YOUR_CF_CLIENT_SECRET",
        },
    })

    req, _ := http.NewRequest("POST",
        "https://production-sfo.browserless.io/content?token="+token,
        bytes.NewBuffer(payload),
    )
    req.Header.Set("Content-Type", "application/json")

    res, err := http.DefaultClient.Do(req)
    if err != nil { panic(err) }
    defer res.Body.Close()

    content, _ := io.ReadAll(res.Body)
    end := len(content)
    if end > 200 { end = 200 }
    fmt.Println(string(content[:end]))
}
```

    
    
**PHP:**

    

    **1. Dependencies**

    This example uses PHP's built-in `curl`. No Composer packages needed.

    **2. Use a saved authenticated profile**

```php
<?php

$token = 'YOUR_API_TOKEN_HERE';

$ch = curl_init("https://production-sfo.browserless.io/screenshot?token={$token}&profile=cf-access-profile");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
    CURLOPT_POSTFIELDS => json_encode(['url' => 'https://internal.example.com/dashboard']),
    CURLOPT_RETURNTRANSFER => true,
]);
$png = curl_exec($ch);
curl_close($ch);
file_put_contents('dashboard.png', $png);
echo "Saved dashboard.png\n";
```

    **3. Inject Service Token headers**

```php
<?php

$token = 'YOUR_API_TOKEN_HERE';

$ch = curl_init("https://production-sfo.browserless.io/content?token={$token}");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
    CURLOPT_POSTFIELDS => json_encode([
        'url' => 'https://internal.example.com/dashboard',
        'setExtraHTTPHeaders' => [
            'CF-Access-Client-Id' => 'YOUR_CF_CLIENT_ID.access',
            'CF-Access-Client-Secret' => 'YOUR_CF_CLIENT_SECRET',
        ],
    ]),
    CURLOPT_RETURNTRANSFER => true,
]);
$content = curl_exec($ch);
curl_close($ch);
echo substr($content, 0, 200) . "\n";
```

    
    
**Ruby:**

    

    **1. Dependencies**

    `net/http` and `json` are part of Ruby's standard library.

    **2. Use a saved authenticated profile**

```ruby
require 'net/http'
require 'json'
require 'uri'

TOKEN = 'YOUR_API_TOKEN_HERE'

uri = URI("https://production-sfo.browserless.io/screenshot?token=#{TOKEN}&profile=cf-access-profile")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true

request = Net::HTTP::Post.new(uri)
request['Content-Type'] = 'application/json'
request.body = JSON.generate({ url: 'https://internal.example.com/dashboard' })

res = http.request(request)
File.binwrite('dashboard.png', res.body)
puts 'Saved dashboard.png'
```

    **3. Inject Service Token headers**

```ruby
require 'net/http'
require 'json'
require 'uri'

TOKEN = 'YOUR_API_TOKEN_HERE'

uri = URI("https://production-sfo.browserless.io/content?token=#{TOKEN}")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true

request = Net::HTTP::Post.new(uri)
request['Content-Type'] = 'application/json'
request.body = JSON.generate({
  url: 'https://internal.example.com/dashboard',
  setExtraHTTPHeaders: {
    'CF-Access-Client-Id' => 'YOUR_CF_CLIENT_ID.access',
    'CF-Access-Client-Secret' => 'YOUR_CF_CLIENT_SECRET',
  },
})

res = http.request(request)
puts res.body[0, 200]
```

    
  

  
  
**Frameworks:**

  Use a browser connection to set Cloudflare Access headers before navigating to a protected page.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Inject Service Token headers**

```js
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint: 'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE',
});

try {
  const page = await browser.newPage();

  await page.setExtraHTTPHeaders({
    'CF-Access-Client-Id': 'YOUR_CF_CLIENT_ID.access',
    'CF-Access-Client-Secret': 'YOUR_CF_CLIENT_SECRET',
  });

  await page.goto('https://internal.example.com/dashboard', {
    waitUntil: 'networkidle2',
  });

  console.log('Title:', await page.title());
  await page.screenshot({ path: 'dashboard.png' });
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Run with `node cf-access.mjs`. The browser navigates directly to the protected page without an auth redirect.

    
    
**Playwright:**

    
      
**JavaScript:**

      

      **1. Install dependencies**

```bash
npm install playwright-core
```

      **2. Inject Service Token headers**

```js
import { chromium } from 'playwright-core';

const browser = await chromium.connectOverCDP(
  'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE'
);

try {
  const context = await browser.newContext({
    extraHTTPHeaders: {
      'CF-Access-Client-Id': 'YOUR_CF_CLIENT_ID.access',
      'CF-Access-Client-Secret': 'YOUR_CF_CLIENT_SECRET',
    },
  });

  const page = await context.newPage();
  await page.goto('https://internal.example.com/dashboard', {
    waitUntil: 'networkidle',
  });

  console.log('Title:', await page.title());
  await page.screenshot({ path: 'dashboard.png' });
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

      **3. Check the output**

      Run with `node cf-access.mjs`. The browser navigates directly to the protected page without an auth redirect.

      
      
**Python:**

      

      **1. Install dependencies**

```bash
pip install playwright
```

      **2. Inject Service Token headers**

```python
from playwright.sync_api import sync_playwright

TOKEN = 'YOUR_API_TOKEN_HERE'
WS_ENDPOINT = f'wss://production-sfo.browserless.io?token={TOKEN}'

with sync_playwright() as playwright:
    browser = playwright.chromium.connect_over_cdp(WS_ENDPOINT)
    try:
        context = browser.new_context(
            extra_http_headers={
                'CF-Access-Client-Id': 'YOUR_CF_CLIENT_ID.access',
                'CF-Access-Client-Secret': 'YOUR_CF_CLIENT_SECRET',
            }
        )
        page = context.new_page()
        page.goto('https://internal.example.com/dashboard',
                  wait_until='networkidle')
        print('Title:', page.title())
        page.screenshot(path='dashboard.png')
    finally:
        # Always close to release the session even on error.
        browser.close()
```

      **3. Check the output**

      Run with `python cf-access.py`. The browser navigates directly to the protected page without an auth redirect.

      
      
**Java:**

      

      **1. Install dependencies**

      Add the Playwright dependency to your `pom.xml`:

```xml
<dependency>
  <groupId>com.microsoft.playwright</groupId>
  <artifactId>playwright</artifactId>
  <version>1.44.0</version>
</dependency>
```

      **2. Inject Service Token headers**

```java
import com.microsoft.playwright.*;
import java.nio.file.Paths;
import java.util.Map;

public class CloudflareAccess {
    public static void main(String[] args) {
        String TOKEN = "YOUR_API_TOKEN_HERE";
        String WS_ENDPOINT = "wss://production-sfo.browserless.io?token=" + TOKEN;

        try (Playwright playwright = Playwright.create()) {
            Browser browser = playwright.chromium().connectOverCDP(WS_ENDPOINT);
            try {
                BrowserContext context = browser.newContext(
                    new Browser.NewContextOptions().setExtraHTTPHeaders(Map.of(
                        "CF-Access-Client-Id", "YOUR_CF_CLIENT_ID.access",
                        "CF-Access-Client-Secret", "YOUR_CF_CLIENT_SECRET"
                    ))
                );
                Page page = context.newPage();
                page.navigate("https://internal.example.com/dashboard");
                page.waitForLoadState(LoadState.NETWORKIDLE);
                System.out.println("Title: " + page.title());
                page.screenshot(new Page.ScreenshotOptions()
                    .setPath(Paths.get("dashboard.png")));
            } finally {
                // Always close to release the session even on error.
                browser.close();
            }
        }
    }
}
```

      **3. Check the output**

      Compile with `mvn compile` and run with `mvn exec:java`. The browser navigates directly to the protected page without an auth redirect.

      
      
**C#:**

      

      **1. Install dependencies**

```bash
dotnet add package Microsoft.Playwright
```

      **2. Inject Service Token headers**

```csharp
using Microsoft.Playwright;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

class CloudflareAccess
{
    static async Task Main()
    {
        const string TOKEN = "YOUR_API_TOKEN_HERE";
        string WS_ENDPOINT = $"wss://production-sfo.browserless.io?token={TOKEN}";

        using var playwright = await Playwright.CreateAsync();
        var browser = await playwright.Chromium.ConnectOverCDPAsync(WS_ENDPOINT);
        try
        {
            var context = await browser.NewContextAsync(new()
            {
                ExtraHTTPHeaders = new Dictionary<string, string>
                {
                    ["CF-Access-Client-Id"] = "YOUR_CF_CLIENT_ID.access",
                    ["CF-Access-Client-Secret"] = "YOUR_CF_CLIENT_SECRET",
                }
            });
            var page = await context.NewPageAsync();
            await page.GotoAsync("https://internal.example.com/dashboard",
                new() { WaitUntil = WaitUntilState.NetworkIdle });
            Console.WriteLine($"Title: {await page.TitleAsync()}");
            await page.ScreenshotAsync(new() { Path = "dashboard.png" });
        }
        finally
        {
            // Always close to release the session even on error.
            await browser.CloseAsync();
        }
    }
}
```

      **3. Check the output**

      Run with `dotnet run`. The browser navigates directly to the protected page without an auth redirect.

      
    

    
    
**Go (chromedp):**

    

    **1. Install dependencies**

```bash
go get github.com/chromedp/chromedp
go get github.com/chromedp/cdproto
```

    **2. Inject Service Token headers**

```go
package main

import (
    "context"
    "fmt"

    "github.com/chromedp/cdproto/network"
    "github.com/chromedp/chromedp"
)

func main() {
    token := "YOUR_API_TOKEN_HERE"
    ws := fmt.Sprintf("wss://production-sfo.browserless.io?token=%s", token)

    allocCtx, cancel := chromedp.NewRemoteAllocator(context.Background(), ws, chromedp.NoModifyURL)
    defer cancel()

    ctx, cancel := chromedp.NewContext(allocCtx)
    defer cancel()

    var title string
    if err := chromedp.Run(ctx,
        // Enable the network domain before setting headers.
        network.Enable(),
        network.SetExtraHTTPHeaders(network.Headers{
            "CF-Access-Client-Id":     "YOUR_CF_CLIENT_ID.access",
            "CF-Access-Client-Secret": "YOUR_CF_CLIENT_SECRET",
        }),
        chromedp.Navigate("https://internal.example.com/dashboard"),
        chromedp.WaitReady("body"),
        chromedp.Title(&title),
    ); err != nil {
        panic(err)
    }

    fmt.Println("Title:", title)
}
```

    **3. Check the output**

    Run with `go run main.go`. The browser navigates directly to the protected page without an auth redirect.

    
  

  

## Next steps

  <a href="/examples/authenticated-profiles" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Save Logins to Authenticated Profiles</h3>
      capture a user session after logging in through Cloudflare Access
    
  </a>
  <a href="/examples/cloudflare" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Solving Cloudflare Challenges</h3>
      bypass Cloudflare bot challenges (not zero-trust access policies)
    
  </a>
  <a href="/examples/sessions" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Log In and Reuse Sessions</h3>
      general session persistence pattern
    
  </a>

---

# Run concurrent browser sessions

URL: https://docs.browserless.io/examples/concurrent-sessions

# Run concurrent browser sessions

Launch multiple browser sessions in parallel to speed up large-scale scraping, testing, or automation. Each session runs independently on Browserless infrastructure.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- A Browserless plan that supports the concurrency level you need

## Steps

  
**AI Agent:**

  Use the [Browserless MCP server](https://github.com/browserless/browserless-mcp) to run concurrent browser sessions from any MCP-compatible AI agent (Claude Desktop, Cursor, Windsurf, ChatGPT, etc.).

  **1. Connect the MCP server**

  Send this prompt to your AI agent to install the Browserless MCP server:

```text
Go to https://github.com/browserless/browserless-mcp/blob/main/install.md
and follow the instructions to install the Browserless MCP server
for my client.
```

  **2. Scrape multiple URLs**

  Use `browserless_smartscraper`. Each URL can be scraped independently in parallel calls.

```text
Use the browserless_smartscraper tool to scrape these 5 URLs
in parallel and return the content of each as markdown:
- https://scraping-sandbox.netlify.app/products
- https://scraping-sandbox.netlify.app/contact-us
- https://scraping-sandbox.netlify.app/receipt
- https://scraping-sandbox.netlify.app/dashboard
- https://scraping-sandbox.netlify.app/helix
```

  
  
**REST API:**

  Each REST request opens an independent browser session on Browserless. Fire them concurrently using your language's async primitives. No WebSocket connection needed.

  
    
**cURL:**

    

    **1. Send requests in parallel**

    Each REST API call opens an independent browser session. Run them concurrently with `&`:

```bash
curl -s -X POST \
  "https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://scraping-sandbox.netlify.app/products", "options": { "type": "png", "fullPage": true } }' \
  --output page-1.png &

curl -s -X POST \
  "https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://scraping-sandbox.netlify.app/contact-us", "options": { "type": "png", "fullPage": true } }' \
  --output page-2.png &

curl -s -X POST \
  "https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://scraping-sandbox.netlify.app/receipt", "options": { "type": "png", "fullPage": true } }' \
  --output page-3.png &

wait
echo "All screenshots saved"
```

    **2. Check the output**

    All three screenshots are captured simultaneously. Total time is roughly the duration of the slowest single request.

```text
All screenshots saved
```

    Three files appear in the current directory: `page-1.png`, `page-2.png`, `page-3.png`.

    
    
**Node.js:**

    

    **1. Fire concurrent requests with `Promise.all`**

```js
import fs from 'fs';

const TOKEN = 'YOUR_API_TOKEN_HERE';
const URLS = [
  'https://scraping-sandbox.netlify.app/products',
  'https://scraping-sandbox.netlify.app/contact-us',
  'https://scraping-sandbox.netlify.app/receipt',
  'https://scraping-sandbox.netlify.app/dashboard',
  'https://scraping-sandbox.netlify.app/helix',
];

await Promise.all(
  URLS.map((url, i) =>
    fetch(
      `https://production-sfo.browserless.io/screenshot?token=${TOKEN}`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ url, options: { type: 'png', fullPage: true } }),
      }
    ).then(async (res) => {
      const buf = Buffer.from(await res.arrayBuffer());
      fs.writeFileSync(`screenshot-${i + 1}.png`, buf);
      console.log(`Saved screenshot-${i + 1}.png`);
    })
  )
);

console.log('All done');
```

    **2. Check the output**

    Run with `node concurrent.mjs`. All 5 screenshots are captured in parallel. Total time is the slowest single page, not 5× sequential.

```text
Saved screenshot-1.png
Saved screenshot-2.png
Saved screenshot-3.png
Saved screenshot-4.png
Saved screenshot-5.png
All done
```

    
    
**Python:**

    

    **1. Install dependencies**

```bash
pip install requests
```

    **2. Use `ThreadPoolExecutor` for parallel requests**

```python
import requests
from concurrent.futures import ThreadPoolExecutor, as_completed

TOKEN = 'YOUR_API_TOKEN_HERE'
URLS = [
    'https://scraping-sandbox.netlify.app/products',
    'https://scraping-sandbox.netlify.app/contact-us',
    'https://scraping-sandbox.netlify.app/receipt',
    'https://scraping-sandbox.netlify.app/dashboard',
    'https://scraping-sandbox.netlify.app/helix',
]

def capture(i_url):
    i, url = i_url
    res = requests.post(
        f'https://production-sfo.browserless.io/screenshot?token={TOKEN}',
        json={'url': url, 'options': {'type': 'png', 'fullPage': True}},
    )
    filename = f'screenshot-{i + 1}.png'
    with open(filename, 'wb') as f:
        f.write(res.content)
    print(f'Saved {filename}')

with ThreadPoolExecutor(max_workers=5) as executor:
    futures = [executor.submit(capture, (i, url)) for i, url in enumerate(URLS)]
    for f in as_completed(futures):
        f.result()

print('All done')
```

    **3. Check the output**

    Run with `python concurrent.py`. All 5 screenshots are captured in parallel.

```text
Saved screenshot-1.png
Saved screenshot-2.png
Saved screenshot-3.png
Saved screenshot-4.png
Saved screenshot-5.png
All done
```

    
    
**Java:**

    

    **1. Dependencies**

    `java.net.http.HttpClient` ships with the JDK (Java 11+). No extra dependencies needed.

    **2. Fire concurrent requests with `CompletableFuture`**

```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.IntStream;

public class ConcurrentScreenshots {
    public static void main(String[] args) {
        String token = "YOUR_API_TOKEN_HERE";
        List urls = List.of(
            "https://scraping-sandbox.netlify.app/products",
            "https://scraping-sandbox.netlify.app/contact-us",
            "https://scraping-sandbox.netlify.app/receipt",
            "https://scraping-sandbox.netlify.app/dashboard",
            "https://scraping-sandbox.netlify.app/helix"
        );

        HttpClient client = HttpClient.newHttpClient();

        CompletableFuture<?>[] futures = IntStream.range(0, urls.size())
            .mapToObj(i -> {
                String body = "{\"url\":\"" + urls.get(i) + "\",\"options\":{\"type\":\"png\",\"fullPage\":true}}";
                HttpRequest request = HttpRequest.newBuilder()
                    .uri(URI.create(
                        "https://production-sfo.browserless.io/screenshot?token=" + token))
                    .header("Content-Type", "application/json")
                    .POST(HttpRequest.BodyPublishers.ofString(body))
                    .build();

                return client
                    .sendAsync(request, HttpResponse.BodyHandlers.ofByteArray())
                    .thenAccept(res -> {
                        try {
                            Files.write(Path.of("screenshot-" + (i + 1) + ".png"), res.body());
                            System.out.println("Saved screenshot-" + (i + 1) + ".png");
                        } catch (Exception e) {
                            throw new RuntimeException(e);
                        }
                    });
            })
            .toArray(CompletableFuture[]::new);

        CompletableFuture.allOf(futures).join();
        System.out.println("All done");
    }
}
```

    **3. Check the output**

    Compile with `javac ConcurrentScreenshots.java` and run with `java ConcurrentScreenshots`. `allOf` blocks until the slowest request finishes.

```text
Saved screenshot-1.png
Saved screenshot-2.png
Saved screenshot-3.png
Saved screenshot-4.png
Saved screenshot-5.png
All done
```

    This example builds the JSON body via string concatenation. For production use, add a library like [Jackson](https://github.com/FasterXML/jackson) to serialize the body safely. URLs containing quotes or backslashes will break the raw string approach.

    
    
**C#:**

    

    **1. Dependencies**

    `System.Net.Http.HttpClient` is part of the .NET standard library. No packages needed.

    **2. Fire concurrent requests with `Task.WhenAll`**

```csharp
using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;

class ConcurrentScreenshots
{
    static async Task Main()
    {
        const string token = "YOUR_API_TOKEN_HERE";
        string[] urls = {
            "https://scraping-sandbox.netlify.app/products",
            "https://scraping-sandbox.netlify.app/contact-us",
            "https://scraping-sandbox.netlify.app/receipt",
            "https://scraping-sandbox.netlify.app/dashboard",
            "https://scraping-sandbox.netlify.app/helix",
        };

        using var client = new HttpClient();

        await Task.WhenAll(urls.Select(async (url, i) =>
        {
            var body = new StringContent(
                $"{{\"url\":\"{url}\",\"options\":{{\"type\":\"png\",\"fullPage\":true}}}}",
                Encoding.UTF8,
                "application/json"
            );
            var response = await client.PostAsync(
                $"https://production-sfo.browserless.io/screenshot?token={token}",
                body
            );
            var bytes = await response.Content.ReadAsByteArrayAsync();
            await File.WriteAllBytesAsync($"screenshot-{i + 1}.png", bytes);
            Console.WriteLine($"Saved screenshot-{i + 1}.png");
        }));

        Console.WriteLine("All done");
    }
}
```

    **3. Check the output**

    Run with `dotnet run`. `Task.WhenAll` fires all requests concurrently and waits for the last one to finish.

```text
Saved screenshot-1.png
Saved screenshot-2.png
Saved screenshot-3.png
Saved screenshot-4.png
Saved screenshot-5.png
All done
```

    This example builds the JSON body via string interpolation. For production use, use `System.Text.Json.JsonSerializer.Serialize` instead. URLs containing quotes or backslashes will break the raw string approach.

    
    
**PHP:**

    

    **1. Install dependencies**

```bash
composer require react/http
```

    **2. Send requests concurrently with ReactPHP**

    ReactPHP's event loop runs all requests in parallel without spawning threads:

```php
<?php

require 'vendor/autoload.php';

use React\Http\Browser;
use React\EventLoop\Loop;

$token = 'YOUR_API_TOKEN_HERE';
$urls = [
    'https://scraping-sandbox.netlify.app/products',
    'https://scraping-sandbox.netlify.app/contact-us',
    'https://scraping-sandbox.netlify.app/receipt',
    'https://scraping-sandbox.netlify.app/dashboard',
    'https://scraping-sandbox.netlify.app/helix',
];

$browser = new Browser();
$promises = [];

foreach ($urls as $i => $url) {
    $index = $i + 1;
    $promises[] = $browser
        ->post(
            "https://production-sfo.browserless.io/screenshot?token={$token}",
            ['Content-Type' => 'application/json'],
            json_encode(['url' => $url, 'options' => ['type' => 'png', 'fullPage' => true]])
        )
        ->then(function ($response) use ($index) {
            file_put_contents("screenshot-{$index}.png", (string) $response->getBody());
            echo "Saved screenshot-{$index}.png\n";
        });
}

\React\Promise\all($promises)->then(function () {
    echo "All done\n";
});

Loop::run();
```

    **3. Check the output**

    Run with `php concurrent.php`. `Loop::run()` processes all requests through the event loop until the last promise resolves.

```text
Saved screenshot-1.png
Saved screenshot-2.png
Saved screenshot-3.png
Saved screenshot-4.png
Saved screenshot-5.png
All done
```

    
    
**Ruby:**

    

    **1. Dependencies**

    `net/http` and `json` are part of Ruby's standard library. No gems required.

    **2. Spawn one thread per URL**

```ruby
require 'net/http'
require 'json'
require 'uri'

TOKEN = 'YOUR_API_TOKEN_HERE'
URLS = [
  'https://scraping-sandbox.netlify.app/products',
  'https://scraping-sandbox.netlify.app/contact-us',
  'https://scraping-sandbox.netlify.app/receipt',
  'https://scraping-sandbox.netlify.app/dashboard',
  'https://scraping-sandbox.netlify.app/helix',
].freeze

threads = URLS.each_with_index.map do |url, i|
  Thread.new do
    uri = URI("https://production-sfo.browserless.io/screenshot?token=#{TOKEN}")
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true

    request = Net::HTTP::Post.new(uri)
    request['Content-Type'] = 'application/json'
    request.body = JSON.generate({ url: url, options: { type: 'png', fullPage: true } })

    response = http.request(request)
    File.binwrite("screenshot-#{i + 1}.png", response.body)
    puts "Saved screenshot-#{i + 1}.png"
  end
end

threads.each(&:join)
puts 'All done'
```

    **3. Check the output**

    Run with `ruby concurrent.rb`. `join` waits for all threads before printing the final line.

```text
Saved screenshot-1.png
Saved screenshot-2.png
Saved screenshot-3.png
Saved screenshot-4.png
Saved screenshot-5.png
All done
```

    
  

  
  
**Frameworks:**

  Each `connect()` call opens a separate managed browser on Browserless. Use `Promise.all` or goroutines to run them in parallel. Sessions are completely isolated from each other.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Open multiple browser connections in parallel**

    Each `puppeteer.connect()` call opens a separate browser session on Browserless:

```js
import puppeteer from 'puppeteer-core';

const TOKEN = 'YOUR_API_TOKEN_HERE';
const WS = `wss://production-sfo.browserless.io?token=${TOKEN}`;

const URLS = [
  'https://scraping-sandbox.netlify.app/products',
  'https://scraping-sandbox.netlify.app/contact-us',
  'https://scraping-sandbox.netlify.app/receipt',
  'https://scraping-sandbox.netlify.app/dashboard',
  'https://scraping-sandbox.netlify.app/helix',
];

const results = await Promise.all(
  URLS.map(async (url, i) => {
    const browser = await puppeteer.connect({ browserWSEndpoint: WS });
    try {
      const page = await browser.newPage();
      await page.goto(url, { waitUntil: 'networkidle2' });
      const title = await page.title();
      console.log(`[${i + 1}] ${title}`);
      return { url, title };
    } finally {
      // Always close to release the session even on error.
      await browser.close();
    }
  })
);

console.log('Results:', results);
```

    **3. Check the output**

    Run with `node concurrent.mjs`. Each session runs in its own browser process on Browserless. There's no shared state between them.

```json
[1] Product Catalog | Scraping Sandbox
[2] Contact Us - Scraping Sandbox | Scraping Sandbox
[3] Scraping Sandbox
[4] Scraping Sandbox
[5] Careers at Helix — Open Positions
Results: [ { url: '...', title: '...' }, ... ]
```

    
    
**Playwright:**

    

    **1. Install dependencies**

```bash
npm install playwright-core
```

    **2. Open multiple connections in parallel**

```js
import { chromium } from 'playwright-core';

const TOKEN = 'YOUR_API_TOKEN_HERE';
const WS = `wss://production-sfo.browserless.io?token=${TOKEN}`;

const URLS = [
  'https://scraping-sandbox.netlify.app/products',
  'https://scraping-sandbox.netlify.app/contact-us',
  'https://scraping-sandbox.netlify.app/receipt',
  'https://scraping-sandbox.netlify.app/dashboard',
  'https://scraping-sandbox.netlify.app/helix',
];

const results = await Promise.all(
  URLS.map(async (url, i) => {
    const browser = await chromium.connectOverCDP(WS);
    try {
      // Use the default context — browser.newPage() creates a new context that
      // doesn't inherit proxy, profile, or launch settings.
      const context = browser.contexts()[0];
      const page = await context.newPage();
      await page.goto(url, { waitUntil: 'networkidle' });
      const title = await page.title();
      console.log(`[${i + 1}] ${title}`);
      return { url, title };
    } finally {
      // Always close to release the session even on error.
      await browser.close();
    }
  })
);

console.log('Results:', results);
```

    **3. Check the output**

    Run with `node concurrent.mjs`. Each session runs in its own browser process on Browserless.

```json
[1] Product Catalog | Scraping Sandbox
[2] Contact Us - Scraping Sandbox | Scraping Sandbox
[3] Scraping Sandbox
[4] Scraping Sandbox
[5] Careers at Helix — Open Positions
Results: [ { url: '...', title: '...' }, ... ]
```

    
    
**Go (chromedp):**

    

    **1. Install dependencies**

```bash
go get github.com/chromedp/chromedp
```

    **2. Spawn one goroutine per URL**

    Each goroutine gets its own `NewRemoteAllocator` context, which opens a separate browser session on Browserless:

```go
package main

import (
    "context"
    "fmt"
    "log"
    "sync"

    "github.com/chromedp/chromedp"
)

func main() {
    token := "YOUR_API_TOKEN_HERE"
    ws := fmt.Sprintf("wss://production-sfo.browserless.io?token=%s", token)

    urls := []string{
        "https://scraping-sandbox.netlify.app/products",
        "https://scraping-sandbox.netlify.app/contact-us",
        "https://scraping-sandbox.netlify.app/receipt",
        "https://scraping-sandbox.netlify.app/dashboard",
        "https://scraping-sandbox.netlify.app/helix",
    }

    var wg sync.WaitGroup

    for i, url := range urls {
        wg.Add(1)
        go func(i int, url string) {
            defer wg.Done()

            // Each goroutine gets its own allocator — a separate browser session per URL.
            allocCtx, cancel := chromedp.NewRemoteAllocator(
                context.Background(), ws, chromedp.NoModifyURL,
            )
            defer cancel()

            ctx, cancel := chromedp.NewContext(allocCtx)
            defer cancel()

            var title string
            if err := chromedp.Run(ctx,
                chromedp.Navigate(url),
                chromedp.Title(&title),
            ); err != nil {
                log.Printf("[%d] error: %v", i+1, err)
                return
            }

            fmt.Printf("[%d] %s\n", i+1, title)
        }(i, url)
    }

    wg.Wait()
    fmt.Println("All done")
}
```

    **3. Check the output**

    Run with `go run main.go`. Each goroutine runs independently. Output lines may arrive out of order since sessions finish at different times.

```json
[3] Scraping Sandbox
[1] Product Catalog | Scraping Sandbox
[5] Careers at Helix — Open Positions
[2] Contact Us - Scraping Sandbox | Scraping Sandbox
[4] Scraping Sandbox
All done
```

    
  

  

## Tips

- **Concurrency limits** — your plan determines how many sessions can run simultaneously. Requests beyond that limit queue automatically.
- **Reuse a saved profile** — add `&profile=my-profile` to the WebSocket URL to start every concurrent session pre-authenticated. See [Save Logins to Authenticated Profiles](/examples/authenticated-profiles).
- **Rate limiting** — add a concurrency cap in your own code (e.g. `p-limit` in Node.js) to avoid overwhelming the target site.

## Next steps

  <a href="/examples/authenticated-profiles" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Save Logins to Authenticated Profiles</h3>
      share a login state across all concurrent sessions
    
  </a>
  <a href="/examples/scrape" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape Structured Data</h3>
      extract data at scale from many pages in parallel
    
  </a>
  <a href="/examples/screenshot" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Take a Screenshot</h3>
      capture screenshots in bulk
    
  </a>

---

# Auto-accept cookie consent banners

URL: https://docs.browserless.io/examples/cookie-consent

# Auto-accept cookie consent banners

Automatically detect and dismiss cookie consent banners before scraping or capturing a page, so they don't block content or appear in screenshots.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Built-in consent blocker

The simplest approach is the `blockConsentModals=true` query parameter. It works on BQL sessions, `/screenshot`, `/pdf`, and other REST endpoints without any selector logic:

```bash
curl -X POST \
  "https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE&blockConsentModals=true" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://scraping-sandbox.netlify.app/clarity-health"}' \
  --output page.png
```

This covers most major consent platforms (OneTrust, CookieBot, GDPR tools). For sites not handled by the built-in blocker, use the custom selector approaches below.

## Steps

  
**REST API:**

  Use BrowserQL to load a page and automatically dismiss cookie consent banners.

  
    
**cURL:**

    

    **1. Build the BrowserQL mutation**

    `goto` with `waitUntil: networkIdle` ensures the page is fully settled before the `if` check runs. `if` returns null (not an error) when the selector is absent, so the mutation completes safely on pages with no banner:

```bash
https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE
```

    **2. Send the request**

```bash
curl -X POST \
  "https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "mutation AcceptCookies { goto(url: \"https://scraping-sandbox.netlify.app/clarity-health\", waitUntil: networkIdle) { status } if(selector: \"[id*=accept], [class*=accept], button[id*=cookie]\", visible: true) { acceptBtn: click(selector: \"[id*=accept], [class*=accept], button[id*=cookie]\") { time } } screenshot { base64 } }"
  }'
```

    **3. Check the output**

    The response includes a base64 screenshot of the page with the banner dismissed.

    
    
**JavaScript:**

    

    **1. Send the request**

```js
import fs from 'fs';

const response = await fetch(
  'https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      query: `mutation AcceptCookies {
        # waitUntil: networkIdle ensures the page is fully settled before the if check runs.
        # if returns null (not an error) when the selector is absent, so this is safe on
        # pages with no banner.
        goto(url: "https://scraping-sandbox.netlify.app/clarity-health", waitUntil: networkIdle) {
          status
        }

        if(selector: "[id*=accept], [class*=accept], button[id*=cookie]", visible: true) {
          acceptBtn: click(selector: "[id*=accept], [class*=accept], button[id*=cookie]") {
            time
          }
        }

        screenshot {
          base64
        }
      }`,
      operationName: 'AcceptCookies',
    }),
  }
);

const { data } = await response.json();
if (data.screenshot?.base64) {
  fs.writeFileSync('page.png', Buffer.from(data.screenshot.base64, 'base64'));
  console.log('Screenshot saved to page.png');
}
```

    **2. Check the output**

    Run with `node cookies.mjs`. The `if` block clicks the accept button only when it's found on the page.

    
    
**Python:**

    

    **1. Install dependencies**

```bash
pip install requests
```

    **2. Send the request**

```python
import base64
import requests

query = """
mutation AcceptCookies {
  # waitUntil: networkIdle ensures the page is fully settled before the if check runs.
  # if returns null (not an error) when the selector is absent, so this is safe on
  # pages with no banner.
  goto(url: "https://scraping-sandbox.netlify.app/clarity-health", waitUntil: networkIdle) {
    status
  }

  if(selector: "[id*=accept], [class*=accept], button[id*=cookie]", visible: true) {
    acceptBtn: click(selector: "[id*=accept], [class*=accept], button[id*=cookie]") {
      time
    }
  }

  screenshot {
    base64
  }
}
"""

response = requests.post(
    'https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE',
    json={'query': query, 'operationName': 'AcceptCookies'},
)

data = response.json()['data']
if data.get('screenshot', {}).get('base64'):
    with open('page.png', 'wb') as f:
        f.write(base64.b64decode(data['screenshot']['base64']))
    print('Screenshot saved to page.png')
```

    **3. Check the output**

    Run with `python cookies.py`. The page is captured after dismissing the banner.

    
  

  
  
**Frameworks:**

  Use a browser connection to detect and click cookie consent banners with custom selector logic.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Connect, dismiss banner, and capture**

```js
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint: 'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE',
});

try {
  const page = await browser.newPage();
  await page.goto('https://scraping-sandbox.netlify.app/clarity-health', { waitUntil: 'networkidle2' });

  // Try common accept-button selectors — banners rarely share a single standard attribute.
  const cookieSelectors = [
    '[id*="accept"]',
    '[class*="accept"]',
    'button[id*="cookie"]',
    '#onetrust-accept-btn-handler',
    '.cc-accept',
  ];

  for (const selector of cookieSelectors) {
    try {
      await page.click(selector);
      console.log(`Dismissed banner with selector: ${selector}`);
      await new Promise(r => setTimeout(r, 500));
      break;
    } catch {
      // selector not found on this page — try the next one.
    }
  }

  await page.screenshot({ path: 'page.png', fullPage: true });
  console.log('Screenshot saved to page.png');
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Run with `node cookies.mjs`. The screenshot shows the page content without the cookie banner overlaying it.

    
    
**Playwright:**

    

    **1. Install dependencies**

```bash
npm install playwright-core
```

    **2. Connect, dismiss banner, and capture**

```js
import { chromium } from 'playwright-core';

const browser = await chromium.connectOverCDP(
  'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE'
);

try {
  // Use the default context — browser.newPage() creates a new context that
  // doesn't inherit proxy, profile, or launch settings.
  const context = browser.contexts()[0];
  const page = await context.newPage();
  await page.goto('https://scraping-sandbox.netlify.app/clarity-health', { waitUntil: 'networkidle' });

  const cookieSelectors = [
    '[id*="accept"]',
    '[class*="accept"]',
    'button[id*="cookie"]',
    '#onetrust-accept-btn-handler',
    '.cc-accept',
  ];

  for (const selector of cookieSelectors) {
    const el = page.locator(selector).first();
    if (await el.isVisible()) {
      await el.click();
      console.log(`Dismissed banner with selector: ${selector}`);
      await new Promise(r => setTimeout(r, 500));
      break;
    }
  }

  await page.screenshot({ path: 'page.png', fullPage: true });
  console.log('Screenshot saved to page.png');
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Run with `node cookies.mjs`. The screenshot shows the page content without the cookie banner overlaying it.

    
    
**BAP:**

    **1. Install dependencies**

```bash
npm install @browserless.io/bap-ts
```

    **2. Connect and dismiss the banner**

```typescript
import Browserless from '@browserless.io/bap-ts';

const browser = Browserless.connect({
  browserWSEndpoint: 'wss://production-sfo.browserless.io/chromium/bql',
  token: 'YOUR_API_TOKEN_HERE',
});

try {
  const page = await browser.newPage();
  await page.goto('https://scraping-sandbox.netlify.app/clarity-health', { waitUntil: 'networkIdle' });

  const selector = '[id*=accept], [class*=accept], button[id*=cookie]';

  // BAP has no `if` mutation, so branch in TypeScript instead.
  if (await page.$(selector)) {
    await page.click(selector);
  }

  await page.screenshot({ path: 'screenshot.png' });
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Adjust the selector list to match the banner framework on the target site (OneTrust, CookieBot, and so on).

    
  

  
  
**BQL:**

  

  **1. Write the mutation**

  `goto` with `waitUntil: networkIdle` ensures the page is fully settled before the `if` check runs. `if` returns null (not an error) when the selector is absent, so the mutation completes safely on pages with no banner. `visible: true` prevents clicking off-screen banner elements that are in the DOM but not rendered:

```graphql
mutation AcceptCookies {
  goto(url: "https://scraping-sandbox.netlify.app/clarity-health", waitUntil: networkIdle) {
    status
  }

  if(selector: "[id*=accept], [class*=accept], button[id*=cookie]", visible: true) {
    acceptBtn: click(selector: "[id*=accept], [class*=accept], button[id*=cookie]") {
      time
    }
  }

  screenshot {
    base64
  }
}
```

  Adjust the selector list to match the specific banner framework used on the target site (OneTrust, CookieBot, etc.).

  **2. Run it**

  Paste into the [BQL IDE](https://account.browserless.io/bql) and click Run, or send via HTTP (see the cURL tab).

  **3. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "if": { "acceptBtn": { "time": 120 } },
    "screenshot": { "base64": "iVBORw0KGgo..." }
  }
}
```

  

## Next steps

  <a href="/examples/screenshot" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Take a Screenshot</h3>
      capture clean page screenshots
    
  </a>
  <a href="/examples/scrape" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape Structured Data</h3>
      extract content after banners are dismissed
    
  </a>
  <a href="/examples/forms" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Fill and Submit a Form</h3>
      automate interactions on pages with cookie gates
    
  </a>

---

# Export pages and assets

URL: https://docs.browserless.io/examples/download-images

# Export pages and assets

Export a full page snapshot (HTML, stylesheets, scripts, images, and fonts) in a single request.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

  
**REST API:**

  POST to the `/export` endpoint with `includeResources: true`. The response is a ZIP file containing the page HTML and its linked assets (same-origin resources are always included; cross-origin resources depend on server configuration).

  
    
**cURL:**

    

    **1. Build the request**

    Append your token to the export endpoint:

```bash
https://production-sfo.browserless.io/export?token=YOUR_API_TOKEN_HERE
```

    **2. Send the request and save the ZIP**

```bash
curl --fail -X POST \
  "https://production-sfo.browserless.io/export?token=YOUR_API_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://scraping-sandbox.netlify.app/harvest-direct","includeResources":true}' \
  --output page.zip

echo "Saved page.zip"
```

    
    
**JavaScript:**

    

    **1. Export the page and save the ZIP**

```js
import fs from 'fs';

const TOKEN = 'YOUR_API_TOKEN_HERE';

const response = await fetch(
  `https://production-sfo.browserless.io/export?token=${TOKEN}`,
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      url: 'https://scraping-sandbox.netlify.app/harvest-direct',
      includeResources: true,
    }),
  }
);

if (!response.ok) {
  throw new Error(`Export failed: ${response.status} ${response.statusText}`);
}

const buf = Buffer.from(await response.arrayBuffer());
fs.writeFileSync('page.zip', buf);
console.log('Saved page.zip');
```

    **2. Check the output**

    Run with `node images.mjs`. The ZIP is saved as `page.zip`.

    
    
**Python:**

    

    **1. Install dependencies**

```bash
pip install requests
```

    **2. Export the page and save the ZIP**

```python
import requests

TOKEN = 'YOUR_API_TOKEN_HERE'

response = requests.post(
    f'https://production-sfo.browserless.io/export?token={TOKEN}',
    json={'url': 'https://scraping-sandbox.netlify.app/harvest-direct', 'includeResources': True},
)
response.raise_for_status()

with open('page.zip', 'wb') as f:
    f.write(response.content)

print('Saved page.zip')
```

    **3. Check the output**

    Run with `python images.py`. The ZIP is saved as `page.zip`.

    
    
**Java:**

    

    **1. Dependencies**

    `java.net.http.HttpClient` ships with the JDK (Java 11+). No extra packages needed.

    **2. Export the page and save the ZIP**

```java
import java.net.URI;
import java.net.http.*;
import java.nio.file.*;

public class DownloadImages {
    public static void main(String[] args) throws Exception {
        String token = "YOUR_API_TOKEN_HERE";

        HttpRequest req = HttpRequest.newBuilder()
            .uri(URI.create("https://production-sfo.browserless.io/export?token=" + token))
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(
                "{\"url\":\"https://scraping-sandbox.netlify.app/harvest-direct\",\"includeResources\":true}"))
            .build();

        HttpResponse<byte[]> res = HttpClient.newHttpClient()
            .send(req, HttpResponse.BodyHandlers.ofByteArray());

        if (res.statusCode() >= 400) {
            throw new RuntimeException("Export failed: " + res.statusCode());
        }

        Files.write(Path.of("page.zip"), res.body());
        System.out.println("Saved page.zip");
    }
}
```

    **3. Check the output**

    Compile with `javac DownloadImages.java` and run with `java DownloadImages`. The ZIP is saved as `page.zip`.

    
    
**C#:**

    

    **1. Dependencies**

    `System.Net.Http.HttpClient` is part of the .NET standard library. No packages needed.

    **2. Export the page and save the ZIP**

```csharp
using System;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;

class DownloadImages
{
    static async Task Main()
    {
        const string token = "YOUR_API_TOKEN_HERE";
        using var client = new HttpClient();

        var body = new StringContent(
            "{\"url\":\"https://scraping-sandbox.netlify.app/harvest-direct\",\"includeResources\":true}",
            Encoding.UTF8,
            "application/json"
        );
        var response = await client.PostAsync(
            $"https://production-sfo.browserless.io/export?token={token}",
            body
        );
        response.EnsureSuccessStatusCode();

        var bytes = await response.Content.ReadAsByteArrayAsync();
        await File.WriteAllBytesAsync("page.zip", bytes);
        Console.WriteLine("Saved page.zip");
    }
}
```

    **3. Check the output**

    Run with `dotnet run`. The ZIP is saved as `page.zip`.

    
    
**Go:**

    

    **1. Dependencies**

    `net/http` and `io` are part of Go's standard library. No extra packages needed.

    **2. Export the page and save the ZIP**

```go
package main

import (
    "bytes"
    "fmt"
    "io"
    "net/http"
    "os"
)

func main() {
    token := "YOUR_API_TOKEN_HERE"

    body := `{"url":"https://scraping-sandbox.netlify.app/harvest-direct","includeResources":true}`
    req, _ := http.NewRequest("POST",
        "https://production-sfo.browserless.io/export?token="+token,
        bytes.NewBufferString(body),
    )
    req.Header.Set("Content-Type", "application/json")

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    if resp.StatusCode >= 400 {
        panic(fmt.Sprintf("Export failed: %d", resp.StatusCode))
    }

    data, _ := io.ReadAll(resp.Body)
    os.WriteFile("page.zip", data, 0644)
    fmt.Println("Saved page.zip")
}
```

    **3. Check the output**

    Run with `go run main.go`. The ZIP is saved as `page.zip`.

    
    
**PHP:**

    

    **1. Dependencies**

    This example uses PHP's built-in `curl`. No Composer packages needed.

    **2. Export the page and save the ZIP**

```php
<?php

$token = 'YOUR_API_TOKEN_HERE';

$ch = curl_init('https://production-sfo.browserless.io/export?token=' . $token);
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => ['Content-Type: application/json'],
    CURLOPT_POSTFIELDS     => json_encode([
        'url'              => 'https://scraping-sandbox.netlify.app/harvest-direct',
        'includeResources' => true,
    ]),
    CURLOPT_RETURNTRANSFER => true,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($status >= 400) {
    die("Export failed: HTTP $status\n");
}

file_put_contents('page.zip', $body);
echo "Saved page.zip\n";
```

    **3. Check the output**

    Run with `php images.php`. The ZIP is saved as `page.zip`.

    
    
**Ruby:**

    

    **1. Dependencies**

    `net/http`, `json`, and `uri` are part of Ruby's standard library. No gems required.

    **2. Export the page and save the ZIP**

```ruby
require 'net/http'
require 'json'
require 'uri'

TOKEN = 'YOUR_API_TOKEN_HERE'

uri = URI("https://production-sfo.browserless.io/export?token=#{TOKEN}")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true

request = Net::HTTP::Post.new(uri)
request['Content-Type'] = 'application/json'
request.body = JSON.generate({ url: 'https://scraping-sandbox.netlify.app/harvest-direct', includeResources: true })

response = http.request(request)
raise "Export failed: #{response.code}" unless response.is_a?(Net::HTTPSuccess)

File.binwrite('page.zip', response.body)
puts 'Saved page.zip'
```

    **3. Check the output**

    Run with `ruby images.rb`. The ZIP is saved as `page.zip`.

    
  

  
  
**Frameworks:**

  Intercept network responses during page load to capture linked resources: stylesheets, scripts, images, and fonts. Puppeteer and Playwright use response interception; the Go tab extracts resource URLs from the DOM instead, covering stylesheets, scripts, and images. The HTML is saved via `page.content()`, and each captured asset is written alongside it.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Connect, intercept responses, and save assets**

```js
import fs from 'fs';
import path from 'path';
import puppeteer from 'puppeteer-core';

const TOKEN = 'YOUR_API_TOKEN_HERE';

const browser = await puppeteer.connect({
  browserWSEndpoint: `wss://production-sfo.browserless.io?token=${TOKEN}`,
});

try {
  const page = await browser.newPage();
  const assetPromises = [];

  page.on('response', (response) => {
    const type = response.request().resourceType();
    if (['stylesheet', 'script', 'image', 'font'].includes(type)) {
      assetPromises.push(
        response.buffer()
          .then((buf) => ({ url: response.url(), buf }))
          .catch(() => null)
      );
    }
  });

  await page.goto('https://scraping-sandbox.netlify.app/harvest-direct', { waitUntil: 'networkidle2' });

  const html = await page.content();
  const assets = (await Promise.all(assetPromises)).filter(Boolean);

  fs.mkdirSync('page', { recursive: true });
  fs.writeFileSync('page/index.html', html);
  console.log('Saved page/index.html');

  for (const [i, { url, buf }] of assets.entries()) {
    const ext = path.extname(new URL(url).pathname) || '';
    const filename = `asset-${i}${ext}`;
    fs.writeFileSync(`page/${filename}`, buf);
    console.log(`Saved page/${filename}`);
  }
} finally {
  await browser.close();
}
```

    **3. Check the output**

    Run with `node images.mjs`. The `page/` directory contains `index.html` and all intercepted assets.

    
    
**Playwright:**

    
      
**JavaScript:**

      

      **1. Install dependencies**

```bash
npm install playwright-core
```

      **2. Connect, intercept responses, and save assets**

```js
import fs from 'fs';
import path from 'path';
import { chromium } from 'playwright-core';

const TOKEN = 'YOUR_API_TOKEN_HERE';

const browser = await chromium.connectOverCDP(
  `wss://production-sfo.browserless.io?token=${TOKEN}`
);

try {
  // Use the default context — browser.newPage() creates a new context that
  // doesn't inherit proxy, profile, or launch settings.
  const context = browser.contexts()[0];
  const page = await context.newPage();
  const assetPromises = [];

  page.on('response', (response) => {
    const type = response.request().resourceType();
    if (['stylesheet', 'script', 'image', 'font'].includes(type)) {
      assetPromises.push(
        response.body()
          .then((buf) => ({ url: response.url(), buf }))
          .catch(() => null)
      );
    }
  });

  await page.goto('https://scraping-sandbox.netlify.app/harvest-direct', { waitUntil: 'networkidle' });

  const html = await page.content();
  const assets = (await Promise.all(assetPromises)).filter(Boolean);

  fs.mkdirSync('page', { recursive: true });
  fs.writeFileSync('page/index.html', html);
  console.log('Saved page/index.html');

  for (const [i, { url, buf }] of assets.entries()) {
    const ext = path.extname(new URL(url).pathname) || '';
    const filename = `asset-${i}${ext}`;
    fs.writeFileSync(`page/${filename}`, buf);
    console.log(`Saved page/${filename}`);
  }
} finally {
  await browser.close();
}
```

      **3. Check the output**

      Run with `node images.mjs`. The `page/` directory contains `index.html` and all intercepted assets.

      
      
**Python:**

      

      **1. Install dependencies**

```bash
pip install playwright
playwright install chromium
```

      **2. Connect, intercept responses, and save assets**

```python
import os
from urllib.parse import urlparse
from playwright.sync_api import sync_playwright

TOKEN = 'YOUR_API_TOKEN_HERE'
WS_ENDPOINT = f'wss://production-sfo.browserless.io?token={TOKEN}'

assets = []

def handle_response(response):
    if response.request.resource_type in ('stylesheet', 'script', 'image', 'font'):
        try:
            assets.append({'url': response.url, 'buf': response.body()})
        except Exception:
            pass

with sync_playwright() as playwright:
    browser = playwright.chromium.connect_over_cdp(WS_ENDPOINT)
    try:
        # Use the default context — browser.new_page() creates a new context that
        # doesn't inherit proxy, profile, or launch settings.
        context = browser.contexts[0]
        page = context.new_page()
        page.on('response', handle_response)
        page.goto('https://scraping-sandbox.netlify.app/harvest-direct')
        page.wait_for_load_state('networkidle')

        html = page.content()

        os.makedirs('page', exist_ok=True)
        with open('page/index.html', 'w', encoding='utf-8') as f:
            f.write(html)
        print('Saved page/index.html')

        for i, asset in enumerate(assets):
            ext = os.path.splitext(urlparse(asset['url']).path)[1]
            filename = f'page/asset-{i}{ext}'
            with open(filename, 'wb') as f:
                f.write(asset['buf'])
            print(f'Saved {filename}')
    finally:
        browser.close()
```

      **3. Check the output**

      Run with `python images.py`. The `page/` directory contains `index.html` and all intercepted assets.

      
      
**Java:**

      

      **1. Install dependencies**

      Add the Playwright dependency to your `pom.xml`:

```xml
<dependency>
  <groupId>com.microsoft.playwright</groupId>
  <artifactId>playwright</artifactId>
  <version>1.44.0</version>
</dependency>
```

      **2. Connect, intercept responses, and save assets**

```java
import com.microsoft.playwright.*;
import java.nio.file.*;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;

public class DownloadImages {
    record Asset(String url, byte[] buf) {}

    public static void main(String[] args) throws Exception {
        String TOKEN = "YOUR_API_TOKEN_HERE";
        String WS_ENDPOINT = "wss://production-sfo.browserless.io?token=" + TOKEN;

        try (Playwright playwright = Playwright.create()) {
            Browser browser = playwright.chromium().connectOverCDP(WS_ENDPOINT);
            try {
                BrowserContext context = browser.contexts().get(0);
                Page page = context.newPage();
                List assets = new ArrayList<>();

                page.onResponse(response -> {
                    String type = response.request().resourceType();
                    if (List.of("stylesheet", "script", "image", "font").contains(type)) {
                        try {
                            assets.add(new Asset(response.url(), response.body()));
                        } catch (Exception ignored) {}
                    }
                });

                page.navigate("https://scraping-sandbox.netlify.app/harvest-direct");
                page.waitForLoadState(LoadState.NETWORKIDLE);

                String html = page.content();

                Files.createDirectories(Path.of("page"));
                Files.writeString(Path.of("page/index.html"), html);
                System.out.println("Saved page/index.html");

                for (int i = 0; i < assets.size(); i++) {
                    String urlPath = new URI(assets.get(i).url()).getPath();
                    int dot = urlPath.lastIndexOf('.');
                    String ext = dot >= 0 ? urlPath.substring(dot) : "";
                    Path dest = Path.of("page/asset-" + i + ext);
                    Files.write(dest, assets.get(i).buf());
                    System.out.println("Saved " + dest);
                }
            } finally {
                browser.close();
            }
        }
    }
}
```

      **3. Check the output**

      Compile with `mvn compile` and run with `mvn exec:java`. The `page/` directory contains `index.html` and all intercepted assets.

      
      
**C#:**

      

      **1. Install dependencies**

```bash
dotnet add package Microsoft.Playwright
playwright install chromium
```

      **2. Connect, intercept responses, and save assets**

```csharp
using Microsoft.Playwright;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

class DownloadImages
{
    static async Task Main()
    {
        const string TOKEN = "YOUR_API_TOKEN_HERE";
        string WS_ENDPOINT = $"wss://production-sfo.browserless.io?token={TOKEN}";

        using var playwright = await Playwright.CreateAsync();
        var browser = await playwright.Chromium.ConnectOverCDPAsync(WS_ENDPOINT);

        try
        {
            var context = browser.Contexts[0];
            var page = await context.NewPageAsync();
            var assetTasks = new List>();

            page.Response += (_, response) =>
            {
                var type = response.Request.ResourceType;
                if (type is "stylesheet" or "script" or "image" or "font")
                {
                    var url = response.Url;
                    assetTasks.Add(response.BodyAsync()
                        .ContinueWith(t => t.IsCompletedSuccessfully
                            ? ((string, byte[])?) (url, t.Result)
                            : null));
                }
            };

            await page.GotoAsync("https://scraping-sandbox.netlify.app/harvest-direct",
                new() { WaitUntil = WaitUntilState.NetworkIdle });

            var html = await page.ContentAsync();
            var assets = (await Task.WhenAll(assetTasks))
                .Where(a => a.HasValue)
                .Select(a => a!.Value);

            Directory.CreateDirectory("page");
            await File.WriteAllTextAsync("page/index.html", html);
            Console.WriteLine("Saved page/index.html");

            int i = 0;
            foreach (var (url, buf) in assets)
            {
                string ext = Path.GetExtension(new Uri(url).AbsolutePath);
                string filename = $"page/asset-{i}{ext}";
                await File.WriteAllBytesAsync(filename, buf);
                Console.WriteLine($"Saved {filename}");
                i++;
            }
        }
        finally
        {
            await browser.CloseAsync();
        }
    }
}
```

      **3. Check the output**

      Run with `dotnet run`. The `page/` directory contains `index.html` and all intercepted assets.

      
    

    
    
**Go (chromedp):**

    

    **1. Install dependencies**

```bash
go get github.com/chromedp/chromedp
```

    **2. Connect, extract HTML and resource URLs, and download each asset**

```go
package main

import (
    "context"
    "fmt"
    "io"
    "net/http"
    "os"
    "path/filepath"
    "strings"

    "github.com/chromedp/chromedp"
)

func main() {
    token := "YOUR_API_TOKEN_HERE"
    ws := fmt.Sprintf("wss://production-sfo.browserless.io?token=%s", token)

    allocCtx, cancel := chromedp.NewRemoteAllocator(context.Background(), ws, chromedp.NoModifyURL)
    defer cancel()

    ctx, cancel := chromedp.NewContext(allocCtx)
    defer cancel()

    var html string
    var resourceURLs []string

    if err := chromedp.Run(ctx,
        chromedp.Navigate("https://scraping-sandbox.netlify.app/harvest-direct"),
        chromedp.WaitReady("body"),
        chromedp.OuterHTML("html", &html),
        chromedp.Evaluate(`[
            ...Array.from(document.querySelectorAll('link[href]'), el => el.href),
            ...Array.from(document.querySelectorAll('script[src]'), el => el.src),
            ...Array.from(document.querySelectorAll('img[src]'), el => el.src),
        ].filter(url => url.startsWith('http'))`, &resourceURLs),
    ); err != nil {
        panic(err)
    }

    os.MkdirAll("page", 0755)
    os.WriteFile("page/index.html", []byte(html), 0644)
    fmt.Println("Saved page/index.html")

    for i, url := range resourceURLs {
        resp, err := http.Get(url)
        if err != nil {
            continue
        }
        data, _ := io.ReadAll(resp.Body)
        resp.Body.Close()

        ext := filepath.Ext(strings.Split(url, "?")[0])
        filename := fmt.Sprintf("page/asset-%d%s", i, ext)
        os.WriteFile(filename, data, 0644)
        fmt.Println("Saved", filename)
    }
}
```

    **3. Check the output**

    Run with `go run main.go`. The `page/` directory contains `index.html` and all downloaded assets.

    
  

  

## Next steps

  <a href="/examples/scrape" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape Structured Data</h3>
      extract text and structured data from pages
    
  </a>
  <a href="/examples/screenshot" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Take a Screenshot</h3>
      capture a visual snapshot of the full page
    
  </a>

---

# Run end-to-end tests

URL: https://docs.browserless.io/examples/e2e-testing

# Run end-to-end tests

Run browser tests against Browserless without installing or managing Chrome locally.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

  
**AI Agent:**

  Use the [Browserless MCP server](https://github.com/browserless/browserless-mcp) to run end-to-end tests from any MCP-compatible AI agent (Claude Desktop, Cursor, Windsurf, ChatGPT, etc.).

  **1. Connect the MCP server**

  Send this prompt to your AI agent to install the Browserless MCP server:

```text
Go to https://github.com/browserless/browserless-mcp/blob/main/install.md
and follow the instructions to install the Browserless MCP server
for my client.
```

  **2. Run an E2E test**

  Use `browserless_agent`. E2E testing requires multi-step interaction with the page.

```text
Use the browserless_agent tool to go to https://scraping-sandbox.netlify.app/products,
verify the page loads correctly, check the product catalog heading
is visible, click on a product, and confirm the product details appear
```

  
  
**REST API:**

  The `/function` endpoint lets you send a browser script as the request body and get structured JSON back. It's the fastest way to drop a smoke test into a CI pipeline without adding a framework dependency. You're just sending HTTP and checking a `passed` boolean.

  
    
**cURL:**

    

    **1. Write the test script**

    The `/function` endpoint accepts a JavaScript module as the request body. The exported function receives a `page` object and can return any JSON-serializable value:

    **2. Send the request**

```bash
curl -X POST \
  "https://production-sfo.browserless.io/function?token=YOUR_API_TOKEN_HERE" \
  -H "Content-Type: application/javascript" \
  --data-raw 'module.exports = async ({ page }) => {
    await page.goto("https://scraping-sandbox.netlify.app/products");

    const title = await page.title();
    const heading = await page.$eval("h1", el => el.textContent.trim());
    const hasLink = (await page.$("a[href]")) !== null;

    const results = [
      { test: "title includes Product Catalog", passed: title.includes("Product Catalog") },
      { test: "h1 reads Product Catalog", passed: heading === "Product Catalog" },
      { test: "page has at least one link", passed: hasLink },
    ];

    return { passed: results.every(r => r.passed), results };
  };'
```

    **3. Check the output**

```json
{
  "passed": true,
  "results": [
    { "test": "title includes Product Catalog", "passed": true },
    { "test": "h1 reads Product Catalog", "passed": true },
    { "test": "page has at least one link", "passed": true }
  ]
}
```

    
    
**JavaScript:**

    

    **1. Send the request**

```js
const code = `module.exports = async ({ page }) => {
  await page.goto('https://scraping-sandbox.netlify.app/products');

  const title = await page.title();
  const heading = await page.$eval('h1', el => el.textContent.trim());
  const hasLink = (await page.$('a[href]')) !== null;

  const results = [
    { test: 'title includes Product Catalog', passed: title.includes('Product Catalog') },
    { test: 'h1 reads Product Catalog', passed: heading === 'Product Catalog' },
    { test: 'page has at least one link', passed: hasLink },
  ];

  return { passed: results.every(r => r.passed), results };
};`;

const response = await fetch(
  'https://production-sfo.browserless.io/function?token=YOUR_API_TOKEN_HERE',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/javascript' },
    body: code,
  }
);

const { passed, results } = await response.json();
results.forEach(r => console.log(`${r.passed ? '✓' : '✗'} ${r.test}`));
if (!passed) process.exit(1);
```

    **2. Check the output**

    Run with `node e2e-testing.mjs`. Each test prints a pass or fail line. The process exits with code `1` if any test fails, so CI systems pick up the failure automatically.

    
    
**Python:**

    

    **1. Install dependencies**

```bash
pip install requests
```

    **2. Send the request**

```python
import sys
import requests

code = """
module.exports = async ({ page }) => {
  await page.goto('https://scraping-sandbox.netlify.app/products');

  const title = await page.title();
  const heading = await page.$eval('h1', el => el.textContent.trim());
  const hasLink = (await page.$('a[href]')) !== null;

  const results = [
    { test: 'title includes Product Catalog', passed: title.includes('Product Catalog') },
    { test: 'h1 reads Product Catalog', passed: heading === 'Product Catalog' },
    { test: 'page has at least one link', passed: hasLink },
  ];

  return { passed: results.every(r => r.passed), results };
};
"""

response = requests.post(
    'https://production-sfo.browserless.io/function?token=YOUR_API_TOKEN_HERE',
    headers={'Content-Type': 'application/javascript'},
    data=code.encode('utf-8'),
)

data = response.json()
for result in data['results']:
    icon = '✓' if result['passed'] else '✗'
    print(f"{icon} {result['test']}")

if not data['passed']:
    sys.exit(1)
```

    **3. Check the output**

    Run with `python e2e_testing.py`. The process exits with code `1` if any test fails.

    
    
**Java:**

    

    **1. Send the request**

```java
import java.net.URI;
import java.net.http.*;

String token = "YOUR_API_TOKEN_HERE";
String endpoint = "https://production-sfo.browserless.io/function?token=" + token;

String code = """
    module.exports = async ({ page }) => {
      await page.goto('https://scraping-sandbox.netlify.app/products');
      const title = await page.title();
      const heading = await page.$eval('h1', el => el.textContent.trim());
      const hasLink = (await page.$('a[href]')) !== null;
      const results = [
        { test: 'title includes Product Catalog', passed: title.includes('Product Catalog') },
        { test: 'h1 reads Product Catalog', passed: heading === 'Product Catalog' },
        { test: 'page has at least one link', passed: hasLink },
      ];
      return { passed: results.every(r => r.passed), results };
    };
    """;

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(endpoint))
    .header("Content-Type", "application/javascript")
    .POST(HttpRequest.BodyPublishers.ofString(code))
    .build();

HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
```

    **2. Check the output**

    Run the class. The response body is JSON with a `passed` boolean and a `results` array containing one entry per assertion.

    
    
**C#:**

    

    **1. Send the request**

```csharp
using System.Net.Http;
using System.Text;

string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"https://production-sfo.browserless.io/function?token={token}";

string code = @"module.exports = async ({ page }) => {
  await page.goto('https://scraping-sandbox.netlify.app/products');
  const title = await page.title();
  const heading = await page.$eval('h1', el => el.textContent.trim());
  const hasLink = (await page.$('a[href]')) !== null;
  const results = [
    { test: 'title includes Product Catalog', passed: title.includes('Product Catalog') },
    { test: 'h1 reads Product Catalog', passed: heading === 'Product Catalog' },
    { test: 'page has at least one link', passed: hasLink },
  ];
  return { passed: results.every(r => r.passed), results };
};";

using (HttpClient httpClient = new HttpClient())
{
    var content = new StringContent(code, Encoding.UTF8, "application/javascript");
    var response = await httpClient.PostAsync(endpoint, content);
    string result = await response.Content.ReadAsStringAsync();
    Console.WriteLine(result);
}
```

    **2. Check the output**

    Run the program. The response body is JSON with a `passed` boolean and a `results` array containing one entry per assertion.

    
  

  
  
**Frameworks:**

  Use a browser framework when you need a full assertion library, multi-step flows, or parallel test execution. Frameworks give you retrying assertions, test lifecycle hooks, and reporters that the raw REST API doesn't provide.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Connect and run assertions**

```js
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint: 'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE',
});

const results = [];

try {
  const page = await browser.newPage();
  await page.goto('https://scraping-sandbox.netlify.app/products', { waitUntil: 'networkidle2' });

  const title = await page.title();
  results.push({
    test: 'title includes "Product Catalog"',
    passed: title.includes('Product Catalog'),
  });

  const heading = await page.$eval('h1', el => el.textContent.trim());
  results.push({
    test: 'h1 reads "Product Catalog"',
    passed: heading === 'Product Catalog',
  });

  const link = await page.$('a[href]');
  results.push({
    test: 'page has at least one link',
    passed: link !== null,
  });
} finally {
  await browser.close();
}

const failed = results.filter(r => !r.passed);
results.forEach(r => console.log(`${r.passed ? '✓' : '✗'} ${r.test}`));
if (failed.length) {
  console.error(`\n${failed.length} test(s) failed`);
  process.exit(1);
}
```

    **3. Check the output**

    Run with `node e2e-testing.mjs`. The process exits with code `1` if any assertion fails, so your CI system registers it as a failure.

```text
✓ title includes "Product Catalog"
✓ h1 reads "Product Catalog"
✓ page has at least one link
```

    
    
**Playwright (JS):**

    

    **1. Install dependencies**

```bash
npm install @playwright/test playwright-core
```

    **2. Configure Browserless**

    Add a `playwright.config.js` file. The `wsEndpoint` uses `/chromium/playwright` rather than a plain CDP endpoint because Playwright's connection protocol requires the dedicated Playwright path. Plain CDP won't negotiate the right capabilities. Set `workers` to control parallelism, `retries: 1` so flaky tests get one retry, and `trace: 'on-first-retry'` so Playwright captures a trace zip only on that retry. Skipping it on the first pass keeps things fast, and you only need it when something actually fails:

```js
import { defineConfig } from '@playwright/test';

export default defineConfig({
  workers: 4,
  retries: 1,
  use: {
    trace: 'on-first-retry',
    connectOptions: {
      wsEndpoint: 'wss://production-sfo.browserless.io/chromium/playwright?token=YOUR_API_TOKEN_HERE',
    },
  },
});
```

    **3. Write tests**

    Playwright's `expect()` assertions are web-first. They poll the DOM until the condition passes or the timeout expires, so you don't need manual waits. Use `page.route()` inside individual tests to intercept or mock specific network requests without affecting other tests:

```js
// tests/example.spec.js
import { test, expect } from '@playwright/test';

test('page has correct title', async ({ page }) => {
  await page.goto('https://scraping-sandbox.netlify.app/products');
  await expect(page).toHaveTitle(/Product Catalog/);
});

test('heading is visible and correct', async ({ page }) => {
  await page.goto('https://scraping-sandbox.netlify.app/products');
  const heading = page.locator('h1');
  await expect(heading).toBeVisible();
  await expect(heading).toHaveText('Product Catalog');
});

test('page contains a link', async ({ page }) => {
  await page.goto('https://scraping-sandbox.netlify.app/products');
  await expect(page.locator('a[href]').first()).toBeVisible();
});

test('page loads without analytics calls', async ({ page }) => {
  const blocked = [];
  await page.route('**/analytics/**', route => {
    blocked.push(route.request().url());
    route.abort();
  });
  await page.goto('https://scraping-sandbox.netlify.app/products');
  await expect(page).toHaveTitle(/Product Catalog/);
  console.log(`Blocked ${blocked.length} analytics request(s)`);
});
```

    **4. Run the tests**

```bash
npx playwright test
```

    **5. Cross-browser testing**

    Browserless exposes separate endpoints for Chromium, Firefox, and WebKit. Use Playwright's `projects` array to target all three from the same config. Each project connects to its own endpoint and Playwright runs them as distinct test suites:

```js
import { defineConfig } from '@playwright/test';

const token = 'YOUR_API_TOKEN_HERE';

export default defineConfig({
  workers: 4,
  retries: 1,
  use: { trace: 'on-first-retry' },
  projects: [
    {
      name: 'chromium',
      use: {
        connectOptions: {
          wsEndpoint: `wss://production-sfo.browserless.io/chromium/playwright?token=${token}`,
        },
      },
    },
    {
      name: 'firefox',
      use: {
        connectOptions: {
          wsEndpoint: `wss://production-sfo.browserless.io/firefox/playwright?token=${token}`,
        },
      },
    },
    {
      name: 'webkit',
      use: {
        connectOptions: {
          wsEndpoint: `wss://production-sfo.browserless.io/webkit/playwright?token=${token}`,
        },
      },
    },
  ],
});
```

    Run with `npx playwright test`. Playwright sends each test to the matching Browserless browser.

    **6. Debug failed tests with Trace Viewer**

    With `trace: 'on-first-retry'` set, Playwright writes a `trace.zip` to `test-results/` whenever a test fails its first run. Open it locally:

```bash
npx playwright show-trace test-results/<test-name>/trace.zip
```

    The Trace Viewer gives you a step-by-step timeline with DOM snapshots, network requests, and console logs. No SSH or access to the remote machine needed.

    **7. Check the output**

```text
Running 4 tests using 4 workers

  ✓  [chromium] › example.spec.js:3 › page has correct title (1.2s)
  ✓  [chromium] › example.spec.js:8 › heading is visible and correct (0.9s)
  ✓  [chromium] › example.spec.js:14 › page contains a link (0.7s)
  ✓  [chromium] › example.spec.js:20 › page loads without analytics calls (1.1s)

  4 passed (1.5s)
```

    
    
**Playwright (Python):**

    

    **1. Install dependencies**

```bash
pip install pytest pytest-playwright playwright pytest-xdist
playwright install
```

    **2. Configure Browserless**

    Override the `browser` fixture in `conftest.py` to connect to Browserless instead of launching a local browser. The `scope="session"` means a single browser connection is shared across all tests in the run. Individual tests each get their own `page` from the default `page` fixture:

```python
# conftest.py
import pytest
from playwright.sync_api import Playwright

@pytest.fixture(scope="session")
def browser(playwright: Playwright):
    browser = playwright.chromium.connect(
        "wss://production-sfo.browserless.io/chromium/playwright?token=YOUR_API_TOKEN_HERE"
    )
    yield browser
    browser.close()
```

    **3. Write tests**

```python
# tests/test_example.py
import re
from playwright.sync_api import expect

def test_page_has_correct_title(page):
    page.goto("https://scraping-sandbox.netlify.app/products")
    expect(page).to_have_title(re.compile(r"Product Catalog"))

def test_heading_is_visible(page):
    page.goto("https://scraping-sandbox.netlify.app/products")
    heading = page.locator("h1")
    expect(heading).to_be_visible()
    expect(heading).to_have_text("Product Catalog")

def test_page_has_link(page):
    page.goto("https://scraping-sandbox.netlify.app/products")
    expect(page.locator("a[href]").first).to_be_visible()

def test_page_loads_without_analytics(page):
    blocked = []

    def handle_route(route):
        blocked.append(route.request.url)
        route.abort()

    page.route("**/analytics/**", handle_route)
    page.goto("https://scraping-sandbox.netlify.app/products")
    expect(page).to_have_title(re.compile(r"Product Catalog"))
    print(f"Blocked {len(blocked)} analytics request(s)")
```

    **4. Run the tests**

```bash
pytest tests/ -n 4
```

    The `-n 4` flag (from `pytest-xdist`) runs 4 tests in parallel, each getting its own Browserless session.

    **5. Cross-browser testing**

    Parameterize the `browser` fixture so every test runs against Chromium, Firefox, and WebKit. Each parameterized value produces a separate `browser` instance connecting to its own endpoint:

```python
# conftest.py
import pytest
from playwright.sync_api import Playwright

TOKEN = "YOUR_API_TOKEN_HERE"
ENDPOINTS = {
    "chromium": f"wss://production-sfo.browserless.io/chromium/playwright?token={TOKEN}",
    "firefox":  f"wss://production-sfo.browserless.io/firefox/playwright?token={TOKEN}",
    "webkit":   f"wss://production-sfo.browserless.io/webkit/playwright?token={TOKEN}",
}

@pytest.fixture(scope="session", params=["chromium", "firefox", "webkit"])
def browser(playwright: Playwright, request):
    name = request.param
    b = getattr(playwright, name).connect(ENDPOINTS[name])
    yield b
    b.close()
```

    **6. Debug with Trace Viewer**

    Pass `--tracing retain-on-failure` to capture traces only for failing tests, so you're not storing trace zips for every passing run:

```bash
pytest --tracing retain-on-failure
```

    Traces are saved to `test-results/`. Open one with:

```bash
playwright show-trace test-results/<test-name>/trace.zip
```

    **7. Check the output**

```text
PASSED tests/test_example.py::test_page_has_correct_title
PASSED tests/test_example.py::test_heading_is_visible
PASSED tests/test_example.py::test_page_has_link
PASSED tests/test_example.py::test_page_loads_without_analytics

4 passed in 2.3s
```

    
    
**Playwright (Java):**

    

    **1. Install dependencies**

    Add to `pom.xml`:

```xml
<dependency>
    <groupId>com.microsoft.playwright</groupId>
    <artifactId>playwright</artifactId>
    <version>1.40.0</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.10.0</version>
    <scope>test</scope>
</dependency>
```

    **2. Set up the test class**

    Connect to Browserless once in `@BeforeAll` so the WebSocket handshake only happens once per suite. Open a fresh context per test in `@BeforeEach`. Separate contexts give each test isolated cookies, storage, and network state, so tests can't bleed state into each other:

```java
import com.microsoft.playwright.*;
import org.junit.jupiter.api.*;

class ExampleTest {
    static Playwright playwright;
    static Browser browser;
    BrowserContext context;
    Page page;

    @BeforeAll
    static void connect() {
        playwright = Playwright.create();
        browser = playwright.chromium().connect(
            "wss://production-sfo.browserless.io/chromium/playwright?token=YOUR_API_TOKEN_HERE"
        );
    }

    @AfterAll
    static void disconnect() {
        playwright.close();
    }

    @BeforeEach
    void createContextAndPage() {
        context = browser.newContext();
        page = context.newPage();
    }

    @AfterEach
    void closeContext() {
        context.close();
    }
}
```

    **3. Write tests**

```java
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
import java.util.*;
import java.util.regex.Pattern;

@Test
void pageHasCorrectTitle() {
    page.navigate("https://scraping-sandbox.netlify.app/products");
    assertThat(page).hasTitle(Pattern.compile("Product Catalog"));
}

@Test
void headingIsVisible() {
    page.navigate("https://scraping-sandbox.netlify.app/products");
    Locator heading = page.locator("h1");
    assertThat(heading).isVisible();
    assertThat(heading).hasText("Product Catalog");
}

@Test
void pageHasLink() {
    page.navigate("https://scraping-sandbox.netlify.app/products");
    assertThat(page.locator("a[href]").first()).isVisible();
}

@Test
void pageLoadsWithoutAnalytics() {
    List blocked = new ArrayList<>();
    page.route("**/analytics/**", route -> {
        blocked.add(route.request().url());
        route.abort();
    });
    page.navigate("https://scraping-sandbox.netlify.app/products");
    assertThat(page).hasTitle(Pattern.compile("Product Catalog"));
    System.out.println("Blocked " + blocked.size() + " analytics request(s)");
}
```

    **4. Run the tests**

```bash
mvn test
```

    To run in parallel, configure the Surefire plugin in `pom.xml`:

```xml
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <configuration>
        <parallel>methods</parallel>
        <threadCount>4</threadCount>
    </configuration>
</plugin>
```

    **5. Cross-browser testing**

    Use `@ParameterizedTest` with `@MethodSource` to run the same test method against each browser endpoint. Each invocation opens its own `Playwright` instance so browser types don't share state:

```java
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;
import java.util.stream.Stream;

static Stream browserEndpoints() {
    String token = "YOUR_API_TOKEN_HERE";
    String base = "wss://production-sfo.browserless.io";
    return Stream.of(
        Arguments.of("chromium", base + "/chromium/playwright?token=" + token),
        Arguments.of("firefox",  base + "/firefox/playwright?token="  + token),
        Arguments.of("webkit",   base + "/webkit/playwright?token="   + token)
    );
}

@ParameterizedTest(name = "{0}")
@MethodSource("browserEndpoints")
void pageHasCorrectTitle(String browserName, String wsEndpoint) {
    try (Playwright pw = Playwright.create()) {
        Browser b = pw.chromium().connect(wsEndpoint);
        Page p = b.newPage();
        p.navigate("https://scraping-sandbox.netlify.app/products");
        assertThat(p).hasTitle(Pattern.compile("Product Catalog"));
    }
}
```

    **6. Debug with Trace Viewer**

    Start and stop tracing in `@BeforeEach` / `@AfterEach` so each test gets its own trace file:

```java
import java.nio.file.Paths;
import org.junit.jupiter.api.TestInfo;

@BeforeEach
void createContextAndPage() {
    context = browser.newContext();
    context.tracing().start(new Tracing.StartOptions()
        .setScreenshots(true).setSnapshots(true));
    page = context.newPage();
}

@AfterEach
void closeContext(TestInfo testInfo) {
    context.tracing().stop(new Tracing.StopOptions()
        .setPath(Paths.get("trace-" + testInfo.getDisplayName() + ".zip")));
    context.close();
}
```

    Open the trace:

```bash
mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="show-trace trace.zip"
```

    **7. Check the output**

```text
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0
```

    
    
**Playwright (C#):**

    

    **1. Install dependencies**

```bash
dotnet add package Microsoft.Playwright.NUnit
```

    **2. Set up the test class**

    Connect to Browserless once in `[OneTimeSetUp]` so the WebSocket handshake only happens once per suite. Open a fresh context per test in `[SetUp]`. Separate contexts give each test isolated cookies, storage, and network state, so tests can't bleed state into each other:

```csharp
using Microsoft.Playwright;
using NUnit.Framework;
using System.Collections.Generic;
using System.Threading.Tasks;

[Parallelizable(ParallelScope.Self)]
[TestFixture]
public class ExampleTests
{
    private IPlaywright _playwright;
    private IBrowser _browser;
    private IBrowserContext _context;
    private IPage _page;

    [OneTimeSetUp]
    public async Task OneTimeSetUp()
    {
        _playwright = await Playwright.CreateAsync();
        _browser = await _playwright.Chromium.ConnectAsync(
            "wss://production-sfo.browserless.io/chromium/playwright?token=YOUR_API_TOKEN_HERE"
        );
    }

    [SetUp]
    public async Task SetUp()
    {
        _context = await _browser.NewContextAsync();
        _page = await _context.NewPageAsync();
    }

    [TearDown]
    public async Task TearDown()
    {
        await _context.CloseAsync();
    }

    [OneTimeTearDown]
    public async Task OneTimeTearDown()
    {
        await _playwright.DisposeAsync();
    }
}
```

    **3. Write tests**

```csharp
using System.Text.RegularExpressions;

[Test]
public async Task PageHasCorrectTitle()
{
    await _page.GotoAsync("https://scraping-sandbox.netlify.app/products");
    await Assertions.Expect(_page).ToHaveTitleAsync(new Regex("Product Catalog"));
}

[Test]
public async Task HeadingIsVisible()
{
    await _page.GotoAsync("https://scraping-sandbox.netlify.app/products");
    var heading = _page.Locator("h1");
    await Assertions.Expect(heading).ToBeVisibleAsync();
    await Assertions.Expect(heading).ToHaveTextAsync("Product Catalog");
}

[Test]
public async Task PageHasLink()
{
    await _page.GotoAsync("https://scraping-sandbox.netlify.app/products");
    await Assertions.Expect(_page.Locator("a[href]").First).ToBeVisibleAsync();
}

[Test]
public async Task PageLoadsWithoutAnalytics()
{
    var blocked = new List<string>();
    await _page.RouteAsync("**/analytics/**", async route =>
    {
        blocked.Add(route.Request.Url);
        await route.AbortAsync();
    });
    await _page.GotoAsync("https://scraping-sandbox.netlify.app/products");
    await Assertions.Expect(_page).ToHaveTitleAsync(new Regex("Product Catalog"));
    Console.WriteLine($"Blocked {blocked.Count} analytics request(s)");
}
```

    **4. Run the tests**

```bash
dotnet test
```

    To configure parallelism, add a `.runsettings` file:

```xml
4
```

    Then run with `dotnet test --settings .runsettings`.

    **5. Cross-browser testing**

    Use NUnit's `[TestCaseSource]` to feed each browser endpoint as a separate test case. Each case creates its own `Playwright` instance so browser types don't share state:

```csharp
private static IEnumerable BrowserEndpoints()
{
    var token = "YOUR_API_TOKEN_HERE";
    var @base = "wss://production-sfo.browserless.io";
    yield return new TestCaseData("chromium", $"{@base}/chromium/playwright?token={token}");
    yield return new TestCaseData("firefox",  $"{@base}/firefox/playwright?token={token}");
    yield return new TestCaseData("webkit",   $"{@base}/webkit/playwright?token={token}");
}

[Test, TestCaseSource(nameof(BrowserEndpoints))]
public async Task PageHasCorrectTitle(string browserName, string wsEndpoint)
{
    var playwright = await Playwright.CreateAsync();
    var browser = await playwright.Chromium.ConnectAsync(wsEndpoint);
    var page = await browser.NewPageAsync();
    await page.GotoAsync("https://scraping-sandbox.netlify.app/products");
    await Assertions.Expect(page).ToHaveTitleAsync(new Regex("Product Catalog"));
    await playwright.DisposeAsync();
}
```

    **6. Debug with Trace Viewer**

    Start tracing in `[SetUp]` and stop it in `[TearDown]` so each test gets its own trace file:

```csharp
[SetUp]
public async Task SetUp()
{
    _context = await _browser.NewContextAsync();
    await _context.Tracing.StartAsync(new TracingStartOptions
    {
        Screenshots = true,
        Snapshots = true,
    });
    _page = await _context.NewPageAsync();
}

[TearDown]
public async Task TearDown()
{
    await _context.Tracing.StopAsync(new TracingStopOptions
    {
        Path = $"trace-{TestContext.CurrentContext.Test.Name}.zip",
    });
    await _context.CloseAsync();
}
```

    Open the trace:

```bash
pwsh bin/Debug/net8.0/playwright.ps1 show-trace trace.zip
```

    **7. Check the output**

```text
Test Run Successful.
Total tests: 4
     Passed: 4
 Total time: 2.5000 Seconds
```

    
  

  
  
**BQL:**

  

  **1. Write the mutation**

  Navigate to the page and select the content you want to verify. BQL returns typed structured data, so you write your assertions in your own application code after the response arrives. No assertion library runs inside Browserless:

```graphql
mutation E2ECheck {
  goto(url: "https://scraping-sandbox.netlify.app/products", waitUntil: domContentLoaded) {
    status
  }

  title: mapSelector(selector: "title") {
    innerText
  }

  heading: mapSelector(selector: "h1") {
    innerText
  }

  links: mapSelector(selector: "a[href]") {
    href: attribute(name: "href") {
      value
    }
  }
}
```

  **2. Run it**

  Paste into the [BQL IDE](https://account.browserless.io/bql) and click Run.

  **3. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "title": [{ "innerText": "Product Catalog" }],
    "heading": [{ "innerText": "Product Catalog" }],
    "links": [{ "href": { "value": "https://scraping-sandbox.netlify.app/contact-us" } }]
  }
}
```

  Assert against the values in your application code. Check `goto.status` for the HTTP status code, `innerText` fields for content, array lengths for element counts, and `attribute` fields for link targets.

  

## Next steps

  <a href="/examples/concurrent-sessions" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Run Concurrent Browser Sessions</h3>
      run tests in parallel across multiple browsers
    
  </a>
  <a href="/examples/sessions" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Log In and Reuse Sessions</h3>
      authenticate once and reuse state across test runs
    
  </a>
  <a href="/examples/reconnect" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Disconnect and Reconnect to a Browser</h3>
      pause and resume long-running test sessions
    
  </a>

---

# Log in via email OTP

URL: https://docs.browserless.io/examples/email-otp

# Log in via email OTP

Automate login flows that send a one-time passcode (OTP) to an email address. Navigate the login page, trigger the OTP email, read the code, and enter it.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)
- An email inbox you can query programmatically (e.g. [Mailosaur](https://mailosaur.com), [Mailslurp](https://mailslurp.com), or your own IMAP server)

## Steps

The flow has three stages: trigger the OTP email, read the code from the inbox, then enter it in the browser. The examples below show the browser automation steps. Substitute `getOtpFromInbox()` with whichever email API you use.

  
**Frameworks:**

  Connect a browser to trigger the OTP email, wait for the OTP field to appear, then enter the code.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Trigger OTP, read code, enter it**

```js
import puppeteer from 'puppeteer-core';

// Swap this stub with your actual inbox API — see the Inbox integrations table below.
async function getOtpFromInbox(email) {
  throw new Error('Implement getOtpFromInbox() with your email provider');
}

const browser = await puppeteer.connect({
  browserWSEndpoint: 'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE',
});

try {
  const page = await browser.newPage();

  // Submit the email to trigger the OTP — the form changes state before the OTP field appears.
  await page.goto('https://app.example.com/login', { waitUntil: 'networkidle2' });
  await page.type('input[type="email"]', 'user@example.com');
  await page.click('button[type="submit"]');
  await page.waitForSelector('input[name="otp"], input[autocomplete="one-time-code"]');

  // Poll the inbox after the OTP field appears, not before — the email may not be sent yet.
  const otp = await getOtpFromInbox('user@example.com');
  console.log('Got OTP:', otp);

  await page.type('input[name="otp"], input[autocomplete="one-time-code"]', otp);
  await page.click('button[type="submit"]');
  await page.waitForNavigation({ waitUntil: 'networkidle2' });

  console.log('Logged in. URL:', page.url());
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Run with `node otp.mjs`. Once you supply an `getOtpFromInbox()` implementation, the script completes the full email OTP login flow.

    
    
**Playwright:**

    
      
**JavaScript:**

      

      **1. Install dependencies**

```bash
npm install playwright-core
```

      **2. Trigger OTP, read code, enter it**

```js
import { chromium } from 'playwright-core';

// Swap this stub with your actual inbox API — see the Inbox integrations table below.
async function getOtpFromInbox(email) {
  throw new Error('Implement getOtpFromInbox() with your email provider');
}

const browser = await chromium.connectOverCDP(
  'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE'
);

try {
  // Use the default context — browser.newPage() creates a new context that
  // doesn't inherit proxy, profile, or launch settings.
  const context = browser.contexts()[0];
  const page = await context.newPage();

  // Submit the email to trigger the OTP — the form changes state before the OTP field appears.
  await page.goto('https://app.example.com/login', { waitUntil: 'networkidle' });
  await page.fill('input[type="email"]', 'user@example.com');
  await page.click('button[type="submit"]');
  await page.waitForSelector('input[name="otp"], input[autocomplete="one-time-code"]');

  // Poll the inbox after the OTP field appears, not before — the email may not be sent yet.
  const otp = await getOtpFromInbox('user@example.com');
  console.log('Got OTP:', otp);

  await page.fill('input[name="otp"], input[autocomplete="one-time-code"]', otp);
  await page.click('button[type="submit"]');
  await page.waitForLoadState('networkidle');

  console.log('Logged in. URL:', page.url());
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

      **3. Check the output**

      Run with `node otp.mjs`. Supply your inbox polling implementation to complete the flow end-to-end.

      
      
**Python:**

      

      **1. Install dependencies**

```bash
pip install playwright
```

      **2. Trigger OTP, read code, enter it**

```python
from playwright.sync_api import sync_playwright

def get_otp_from_inbox(email: str) -> str:
    # Swap this stub with your actual inbox API — see the Inbox integrations table below.
    raise NotImplementedError('Implement get_otp_from_inbox() with your email provider')

TOKEN = 'YOUR_API_TOKEN_HERE'
WS_ENDPOINT = f'wss://production-sfo.browserless.io/chromium/playwright?token={TOKEN}'

with sync_playwright() as playwright:
    browser = playwright.chromium.connect(WS_ENDPOINT)
    try:
        context = browser.new_context()
        page = context.new_page()

        # Submit the email to trigger the OTP — the form changes state before the OTP field appears.
        page.goto('https://app.example.com/login')
        page.fill('input[type="email"]', 'user@example.com')
        page.click('button[type="submit"]')
        page.wait_for_selector('input[name="otp"], input[autocomplete="one-time-code"]')

        # Poll the inbox after the OTP field appears, not before — the email may not be sent yet.
        otp = get_otp_from_inbox('user@example.com')
        print(f'Got OTP: {otp}')

        page.fill('input[name="otp"], input[autocomplete="one-time-code"]', otp)
        page.click('button[type="submit"]')
        page.wait_for_load_state('networkidle')

        print(f'Logged in. URL: {page.url}')
    finally:
        # Always close to release the session even on error.
        browser.close()
```

      **3. Check the output**

      Run with `python otp.py`. Supply your inbox polling implementation to complete the flow end-to-end.

      
      
**Java:**

      

      **1. Add dependencies**

```xml
<dependency>
  <groupId>com.microsoft.playwright</groupId>
  <artifactId>playwright</artifactId>
  <version>1.44.0</version>
</dependency>
```

      **2. Trigger OTP, read code, enter it**

```java
import com.microsoft.playwright.*;

public class OtpLogin {
    static String getOtpFromInbox(String email) {
        // Swap this stub with your actual inbox API — see the Inbox integrations table below.
        throw new UnsupportedOperationException("Implement getOtpFromInbox() with your email provider");
    }

    public static void main(String[] args) {
        String token = "YOUR_API_TOKEN_HERE";
        String wsEndpoint = "wss://production-sfo.browserless.io?token=" + token;

        try (Playwright playwright = Playwright.create()) {
            Browser browser = playwright.chromium().connectOverCDP(wsEndpoint);
            try {
                // Reuse the existing page from the default context so Browserless CDP
                // events are visible on this page object.
                BrowserContext context = browser.contexts().get(0);
                Page page = context.pages().get(0);

                // Submit the email to trigger the OTP — the form changes state before the OTP field appears.
                page.navigate("https://app.example.com/login");
                page.fill("input[type='email']", "user@example.com");
                page.click("button[type='submit']");
                page.waitForSelector("input[name='otp'], input[autocomplete='one-time-code']");

                // Poll the inbox after the OTP field appears, not before — the email may not be sent yet.
                String otp = getOtpFromInbox("user@example.com");
                System.out.println("Got OTP: " + otp);

                page.fill("input[name='otp'], input[autocomplete='one-time-code']", otp);
                page.click("button[type='submit']");
                page.waitForLoadState(LoadState.NETWORKIDLE);

                System.out.println("Logged in. URL: " + page.url());
            } finally {
                // Always close to release the session even on error.
                browser.close();
            }
        }
    }
}
```

      **3. Check the output**

      Run with `mvn exec:java`. Supply your inbox polling implementation to complete the flow end-to-end.

      
      
**C#:**

      

      **1. Add dependencies**

```bash
dotnet add package Microsoft.Playwright
```

      **2. Trigger OTP, read code, enter it**

```csharp
using Microsoft.Playwright;

static string GetOtpFromInbox(string email)
{
    // Swap this stub with your actual inbox API — see the Inbox integrations table below.
    throw new NotImplementedException("Implement GetOtpFromInbox() with your email provider");
}

const string Token = "YOUR_API_TOKEN_HERE";
const string WsEndpoint = $"wss://production-sfo.browserless.io?token={Token}";

using var playwright = await Playwright.CreateAsync();
var browser = await playwright.Chromium.ConnectOverCDPAsync(WsEndpoint);

try
{
    // Reuse the existing page from the default context so Browserless CDP
    // events are visible on this page object.
    var context = browser.Contexts[0];
    var page = context.Pages[0];

    // Submit the email to trigger the OTP — the form changes state before the OTP field appears.
    await page.GotoAsync("https://app.example.com/login");
    await page.FillAsync("input[type='email']", "user@example.com");
    await page.ClickAsync("button[type='submit']");
    await page.WaitForSelectorAsync("input[name='otp'], input[autocomplete='one-time-code']");

    // Poll the inbox after the OTP field appears, not before — the email may not be sent yet.
    var otp = GetOtpFromInbox("user@example.com");
    Console.WriteLine($"Got OTP: {otp}");

    await page.FillAsync("input[name='otp'], input[autocomplete='one-time-code']", otp);
    await page.ClickAsync("button[type='submit']");
    await page.WaitForLoadStateAsync(LoadState.NetworkIdle);

    Console.WriteLine($"Logged in. URL: {page.Url}");
}
finally
{
    // Always close to release the session even on error.
    await browser.CloseAsync();
}
```

      **3. Check the output**

      Run with `dotnet run`. Supply your inbox polling implementation to complete the flow end-to-end.

      
    

    
    
**BAP:**

    **1. Install dependencies**

```bash
npm install @browserless.io/bap-ts
```

    **2. Connect and trigger the code**

```typescript
import Browserless from '@browserless.io/bap-ts';

const browser = Browserless.connect({
  browserWSEndpoint: 'wss://production-sfo.browserless.io/chromium/bql',
  token: 'YOUR_API_TOKEN_HERE',
});

try {
  const page = await browser.newPage();
  await page.goto('https://app.example.com/login');

  await page.type('input[type=email]', 'user@example.com');
  await page.click('button[type=submit]');
  await page.waitForSelector('input[name=otp], input[autocomplete=one-time-code]');

  // Hand the session off so another process can enter the code it received.
  const { browserQLEndpoint } = await page.reconnect({ timeout: 60000 });
  console.log(browserQLEndpoint);

  // No close() here. Closing would destroy the page holding the OTP field
  // before the inbox reader can reconnect to it.
} catch (error) {
  // Only close when the handoff never happened, so nothing is left stranded.
  await browser.close();
  throw error;
}
```

    **3. Check the output**

    Reconnect to `browserQLEndpoint` from wherever you read the inbox, then type the code into the waiting page. The reconnect timeout sets how long you have to do it.

    
  

  
  
**BQL:**

  

  **1. Write the first mutation**

  Include `reconnect` at the end of the first mutation. Without it, the session closes as soon as the mutation completes and there is nothing to send the second mutation to. `reconnect` keeps the browser alive and returns a `browserQLEndpoint` URL for the follow-up request:

  > The reconnect timeout can be as long as your plan's maximum session duration: 2 minutes on Free, 15 minutes on Prototyping, 30 minutes on Starter, and 60 minutes on Scale. A reconnect never extends the session past that original deadline, and each reconnect is a new billable browser connection.

```graphql
mutation TriggerOTP {
  goto(url: "https://app.example.com/login") {
    status
  }
  typeEmail: type(selector: "input[type=email]", text: "user@example.com") {
    time
  }
  submit: click(selector: "button[type=submit]") {
    time
  }
  waitForOTPField: waitForSelector(
    selector: "input[name=otp], input[autocomplete=one-time-code]"
  ) {
    selector
  }
  reconnect(timeout: 60000) {
    browserQLEndpoint
  }
}
```

  **2. Poll your inbox, then send the second mutation**

  The `TriggerOTP` response includes `data.reconnect.browserQLEndpoint`. Append `?token=YOUR_API_TOKEN_HERE` to that URL and POST `EnterOTP` to it:

```graphql
mutation EnterOTP {
  typeOTP: type(
    selector: "input[name=otp], input[autocomplete=one-time-code]"
    text: "YOUR_OTP_HERE"
  ) {
    time
  }
  submit: click(selector: "button[type=submit]") {
    time
  }
  waitForNavigation {
    status
  }
}
```

  **3. Run it**

  Paste the first mutation into the [BQL IDE](https://account.browserless.io/bql) and click Run, or send it via HTTP. After getting the OTP from your inbox, POST the second mutation to the returned `browserQLEndpoint`.

  

## Inbox integrations

| Service | How to get the OTP |
|---------|-------------------|
| [Mailosaur](https://mailosaur.com) | Use the Mailosaur Node/Python SDK — `client.messages.get(serverId, ...)` |
| [Mailslurp](https://mailslurp.com) | `inbox.waitForLatestEmail()` with a regex to extract the code |
| Gmail API | Search for the OTP email subject and parse the body |
| IMAP | Use `imaplib` (Python) or `imap-simple` (Node.js) to fetch the latest message |

## Next steps

  <a href="/examples/authenticated-profiles" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Save Logins to Authenticated Profiles</h3>
      save the post-OTP session to avoid repeating the flow
    
  </a>
  <a href="/examples/browser-agent-login" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Log In with BQL and Browser Automation</h3>
      automate login flows with dynamic selector detection
    
  </a>
  <a href="/examples/forms" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Fill and Submit a Form</h3>
      general form automation patterns
    
  </a>

---

# Export a slide deck to PDF or PNG

URL: https://docs.browserless.io/examples/export-slide-deck

# Export a slide deck to PDF or PNG

Export a slide presentation to PDF or PNG by locking the viewport to the slide's aspect ratio and forcing screen media so backgrounds and fonts render as intended.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

> **Why `emulateMedia: screen`?**
> PDF generation defaults to print media, which strips background colors, changes fonts, and removes visual styling that slide decks rely on. Setting media to `screen` captures the presentation exactly as it appears in a browser.

## Steps

  
**AI Agent:**

  Use the [Browserless MCP server](https://github.com/browserless/browserless-mcp) to export a slide deck to PDF or PNG from any MCP-compatible AI agent (Claude Desktop, Cursor, Windsurf, ChatGPT, etc.).

  **1. Connect the MCP server**

  Send this prompt to your AI agent to install the Browserless MCP server:

```text
Go to https://github.com/browserless/browserless-mcp/blob/main/install.md
and follow the instructions to install the Browserless MCP server
for my client.
```

  **2. Export a slide deck**

  Use `browserless_export`. The output format (PDF/PNG) is the primary goal.

```text
Use the browserless_export tool to export the slide deck at
https://slides.example.com/presentation as a PDF
```

  **3. Slide deck behind interaction**

  Use `browserless_agent`. The slides require interaction (clicking through pages) before exporting.

```text
Use the browserless_agent tool to navigate to
https://slides.example.com/presentation, advance through all
slides, then export the full deck as a PDF
```

  
  
**REST API:**

  Send a POST to the `/pdf` endpoint with screen media emulation and a 16:9 viewport. The viewport dimensions (1280×720) match the standard slide aspect ratio so each page in the PDF maps to exactly one slide.

  
    
**cURL:**

    

    **1. Build the request**

    Append your token to the PDF endpoint:

```bash
https://production-sfo.browserless.io/pdf?token=YOUR_API_TOKEN_HERE
```

    **2. Send the request**

```bash
curl -X POST \
  "https://production-sfo.browserless.io/pdf?token=YOUR_API_TOKEN_HERE" \
  -H "Cache-Control: no-cache" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/my-presentation",
    "emulateMedia": "screen",
    "viewport": { "width": 1280, "height": 720 },
    "options": {
      "printBackground": true,
      "width": "1280px",
      "height": "720px"
    }
  }' \
  -o slides.pdf
```

    **3. Check the output**

    The response body is a raw PDF binary written to `slides.pdf`. Each page is 1280×720px, one slide per page.

    :::tip Exporting to PNG
    Replace the endpoint with `/screenshot` and remove the `options` field to capture the first slide as a PNG instead:

```bash
curl -X POST \
  "https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/my-presentation",
    "emulateMedia": "screen",
    "viewport": { "width": 1280, "height": 720 },
    "options": { "type": "png" }
  }' \
  -o slide-1.png
```
    :::

    
    
**JavaScript:**

    

    **1. Send the request**

```js
import fs from 'fs';

const response = await fetch(
  'https://production-sfo.browserless.io/pdf?token=YOUR_API_TOKEN_HERE',
  {
    method: 'POST',
    headers: {
      'Cache-Control': 'no-cache',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      url: 'https://example.com/my-presentation',
      emulateMedia: 'screen',
      viewport: { width: 1280, height: 720 },
      options: {
        printBackground: true,
        width: '1280px',
        height: '720px',
      },
    }),
  }
);

const buffer = await response.arrayBuffer();
fs.writeFileSync('slides.pdf', Buffer.from(buffer));
```

    **2. Check the output**

    Run with `node export-slide-deck.mjs`. You'll find `slides.pdf` in the current directory with one slide per page.

    
    
**Python:**

    

    **1. Install dependencies**

```bash
pip install requests
```

    **2. Send the request**

```python
import requests

response = requests.post(
    'https://production-sfo.browserless.io/pdf?token=YOUR_API_TOKEN_HERE',
    headers={'Cache-Control': 'no-cache', 'Content-Type': 'application/json'},
    json={
        'url': 'https://example.com/my-presentation',
        'emulateMedia': 'screen',
        'viewport': {'width': 1280, 'height': 720},
        'options': {
            'printBackground': True,
            'width': '1280px',
            'height': '720px',
        },
    },
)

with open('slides.pdf', 'wb') as f:
    f.write(response.content)
```

    **3. Check the output**

    Run with `python export_slide_deck.py`. You'll find `slides.pdf` in the current directory.

    
    
**Java:**

    

    **1. Send the request**

```java
import java.io.*;
import java.net.URI;
import java.net.http.*;

String token = "YOUR_API_TOKEN_HERE";
String endpoint = "https://production-sfo.browserless.io/pdf?token=" + token;

String payload = """
    {
      "url": "https://example.com/my-presentation",
      "emulateMedia": "screen",
      "viewport": { "width": 1280, "height": 720 },
      "options": {
        "printBackground": true,
        "width": "1280px",
        "height": "720px"
      }
    }
    """;

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(endpoint))
    .header("Cache-Control", "no-cache")
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(payload))
    .build();

HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofInputStream());
try (FileOutputStream fos = new FileOutputStream("slides.pdf")) {
    response.body().transferTo(fos);
}
System.out.println("PDF saved as slides.pdf");
```

    **2. Check the output**

    Run the class. You'll find `slides.pdf` in the current directory.

    
    
**C#:**

    

    **1. Send the request**

```csharp
using System.IO;
using System.Net.Http;
using System.Text;
using System.Text.Json;

string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"https://production-sfo.browserless.io/pdf?token={token}";

var payload = new
{
    url = "https://example.com/my-presentation",
    emulateMedia = "screen",
    viewport = new { width = 1280, height = 720 },
    options = new
    {
        printBackground = true,
        width = "1280px",
        height = "720px",
    },
};

using (HttpClient httpClient = new HttpClient())
{
    var request = new HttpRequestMessage(HttpMethod.Post, endpoint);
    request.Headers.Add("Cache-Control", "no-cache");
    request.Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");

    var response = await httpClient.SendAsync(request);
    var contentStream = await response.Content.ReadAsStreamAsync();
    using (var fileStream = new FileStream("slides.pdf", FileMode.Create, FileAccess.Write))
    {
        await contentStream.CopyToAsync(fileStream);
    }
    Console.WriteLine("PDF saved as slides.pdf");
}
```

    **2. Check the output**

    Run the program. You'll find `slides.pdf` in the current directory.

    
  

  
  
**Frameworks:**

  Use a direct browser connection when you need control over timing, for example when waiting for CSS animations or JavaScript transitions to finish before capturing. This also lets you produce both PDF and PNG from a single browser session without opening the URL twice.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Connect and export**

```js
import puppeteer from 'puppeteer-core';
import fs from 'fs';

const browser = await puppeteer.connect({
  browserWSEndpoint: 'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE',
});

try {
  const page = await browser.newPage();

  await page.setViewport({ width: 1280, height: 720 });
  await page.emulateMedia('screen');

  await page.goto('https://example.com/my-presentation', {
    waitUntil: 'networkidle0',
  });

  // Export full deck as PDF
  const pdf = await page.pdf({
    printBackground: true,
    width: '1280px',
    height: '720px',
  });
  fs.writeFileSync('slides.pdf', pdf);

  // Export first slide as PNG
  const png = await page.screenshot({ type: 'png' });
  fs.writeFileSync('slide-1.png', png);
} finally {
  await browser.close();
}
```

    **3. Check the output**

    Run with `node export-slide-deck.mjs`. Both `slides.pdf` and `slide-1.png` will appear in the current directory.

    
    
**Playwright:**

    

    **1. Install dependencies**

```bash
npm install playwright-core
```

    **2. Connect and export**

```js
import { chromium } from 'playwright-core';
import fs from 'fs';

const browser = await chromium.connect(
  'wss://production-sfo.browserless.io/chromium/playwright?token=YOUR_API_TOKEN_HERE'
);

try {
  const page = await browser.newPage();

  await page.setViewportSize({ width: 1280, height: 720 });
  await page.emulateMedia({ media: 'screen' });

  await page.goto('https://example.com/my-presentation', {
    waitUntil: 'networkidle',
  });

  // Export full deck as PDF
  const pdf = await page.pdf({
    printBackground: true,
    width: '1280px',
    height: '720px',
  });
  fs.writeFileSync('slides.pdf', pdf);

  // Export first slide as PNG
  const png = await page.screenshot({ type: 'png' });
  fs.writeFileSync('slide-1.png', png);
} finally {
  await browser.close();
}
```

    **3. Check the output**

    Run with `node export-slide-deck.mjs`. Both `slides.pdf` and `slide-1.png` will appear in the current directory.

    
    
**BAP:**

    **1. Install dependencies**

```bash
npm install @browserless.io/bap-ts
```

    **2. Connect and export**

```typescript
import Browserless from '@browserless.io/bap-ts';

const browser = Browserless.connect({
  browserWSEndpoint: 'wss://production-sfo.browserless.io/chromium/bql',
  token: 'YOUR_API_TOKEN_HERE',
});

try {
  const page = await browser.newPage();
  await page.goto('https://example.com/my-presentation', { waitUntil: 'networkIdle' });

  // width/height take CSS strings, sized here to a 16:9 slide.
  await page.pdf({
    path: 'deck.pdf',
    printBackground: true,
    width: '1280px',
    height: '720px',
  });
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    The file `deck.pdf` appears in the current directory, one slide per page.

    
  

  
  
**BQL:**

  

  **1. Write the mutation**

  Navigate to the presentation and export it as a base64-encoded PDF:

```graphql
mutation ExportSlideDeck {
  goto(url: "https://example.com/my-presentation", waitUntil: networkIdle) {
    status
  }

  pdf(printBackground: true, width: "1280px", height: "720px") {
    base64
  }
}
```

  **2. Run it**

  Paste into the [BQL IDE](https://account.browserless.io/bql) and click Run.

  **3. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "pdf": { "base64": "JVBERi0xLjQ..." }
  }
}
```

  BQL returns the PDF as a base64 string rather than raw binary because the response is JSON. Decode it and write the bytes to a file to get the final PDF.

  

## Next steps

  <a href="/examples/pdf" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Generate a PDF</h3>
      export any webpage as a PDF without slide-specific configuration
    
  </a>
  <a href="/examples/screenshot" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Take a Screenshot</h3>
      capture a page as a PNG or JPEG
    
  </a>
  <a href="/examples/download-images" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Export Pages and Assets</h3>
      download a full page snapshot with all linked assets
    
  </a>

---

# Extract content from a URL

URL: https://docs.browserless.io/examples/extract-content

# Extract content from a URL

Fetch the fully rendered HTML of any page, including content generated by JavaScript, using the Browserless `/content` endpoint.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

  
**AI Agent:**

  Use the [Browserless MCP server](https://github.com/browserless/browserless-mcp) to extract content from a URL from any MCP-compatible AI agent (Claude Desktop, Cursor, Windsurf, ChatGPT, etc.).

  **1. Connect the MCP server**

  Send this prompt to your AI agent to install the Browserless MCP server:

```text
Go to https://github.com/browserless/browserless-mcp/blob/main/install.md
and follow the instructions to install the Browserless MCP server
for my client.
```

  **2. Extract content**

  Use `browserless_smartscraper`. It extracts page content in one call with automatic bot-protection handling.

```text
Use the browserless_smartscraper tool to extract the main content
of https://scraping-sandbox.netlify.app/javascript-enabled and return it as clean markdown
```

  
  
**REST API:**

  Use the `/content` REST endpoint to retrieve fully rendered HTML. No WebSocket connection needed.

  
    
**cURL:**

    

    **1. Build the request**

    The `/content` endpoint returns the full rendered HTML of a page after JavaScript execution:

```bash
https://production-sfo.browserless.io/content?token=YOUR_API_TOKEN_HERE
```

    **2. Send the request**

```bash
curl -X POST \
  "https://production-sfo.browserless.io/content?token=YOUR_API_TOKEN_HERE" \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/json' \
  -d '{ "url": "https://scraping-sandbox.netlify.app/javascript-enabled" }'
```

    **3. Check the output**

    The response body is raw HTML: the full DOM as it exists after JavaScript has run, not just the original server response.

```html
<!DOCTYPE html><html><head><meta charSet="utf-8"/>
<title>Javascript Enabled</title>
...
```

    
    
**JavaScript:**

    

    **1. Send the request**

```js
const response = await fetch(
  'https://production-sfo.browserless.io/content?token=YOUR_API_TOKEN_HERE',
  {
    method: 'POST',
    headers: {
      'Cache-Control': 'no-cache',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ url: 'https://scraping-sandbox.netlify.app/javascript-enabled' }),
  }
);

const html = await response.text();
console.log(html.slice(0, 500));
```

    **2. Check the output**

    Run with `node content.mjs`. The variable `html` contains the fully rendered page source.

```html
<!DOCTYPE html><html><head><meta charSet="utf-8"/>
<title>Javascript Enabled</title>
...
```

    
    
**Python:**

    

    **1. Install dependencies**

```bash
pip install requests
```

    **2. Send the request**

```python
import requests

response = requests.post(
    'https://production-sfo.browserless.io/content?token=YOUR_API_TOKEN_HERE',
    headers={'Cache-Control': 'no-cache', 'Content-Type': 'application/json'},
    json={'url': 'https://scraping-sandbox.netlify.app/javascript-enabled'},
)

html = response.text
print(html[:500])
```

    **3. Check the output**

    Run with `python content.py`. The variable `html` contains the fully rendered page source. Pipe it through `BeautifulSoup` or similar to parse.

```html
<!DOCTYPE html><html><head><meta charSet="utf-8"/>
<title>Javascript Enabled</title>
...
```

    
    
**Java:**

    

    **1. Send the request**

```java
import java.net.URI;
import java.net.http.*;

String token = "YOUR_API_TOKEN_HERE";
String endpoint = "https://production-sfo.browserless.io/content?token=" + token;

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(endpoint))
    .header("Cache-Control", "no-cache")
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString("{\"url\": \"https://scraping-sandbox.netlify.app/javascript-enabled\"}"))
    .build();

HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
String html = response.body();
System.out.println(html.substring(0, Math.min(500, html.length())));
```

    **2. Check the output**

    Run the class. The response body is the fully rendered HTML.

```html
<!DOCTYPE html><html><head><meta charSet="utf-8"/>
<title>Javascript Enabled</title>
...
```

    
    
**C#:**

    

    **1. Send the request**

```csharp
using System.Net.Http;
using System.Text;
using System.Text.Json;

string url = "https://production-sfo.browserless.io/content";
string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"{url}?token={token}";

var payload = new { url = "https://scraping-sandbox.netlify.app/javascript-enabled" };

using (HttpClient httpClient = new HttpClient())
{
    var request = new HttpRequestMessage(HttpMethod.Post, endpoint);
    request.Headers.Add("Cache-Control", "no-cache");
    request.Content = new StringContent(
        JsonSerializer.Serialize(payload),
        Encoding.UTF8,
        "application/json"
    );
    var response = await httpClient.SendAsync(request);
    string responseBody = await response.Content.ReadAsStringAsync();
    Console.WriteLine(responseBody[..Math.Min(500, responseBody.Length)]);
}
```

    **2. Check the output**

    Run the program. The response body is the fully rendered HTML.

```html
<!DOCTYPE html><html><head><meta charSet="utf-8"/>
<title>Javascript Enabled</title>
...
```

    
  

  
  
**Frameworks:**

  Use a browser connection to navigate to the page and retrieve rendered content directly.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Connect and extract content**

```js
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint: 'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE',
});

try {
  const page = await browser.newPage();
  await page.goto('https://scraping-sandbox.netlify.app/javascript-enabled', { waitUntil: 'networkidle2' });
  const html = await page.content();
  console.log(html.slice(0, 500));
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Run with `node content.mjs`. `page.content()` returns the serialized DOM after all scripts have executed.

```html
<!DOCTYPE html><html><head><meta charSet="utf-8"/>
<title>Javascript Enabled</title>
...
```

    
    
**Playwright:**

    

    **1. Install dependencies**

```bash
npm install playwright-core
```

    **2. Connect and extract content**

```js
import { chromium } from 'playwright-core';

const browser = await chromium.connect(
  'wss://production-sfo.browserless.io/chromium/playwright?token=YOUR_API_TOKEN_HERE'
);

try {
  const page = await browser.newPage();
  await page.goto('https://scraping-sandbox.netlify.app/javascript-enabled', { waitUntil: 'networkidle' });
  const html = await page.content();
  console.log(html.slice(0, 500));
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Run with `node content.mjs`. `page.content()` returns the serialized DOM after all scripts have executed.

```html
<!DOCTYPE html><html><head><meta charSet="utf-8"/>
<title>Javascript Enabled</title>
...
```

    
    
**BAP:**

    **1. Install dependencies**

```bash
npm install @browserless.io/bap-ts
```

    **2. Connect and extract content**

```typescript
import Browserless from '@browserless.io/bap-ts';

const browser = Browserless.connect({
  browserWSEndpoint: 'wss://production-sfo.browserless.io/chromium/bql',
  token: 'YOUR_API_TOKEN_HERE',
});

try {
  const page = await browser.newPage();
  await page.goto('https://scraping-sandbox.netlify.app/javascript-enabled', { waitUntil: 'domContentLoaded' });

  // Pass a `selector` to html() to scope the output to part of the page.
  const { html } = await page.html();
  console.log(html?.slice(0, 500));
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Run with `node content.mjs`. `html()` returns the serialized DOM after all scripts have executed.

```html
<!DOCTYPE html><html><head><meta charSet="utf-8"/>
<title>Javascript Enabled</title>
...
```

    
  

  
  
**BQL:**

  

  **1. Write the mutation**

  Use `html` to get the rendered content, or `innerText` for just the visible text:

```graphql
mutation ExtractContent {
  goto(url: "https://scraping-sandbox.netlify.app/javascript-enabled", waitUntil: domContentLoaded) {
    status
  }
  html {
    html
  }
}
```

  **2. Run it**

  Paste into the [BQL IDE](https://account.browserless.io/bql) and click Run.

  **3. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "html": { "html": "<!DOCTYPE html><html>...</html>" }
  }
}
```

  

## Next steps

  <a href="/examples/scrape" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape Structured Data</h3>
      extract specific elements rather than the full HTML
    
  </a>
  <a href="/examples/google-search" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Automate Google Search</h3>
      navigate and extract content from dynamic pages
    
  </a>
  <a href="/examples/screenshot" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Take a Screenshot</h3>
      capture a visual snapshot alongside the HTML
    
  </a>

---

# Upload and download files

URL: https://docs.browserless.io/examples/file-transfers

# Upload and download files

Send a local file into an `<input type="file">` and stream completed downloads back to your machine, without sharing a filesystem with the remote browser.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Upload a file

Send a file from your local machine to a file input on the remote page. Browserless handles the transfer so the page's upload handlers run as if a user picked the file.

  
**AI Agent:**

  Use the [Browserless MCP server](https://github.com/browserless/browserless-mcp) to upload files from any MCP-compatible AI agent (Claude Desktop, Cursor, Windsurf, ChatGPT, etc.).

  **1. Connect the MCP server**

  Send this prompt to your AI agent to install the Browserless MCP server:

```text
Go to https://github.com/browserless/browserless-mcp/blob/main/install.md
and follow the instructions to install the Browserless MCP server
for my client.
```

  **2. Upload a file**

  Use `browserless_agent`. Uploading requires interaction: attaching a file to an input and submitting a form.

```text
Use the browserless_agent tool to go to https://example.com/upload,
upload /absolute/path/to/file.ext to the file input, and submit the form.
```

  
  
**Puppeteer:**

**1. Install dependencies**

```bash
npm install puppeteer-core
```

**2. Connect and upload**

```js

const TOKEN = "YOUR_API_TOKEN_HERE";

const fileToUpload = {
  name: "image.png",
  content: fs
    .readFileSync(path.join(process.cwd(), "image.png"))
    .toString("base64"),
  mimeType: "image/png",
};

const browser = await puppeteer.connect({
  browserWSEndpoint: `wss://production-sfo.browserless.io?token=${TOKEN}`,
});

try {
  const page = await browser.newPage();
  await page.goto("https://jimpl.com/");
  await page.waitForSelector("input[type=file]");

  const cdp = await page.createCDPSession();
  const result = await cdp.send("Browserless.uploadFile", {
    selector: "input[type=file]",
    files: [fileToUpload],
  });

  console.log(result);
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

**3. Check the output**

The call returns `{ ok: true }`. On failure, `ok` is `false` with an `error` such as `SelectorNotFound`, `InvalidTarget`, or `FileTooLarge` (the combined decoded size is capped at 50 MB).

  
  
**Playwright:**

**1. Install dependencies**

```bash
npm install playwright-core
```

**2. Connect and upload**

```js

const TOKEN = "YOUR_API_TOKEN_HERE";

const fileToUpload = {
  name: "image.png",
  content: fs
    .readFileSync(path.join(process.cwd(), "image.png"))
    .toString("base64"),
  mimeType: "image/png",
};

// connectOverCDP uses the raw CDP endpoint, which is where the custom
// Browserless.* methods live — the Playwright protocol server doesn't expose them.
const browser = await chromium.connectOverCDP(
  `wss://production-sfo.browserless.io?token=${TOKEN}`,
);

try {
  const context = browser.contexts()[0];
  const page = context.pages()[0] ?? (await context.newPage());
  await page.goto("https://jimpl.com/");
  await page.waitForSelector("input[type=file]");

  const cdp = await context.newCDPSession(page);
  const result = await cdp.send("Browserless.uploadFile", {
    selector: "input[type=file]",
    files: [fileToUpload],
  });

  console.log(result);
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

**3. Check the output**

The call returns `{ ok: true }`. On failure, `ok` is `false` with an `error` such as `SelectorNotFound`, `InvalidTarget`, or `FileTooLarge` (the combined decoded size is capped at 50 MB).

  

## Download a file

Trigger a download in the remote browser and stream the completed file back to your machine.

  
**AI Agent:**

  Use the [Browserless MCP server](https://github.com/browserless/browserless-mcp) to download files from any MCP-compatible AI agent (Claude Desktop, Cursor, Windsurf, ChatGPT, etc.).

  **1. Connect the MCP server**

  Send this prompt to your AI agent to install the Browserless MCP server:

```text
Go to https://github.com/browserless/browserless-mcp/blob/main/install.md
and follow the instructions to install the Browserless MCP server
for my client.
```

  **2. Download a file**

  Use `browserless_agent`. Downloading requires interaction: navigating to the page, clicking a download trigger, and saving the result.

```text
Use the browserless_agent tool to go to https://example.com/reports,
click the latest CSV export, and save it to ~/Desktop.
```

  
  
**Puppeteer:**

**1. Install dependencies**

```bash
npm install puppeteer-core
```

**2. Connect and capture the download**

```js

const TOKEN = "YOUR_API_TOKEN_HERE";

const browser = await puppeteer.connect({
  browserWSEndpoint: `wss://production-sfo.browserless.io?token=${TOKEN}`,
});

try {
  const page = await browser.newPage();
  const cdp = await page.createCDPSession();

  // Off by default — download bytes count against data transfer, so opt in.
  await cdp.send("Browserless.setDownloadEnabled", { enabled: true });

  // Register before the click so the completed download can't be missed.
  cdp.on("Browserless.fileDownloaded", ({ filename, data }) => {
    const buffer = Buffer.from(data, "base64");
    fs.writeFileSync(path.join(process.cwd(), filename), buffer);
    console.log(`saved: ${filename} (${buffer.length} bytes)`);
  });

  await page.goto("https://scraping-sandbox.netlify.app/downloadsamples");
  await page.click('a[href="/samples/sample.json"]');

  // Give the download time to finish before closing the connection.
  await new Promise((res) => setTimeout(res, 3000));
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

**3. Check the output**

The `Browserless.fileDownloaded` event carries `filename`, `mimeType`, `size`, and base64 `data`. It fires once per completed download.

  
  
**Playwright:**

**1. Install dependencies**

```bash
npm install playwright-core
```

**2. Connect and save the download**

```js

const TOKEN = "YOUR_API_TOKEN_HERE";

const browser = await chromium.connect(
  `wss://production-sfo.browserless.io/chromium/playwright?token=${TOKEN}`,
);

try {
  const page = await browser.newPage();
  await page.goto("https://scraping-sandbox.netlify.app/downloadsamples");

  // Register before clicking so the download can't be missed.
  const downloadPromise = page.waitForEvent("download");
  await page.click('a[href="/samples/sample.json"]');
  const download = await downloadPromise;

  // Saves on the machine running this script, not the remote browser.
  await download.saveAs(download.suggestedFilename());
  console.log(`saved: ${download.suggestedFilename()}`);
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

**3. Check the output**

`download.saveAs()` writes the file to the machine running the script. Use `download.suggestedFilename()` for the browser's filename, or pass your own path.

  

## Next steps

  <a href="/baas/features/file-transfers" className="next-step-card-link">
    
      <h3 className="next-step-card-title">File Transfers feature guide</h3>
      full parameter and event reference
    
  </a>
  <a href="/examples/forms" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Fill and Submit a Form</h3>
      automate the inputs around a file upload
    
  </a>
  <a href="/examples/download-images" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Extract and Download Images</h3>
      pull every image off a page
    
  </a>

---

# Fill out a job application form

URL: https://docs.browserless.io/examples/fill-job-application

# Fill out a job application form

Navigate to a sandbox job listing, open the application tab, fill in personal details, and submit the form.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

This example uses a [Browserless sandbox site](https://scraping-sandbox.netlify.app/helix/software-engineer-pipelines) that simulates a job listing with an application form behind a tab. You can safely test form automation without submitting real applications.

The workflow navigates to the job listing, clicks the "Application" tab to reveal the form, fills in name, email, and a cover message, then clicks "Submit Application".

  
**AI Agent:**

  Use the [Browserless MCP server](https://github.com/browserless/browserless-mcp) to fill out a job application from any MCP-compatible AI agent (Claude Desktop, Cursor, Windsurf, ChatGPT, etc.).

  **1. Connect the MCP server**

  Send this prompt to your AI agent to install the Browserless MCP server:

```text
Go to https://github.com/browserless/browserless-mcp/blob/main/install.md
and follow the instructions to install the Browserless MCP server
for my client.
```

  **2. Fill out the application**

  Use `browserless_agent`. The task requires interaction: clicking a tab, filling fields, and clicking submit.

```text
Use the browserless_agent tool to go to
https://scraping-sandbox.netlify.app/helix/software-engineer-pipelines,
click the "Application" tab, fill in "Jane Smith" for name,
"jane@example.com" for email, "Excited to contribute to the team!"
as the cover message, and click "Submit Application"
```

  
  
**REST API:**

  Send the BQL mutation over HTTP to fill and submit the form. This is a sandbox site, so no stealth mode is needed.

  
    
**cURL:**

    

    **1. Send the request**

```bash
curl -X POST \
  "https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "mutation FillJobApplication { goto(url: \"https://scraping-sandbox.netlify.app/helix/software-engineer-pipelines\", waitUntil: networkIdle) { status } clickApplicationTab: click(selector: \"button:nth-child(2)\") { time } waitForInputs: waitForSelector(selector: \"input[type=text]\", timeout: 10000) { time } typeName: type(selector: \"input[type=text]\", text: \"Jane Smith\") { time } typeEmail: type(selector: \"input[type=email]\", text: \"jane@example.com\") { time } typeMessage: type(selector: \"textarea\", text: \"Excited to contribute to the team!\") { time } submit: click(selector: \"div > button:only-of-type\") { time } }",
    "variables": {}
  }'
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "clickApplicationTab": { "time": 179 },
    "waitForInputs": { "time": 6 },
    "typeName": { "time": 1291 },
    "typeEmail": { "time": 1948 },
    "typeMessage": { "time": 4547 },
    "submit": { "time": 169 }
  }
}
```

    
    
**JavaScript:**

    

    **1. Send the request**

```js
const query = `mutation FillJobApplication {
  goto(url: "https://scraping-sandbox.netlify.app/helix/software-engineer-pipelines", waitUntil: networkIdle) {
    status
  }
  clickApplicationTab: click(selector: "button:nth-child(2)") {
    time
  }
  waitForInputs: waitForSelector(selector: "input[type=text]", timeout: 10000) {
    time
  }
  typeName: type(selector: "input[type=text]", text: "Jane Smith") {
    time
  }
  typeEmail: type(selector: "input[type=email]", text: "jane@example.com") {
    time
  }
  typeMessage: type(selector: "textarea", text: "Excited to contribute to the team!") {
    time
  }
  submit: click(selector: "div > button:only-of-type") {
    time
  }
}`;

const response = await fetch(
  'https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ query, variables: {} }),
  }
);

const result = await response.json();
console.log(JSON.stringify(result, null, 2));
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "clickApplicationTab": { "time": 179 },
    "waitForInputs": { "time": 6 },
    "typeName": { "time": 1291 },
    "typeEmail": { "time": 1948 },
    "submit": { "time": 169 }
  }
}
```

    
    
**Python:**

    

    **1. Install dependencies**

```bash
pip install requests
```

    **2. Send the request**

```python
import requests

query = """
mutation FillJobApplication {
  goto(url: "https://scraping-sandbox.netlify.app/helix/software-engineer-pipelines", waitUntil: networkIdle) {
    status
  }
  clickApplicationTab: click(selector: "button:nth-child(2)") {
    time
  }
  waitForInputs: waitForSelector(selector: "input[type=text]", timeout: 10000) {
    time
  }
  typeName: type(selector: "input[type=text]", text: "Jane Smith") {
    time
  }
  typeEmail: type(selector: "input[type=email]", text: "jane@example.com") {
    time
  }
  typeMessage: type(selector: "textarea", text: "Excited to contribute to the team!") {
    time
  }
  submit: click(selector: "div > button:only-of-type") {
    time
  }
}
"""

response = requests.post(
    'https://production-sfo.browserless.io/chromium/bql',
    params={'token': 'YOUR_API_TOKEN_HERE'},
    json={'query': query, 'variables': {}},
)

print(response.json())
```

    **3. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "clickApplicationTab": { "time": 179 },
    "typeName": { "time": 1291 },
    "typeEmail": { "time": 1948 },
    "submit": { "time": 169 }
  }
}
```

    
    
**Java:**

    

    **1. Send the request**

```java
import java.net.URI;
import java.net.http.*;

String token = "YOUR_API_TOKEN_HERE";
String endpoint = "https://production-sfo.browserless.io/chromium/bql?token=" + token;

String query = "mutation FillJobApplication {"
    + " goto(url: \\\"https://scraping-sandbox.netlify.app/helix/software-engineer-pipelines\\\", waitUntil: networkIdle) { status }"
    + " clickApplicationTab: click(selector: \\\"button:nth-child(2)\\\") { time }"
    + " waitForInputs: waitForSelector(selector: \\\"input[type=text]\\\", timeout: 10000) { time }"
    + " typeName: type(selector: \\\"input[type=text]\\\", text: \\\"Jane Smith\\\") { time }"
    + " typeEmail: type(selector: \\\"input[type=email]\\\", text: \\\"jane@example.com\\\") { time }"
    + " typeMessage: type(selector: \\\"textarea\\\", text: \\\"Excited to contribute to the team!\\\") { time }"
    + " submit: click(selector: \\\"div > button:only-of-type\\\") { time }"
    + " }";

String payload = "{\"query\": \"" + query + "\", \"variables\": {}}";

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(endpoint))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(payload))
    .build();

HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "typeName": { "time": 1291 },
    "submit": { "time": 169 }
  }
}
```

    
    
**C#:**

    

    **1. Send the request**

```csharp
using System.Net.Http;
using System.Text;
using System.Text.Json;

string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"https://production-sfo.browserless.io/chromium/bql?token={token}";

var payload = new
{
    query = @"mutation FillJobApplication {
      goto(url: ""https://scraping-sandbox.netlify.app/helix/software-engineer-pipelines"", waitUntil: networkIdle) { status }
      clickApplicationTab: click(selector: ""button:nth-child(2)"") { time }
      waitForInputs: waitForSelector(selector: ""input[type=text]"", timeout: 10000) { time }
      typeName: type(selector: ""input[type=text]"", text: ""Jane Smith"") { time }
      typeEmail: type(selector: ""input[type=email]"", text: ""jane@example.com"") { time }
      typeMessage: type(selector: ""textarea"", text: ""Excited to contribute to the team!"") { time }
      submit: click(selector: ""div > button:only-of-type"") { time }
    }",
    variables = new { },
};

using (HttpClient httpClient = new HttpClient())
{
    var content = new StringContent(
        JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
    var response = await httpClient.PostAsync(endpoint, content);
    string body = await response.Content.ReadAsStringAsync();
    Console.WriteLine(body);
}
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "typeName": { "time": 1291 },
    "submit": { "time": 169 }
  }
}
```

    
  

  
  
**Frameworks:**

  Connect a headless browser to the sandbox job listing, click the Application tab, fill in each field, and submit.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Connect and fill the form**

```js
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint: 'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE',
});

try {
  const page = await browser.newPage();
  await page.goto('https://scraping-sandbox.netlify.app/helix/software-engineer-pipelines', {
    waitUntil: 'networkidle2',
  });

  const buttons = await page.$$('button');
  for (const btn of buttons) {
    const text = await btn.evaluate((el) => el.innerText);
    if (text.trim() === 'Application') {
      await btn.click();
      break;
    }
  }

  await page.waitForSelector('input[type="text"]');
  await page.type('input[type="text"]', 'Jane Smith');
  await page.type('input[type="email"]', 'jane@example.com');
  await page.type('textarea', 'Excited to contribute to the team!');

  const submitButtons = await page.$$('button');
  for (const btn of submitButtons) {
    const text = await btn.evaluate((el) => el.innerText);
    if (text.trim() === 'Submit Application') {
      await btn.click();
      break;
    }
  }

  console.log('Application submitted successfully');
} finally {
  await browser.close();
}
```

    **3. Check the output**

    Run with `node fill-job-application.mjs`. The script logs a confirmation message after submission.

    
    
**Playwright:**

    

    **1. Install dependencies**

```bash
npm install playwright-core
```

    **2. Connect and fill the form**

```js
import { chromium } from 'playwright-core';

const browser = await chromium.connectOverCDP(
  'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE'
);

try {
  const context = browser.contexts()[0];
  const page = await context.newPage();
  await page.goto('https://scraping-sandbox.netlify.app/helix/software-engineer-pipelines', {
    waitUntil: 'networkidle',
  });

  await page.getByRole('button', { name: 'Application' }).click();
  await page.waitForSelector('input[type="text"]');
  await page.fill('input[type="text"]', 'Jane Smith');
  await page.fill('input[type="email"]', 'jane@example.com');
  await page.fill('textarea', 'Excited to contribute to the team!');
  await page.getByRole('button', { name: 'Submit Application' }).click();

  console.log('Application submitted successfully');
} finally {
  await browser.close();
}
```

    **3. Check the output**

    Run with `node fill-job-application.mjs`. The script logs a confirmation message after submission.

    
    
**BAP:**

    **1. Install dependencies**

```bash
npm install @browserless.io/bap-ts
```

    **2. Connect and apply**

```typescript
import Browserless from '@browserless.io/bap-ts';

const browser = Browserless.connect({
  browserWSEndpoint: 'wss://production-sfo.browserless.io/chromium/bql',
  token: 'YOUR_API_TOKEN_HERE',
});

try {
  const page = await browser.newPage();
  await page.goto('https://scraping-sandbox.netlify.app/helix/software-engineer-pipelines', { waitUntil: 'networkIdle' });

  await page.click('button:nth-child(2)');
  await page.waitForSelector('input[type=text]', { timeout: 10000 });

  await page.type('input[type=text]', 'Jane Smith');
  await page.type('input[type=email]', 'jane@example.com');
  await page.type('textarea', 'Excited to contribute to the team!');

  await page.click('div > button:only-of-type');
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Each call returns once the server has applied it, so the steps stay in order without manual waits between them.

    
  

  
  
**BQL:**

  

  **1. Write the mutation**

  Navigate to the job listing, click the Application tab, fill in the form fields, and submit. This is a sandbox site, so no stealth mode is needed.

```graphql
mutation FillJobApplication {
  goto(url: "https://scraping-sandbox.netlify.app/helix/software-engineer-pipelines", waitUntil: networkIdle) {
    status
  }
  clickApplicationTab: click(selector: "button:nth-child(2)") {
    time
  }
  waitForInputs: waitForSelector(selector: "input[type=text]", timeout: 10000) {
    time
  }
  typeName: type(selector: "input[type=text]", text: "Jane Smith") {
    time
  }
  typeEmail: type(selector: "input[type=email]", text: "jane@example.com") {
    time
  }
  typeMessage: type(selector: "textarea", text: "Excited to contribute to the team!") {
    time
  }
  submit: click(selector: "div > button:only-of-type") {
    time
  }
}
```

  **2. Run it**

  Paste into the [BQL IDE](https://account.browserless.io/bql) and click Run.

  **3. Check the output**

  The response confirms each step completed:

```json
{
  "data": {
    "goto": { "status": 200 },
    "clickApplicationTab": { "time": 179 },
    "waitForInputs": { "time": 6 },
    "typeName": { "time": 1291 },
    "typeEmail": { "time": 1948 },
    "typeMessage": { "time": 4547 },
    "submit": { "time": 169 }
  }
}
```

  

## Next steps

  <a href="/examples/forms" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Fill and Submit a Form</h3>
      more form automation examples including CAPTCHA solving
    
  </a>
  <a href="/examples/sessions" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Authenticated Sessions</h3>
      persist login state across requests
    
  </a>
  <a href="/examples/screenshot" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Take a Screenshot</h3>
      capture the confirmation page after submission
    
  </a>

---

# Fill and submit a form

URL: https://docs.browserless.io/examples/forms

# Fill and submit a form

Automate form interactions: typing into fields, selecting options, solving CAPTCHAs, and clicking submit.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

  
**AI Agent:**

  Use the [Browserless MCP server](https://github.com/browserless/browserless-mcp) to fill and submit a form from any MCP-compatible AI agent (Claude Desktop, Cursor, Windsurf, ChatGPT, etc.).

  **1. Connect the MCP server**

  Send this prompt to your AI agent to install the Browserless MCP server:

```text
Go to https://github.com/browserless/browserless-mcp/blob/main/install.md
and follow the instructions to install the Browserless MCP server
for my client.
```

  **2. Fill and submit a form**

  Use `browserless_agent`. The task requires interaction: filling fields and clicking submit.

```text
Use the browserless_agent tool to go to https://example.com/contact,
fill in the name field with "Jane Doe", the email field with
"jane@example.com", the message with "Hello!", and submit the form
```

  
  
**REST API:**

  Use the REST API to fill and submit a form without opening a persistent browser connection.

  
    
**cURL:**

    

    **1. Build the BrowserQL mutation**

    Use the BrowserQL endpoint to navigate, fill fields, and submit the form in a single request:

```bash
https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE
```

    **2. Send the request**

```bash
curl -X POST \
  "https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "mutation FormExample { goto(url: \"https://www.browserless.io/practice-form\") { status } typeEmail: type(text: \"user@example.com\", selector: \"#Email\") { time } typeMessage: type(selector: \"#Message\", text: \"Hello from Browserless!\") { time } subject: select(selector: \"select#Subject\", value: \"Support\") { selector } solve { time solved } }",
    "variables": {},
    "operationName": "FormExample"
  }'
```

    The complete mutation including `submitForm` is shown in the BQL tab.

    **3. Check the output**

    The response is JSON confirming each mutation completed, including whether the CAPTCHA was solved:

```json
{
  "data": {
    "goto": { "status": 200 },
    "typeEmail": { "time": 120 },
    "solve": { "time": 4200, "solved": true }
  }
}
```

    
    
**JavaScript:**

    

    **1. Send the request**

```js
const response = await fetch(
  'https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      query: `mutation FormExample {
        goto(url: "https://www.browserless.io/practice-form") {
          status
        }
        typeEmail: type(text: "user@example.com", selector: "#Email") {
          time
        }
        typeMessage: type(selector: "#Message", text: "Hello from Browserless!") {
          time
        }
        subject: select(selector: "select#Subject", value: "Support") {
          selector
        }
        solve {
          time
          solved
        }
        submitForm: click(selector: "button[type='submit']") {
          time
        }
      }`,
      variables: {},
      operationName: 'FormExample',
    }),
  }
);

const result = await response.json();
console.log(result);
```

    **2. Check the output**

    Run the script with `node form.mjs`. The response confirms each mutation completed and whether the CAPTCHA was solved.

    
    
**Python:**

    

    **1. Install dependencies**

```bash
pip install requests
```

    **2. Send the request**

```python
import requests

endpoint = 'https://production-sfo.browserless.io/chromium/bql'
query_string = {'token': 'YOUR_API_TOKEN_HERE'}
headers = {'Content-Type': 'application/json'}
payload = {
    'query': """
mutation FormExample {
  goto(url: "https://www.browserless.io/practice-form") {
    status
  }
  typeEmail: type(text: "user@example.com", selector: "#Email") {
    time
  }
  typeMessage: type(selector: "#Message", text: "Hello from Browserless!") {
    time
  }
  subject: select(selector: "select#Subject", value: "Support") {
    selector
  }
  solve {
    time
    solved
  }
  submitForm: click(selector: "button[type='submit']") {
    time
  }
}
""",
    'variables': {},
    'operationName': 'FormExample',
}

response = requests.post(endpoint, params=query_string, headers=headers, json=payload)
print(response.json())
```

    **3. Check the output**

    Run the script with `python form.py`. The response confirms each mutation completed, including CAPTCHA solving status.

    
    
**Java:**

    

    **1. Add dependencies**

```xml
<!-- https://kong.github.io/unirest-java/ -->
<dependency>
  <groupId>com.konghq</groupId>
  <artifactId>unirest-java</artifactId>
  <version>3.14.5</version>
</dependency>
```

    **2. Send the request**

```java
import kong.unirest.HttpResponse;
import kong.unirest.Unirest;

String url = "https://production-sfo.browserless.io/chromium/bql";
String token = "YOUR_API_TOKEN_HERE";
String endpoint = String.format("%s?token=%s", url, token);

HttpResponse response = Unirest.post(endpoint)
    .header("Content-Type", "application/json")
    .body("{\"query\":\"mutation FormExample {\\n  goto(url: \\\"https://www.browserless.io/practice-form\\\") {\\n    status\\n  }\\n\\n  typeEmail: type(text: \\\"user@example.com\\\", selector: \\\"#Email\\\") {\\n    time\\n  }\\n\\n  typeMessage: type(selector: \\\"#Message\\\", text: \\\"Hello from Browserless!\\\") {\\n    time\\n  }\\n\\n  subject: select(selector: \\\"select#Subject\\\", value: \\\"Support\\\") {\\n    selector\\n  }\\n\\n  solve {\\n    time\\n    solved\\n  }\\n\\n  submitForm: click(selector: \\\"button[type='submit']\\\") {\\n    time\\n  }\\n}\",\"variables\":\"\",\"operationName\":\"FormExample\"}")
    .asString();

System.out.println(response.getBody());
```

    **3. Check the output**

    Run the class. The response JSON confirms each mutation completed, including CAPTCHA solving status.

    
    
**C#:**

    

    **1. Send the request**

```csharp
using System.Net.Http;
using System.Text;
using System.Text.Json;

string url = "https://production-sfo.browserless.io/chromium/bql";
string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"{url}?token={token}";

var payload = new
{
    query = @"mutation FormExample {
  goto(url: ""https://www.browserless.io/practice-form"") {
    status
  }

  typeEmail: type(text: ""user@example.com"", selector: ""#Email"") {
    time
  }

  typeMessage: type(selector: ""#Message"", text: ""Hello from Browserless!"") {
    time
  }

  subject: select(selector: ""select#Subject"", value: ""Support"") {
    selector
  }

  solve {
    time
    solved
  }

  submitForm: click(selector: ""button[type='submit']"") {
    time
  }
}",
    variables = "",
    operationName = "FormExample",
};

using (HttpClient httpClient = new HttpClient())
{
    var jsonPayload = JsonSerializer.Serialize(payload);
    var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
    var response = await httpClient.PostAsync(endpoint, content);
    string responseBody = await response.Content.ReadAsStringAsync();
    Console.WriteLine(responseBody);
}
```

    **2. Check the output**

    Run the program. The response JSON confirms each mutation completed, including CAPTCHA solving status.

    
  

  
  
**Frameworks:**

  Use a browser connection to interact with the form: click, type, and wait for navigation.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Connect and interact**

```js
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint: 'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE',
});

try {
  const page = await browser.newPage();
  await page.goto('https://www.browserless.io/practice-form', {
    waitUntil: 'networkidle2',
  });

  await page.type('#Email', 'user@example.com');
  await page.type('#Message', 'Hello from Browserless!');
  await page.select('select#Subject', 'Support');
  await page.click("button[type='submit']");
  await page.waitForNavigation({ waitUntil: 'networkidle2' });

  console.log('Form submitted, current URL:', page.url());
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Run the script with `node form.mjs`. The script logs the URL after form submission.

    
    
**Playwright:**

    

    **1. Install dependencies**

```bash
npm install playwright-core
```

    **2. Connect and interact**

```js
import { chromium } from 'playwright-core';

const browser = await chromium.connect(
  'wss://production-sfo.browserless.io/chromium/playwright?token=YOUR_API_TOKEN_HERE'
);

try {
  const page = await browser.newPage();
  await page.goto('https://www.browserless.io/practice-form', {
    waitUntil: 'networkidle',
  });

  await page.fill('#Email', 'user@example.com');
  await page.fill('#Message', 'Hello from Browserless!');
  await page.selectOption('select#Subject', 'Support');
  await page.click("button[type='submit']");
  await page.waitForLoadState('networkidle');

  console.log('Form submitted, current URL:', page.url());
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Run the script with `node form.mjs`. The script logs the URL after form submission.

    
    
**BAP:**

    **1. Install dependencies**

```bash
npm install @browserless.io/bap-ts
```

    **2. Connect and submit**

```typescript
import Browserless from '@browserless.io/bap-ts';

const browser = Browserless.connect({
  browserWSEndpoint: 'wss://production-sfo.browserless.io/chromium/bql',
  token: 'YOUR_API_TOKEN_HERE',
});

try {
  const page = await browser.newPage();
  await page.goto('https://www.browserless.io/practice-form');

  await page.type('#Email', 'user@example.com');
  await page.type('#Message', 'Hello from Browserless!');
  await page.select('select#Subject', 'Support');

  // Clears the CAPTCHA guarding the form before submitting.
  const { solved } = await page.solve();
  console.log({ solved });

  await page.click("button[type='submit']");
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    The form posts once the CAPTCHA is cleared, the same sequence the BQL tab runs.

    
  

  
  
**BQL:**

  

  **1. Write the mutation**

  Chain navigation, typing, selection, CAPTCHA solving, and clicking in a single query:

```graphql
mutation FormExample {
  goto(url: "https://www.browserless.io/practice-form") {
    status
  }

  typeEmail: type(text: "user@example.com", selector: "#Email") {
    time
  }

  typeMessage: type(selector: "#Message", text: "Hello from Browserless!") {
    time
  }

  subject: select(selector: "select#Subject", value: "Support") {
    selector
  }

  solve {
    time
    solved
  }

  submitForm: click(selector: "button[type='submit']") {
    time
  }
}
```

  **2. Run it**

  Paste the mutation into the [BQL IDE](https://account.browserless.io/bql) and click Run, or send via HTTP (see the cURL tab for the full request wrapper).

  **3. Check the output**

  The response confirms each step completed, including whether the CAPTCHA was solved:

```json
{
  "data": {
    "goto": { "status": 200 },
    "typeEmail": { "time": 132 },
    "solve": { "time": 4200, "solved": true },
    "submitForm": { "time": 89 }
  }
}
```

  

## Next steps

  <a href="/examples/sessions" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Authenticated Sessions</h3>
      persist login state across requests
    
  </a>
  <a href="/examples/screenshot" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Take a Screenshot</h3>
      capture a screenshot after form submission
    
  </a>
  <a href="/examples/scrape" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape Structured Data</h3>
      extract data from the resulting page
    
  </a>

---

# Automate Google search

URL: https://docs.browserless.io/examples/google-search

# Automate Google search

Navigate to Google, submit a search query, and extract result titles and links.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

  
**AI Agent:**

  Use the [Browserless MCP server](https://github.com/browserless/browserless-mcp) to automate a Google search from any MCP-compatible AI agent (Claude Desktop, Cursor, Windsurf, ChatGPT, etc.).

  **1. Connect the MCP server**

  Send this prompt to your AI agent to install the Browserless MCP server:

```text
Go to https://github.com/browserless/browserless-mcp/blob/main/install.md
and follow the instructions to install the Browserless MCP server
for my client.
```

  **2. Search the web**

  Use `browserless_search`. The task starts with a search query rather than a known URL.

```text
Use the browserless_search tool to search Google for
"best headless browser API" and return the top results
```

  
  
**REST API:**

  Send the BQL mutation over HTTP to the stealth endpoint. Google's bot detection blocks the `/scrape` endpoint, so use `/stealth/bql` instead.

  
    
**cURL:**

    **1. Send the request**

```bash
curl -X POST \
  "https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d "{\"query\": \"mutation GoogleSearch { goto(url: \\\"https://www.google.com/search?q=Browserless+headless+browser\\\") { status } results: mapSelector(selector: \\\"h3\\\", wait: true) { innerText } }\", \"variables\": {}}"
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "results": [
      { "innerText": "Browserless | Headless Browser Automation & Scraping" },
      { "innerText": "Getting Started | Browserless" }
    ]
  }
}
```

    
    
**JavaScript:**

    **1. Send the request**

```js
const query = `
  mutation GoogleSearch {
    goto(url: "https://www.google.com/search?q=Browserless+headless+browser") {
      status
    }
    results: mapSelector(selector: "h3", wait: true) {
      innerText
    }
  }
`;

const response = await fetch(
  'https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ query, variables: {} }),
  }
);

const result = await response.json();
console.log(result.data.results);
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "results": [
      { "innerText": "Browserless | Headless Browser Automation & Scraping" },
      { "innerText": "Getting Started | Browserless" }
    ]
  }
}
```

    
    
**Python:**

    **1. Install dependencies**

```bash
pip install requests
```

    **2. Send the request**

```python
import requests

query = """
  mutation GoogleSearch {
    goto(url: "https://www.google.com/search?q=Browserless+headless+browser") {
      status
    }
    results: mapSelector(selector: "h3", wait: true) {
      innerText
    }
  }
"""

response = requests.post(
    'https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE',
    json={'query': query, 'variables': {}},
)

print(response.json())
```

    **3. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "results": [
      { "innerText": "Browserless | Headless Browser Automation & Scraping" },
      { "innerText": "Getting Started | Browserless" }
    ]
  }
}
```

    
    
**Java:**

    **1. Send the request**

```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

String endpoint = "https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE";

String mutation = "mutation GoogleSearch { goto(url: \\\"https://www.google.com/search?q=Browserless+headless+browser\\\") { status } results: mapSelector(selector: \\\"h3\\\", wait: true) { innerText } }";
String payload = "{\"query\": \"" + mutation + "\", \"variables\": {}}";

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(endpoint))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(payload))
    .build();

HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "results": [
      { "innerText": "Browserless | Headless Browser Automation & Scraping" },
      { "innerText": "Getting Started | Browserless" }
    ]
  }
}
```

    
    
**C#:**

    **1. Send the request**

```csharp
using System.Net.Http;
using System.Text;
using System.Text.Json;

string endpoint = "https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE";

string mutation = "mutation GoogleSearch { goto(url: \"https://www.google.com/search?q=Browserless+headless+browser\") { status } results: mapSelector(selector: \"h3\", wait: true) { innerText } }";

var payload = new { query = mutation, variables = new { } };

using HttpClient httpClient = new HttpClient();
var jsonPayload = JsonSerializer.Serialize(payload);
var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
var response = await httpClient.PostAsync(endpoint, content);
string responseBody = await response.Content.ReadAsStringAsync();
Console.WriteLine(responseBody);
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "results": [
      { "innerText": "Browserless | Headless Browser Automation & Scraping" },
      { "innerText": "Getting Started | Browserless" }
    ]
  }
}
```

    
  

  
  
**Frameworks:**

  Use a browser connection to navigate to Google and extract results from the rendered DOM.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Connect and search**

```js
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint: 'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE',
});

try {
  const page = await browser.newPage();
  await page.goto('https://www.google.com', { waitUntil: 'networkidle2' });

  await page.type('textarea[name="q"]', 'Browserless headless browser');
  await page.keyboard.press('Enter');
  await page.waitForSelector('#search');

  const results = await page.evaluate(() =>
    Array.from(document.querySelectorAll('h3')).map((h) => ({
      title: h.innerText,
      url: h.closest('a')?.href ?? null,
    }))
  );

  console.log(results);
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Run with `node search.mjs`. Each object in the array has a `title` and `url`.

    
    
**Playwright:**

    

    **1. Install dependencies**

```bash
npm install playwright-core
```

    **2. Connect and search**

```js
import { chromium } from 'playwright-core';

const browser = await chromium.connect(
  'wss://production-sfo.browserless.io/chromium/playwright?token=YOUR_API_TOKEN_HERE'
);

try {
  const page = await browser.newPage();
  await page.goto('https://www.google.com', { waitUntil: 'networkidle' });

  await page.fill('textarea[name="q"]', 'Browserless headless browser');
  await page.keyboard.press('Enter');
  await page.waitForSelector('#search');

  const results = await page.evaluate(() =>
    Array.from(document.querySelectorAll('h3')).map((h) => ({
      title: h.innerText,
      url: h.closest('a')?.href ?? null,
    }))
  );

  console.log(results);
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Run with `node search.mjs`. Each object in the array has a `title` and `url`.

    
    
**BAP:**

    **1. Install dependencies**

```bash
npm install @browserless.io/bap-ts
```

    **2. Connect and search**

```typescript
import Browserless from '@browserless.io/bap-ts';

const browser = Browserless.connect({
  browserWSEndpoint: 'wss://production-sfo.browserless.io/chromium/bql',
  token: 'YOUR_API_TOKEN_HERE',
});

try {
  const page = await browser.newPage();

  // BAP has no keyboard API, so navigate straight to the results URL.
  await page.goto('https://www.google.com/search?q=Browserless+headless+browser');

  // `wait: true` holds until the selector appears, so no separate wait call.
  const results = await page.mapSelector('h3', { wait: true });
  console.log(results.map((result) => result.innerText));
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Run with `node search.mjs`. Each result carries the heading text from the search page.

    
  

  
  
**BQL:**

  

  **1. Write the mutation**

  Pass the query in the URL to skip typing, then use `mapSelector` to extract all result headings:

```graphql
mutation GoogleSearch {
  goto(url: "https://www.google.com/search?q=Browserless+headless+browser") {
    status
  }
  results: mapSelector(selector: "h3", wait: true) {
    innerText
  }
}
```

  **2. Run it**

  Paste into the [BQL IDE](https://account.browserless.io/bql) and click Run.

  **3. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "results": [
      { "innerText": "Browserless | Headless Browser Automation & Scraping" },
      { "innerText": "Getting Started | Browserless" }
    ]
  }
}
```

  

## Next steps

  <a href="/examples/scrape" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape Structured Data</h3>
      extract more complex data with CSS selectors
    
  </a>
  <a href="/examples/extract-content" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Extract Content from a URL</h3>
      retrieve the full rendered HTML of any page
    
  </a>

---

# Healthcare portal extraction

URL: https://docs.browserless.io/examples/healthcare-portal-extraction

# Healthcare portal extraction

Log into a sandbox healthcare portal and extract patient medication records from the dashboard.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

This example uses a [Browserless sandbox site](https://scraping-sandbox.netlify.app/clarity-health/patient-portal) that simulates a healthcare portal with a login screen. You can safely test data extraction without accessing real patient data.

The workflow navigates to the patient portal, logs in with demo credentials, waits for the dashboard to render, then extracts medication records from the medications table.

  
**AI Agent:**

  Use the [Browserless MCP server](https://github.com/browserless/browserless-mcp) to extract data from the healthcare portal from any MCP-compatible AI agent (Claude Desktop, Cursor, Windsurf, ChatGPT, etc.).

  **1. Connect the MCP server**

  Send this prompt to your AI agent to install the Browserless MCP server:

```text
Go to https://github.com/browserless/browserless-mcp/blob/main/install.md
and follow the instructions to install the Browserless MCP server
for my client.
```

  **2. Extract portal data**

  Use `browserless_agent` to log in and extract medication data.

```text
Use the browserless_agent tool to log into
https://scraping-sandbox.netlify.app/clarity-health/patient-portal
with patient@example.com / health2025 and get the most recent invoice.
```

  
  
**REST API:**

  Send the BQL mutation over HTTP to log in and extract medication data. This is a sandbox site, so no stealth mode is needed.

  
    
**cURL:**

    

    **1. Send the request**

```bash
curl -X POST \
  "https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "mutation HealthcarePortal { goto(url: \"https://scraping-sandbox.netlify.app/clarity-health/patient-portal\", waitUntil: networkIdle) { status } waitForLogin: waitForSelector(selector: \"#patient-email\", timeout: 10000) { time } typeEmail: type(selector: \"#patient-email\", text: \"patient@example.com\") { time } typePassword: type(selector: \"#patient-password\", text: \"health2025\") { time } submitLogin: click(selector: \"#patient-login-submit\") { time } waitForDashboard: waitForSelector(selector: \"#medicationlist\", timeout: 10000) { time } medications: mapSelector(selector: \"#medicationlist table tbody tr\") { medication: mapSelector(selector: \"td:nth-child(1)\") { innerText } dosage: mapSelector(selector: \"td:nth-child(2)\") { innerText } frequency: mapSelector(selector: \"td:nth-child(3)\") { innerText } prescriber: mapSelector(selector: \"td:nth-child(4)\") { innerText } refills: mapSelector(selector: \"td:nth-child(5)\") { innerText } } }",
    "variables": {}
  }'
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForLogin": { "time": 2 },
    "typeEmail": { "time": 2328 },
    "typePassword": { "time": 1567 },
    "submitLogin": { "time": 152 },
    "waitForDashboard": { "time": 780 },
    "medications": [
      {
        "medication": [{ "innerText": "Atorvastatin" }],
        "dosage": [{ "innerText": "10 mg — Oral" }],
        "frequency": [{ "innerText": "Once daily at bedtime" }],
        "prescriber": [{ "innerText": "Dr. Marcus Webb" }],
        "refills": [{ "innerText": "4" }]
      },
      {
        "medication": [{ "innerText": "Vitamin D3" }],
        "dosage": [{ "innerText": "2000 IU — Oral" }],
        "frequency": [{ "innerText": "Once daily" }],
        "prescriber": [{ "innerText": "Dr. Sarah Chen" }],
        "refills": [{ "innerText": "2" }]
      }
    ]
  }
}
```

    
    
**JavaScript:**

    

    **1. Send the request**

```js
const query = `mutation HealthcarePortal {
  goto(url: "https://scraping-sandbox.netlify.app/clarity-health/patient-portal", waitUntil: networkIdle) {
    status
  }
  waitForLogin: waitForSelector(selector: "#patient-email", timeout: 10000) {
    time
  }
  typeEmail: type(selector: "#patient-email", text: "patient@example.com") {
    time
  }
  typePassword: type(selector: "#patient-password", text: "health2025") {
    time
  }
  submitLogin: click(selector: "#patient-login-submit") {
    time
  }
  waitForDashboard: waitForSelector(selector: "#medicationlist", timeout: 10000) {
    time
  }
  medications: mapSelector(selector: "#medicationlist table tbody tr") {
    medication: mapSelector(selector: "td:nth-child(1)") { innerText }
    dosage: mapSelector(selector: "td:nth-child(2)") { innerText }
    frequency: mapSelector(selector: "td:nth-child(3)") { innerText }
    prescriber: mapSelector(selector: "td:nth-child(4)") { innerText }
    refills: mapSelector(selector: "td:nth-child(5)") { innerText }
  }
}`;

const response = await fetch(
  'https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ query, variables: {} }),
  }
);

const { data } = await response.json();
const medications = data.medications.map((row) => ({
  medication: row.medication?.[0]?.innerText ?? '',
  dosage: row.dosage?.[0]?.innerText ?? '',
  frequency: row.frequency?.[0]?.innerText ?? '',
  prescriber: row.prescriber?.[0]?.innerText ?? '',
  refills: row.refills?.[0]?.innerText ?? '',
}));
console.log(JSON.stringify(medications, null, 2));
```

    **2. Check the output**

```json
[
  {
    "medication": "Atorvastatin",
    "dosage": "10 mg — Oral",
    "frequency": "Once daily at bedtime",
    "prescriber": "Dr. Marcus Webb",
    "refills": "4"
  },
  {
    "medication": "Vitamin D3",
    "dosage": "2000 IU — Oral",
    "frequency": "Once daily",
    "prescriber": "Dr. Sarah Chen",
    "refills": "2"
  }
]
```

    
    
**Python:**

    

    **1. Install dependencies**

```bash
pip install requests
```

    **2. Send the request**

```python
import requests

query = """
mutation HealthcarePortal {
  goto(url: "https://scraping-sandbox.netlify.app/clarity-health/patient-portal", waitUntil: networkIdle) {
    status
  }
  waitForLogin: waitForSelector(selector: "#patient-email", timeout: 10000) {
    time
  }
  typeEmail: type(selector: "#patient-email", text: "patient@example.com") {
    time
  }
  typePassword: type(selector: "#patient-password", text: "health2025") {
    time
  }
  submitLogin: click(selector: "#patient-login-submit") {
    time
  }
  waitForDashboard: waitForSelector(selector: "#medicationlist", timeout: 10000) {
    time
  }
  medications: mapSelector(selector: "#medicationlist table tbody tr") {
    medication: mapSelector(selector: "td:nth-child(1)") { innerText }
    dosage: mapSelector(selector: "td:nth-child(2)") { innerText }
    frequency: mapSelector(selector: "td:nth-child(3)") { innerText }
    prescriber: mapSelector(selector: "td:nth-child(4)") { innerText }
    refills: mapSelector(selector: "td:nth-child(5)") { innerText }
  }
}
"""

response = requests.post(
    'https://production-sfo.browserless.io/chromium/bql',
    params={'token': 'YOUR_API_TOKEN_HERE'},
    json={'query': query, 'variables': {}},
)

data = response.json()['data']
for med in data['medications']:
    name = med['medication'][0]['innerText']
    dosage = med['dosage'][0]['innerText']
    frequency = med['frequency'][0]['innerText']
    prescriber = med['prescriber'][0]['innerText']
    refills = med['refills'][0]['innerText']
    print(f'{name} | {dosage} | {frequency} | {prescriber} | {refills}')
```

    **3. Check the output**

```text
Atorvastatin | 10 mg — Oral | Once daily at bedtime | Dr. Marcus Webb | 4
Vitamin D3 | 2000 IU — Oral | Once daily | Dr. Sarah Chen | 2
Naproxen | 500 mg — Oral | Twice daily with food as needed | Dr. James Torres | 1
```

    
    
**Java:**

    

    **1. Send the request**

```java
import java.net.URI;
import java.net.http.*;

String token = "YOUR_API_TOKEN_HERE";
String endpoint = "https://production-sfo.browserless.io/chromium/bql?token=" + token;

String query = "mutation HealthcarePortal {"
    + " goto(url: \\\"https://scraping-sandbox.netlify.app/clarity-health/patient-portal\\\", waitUntil: networkIdle) { status }"
    + " waitForLogin: waitForSelector(selector: \\\"#patient-email\\\", timeout: 10000) { time }"
    + " typeEmail: type(selector: \\\"#patient-email\\\", text: \\\"patient@example.com\\\") { time }"
    + " typePassword: type(selector: \\\"#patient-password\\\", text: \\\"health2025\\\") { time }"
    + " submitLogin: click(selector: \\\"#patient-login-submit\\\") { time }"
    + " waitForDashboard: waitForSelector(selector: \\\"#medicationlist\\\", timeout: 10000) { time }"
    + " medications: mapSelector(selector: \\\"#medicationlist table tbody tr\\\") {"
    + "   medication: mapSelector(selector: \\\"td:nth-child(1)\\\") { innerText }"
    + "   dosage: mapSelector(selector: \\\"td:nth-child(2)\\\") { innerText }"
    + "   frequency: mapSelector(selector: \\\"td:nth-child(3)\\\") { innerText }"
    + "   prescriber: mapSelector(selector: \\\"td:nth-child(4)\\\") { innerText }"
    + "   refills: mapSelector(selector: \\\"td:nth-child(5)\\\") { innerText }"
    + " }"
    + " }";

String payload = "{\"query\": \"" + query + "\", \"variables\": {}}";

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(endpoint))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(payload))
    .build();

HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForDashboard": { "time": 780 },
    "medications": [
      {
        "medication": [{ "innerText": "Atorvastatin" }],
        "prescriber": [{ "innerText": "Dr. Marcus Webb" }]
      }
    ]
  }
}
```

    
    
**C#:**

    

    **1. Send the request**

```csharp
using System.Net.Http;
using System.Text;
using System.Text.Json;

string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"https://production-sfo.browserless.io/chromium/bql?token={token}";

var payload = new
{
    query = @"mutation HealthcarePortal {
      goto(url: ""https://scraping-sandbox.netlify.app/clarity-health/patient-portal"", waitUntil: networkIdle) { status }
      waitForLogin: waitForSelector(selector: ""#patient-email"", timeout: 10000) { time }
      typeEmail: type(selector: ""#patient-email"", text: ""patient@example.com"") { time }
      typePassword: type(selector: ""#patient-password"", text: ""health2025"") { time }
      submitLogin: click(selector: ""#patient-login-submit"") { time }
      waitForDashboard: waitForSelector(selector: ""#medicationlist"", timeout: 10000) { time }
      medications: mapSelector(selector: ""#medicationlist table tbody tr"") {
        medication: mapSelector(selector: ""td:nth-child(1)"") { innerText }
        dosage: mapSelector(selector: ""td:nth-child(2)"") { innerText }
        frequency: mapSelector(selector: ""td:nth-child(3)"") { innerText }
        prescriber: mapSelector(selector: ""td:nth-child(4)"") { innerText }
        refills: mapSelector(selector: ""td:nth-child(5)"") { innerText }
      }
    }",
    variables = new { },
};

using (HttpClient httpClient = new HttpClient())
{
    var content = new StringContent(
        JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
    var response = await httpClient.PostAsync(endpoint, content);
    string body = await response.Content.ReadAsStringAsync();
    Console.WriteLine(body);
}
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForDashboard": { "time": 780 },
    "medications": [
      {
        "medication": [{ "innerText": "Atorvastatin" }],
        "prescriber": [{ "innerText": "Dr. Marcus Webb" }]
      }
    ]
  }
}
```

    
  

  
  
**Frameworks:**

  Connect a headless browser to the sandbox portal, log in, and extract medication records from the dashboard.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Connect and extract**

```js
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint: 'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE',
});

try {
  const page = await browser.newPage();
  await page.goto('https://scraping-sandbox.netlify.app/clarity-health/patient-portal', {
    waitUntil: 'networkidle2',
  });

  await page.waitForSelector('#patient-email');
  await page.type('#patient-email', 'patient@example.com');
  await page.type('#patient-password', 'health2025');
  await page.click('#patient-login-submit');

  await page.waitForSelector('#medicationlist');

  const medications = await page.evaluate(() =>
    Array.from(document.querySelectorAll('#medicationlist table tbody tr')).map((row) => {
      const cells = row.querySelectorAll('td');
      return {
        medication: cells[0]?.innerText?.trim() ?? '',
        dosage: cells[1]?.innerText?.trim() ?? '',
        frequency: cells[2]?.innerText?.trim() ?? '',
        prescriber: cells[3]?.innerText?.trim() ?? '',
        refills: cells[4]?.innerText?.trim() ?? '',
      };
    })
  );

  console.log(JSON.stringify(medications, null, 2));
} finally {
  await browser.close();
}
```

    **3. Check the output**

    Run with `node healthcare-portal-extraction.mjs`. Each object has `medication`, `dosage`, `frequency`, `prescriber`, and `refills` fields.

```json
[
  {
    "medication": "Atorvastatin",
    "dosage": "10 mg — Oral",
    "frequency": "Once daily at bedtime",
    "prescriber": "Dr. Marcus Webb",
    "refills": "4"
  }
]
```

    
    
**Playwright:**

    

    **1. Install dependencies**

```bash
npm install playwright-core
```

    **2. Connect and extract**

```js
import { chromium } from 'playwright-core';

const browser = await chromium.connectOverCDP(
  'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE'
);

try {
  const context = browser.contexts()[0];
  const page = await context.newPage();
  await page.goto('https://scraping-sandbox.netlify.app/clarity-health/patient-portal', {
    waitUntil: 'networkidle',
  });

  await page.waitForSelector('#patient-email');
  await page.fill('#patient-email', 'patient@example.com');
  await page.fill('#patient-password', 'health2025');
  await page.click('#patient-login-submit');

  await page.waitForSelector('#medicationlist');

  const medications = await page.evaluate(() =>
    Array.from(document.querySelectorAll('#medicationlist table tbody tr')).map((row) => {
      const cells = row.querySelectorAll('td');
      return {
        medication: cells[0]?.innerText?.trim() ?? '',
        dosage: cells[1]?.innerText?.trim() ?? '',
        frequency: cells[2]?.innerText?.trim() ?? '',
        prescriber: cells[3]?.innerText?.trim() ?? '',
        refills: cells[4]?.innerText?.trim() ?? '',
      };
    })
  );

  console.log(JSON.stringify(medications, null, 2));
} finally {
  await browser.close();
}
```

    **3. Check the output**

    Run with `node healthcare-portal-extraction.mjs`. Each object has `medication`, `dosage`, `frequency`, `prescriber`, and `refills` fields.

```json
[
  {
    "medication": "Atorvastatin",
    "dosage": "10 mg — Oral",
    "frequency": "Once daily at bedtime",
    "prescriber": "Dr. Marcus Webb",
    "refills": "4"
  }
]
```

    
  

  
  
**BQL:**

  

  **1. Write the mutation**

  Navigate to the sandbox healthcare portal, log in with demo credentials, and extract medication records. This is a sandbox site, so no stealth mode is needed.

```graphql
mutation HealthcarePortal {
  goto(url: "https://scraping-sandbox.netlify.app/clarity-health/patient-portal", waitUntil: networkIdle) {
    status
  }
  waitForLogin: waitForSelector(selector: "#patient-email", timeout: 10000) {
    time
  }
  typeEmail: type(selector: "#patient-email", text: "patient@example.com") {
    time
  }
  typePassword: type(selector: "#patient-password", text: "health2025") {
    time
  }
  submitLogin: click(selector: "#patient-login-submit") {
    time
  }
  waitForDashboard: waitForSelector(selector: "#medicationlist", timeout: 10000) {
    time
  }
  medications: mapSelector(selector: "#medicationlist table tbody tr") {
    medication: mapSelector(selector: "td:nth-child(1)") { innerText }
    dosage: mapSelector(selector: "td:nth-child(2)") { innerText }
    frequency: mapSelector(selector: "td:nth-child(3)") { innerText }
    prescriber: mapSelector(selector: "td:nth-child(4)") { innerText }
    refills: mapSelector(selector: "td:nth-child(5)") { innerText }
  }
}
```

  **2. Run it**

  Paste into the [BQL IDE](https://account.browserless.io/bql) and click Run.

  **3. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForLogin": { "time": 2 },
    "typeEmail": { "time": 2328 },
    "typePassword": { "time": 1567 },
    "submitLogin": { "time": 152 },
    "waitForDashboard": { "time": 780 },
    "medications": [
      {
        "medication": [{ "innerText": "Atorvastatin" }],
        "dosage": [{ "innerText": "10 mg — Oral" }],
        "frequency": [{ "innerText": "Once daily at bedtime" }],
        "prescriber": [{ "innerText": "Dr. Marcus Webb" }],
        "refills": [{ "innerText": "4" }]
      },
      {
        "medication": [{ "innerText": "Vitamin D3" }],
        "dosage": [{ "innerText": "2000 IU — Oral" }],
        "frequency": [{ "innerText": "Once daily" }],
        "prescriber": [{ "innerText": "Dr. Sarah Chen" }],
        "refills": [{ "innerText": "2" }]
      }
    ]
  }
}
```

  

## Next steps

  <a href="/examples/scrape" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape Structured Data</h3>
      extract data from other sites using the `/scrape` endpoint
    
  </a>
  <a href="/examples/forms" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Fill and Submit a Form</h3>
      automate form interactions on portals
    
  </a>
  <a href="/examples/sessions" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Authenticated Sessions</h3>
      persist login state for portals that require authentication
    
  </a>

---

# Wait for navigation after a click

URL: https://docs.browserless.io/examples/navigate-after-click

# Wait for navigation after a click

Clicking a link or button that triggers a page load requires `Promise.all` to guarantee the navigation wait is registered before the click fires. This pattern is essential for reliable [session management](/baas/session-management) with remote browsers.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

  
**AI Agent:**

  Use the [Browserless MCP server](https://github.com/browserless/browserless-mcp) to wait for navigation after a click from any MCP-compatible AI agent (Claude Desktop, Cursor, Windsurf, ChatGPT, etc.).

  **1. Connect the MCP server**

  Send this prompt to your AI agent to install the Browserless MCP server:

```text
Go to https://github.com/browserless/browserless-mcp/blob/main/install.md
and follow the instructions to install the Browserless MCP server
for my client.
```

  **2. Navigate after a click**

  Use `browserless_agent`. The task requires interaction: clicking an element and waiting for the page to load.

```text
Use the browserless_agent tool to go to
https://scraping-sandbox.netlify.app/products,
click the "Contact Us" link, wait for the new page to load,
then scrape the content
```

  
  
**REST API:**

  The `/function` endpoint runs your script inside the browser process, so the same `Promise.all` pattern applies. Use it to avoid the race condition even here.

  
    
**cURL:**

    

    **1. Send the request**

```bash
curl -X POST \
  "https://production-sfo.browserless.io/function?token=YOUR_API_TOKEN_HERE" \
  -H "Content-Type: application/javascript" \
  --data-raw 'module.exports = async ({ page }) => {
    await page.goto("https://scraping-sandbox.netlify.app/products");

    // Register the navigation wait before clicking — if you click first,
    // the page load can complete before waitForNavigation() starts listening.
    await Promise.all([
      page.waitForNavigation({ waitUntil: "domcontentloaded" }),
      page.click("a[href=\"/contact-us\"]"),
    ]);

    const heading = await page.$eval("h1", el => el.textContent.trim());
    return { url: page.url(), heading };
  };'
```

    **2. Check the output**

```json
{
  "url": "https://scraping-sandbox.netlify.app/contact-us",
  "heading": "Contact Us"
}
```

    
    
**JavaScript:**

    

    **1. Send the request**

```js
const code = `module.exports = async ({ page }) => {
  await page.goto('https://scraping-sandbox.netlify.app/products');

  // Register the navigation wait before clicking — if you click first,
  // the page load can complete before waitForNavigation() starts listening.
  await Promise.all([
    page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
    page.click('a[href="/contact-us"]'),
  ]);

  const heading = await page.$eval('h1', el => el.textContent.trim());
  return { url: page.url(), heading };
};`;

const response = await fetch(
  'https://production-sfo.browserless.io/function?token=YOUR_API_TOKEN_HERE',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/javascript' },
    body: code,
  }
);

const result = await response.json();
console.log(result);
```

    **2. Check the output**

    Run with `node navigate-after-click.mjs`. You'll see the Contact Us page URL and its `h1`. This confirms the script landed on the page the link pointed to, not the products page.

```json
{
  "url": "https://scraping-sandbox.netlify.app/contact-us",
  "heading": "Contact Us"
}
```

    
    
**Python:**

    

    **1. Install dependencies**

```bash
pip install requests
```

    **2. Send the request**

```python
import requests

code = """
module.exports = async ({ page }) => {
  await page.goto('https://scraping-sandbox.netlify.app/products');

  // Register the navigation wait before clicking — if you click first,
  // the page load can complete before waitForNavigation() starts listening.
  await Promise.all([
    page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
    page.click('a[href="/contact-us"]'),
  ]);

  const heading = await page.$eval('h1', el => el.textContent.trim());
  return { url: page.url(), heading };
};
"""

response = requests.post(
    'https://production-sfo.browserless.io/function?token=YOUR_API_TOKEN_HERE',
    headers={'Content-Type': 'application/javascript'},
    data=code.encode('utf-8'),
)

print(response.json())
```

    **3. Check the output**

    Run with `python navigate_after_click.py`. You'll see the Contact Us page URL and its `h1`. This confirms the script landed on the page the link pointed to, not the products page.

```json
{
  "url": "https://scraping-sandbox.netlify.app/contact-us",
  "heading": "Contact Us"
}
```

    
    
**Java:**

    

    **1. Send the request**

```java
import java.net.URI;
import java.net.http.*;

String token = "YOUR_API_TOKEN_HERE";
String endpoint = "https://production-sfo.browserless.io/function?token=" + token;

String code = """
    module.exports = async ({ page }) => {
      await page.goto('https://scraping-sandbox.netlify.app/products');

      // Register the navigation wait before clicking — if you click first,
      // the page load can complete before waitForNavigation() starts listening.
      await Promise.all([
        page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
        page.click('a[href="/contact-us"]'),
      ]);

      const heading = await page.$eval('h1', el => el.textContent.trim());
      return { url: page.url(), heading };
    };
    """;

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(endpoint))
    .header("Content-Type", "application/javascript")
    .POST(HttpRequest.BodyPublishers.ofString(code))
    .build();

HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
```

    **2. Check the output**

    Run the class. The JSON body confirms navigation completed: `url` is the Contact Us page, not the products page.

```json
{
  "url": "https://scraping-sandbox.netlify.app/contact-us",
  "heading": "Contact Us"
}
```

    
    
**C#:**

    

    **1. Send the request**

```csharp
using System.Net.Http;
using System.Text;

string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"https://production-sfo.browserless.io/function?token={token}";

string code = @"module.exports = async ({ page }) => {
  await page.goto('https://scraping-sandbox.netlify.app/products');

  // Register the navigation wait before clicking — if you click first,
  // the page load can complete before waitForNavigation() starts listening.
  await Promise.all([
    page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
    page.click('a[href=""/contact-us""]'),
  ]);

  const heading = await page.$eval('h1', el => el.textContent.trim());
  return { url: page.url(), heading };
};";

using (HttpClient httpClient = new HttpClient())
{
    var content = new StringContent(code, Encoding.UTF8, "application/javascript");
    var response = await httpClient.PostAsync(endpoint, content);
    string result = await response.Content.ReadAsStringAsync();
    Console.WriteLine(result);
}
```

    **2. Check the output**

    Run the program. The JSON body confirms navigation completed: `url` is the Contact Us page, not the products page.

```json
{
  "url": "https://scraping-sandbox.netlify.app/contact-us",
  "heading": "Contact Us"
}
```

    
  

  
  
**Frameworks:**

  Direct framework connections give you full control over timing, which means you own the navigation wait. This is where the bug most commonly surfaces. There's no wrapper to catch a missed `waitForNavigation`.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Connect and click**

```js
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint: 'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE',
});

try {
  const page = await browser.newPage();
  await page.goto('https://scraping-sandbox.netlify.app/products', { waitUntil: 'domcontentloaded' });

  // Wrong — navigation may finish between these two lines:
  // await page.click('a[href="/contact-us"]');
  // await page.waitForNavigation({ waitUntil: 'domcontentloaded' });

  // Right — Promise.all starts the wait before the click fires:
  await Promise.all([
    page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
    page.click('a[href="/contact-us"]'),
  ]);

  const heading = await page.$eval('h1', el => el.textContent.trim());
  console.log('URL:', page.url());
  console.log('Heading:', heading);
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Run with `node navigate-after-click.mjs`. The console output shows the Contact Us page URL and `h1` text. If you drop the `Promise.all` and see the products page heading instead, that's the race condition in action.

    
    
**Playwright:**

    

    **1. Install dependencies**

```bash
npm install playwright-core
```

    **2. Connect and click**

    Playwright's `page.click()` auto-waits for the element to be actionable (visible and enabled), but it does NOT wait for any resulting navigation. That's still your responsibility. Two approaches work here:

    - **`Promise.all` with `waitForNavigation`** — use when you don't know the destination URL in advance.
    - **`waitForURL`** — use when you know the destination URL pattern. Because you click first and then await `waitForURL`, you skip the `Promise.all` wrapper entirely. Playwright polls for the URL match rather than listening for a navigation event, so the ordering doesn't matter.

```js
import { chromium } from 'playwright-core';

const browser = await chromium.connect(
  'wss://production-sfo.browserless.io/chromium/playwright?token=YOUR_API_TOKEN_HERE'
);

try {
  const page = await browser.newPage();
  await page.goto('https://scraping-sandbox.netlify.app/products', { waitUntil: 'domcontentloaded' });

  // Approach 1 — works when you don't know the destination URL:
  await Promise.all([
    page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
    page.click('a[href="/contact-us"]'),
  ]);

  // Approach 2 — simpler when you know the destination URL pattern:
  // await page.click('a[href="/contact-us"]');
  // await page.waitForURL('**/contact-us');

  const heading = await page.$eval('h1', el => el.textContent.trim());
  console.log('URL:', page.url());
  console.log('Heading:', heading);
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Run with `node navigate-after-click.mjs`. The console output shows the Contact Us page URL and `h1` text. Both approaches correctly waited for the destination page before reading the DOM.

    
    
**BAP:**

    **1. Install dependencies**

```bash
npm install @browserless.io/bap-ts
```

    **2. Connect and follow the link**

```typescript
import Browserless from '@browserless.io/bap-ts';

const browser = Browserless.connect({
  browserWSEndpoint: 'wss://production-sfo.browserless.io/chromium/bql',
  token: 'YOUR_API_TOKEN_HERE',
});

try {
  const page = await browser.newPage();
  await page.goto('https://scraping-sandbox.netlify.app/products', { waitUntil: 'domContentLoaded' });

  await page.click('a[href="/contact-us"]');

  // click() doesn't imply navigation, so wait for the load before reading
  // the new page — otherwise you may read the old DOM.
  await page.waitForNavigation({ waitUntil: 'domContentLoaded' });

  const heading = await page.$eval('h1');
  console.log(heading);
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Reading the heading after the click confirms the navigation completed.

    
  

  
  
**BQL:**

  

  **1. Write the mutation**

  [BQL](/browserql/writing-bql/language-basics) executes actions sequentially, and the `click` action automatically waits for any resulting navigation to settle before moving to the next action. No `Promise.all` needed.

```graphql
mutation NavigateAfterClick {
  goto(url: "https://scraping-sandbox.netlify.app/products", waitUntil: domContentLoaded) {
    status
  }
  clicked: click(selector: "a[href=\"/contact-us\"]") {
    x
    y
  }
  heading: text(selector: "h1")
}
```

  After `click`, BQL waits for the page to settle before reading `h1`, so the text value reflects the destination page.

  **2. Run it**

  Paste into the [BQL IDE](https://account.browserless.io/bql) and click Run.

  **3. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "clicked": { "x": 200, "y": 210 },
    "heading": "Contact Us"
  }
}
```

---

# Generate a PDF

URL: https://docs.browserless.io/examples/pdf

# Generate a PDF

Export any webpage or HTML content as a PDF using Browserless.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

  
**AI Agent:**

  Use the [Browserless MCP server](https://github.com/browserless/browserless-mcp) to generate a PDF from any MCP-compatible AI agent (Claude Desktop, Cursor, Windsurf, ChatGPT, etc.).

  **1. Connect the MCP server**

  Send this prompt to your AI agent to install the Browserless MCP server:

```text
Go to https://github.com/browserless/browserless-mcp/blob/main/install.md
and follow the instructions to install the Browserless MCP server
for my client.
```

  **2. Generate a PDF**

  Use `browserless_smartscraper`. It captures the page as a PDF in one call with automatic bot-protection handling.

```text
Use the browserless_smartscraper tool to generate a full-page
PDF of https://scraping-sandbox.netlify.app/dashboard-report and save it as output.pdf
```

  
  
**REST API:**

  Use the `/pdf` REST endpoint to generate a PDF from a URL. No WebSocket connection needed.

  

  
    
**cURL:**

    

    **1. Build the request**

    Append your token to the PDF endpoint:

```bash
https://production-sfo.browserless.io/pdf?token=YOUR_API_TOKEN_HERE
```

    **2. Send the request**

```bash
curl -X POST \
  "https://production-sfo.browserless.io/pdf?token=YOUR_API_TOKEN_HERE" \
  -H 'Cache-Control: no-cache' \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://scraping-sandbox.netlify.app/dashboard-report",
    "options": {
      "displayHeaderFooter": true,
      "printBackground": true,
      "format": "A4"
    }
  }' \
  -o output.pdf
```

    **3. Check the output**

    The response is a raw PDF binary written to `output.pdf` in the current directory.

    
    
**JavaScript:**

    

    **1. Send the request**

```js
import fs from 'fs';

const response = await fetch(
  'https://production-sfo.browserless.io/pdf?token=YOUR_API_TOKEN_HERE',
  {
    method: 'POST',
    headers: {
      'Cache-Control': 'no-cache',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      url: 'https://scraping-sandbox.netlify.app/dashboard-report',
      options: {
        displayHeaderFooter: true,
        printBackground: true,
        format: 'A4',
      },
    }),
  }
);

const buffer = await response.arrayBuffer();
fs.writeFileSync('output.pdf', Buffer.from(buffer));
```

    **2. Check the output**

    Run the script with `node pdf.mjs`. The file `output.pdf` will appear in the current directory.

    
    
**Python:**

    

    **1. Install dependencies**

```bash
pip install requests
```

    **2. Send the request**

```python
import requests

response = requests.post(
    'https://production-sfo.browserless.io/pdf?token=YOUR_API_TOKEN_HERE',
    headers={'Cache-Control': 'no-cache', 'Content-Type': 'application/json'},
    json={
        'url': 'https://scraping-sandbox.netlify.app/dashboard-report',
        'options': {
            'displayHeaderFooter': True,
            'printBackground': True,
            'format': 'A4',
        },
    },
)

with open('output.pdf', 'wb') as f:
    f.write(response.content)
```

    **3. Check the output**

    Run the script with `python pdf.py`. The file `output.pdf` will appear in the current directory.

    
    
**Java:**

    

    **1. Send the request**

```java
import java.io.*;
import java.net.URI;
import java.net.http.*;

String token = "YOUR_API_TOKEN_HERE";
String endpoint = "https://production-sfo.browserless.io/pdf?token=" + token;

String jsonData = """
    {
        "url": "https://scraping-sandbox.netlify.app/dashboard-report",
        "options": {
            "displayHeaderFooter": true,
            "printBackground": true,
            "format": "A4"
        }
    }
""";

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(endpoint))
    .header("Cache-Control", "no-cache")
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(jsonData))
    .build();

HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofInputStream());
try (FileOutputStream fos = new FileOutputStream("output.pdf")) {
    response.body().transferTo(fos);
}
System.out.println("PDF saved as output.pdf");
```

    **2. Check the output**

    Run the class. The file `output.pdf` will appear in the current directory.

    
    
**C#:**

    

    **1. Send the request**

```csharp
using System.IO;
using System.Net.Http;
using System.Text;
using System.Text.Json;

string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"https://production-sfo.browserless.io/pdf?token={token}";

var payload = new
{
    url = "https://scraping-sandbox.netlify.app/dashboard-report",
    options = new { displayHeaderFooter = true, printBackground = true, format = "A4" },
};

using (HttpClient httpClient = new HttpClient())
{
    var request = new HttpRequestMessage(HttpMethod.Post, endpoint);
    request.Headers.Add("Cache-Control", "no-cache");
    request.Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");

    var response = await httpClient.SendAsync(request);
    var contentStream = await response.Content.ReadAsStreamAsync();
    using (var fileStream = new FileStream("output.pdf", FileMode.Create, FileAccess.Write))
    {
        await contentStream.CopyToAsync(fileStream);
    }
    Console.WriteLine("PDF saved as output.pdf");
}
```

    **2. Check the output**

    Run the program. The file `output.pdf` will appear in the current directory.

    
  

  
  
**Frameworks:**

  Use a browser connection when you need to interact with the page or wait for dynamic content before generating the PDF.

  

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Connect and generate**

```js
import puppeteer from 'puppeteer-core';
import fs from 'fs';

const browser = await puppeteer.connect({
  browserWSEndpoint: 'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE',
});

try {
  const page = await browser.newPage();
  await page.goto('https://scraping-sandbox.netlify.app/dashboard-report', { waitUntil: 'networkidle2' });

  const pdf = await page.pdf({
    format: 'A4',
    printBackground: true,
    displayHeaderFooter: true,
  });

  fs.writeFileSync('output.pdf', pdf);
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Run the script with `node pdf.mjs`. The file `output.pdf` will appear in the current directory.

    
    
**Playwright:**

    

    **1. Install dependencies**

```bash
npm install playwright-core
```

    **2. Connect and generate**

```js
import { chromium } from 'playwright-core';
import fs from 'fs';

const browser = await chromium.connect(
  'wss://production-sfo.browserless.io/chromium/playwright?token=YOUR_API_TOKEN_HERE'
);

try {
  const page = await browser.newPage();
  await page.goto('https://scraping-sandbox.netlify.app/dashboard-report', { waitUntil: 'networkidle' });

  const pdf = await page.pdf({
    format: 'A4',
    printBackground: true,
    displayHeaderFooter: true,
  });

  fs.writeFileSync('output.pdf', pdf);
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Run the script with `node pdf.mjs`. The file `output.pdf` will appear in the current directory.

    
    
**BAP:**

    **1. Install dependencies**

```bash
npm install @browserless.io/bap-ts
```

    **2. Connect and generate**

```typescript
import Browserless from '@browserless.io/bap-ts';

const browser = Browserless.connect({
  browserWSEndpoint: 'wss://production-sfo.browserless.io/chromium/bql',
  token: 'YOUR_API_TOKEN_HERE',
});

try {
  const page = await browser.newPage();
  await page.goto('https://scraping-sandbox.netlify.app/dashboard-report');

  // `path` writes the file directly, so no fs call is needed in Node.js.
  await page.pdf({
    path: 'output.pdf',
    format: 'a4',
    printBackground: true,
    displayHeaderFooter: true,
  });
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Run the script with `node pdf.mjs`. The file `output.pdf` will appear in the current directory.

    
  

  
  
**BQL:**

  

  **1. Write the mutation**

  Navigate to the URL and generate a PDF in one operation:

```graphql
mutation PDF {
  goto(url: "https://scraping-sandbox.netlify.app/dashboard-report") {
    status
  }

  pdf(format: a4, printBackground: true, displayHeaderFooter: true) {
    base64
  }
}
```

  **2. Run it**

  Paste into the [BQL IDE](https://account.browserless.io/bql) and click Run.

  **3. Check the output**

  The response includes a `base64`-encoded PDF. Each example above decodes it and saves it as `output.pdf`.

```json
{
  "data": {
    "goto": { "status": 200 },
    "pdf": { "base64": "JVBERi0xLjQ..." }
  }
}
```

  

## Next steps

  <a href="/examples/screenshot" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Take a Screenshot</h3>
      capture the page as an image instead
    
  </a>
  <a href="/examples/scrape" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape Structured Data</h3>
      extract data from the page
    
  </a>
  <a href="/examples/forms" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Fill and Submit a Form</h3>
      automate form interactions before generating a PDF
    
  </a>

---

# Continue browser state across runs

URL: https://docs.browserless.io/examples/persist-session

# Continue browser state across runs

Create a long-lived browser session via the Session API, store data like cookies and localStorage, disconnect, then reconnect later and verify state continues across runs.

> **Short-lived reconnections**
> Need to briefly pause your script and resume the same browser within seconds or minutes? See [Disconnect and Reconnect to a Browser](/examples/reconnect) for a simpler approach that doesn't require managing sessions via API.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

The Session API gives you full REST control over session lifecycle. Create a session with a TTL, connect and set state, disconnect safely, then reconnect and verify your data is still there.

  
**REST API:**

  Use the Session API REST endpoints to create, connect to, and manage long-lived browser sessions.

  
    
**cURL:**

    

    **1. Create a session**

```bash
curl -X POST "https://production-sfo.browserless.io/session?token=YOUR_API_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "ttl": 300000,
    "stealth": true,
    "headless": false
  }'
```

    The response returns `connect` (WebSocket URL) and `stop` (DELETE URL) fields.

    **2. Stop the session when done**

```bash
curl -X DELETE "STOP_URL_FROM_STEP_1&force=true"
```

    
    
**JavaScript:**

    

    **1. Create the session and set state**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";

const sessionResponse = await fetch(
  `https://production-sfo.browserless.io/session?token=${TOKEN}`,
  {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ ttl: 300000, stealth: true, headless: false }),
  }
);
const session = await sessionResponse.json();
console.log("Session created:", session.id);
```

    **2. Connect via Puppeteer and store data**

```js
import puppeteer from "puppeteer-core";

const browser = await puppeteer.connect({ browserWSEndpoint: session.connect });
const page = await browser.newPage();

await page.goto("https://automationexercise.com", { waitUntil: "networkidle2" });

await page.evaluate(() => {
  localStorage.setItem(
    "shoppingCart",
    JSON.stringify({ items: [{ id: 1 }], totalItems: 1, totalPrice: 500 })
  );
  localStorage.setItem("userPreferences", JSON.stringify({ theme: "dark" }));
});

await browser.disconnect();
```

    **3. Reconnect and verify state persists**

```js
const browser2 = await puppeteer.connect({ browserWSEndpoint: session.connect });
const page2 = await browser2.newPage();
await page2.goto("https://automationexercise.com", { waitUntil: "networkidle2" });

const cart = await page2.evaluate(() => localStorage.getItem("shoppingCart"));
console.log("Cart persisted:", JSON.parse(cart));

await browser2.close();
```

    **4. Delete the session**

```js
await fetch(`${session.stop}&force=true`, { method: "DELETE" });
console.log("Session stopped.");
```

    
    
**Python:**

    

    **1. Install dependencies**

```bash
pip install requests
```

    **2. Create the session**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"

session_response = requests.post(
    f"https://production-sfo.browserless.io/session?token={TOKEN}",
    json={"ttl": 300000, "stealth": True, "headless": False},
)
session = session_response.json()
print("Session created:", session["id"])
print("Connect URL:", session["connect"])
print("Stop URL:", session["stop"])
```

    **3. Connect and verify with Playwright**

```python
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.connect_over_cdp(session["connect"])
    context = browser.contexts[0] if browser.contexts else browser.new_context()
    page = context.new_page()
    page.goto("https://automationexercise.com")

    page.evaluate("""() => {
        localStorage.setItem('shoppingCart', JSON.stringify({items: [{id: 1}]}));
    }""")

    browser.close()

    # Reconnect and verify
    browser2 = p.chromium.connect_over_cdp(session["connect"])
    context2 = browser2.contexts[0] if browser2.contexts else browser2.new_context()
    page2 = context2.new_page()
    page2.goto("https://automationexercise.com")

    cart = page2.evaluate("() => localStorage.getItem('shoppingCart')")
    print("Cart persisted:", cart)

    browser2.close()
```

    **4. Stop the session**

```python
requests.delete(f"{session['stop']}&force=true")
print("Session stopped.")
```

    
    
**Java:**

    

    **1. Create the session**

```java
import java.net.URI;
import java.net.http.*;

String token = "YOUR_API_TOKEN_HERE";
String endpoint = "https://production-sfo.browserless.io/session?token=" + token;

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(endpoint))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(
        "{\"ttl\": 300000, \"stealth\": true, \"headless\": false}"
    ))
    .build();

HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
String body = response.body();
System.out.println("Session created: " + body);

// Parse connect and stop URLs from response
int connectStart = body.indexOf("\"connect\":\"") + 11;
int connectEnd = body.indexOf("\"", connectStart);
String connectUrl = body.substring(connectStart, connectEnd);

int stopStart = body.indexOf("\"stop\":\"") + 8;
int stopEnd = body.indexOf("\"", stopStart);
String stopUrl = body.substring(stopStart, stopEnd);
```

    **2. Stop the session when done**

```java
HttpRequest deleteRequest = HttpRequest.newBuilder()
    .uri(URI.create(stopUrl + "&force=true"))
    .DELETE()
    .build();

client.send(deleteRequest, HttpResponse.BodyHandlers.ofString());
System.out.println("Session stopped.");
```

    
    
**C#:**

    

    **1. Create the session**

```csharp
using System.Net.Http;
using System.Text;
using System.Text.Json;

var token = "YOUR_API_TOKEN_HERE";
var client = new HttpClient();

var content = new StringContent(
    JsonSerializer.Serialize(new { ttl = 300000, stealth = true, headless = false }),
    Encoding.UTF8,
    "application/json"
);

var response = await client.PostAsync(
    $"https://production-sfo.browserless.io/session?token={token}",
    content
);
var body = await response.Content.ReadAsStringAsync();
var session = JsonSerializer.Deserialize(body);

var connectUrl = session.GetProperty("connect").GetString();
var stopUrl = session.GetProperty("stop").GetString();
Console.WriteLine($"Session created: {session.GetProperty("id")}");
```

    **2. Stop the session when done**

```csharp
await client.DeleteAsync($"{stopUrl}&force=true");
Console.WriteLine("Session stopped.");
```

    
  

  
  
**Frameworks:**

  Connect to a Session API session with Puppeteer or Playwright, store browser state, disconnect, then reconnect and verify persistence.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Create a session and connect**

```js
import puppeteer from "puppeteer-core";

const TOKEN = "YOUR_API_TOKEN_HERE";

// Create a long-lived session
const sessionResponse = await fetch(
  `https://production-sfo.browserless.io/session?token=${TOKEN}`,
  {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ ttl: 300000, stealth: true }),
  }
);
const session = await sessionResponse.json();

// Connect and set browser state
const browser = await puppeteer.connect({ browserWSEndpoint: session.connect });
try {
  const page = await browser.newPage();
  await page.goto("https://automationexercise.com", { waitUntil: "networkidle2" });

  await page.evaluate(() => {
    localStorage.setItem(
      "shoppingCart",
      JSON.stringify({ items: [{ id: 1, name: "Blue Top" }], totalItems: 1, totalPrice: 500 })
    );
    localStorage.setItem("userPreferences", JSON.stringify({ theme: "dark", language: "en" }));
  });

  console.log("State set. Disconnecting...");
} finally {
  await browser.disconnect();
}
```

    **3. Reconnect and verify state**

```js
// Reconnect to the same session. State persists across disconnects.
const browser2 = await puppeteer.connect({ browserWSEndpoint: session.connect });
try {
  const page2 = await browser2.newPage();
  await page2.goto("https://automationexercise.com", { waitUntil: "networkidle2" });

  const cart = await page2.evaluate(() => localStorage.getItem("shoppingCart"));
  const prefs = await page2.evaluate(() => localStorage.getItem("userPreferences"));

  console.log("Cart:", JSON.parse(cart));
  console.log("Preferences:", JSON.parse(prefs));
} finally {
  await browser2.close();
}

// Clean up
await fetch(`${session.stop}&force=true`, { method: "DELETE" });
```

    
    
**Playwright:**

    

    **1. Install dependencies**

```bash
npm install playwright-core
```

    **2. Create a session and connect**

```js
import { chromium } from "playwright-core";

const TOKEN = "YOUR_API_TOKEN_HERE";

// Create a long-lived session
const sessionResponse = await fetch(
  `https://production-sfo.browserless.io/session?token=${TOKEN}`,
  {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ ttl: 300000, stealth: true }),
  }
);
const session = await sessionResponse.json();

// Connect and set browser state
const browser = await chromium.connectOverCDP(session.connect);
try {
  const context = browser.contexts()[0] ?? await browser.newContext();
  const page = await context.newPage();
  await page.goto("https://automationexercise.com", { waitUntil: "networkidle" });

  await page.evaluate(() => {
    localStorage.setItem(
      "shoppingCart",
      JSON.stringify({ items: [{ id: 1, name: "Blue Top" }], totalItems: 1, totalPrice: 500 })
    );
    localStorage.setItem("userPreferences", JSON.stringify({ theme: "dark", language: "en" }));
  });

  console.log("State set. Closing browser...");
} finally {
  await browser.close();
}
```

    **3. Reconnect and verify state**

```js
// Reconnect — a new browser process starts but loads persisted data from disk
const browser2 = await chromium.connectOverCDP(session.connect);
try {
  const context2 = browser2.contexts()[0] ?? await browser2.newContext();
  const page2 = await context2.newPage();
  await page2.goto("https://automationexercise.com", { waitUntil: "networkidle" });

  const cart = await page2.evaluate(() => localStorage.getItem("shoppingCart"));
  const prefs = await page2.evaluate(() => localStorage.getItem("userPreferences"));

  console.log("Cart:", JSON.parse(cart));
  console.log("Preferences:", JSON.parse(prefs));
} finally {
  await browser2.close();
}

// Clean up
await fetch(`${session.stop}&force=true`, { method: "DELETE" });
```

    
    
**BAP:**

    **1. Install dependencies**

```bash
npm install @browserless.io/bap-ts
```

    **2. Connect and seed state**

```typescript
import Browserless from '@browserless.io/bap-ts';

const browser = Browserless.connect({
  browserWSEndpoint: 'wss://production-sfo.browserless.io/chromium/bql',
  token: 'YOUR_API_TOKEN_HERE',
});

try {
  const page = await browser.newPage();
  await page.goto('https://automationexercise.com', { waitUntil: 'networkIdle' });

  // evaluate() resolves to `string | null` — null when the expression
  // returns nothing, so check before using the value.
  const result = await page.evaluate(`
    localStorage.setItem('shoppingCart', JSON.stringify({items: [{id: 1}], totalItems: 1}));
    localStorage.setItem('userPreferences', JSON.stringify({theme: 'dark'}));
    'state set'
  `);

  console.log(result ?? 'no value returned');

  // Call page.reconnect() here and skip the close below if you want another
  // client to reuse this state. Closing discards the seeded localStorage.
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Logs `state set`. The seeded `localStorage` lives only as long as this session. To reuse it, hand the session off with `reconnect()` instead of closing, or save it to an [authenticated profile](/examples/authenticated-profiles) for reuse across separate sessions.

    
  

  
  
**BQL:**

  

  The Session API returns a `browserQL` endpoint for running BQL queries against a persistent session. State set in one query is available in subsequent queries against the same session.

  :::info Stealth is optional
  The `browserQL` endpoint accepts queries whether or not the session was created with `stealth: true`. The example below enables it because it helps on bot-protected targets.
  :::

  **1. Create a session**

```bash
curl -X POST "https://production-sfo.browserless.io/session?token=YOUR_API_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{"ttl": 300000, "stealth": true}'
```

  Save the `browserQL` field from the response. This is your BQL endpoint for this session.

  **2. Write a mutation that sets state**

```graphql
mutation SetState {
  goto(url: "https://automationexercise.com", waitUntil: networkIdle) {
    status
  }

  evaluate(
    content: """
      localStorage.setItem('shoppingCart', JSON.stringify({items: [{id: 1}], totalItems: 1}));
      localStorage.setItem('userPreferences', JSON.stringify({theme: 'dark'}));
      return 'state set';
    """
  ) {
    value
  }
}
```

  **3. Run it against the session**

  Paste into the [BQL IDE](https://account.browserless.io/bql) and click Run.

  **4. Run a follow-up query to verify state**

  Send another query to the same `browserQL` endpoint. The localStorage from the first query is still available:

```graphql
mutation VerifyState {
  goto(url: "https://automationexercise.com", waitUntil: networkIdle) {
    status
  }

  evaluate(
    content: "localStorage.getItem('shoppingCart')"
  ) {
    value
  }
}
```

  **5. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "evaluate": { "value": "{\"items\":[{\"id\":1}],\"totalItems\":1}" }
  }
}
```

  State from the first query persists across subsequent BQL calls against the same session.

  

## Next steps

  <a href="/examples/reconnect" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Reconnect to a Browser</h3>
      lightweight mid-script reconnection for short-lived sessions
    
  </a>
  <a href="/examples/sessions" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Log In and Reuse Sessions</h3>
      save login state to a named profile for reuse
    
  </a>
  <a href="/examples/authenticated-profiles" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Save Logins to Authenticated Profiles</h3>
      capture and replay authenticated browser state
    
  </a>

---

# Basic Playwright connection

URL: https://docs.browserless.io/examples/playwright-connection

# Basic Playwright connection

Connect an existing Playwright script to Browserless by replacing `chromium.launch()` with a single `connectOverCDP()` call.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

  
**JavaScript:**

  

  **1. Install dependencies**

```bash
npm install playwright-core
```

  Use `playwright-core` instead of `playwright`. It doesn't bundle local browser binaries, which you don't need when connecting to Browserless.

  **2. Replace `launch` with `connectOverCDP`**

  The only change needed is swapping `chromium.launch()` for `chromium.connectOverCDP()` with the Browserless WebSocket endpoint:

```js
// Before — runs a local browser.
// const browser = await chromium.launch();

// After — runs on Browserless.
import { chromium } from 'playwright-core';

const browser = await chromium.connectOverCDP(
  'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE'
);

try {
  // Use the default context — browser.newPage() creates a new context that
  // doesn't inherit proxy, profile, or launch settings.
  const context = browser.contexts()[0];
  const page = await context.newPage();
  await page.goto('https://scraping-sandbox.netlify.app/products', { waitUntil: 'networkidle' });
  console.log('Title:', await page.title());
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

  **3. Run the script**

```bash
node script.mjs
```

  **4. Check the output**

```text
Title: Product Catalog | Scraping Sandbox
```

  The script runs against a cloud browser on Browserless instead of a local Chrome install.

  
  
**Python:**

  

  **1. Install dependencies**

```bash
pip install playwright
```

  No need to install local browser binaries. You're connecting to Browserless instead of launching a local browser.

  **2. Replace `launch` with `connect_over_cdp`**

```python
from playwright.sync_api import sync_playwright

TOKEN = 'YOUR_API_TOKEN_HERE'
WS_ENDPOINT = f'wss://production-sfo.browserless.io?token={TOKEN}'

with sync_playwright() as playwright:
    # Before — runs a local browser.
    # browser = playwright.chromium.launch()

    # After — runs on Browserless.
    browser = playwright.chromium.connect_over_cdp(WS_ENDPOINT)
    try:
        # Use the default context — new_context() doesn't inherit launch settings.
        context = browser.contexts[0]
        page = context.new_page()
        page.goto('https://scraping-sandbox.netlify.app/products')
        print('Title:', page.title())
    finally:
        # Always close to release the session even on error.
        browser.close()
```

  **3. Run the script**

```bash
python script.py
```

  **4. Check the output**

```text
Title: Product Catalog | Scraping Sandbox
```

  
  
**TypeScript:**

  

  **1. Install dependencies**

```bash
npm install playwright-core
npm install --save-dev typescript @types/node
```

  Use `playwright-core`. It skips bundling local browser binaries since you're connecting to Browserless instead.

  **2. Replace `launch` with `connectOverCDP`**

```ts
import { chromium, Browser } from 'playwright-core';

const browser: Browser = await chromium.connectOverCDP(
  'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE'
);

try {
  // Use the default context — browser.newPage() creates a new context that
  // doesn't inherit proxy, profile, or launch settings.
  const context = browser.contexts()[0];
  const page = await context.newPage();
  await page.goto('https://scraping-sandbox.netlify.app/products', { waitUntil: 'networkidle' });
  console.log('Title:', await page.title());
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

  **3. Run the script**

```bash
npx ts-node script.ts
```

  **4. Check the output**

```text
Title: Product Catalog | Scraping Sandbox
```

  

## Connection options

Append options to the WebSocket URL as query parameters:

| Parameter | Description |
|-----------|-------------|
| `token` | Your Browserless API token (required) |
| `blockAds=true` | Block ad network requests |
| `profile` | Load a saved authenticated profile |
| `solveCaptchas=true` | Enable automatic CAPTCHA solving |

For stealth mode, change the path instead of adding a query parameter:

```bash
wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE
```

Example with stealth and additional options:

```bash
wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE&blockAds=true
```

## Next steps

  <a href="/examples/puppeteer-connection" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Basic Puppeteer Connection</h3>
      the same pattern for Puppeteer
    
  </a>
  <a href="/examples/authenticated-profiles" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Save Logins to Authenticated Profiles</h3>
      persist login state across sessions
    
  </a>
  <a href="/examples/screenshot" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Take a Screenshot</h3>
      capture pages using the REST API instead
    
  </a>

---

# Change your browser's IP address using proxies

URL: https://docs.browserless.io/examples/proxy

# Change your browser's IP address using proxies

Route browser traffic through a residential or datacenter proxy so target websites see a different IP. This is useful for geo-restricted content, rate-limit avoidance, and reducing bot-detection signals tied to datacenter ranges.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

Add `proxy=residential` (or `proxy=datacenter`) and optionally `proxyCountry` to your connection URL. The example navigates to an IP-check page and returns the proxy IP to confirm routing is working.

> **More proxy options**
> - `proxy=datacenter` — faster and cheaper (2 units/MB vs 6 units/MB for residential), but easier for sites to detect
> - `proxySticky=true` — keep the same IP for the full browser session. `/scrape` and BQL are already sticky by default (a mid-session IP rotation can re-trigger Akamai/DataDome, which bind their sensor to the egress IP); pass `proxySticky=false` to rotate per request instead. Plain Puppeteer/Playwright connections still rotate by default, so add `proxySticky=true` to pin them.
> - `externalProxyServer=http://user:pass@host:port` — route through your own proxy server instead
> - `proxyLocaleMatch=true` — automatically set the browser locale to match the proxy country

  
**REST API:**

  Add proxy parameters to the `/scrape` endpoint URL. Browserless handles proxy negotiation server-side, so your request looks identical to a normal scrape. Only the egress IP changes.

  
    
**cURL:**

    

    **1. Send the request**

```bash
curl -X POST \
  "https://production-sfo.browserless.io/scrape?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://api.ipify.org?format=json",
    "elements": [{ "selector": "body" }]
  }'
```

    **2. Check the output**

    The `text` field contains the JSON response from ipify showing the proxy IP. Confirm it differs from your local or datacenter IP:

```json
{
  "data": [
    {
      "selector": "body",
      "results": [
        {
          "text": "{\"ip\": \"203.0.113.42\"}",
          "html": "{\"ip\": \"203.0.113.42\"}"
        }
      ]
    }
  ]
}
```

    
    
**JavaScript:**

    

    **1. Send the request**

```js
const response = await fetch(
  'https://production-sfo.browserless.io/scrape?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      url: 'https://api.ipify.org?format=json',
      elements: [{ selector: 'body' }],
    }),
  }
);

const { data } = await response.json();
const ip = JSON.parse(data[0].results[0].text).ip;
console.log('Proxy IP:', ip);
```

    **2. Check the output**

    Run with `node proxy.mjs`. The logged IP is the residential proxy address, not the Browserless datacenter IP.

    
    
**Python:**

    

    **1. Install dependencies**

```bash
pip install requests
```

    **2. Send the request**

```python
import requests
import json

response = requests.post(
    'https://production-sfo.browserless.io/scrape',
    params={
        'token': 'YOUR_API_TOKEN_HERE',
        'proxy': 'residential',
        'proxyCountry': 'us',
    },
    json={
        'url': 'https://api.ipify.org?format=json',
        'elements': [{'selector': 'body'}],
    },
)

data = response.json()['data']
ip = json.loads(data[0]['results'][0]['text'])['ip']
print('Proxy IP:', ip)
```

    **3. Check the output**

    Run with `python proxy.py`. The logged IP is the residential proxy address, not the Browserless datacenter IP.

    
    
**Java:**

    

    **1. Send the request**

```java
import java.net.URI;
import java.net.http.*;

String token = "YOUR_API_TOKEN_HERE";
String endpoint = "https://production-sfo.browserless.io/scrape?token=" + token
    + "&proxy=residential&proxyCountry=us";

String payload = "{\"url\": \"https://api.ipify.org?format=json\", \"elements\": [{\"selector\": \"body\"}]}";

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(endpoint))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(payload))
    .build();

HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
```

    **2. Check the output**

    Compile and run the class. The response JSON contains the proxy IP inside the body text field. Confirm it differs from your local or datacenter IP.

    
    
**C#:**

    

    **1. Send the request**

```csharp
using System.Net.Http;
using System.Text;
using System.Text.Json;

string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"https://production-sfo.browserless.io/scrape?token={token}&proxy=residential&proxyCountry=us";

var payload = new
{
    url = "https://api.ipify.org?format=json",
    elements = new[] { new { selector = "body" } },
};

using (HttpClient httpClient = new HttpClient())
{
    var content = new StringContent(
        JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
    var response = await httpClient.PostAsync(endpoint, content);
    string body = await response.Content.ReadAsStringAsync();
    Console.WriteLine(body);
}
```

    **2. Check the output**

    Run the program. The response body contains the proxy IP inside the body text field. Confirm it differs from your local or datacenter IP.

    
  

  
  
**Frameworks:**

  Add proxy parameters to the WebSocket connection URL. Because the parameters attach at the session level, every page opened in that browser instance exits through the same proxy. You don't need to configure proxies inside Puppeteer or Playwright separately.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Connect and check the IP**

```js
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint:
    'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us',
});

try {
  const page = await browser.newPage();
  await page.goto('https://api.ipify.org?format=json', { waitUntil: 'networkidle2' });
  const ip = await page.evaluate(() => JSON.parse(document.body.innerText).ip);
  console.log('Proxy IP:', ip);
} finally {
  await browser.close();
}
```

    **3. Check the output**

    Run with `node proxy.mjs`. The logged IP is the residential proxy address, not the Browserless datacenter IP.

    
    
**Playwright:**

    

    **1. Install dependencies**

```bash
npm install playwright-core
```

    **2. Connect and check the IP**

```js
import { chromium } from 'playwright-core';

const browser = await chromium.connect(
  'wss://production-sfo.browserless.io/chromium/playwright?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us'
);

try {
  const page = await browser.newPage();
  await page.goto('https://api.ipify.org?format=json', { waitUntil: 'networkidle' });
  const ip = await page.evaluate(() => JSON.parse(document.body.innerText).ip);
  console.log('Proxy IP:', ip);
} finally {
  await browser.close();
}
```

    **3. Check the output**

    Run with `node proxy.mjs`. The logged IP is the residential proxy address, not the Browserless datacenter IP.

    
    
**BAP:**

    **1. Install dependencies**

```bash
npm install @browserless.io/bap-ts
```

    **2. Connect and route through a proxy**

```typescript
import Browserless from '@browserless.io/bap-ts';

// Proxy routing is set on the connection URL, the same as the REST and BQL
// tabs. The SDK appends `&token=` when the endpoint already has a query.
const browser = Browserless.connect({
  browserWSEndpoint:
    'wss://production-sfo.browserless.io/chromium/bql?proxy=residential&proxyCountry=us',
  token: 'YOUR_API_TOKEN_HERE',
});

try {
  const page = await browser.newPage();
  await page.goto('https://api.ipify.org?format=json', { waitUntil: 'domContentLoaded' });

  const { text } = await page.text({ selector: 'body' });
  console.log(text);
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    The returned IP belongs to the proxy pool rather than the browser host.

    To proxy only some requests rather than the whole session, `page.proxy()`
    takes a matcher — `url`, `type`, or `method` — alongside the routing
    options. Without one, nothing matches and no traffic is proxied.

    
  

  
  
**BQL:**

  

  **1. Write the mutation**

  Navigate to the IP-check endpoint and read the body text. Pass proxy parameters in the BQL endpoint URL query string. BQL itself has no proxy field, so the query string is the right place for them.

```graphql
mutation CheckIP {
  goto(url: "https://api.ipify.org?format=json", waitUntil: domContentLoaded) {
    status
  }
  ip: innerText(selector: "body") {
    innerText
  }
}
```

  **2. Run it**

  Paste into the [BQL IDE](https://account.browserless.io/bql) and click Run.

  **3. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "ip": { "innerText": "{\"ip\": \"203.0.113.42\"}" }
  }
}
```

  

## Next steps

  <a href="/examples/cloudflare" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Solving Cloudflare Challenges</h3>
      combine proxies with Cloudflare bypass for full bot-detection avoidance
    
  </a>
  <a href="/examples/scrape-etsy" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape Etsy Product Listings</h3>
      proxy-backed scraping of a real marketplace
    
  </a>
  <a href="/examples/captcha" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Solving reCAPTCHAs</h3>
      use residential proxies to reduce CAPTCHA frequency
    
  </a>

---

# Basic Puppeteer connection

URL: https://docs.browserless.io/examples/puppeteer-connection

# Basic Puppeteer connection

Connect an existing Puppeteer script to Browserless by replacing `puppeteer.launch()` with a single `puppeteer.connect()` call.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

  
**JavaScript:**

  

  **1. Install dependencies**

```bash
npm install puppeteer-core
```

  Use `puppeteer-core` instead of `puppeteer`. It doesn't bundle local browser binaries, which you don't need when connecting to Browserless.

  **2. Replace `launch` with `connect`**

  The only change needed is swapping `puppeteer.launch()` for `puppeteer.connect()` with the Browserless WebSocket endpoint:

```js
// Before — runs a local browser.
// const browser = await puppeteer.launch();

// After — runs on Browserless.
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint: 'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE',
});

try {
  const page = await browser.newPage();
  await page.goto('https://scraping-sandbox.netlify.app/products', { waitUntil: 'networkidle2' });
  console.log('Title:', await page.title());
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

  **3. Run the script**

```bash
node script.mjs
```

  **4. Check the output**

```text
Title: Product Catalog | Scraping Sandbox
```

  The script runs against a cloud browser on Browserless instead of a local Chrome install.

  
  
**TypeScript:**

  

  **1. Install dependencies**

```bash
npm install puppeteer-core
npm install --save-dev typescript @types/node
```

  Use `puppeteer-core`. It skips bundling local browser binaries since you're connecting to Browserless instead.

  **2. Replace `launch` with `connect`**

```ts
import puppeteer, { Browser } from 'puppeteer-core';

const browser: Browser = await puppeteer.connect({
  browserWSEndpoint: 'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE',
});

try {
  const page = await browser.newPage();
  await page.goto('https://scraping-sandbox.netlify.app/products', { waitUntil: 'networkidle2' });
  console.log('Title:', await page.title());
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

  **3. Run the script**

```bash
npx ts-node script.ts
```

  **4. Check the output**

```text
Title: Product Catalog | Scraping Sandbox
```

  

## Connection options

Append options to the WebSocket URL as query parameters:

| Parameter | Description |
|-----------|-------------|
| `token` | Your Browserless API token (required) |
| `blockAds=true` | Block ad network requests |
| `profile` | Load a saved authenticated profile |
| `solveCaptchas=true` | Enable automatic CAPTCHA solving |

For stealth mode, change the path instead of adding a query parameter:

```bash
wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE
```

Example with stealth and additional options:

```bash
wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE&blockAds=true
```

## Next steps

  <a href="/examples/playwright-connection" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Basic Playwright Connection</h3>
      the same pattern for Playwright
    
  </a>
  <a href="/examples/authenticated-profiles" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Save Logins to Authenticated Profiles</h3>
      persist login state across sessions
    
  </a>
  <a href="/examples/screenshot" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Take a Screenshot</h3>
      capture pages using the REST API instead
    
  </a>

---

# Disconnect and reconnect to a browser

URL: https://docs.browserless.io/examples/reconnect

# Disconnect and reconnect to a browser

Keep a browser session alive for a short window after disconnecting and reconnect to it with all cookies, localStorage, and state intact. Ideal for scripts that need to briefly detach and resume within seconds or minutes.

> **Long-lived sessions**
> Need cookies and data to survive for hours or days between runs? See [Continue Browser State Across Runs](/examples/persist-session) for an approach that keeps browser state alive much longer.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

Reconnection works in two phases: first you **start a session and request a reconnection endpoint**, then you **reconnect** using the returned URL. The browser stays alive in the background for the duration you specify.

  
**REST API:**

  Use BrowserQL over HTTP to start a session, perform actions, and get a reconnection endpoint for follow-up requests.

  
    
**cURL:**

    

    **1. Start a session and get the reconnection endpoint**

    Send a BQL mutation that navigates to a page and calls `reconnect` to keep the browser alive:

```bash
curl -X POST \
  "https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "mutation StartSession { goto(url: \"https://scraping-sandbox.netlify.app/aether\", waitUntil: domContentLoaded) { status } reconnect(timeout: 60000) { browserQLEndpoint browserWSEndpoint } }",
    "variables": {},
    "operationName": "StartSession"
  }'
```

    **2. Reconnect with the returned endpoint**

    Use the `browserQLEndpoint` from the response (append your token) to send a follow-up mutation to the same browser:

```bash
curl -X POST \
  "RETURNED_BROWSERQL_ENDPOINT?token=YOUR_API_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "mutation ContinueSession { html { html } }",
    "variables": {},
    "operationName": "ContinueSession"
  }'
```

    **3. Check the output**

    The first response includes the reconnection URLs:

```json
{
  "data": {
    "goto": { "status": 200 },
    "reconnect": {
      "browserQLEndpoint": "https://production-sfo.browserless.io/stealth/bql/abc123",
      "browserWSEndpoint": "wss://production-sfo.browserless.io/chromium/abc123"
    }
  }
}
```

    The second response returns the page HTML from the still-running browser.

    
    
**JavaScript:**

    

    **1. Start a session and reconnect**

```js
const TOKEN = 'YOUR_API_TOKEN_HERE';
const BQL_URL = `https://production-sfo.browserless.io/stealth/bql?token=${TOKEN}`;

const startQuery = `mutation StartSession {
  goto(url: "https://scraping-sandbox.netlify.app/aether", waitUntil: domContentLoaded) {
    status
  }
  reconnect(timeout: 60000) {
    browserQLEndpoint
  }
}`;

const startResponse = await fetch(BQL_URL, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    query: startQuery,
    variables: {},
    operationName: 'StartSession',
  }),
});

const { data } = await startResponse.json();
const reconnectURL = `${data.reconnect.browserQLEndpoint}?token=${TOKEN}`;

const continueResponse = await fetch(reconnectURL, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    query: 'mutation ContinueSession { html { html } }',
    variables: {},
    operationName: 'ContinueSession',
  }),
});

const result = await continueResponse.json();
console.log(result.data.html.html.substring(0, 200));
```

    **2. Check the output**

    Run with `node reconnect.mjs`. The script navigates, disconnects, reconnects, and prints the page HTML from the still-running session.

    
    
**Python:**

    

    **1. Install dependencies**

```bash
pip install requests
```

    **2. Start a session and reconnect**

```python
import requests

TOKEN = 'YOUR_API_TOKEN_HERE'
BQL_URL = f'https://production-sfo.browserless.io/stealth/bql?token={TOKEN}'

start_query = """
mutation StartSession {
  goto(url: "https://scraping-sandbox.netlify.app/aether", waitUntil: domContentLoaded) {
    status
  }
  reconnect(timeout: 60000) {
    browserQLEndpoint
  }
}
"""

start_response = requests.post(
    BQL_URL,
    json={
        'query': start_query,
        'variables': {},
        'operationName': 'StartSession',
    },
)

data = start_response.json()['data']
reconnect_url = f"{data['reconnect']['browserQLEndpoint']}?token={TOKEN}"

continue_response = requests.post(
    reconnect_url,
    json={
        'query': 'mutation ContinueSession { html { html } }',
        'variables': {},
        'operationName': 'ContinueSession',
    },
)

result = continue_response.json()
print(result['data']['html']['html'][:200])
```

    **3. Check the output**

    Run with `python reconnect.py`. The script prints the page HTML from the reconnected session.

    
    
**Java:**

    

    **1. Start a session and reconnect**

```java
import java.net.URI;
import java.net.http.*;

String token = "YOUR_API_TOKEN_HERE";
String bqlUrl = "https://production-sfo.browserless.io/stealth/bql?token=" + token;

String startPayload = """
    {
      "query": "mutation StartSession { goto(url: \\"https://scraping-sandbox.netlify.app/aether\\", waitUntil: domContentLoaded) { status } reconnect(timeout: 60000) { browserQLEndpoint } }",
      "variables": {},
      "operationName": "StartSession"
    }
    """;

HttpClient client = HttpClient.newHttpClient();
HttpRequest startRequest = HttpRequest.newBuilder()
    .uri(URI.create(bqlUrl))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(startPayload))
    .build();

HttpResponse startResponse = client.send(startRequest, HttpResponse.BodyHandlers.ofString());
String body = startResponse.body();

// Extract browserQLEndpoint from the JSON response
String marker = "\"browserQLEndpoint\":\"";
int start = body.indexOf(marker) + marker.length();
int end = body.indexOf("\"", start);
String reconnectUrl = body.substring(start, end) + "?token=" + token;

// Send follow-up mutation to the reconnected session
String continuePayload = """
    {
      "query": "mutation ContinueSession { html { html } }",
      "variables": {},
      "operationName": "ContinueSession"
    }
    """;

HttpRequest continueRequest = HttpRequest.newBuilder()
    .uri(URI.create(reconnectUrl))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(continuePayload))
    .build();

HttpResponse continueResponse = client.send(continueRequest, HttpResponse.BodyHandlers.ofString());
System.out.println(continueResponse.body());
```

    **2. Check the output**

    Run the class. The script starts a session, reconnects, and prints the page HTML from the still-running browser.

    
    
**C#:**

    

    **1. Start a session and reconnect**

```csharp
using System.Net.Http;
using System.Text;
using System.Text.Json;

string token = "YOUR_API_TOKEN_HERE";
string bqlUrl = $"https://production-sfo.browserless.io/stealth/bql?token={token}";

var startPayload = new
{
    query = @"mutation StartSession {
      goto(url: ""https://scraping-sandbox.netlify.app/aether"", waitUntil: domContentLoaded) { status }
      reconnect(timeout: 60000) { browserQLEndpoint }
    }",
    variables = new { },
    operationName = "StartSession",
};

using (HttpClient httpClient = new HttpClient())
{
    var json = JsonSerializer.Serialize(startPayload);
    var content = new StringContent(json, Encoding.UTF8, "application/json");
    var startResponse = await httpClient.PostAsync(bqlUrl, content);
    string startBody = await startResponse.Content.ReadAsStringAsync();

    var doc = JsonDocument.Parse(startBody);
    string reconnectUrl = doc.RootElement
        .GetProperty("data")
        .GetProperty("reconnect")
        .GetProperty("browserQLEndpoint")
        .GetString() + $"?token={token}";

    var continuePayload = new
    {
        query = "mutation ContinueSession { html { html } }",
        variables = new { },
        operationName = "ContinueSession",
    };

    var continueContent = new StringContent(
        JsonSerializer.Serialize(continuePayload), Encoding.UTF8, "application/json");
    var continueResponse = await httpClient.PostAsync(reconnectUrl, continueContent);
    string continueBody = await continueResponse.Content.ReadAsStringAsync();
    Console.WriteLine(continueBody);
}
```

    **2. Check the output**

    Run the program. The script reconnects to the still-running browser and prints the page HTML.

    
  

  
  
**Frameworks:**

  Use the `Browserless.reconnect` CDP command to keep a session alive and reconnect with Puppeteer or Playwright.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Start a session and request reconnection**

```js
import puppeteer from 'puppeteer-core';

const TOKEN = 'YOUR_API_TOKEN_HERE';

const browser = await puppeteer.connect({
  browserWSEndpoint: `wss://production-sfo.browserless.io?token=${TOKEN}`,
});

try {
  const page = await browser.newPage();
  const cdp = await page.createCDPSession();

  await page.goto('https://scraping-sandbox.netlify.app/aether', { waitUntil: 'networkidle2' });

  // Keep the browser alive for 60 seconds after disconnecting.
  const { browserWSEndpoint } = await cdp.send('Browserless.reconnect', {
    timeout: 60000,
  });

  // Disconnect (does not kill the browser — it stays alive).
  await browser.close();

  console.log('Reconnect URL:', browserWSEndpoint);

  // Reconnect to the same browser session.
  const reconnected = await puppeteer.connect({
    browserWSEndpoint: `${browserWSEndpoint}?token=${TOKEN}`,
  });

  const pages = await reconnected.pages();
  const existingPage = pages[0];
  console.log('Still on:', await existingPage.title());

  await reconnected.close();
} catch (err) {
  await browser.close();
  throw err;
}
```

    **3. Check the output**

    Run with `node reconnect.mjs`. The script connects, navigates, disconnects, reconnects, and confirms the page title is preserved.

    
    
**Playwright:**

    

    **1. Install dependencies**

```bash
npm install playwright-core
```

    **2. Start a session and request reconnection**

```js
import { chromium } from 'playwright-core';

const TOKEN = 'YOUR_API_TOKEN_HERE';

const browser = await chromium.connect(
  `wss://production-sfo.browserless.io/chromium/playwright?token=${TOKEN}`
);

try {
  const page = await browser.newPage();
  const cdp = await page.context().newCDPSession(page);

  await page.goto('https://scraping-sandbox.netlify.app/aether', { waitUntil: 'networkidle' });

  // Keep the browser alive for 60 seconds after disconnecting.
  const { browserWSEndpoint } = await cdp.send('Browserless.reconnect', {
    timeout: 60000,
  });

  // Disconnect (does not kill the browser — it stays alive).
  await browser.close();

  console.log('Reconnect URL:', browserWSEndpoint);

  // Reconnect to the same browser session.
  const reconnected = await chromium.connect(
    `${browserWSEndpoint}?token=${TOKEN}`
  );

  const pages = reconnected.contexts()[0]?.pages() ?? [];
  if (pages.length > 0) {
    console.log('Still on:', await pages[0].title());
  }

  await reconnected.close();
} catch (err) {
  await browser.close();
  throw err;
}
```

    **3. Check the output**

    Run with `node reconnect.mjs`. The script connects, navigates, disconnects, reconnects, and confirms the page title is preserved.

    
    
**BAP:**

    **1. Install dependencies**

```bash
npm install @browserless.io/bap-ts
```

    **2. Connect and hand off the session**

```typescript
import Browserless from '@browserless.io/bap-ts';

const TOKEN = 'YOUR_API_TOKEN_HERE';

const browser = Browserless.connect({
  browserWSEndpoint: 'wss://production-sfo.browserless.io/chromium/bql',
  token: TOKEN,
});

try {
  const page = await browser.newPage();
  await page.goto('https://scraping-sandbox.netlify.app/aether', { waitUntil: 'domContentLoaded' });

  const { browserQLEndpoint, browserWSEndpoint } = await page.reconnect({ timeout: 60000 });

  // Endpoints come back without credentials, so hand them on as-is and let
  // the next client add its own token. Never log a token-bearing URL.
  console.log({ browserQLEndpoint, browserWSEndpoint });

  // No close() here. The session has to outlive this script for the next
  // client to reach it, and the reconnect timeout bounds how long it lives.
} catch (error) {
  // Only close when the handoff never happened, so nothing is left stranded.
  await browser.close();
  throw error;
}
```

    **3. Reconnect from the next client**

    `reconnect()` returns endpoints **without** the token, so every follow-up connection has to add its own or it gets a `401 Unauthorized`:

```typescript
// More BAP work on the same session.
const resumed = Browserless.connect({
  browserWSEndpoint: browserQLEndpoint,
  token: TOKEN,
});

// …or hand the page to Puppeteer over CDP.
const puppeteerBrowser = await puppeteer.connect({
  browserWSEndpoint: `${browserWSEndpoint}?token=${TOKEN}`,
});
```

    **4. Check the output**

    Both clients attach to the same browser, so cookies, `localStorage`, and page state carry over. The session stays alive until the timeout expires or the next client closes it.

    
  

  
  
**BQL:**

  

  **1. Write the first mutation**

  Navigate to a page and call `reconnect` to keep the browser alive. The response includes a `browserQLEndpoint` for follow-up requests and a `browserWSEndpoint` for framework connections:

  > `reconnect(timeout: 60000)` requires at least a Starter plan. The Free plan caps reconnect timeouts at 10 seconds. Scale plans support up to 5 minutes.

```graphql
mutation StartSession {
  goto(url: "https://scraping-sandbox.netlify.app/aether", waitUntil: domContentLoaded) {
    status
  }

  reconnect(timeout: 60000) {
    browserQLEndpoint
    browserWSEndpoint
  }
}
```

  **2. Send a follow-up mutation to the same browser**

  Use the `browserQLEndpoint` from the response (append `?token=YOUR_API_TOKEN_HERE`) and POST a new mutation to it:

```graphql
mutation ContinueSession {
  html {
    html
  }
}
```

  **3. Run it**

  Paste the first mutation into the [BQL IDE](https://account.browserless.io/bql) and click Run. Copy the `browserQLEndpoint` from the response, open a new BQL IDE tab pointed at that endpoint, and run the second mutation.

  **4. Check the output**

  The first response includes the reconnection URLs:

```json
{
  "data": {
    "goto": { "status": 200 },
    "reconnect": {
      "browserQLEndpoint": "https://production-sfo.browserless.io/stealth/bql/abc123",
      "browserWSEndpoint": "wss://production-sfo.browserless.io/chromium/abc123"
    }
  }
}
```

  The second response returns data from the same browser. All cookies, localStorage, and page state are preserved.

  

## Next steps

  <a href="/examples/sessions" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Log In and Reuse Sessions</h3>
      save login state to a named profile for reuse across sessions
    
  </a>
  <a href="/examples/email-otp" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Log In via Email OTP</h3>
      use reconnect to bridge multi-step login flows
    
  </a>
  <a href="/examples/authenticated-profiles" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Save Logins to Authenticated Profiles</h3>
      capture and replay authenticated browser state
    
  </a>

---

# Retry browser sessions with exponential backoff

URL: https://docs.browserless.io/examples/retry-backoff

# Retry browser sessions with exponential backoff

When Browserless is over capacity it returns a 429, and individual browser sessions can fail mid-run due to transient network or browser errors. Exponential backoff handles both by waiting `baseDelay * 2^attempt` ms between retries so you don't hammer a busy service and you don't lose work to one-off failures. For details on capacity and queue behavior, see [session management](/baas/session-management).

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

  
**REST API:**

  Browserless queues REST requests automatically when below `MAX_QUEUE_LENGTH`, so most capacity issues resolve without client-side retry. Network failures and 429s above the queue limit still need handling. Wrap your fetch in a retry loop that checks the response status before parsing.

  
    
**cURL:**

    

    **1. Send the request with retry**

    The loop doubles `DELAY` after each failed attempt. A 200 exits immediately; anything else retries up to `MAX_RETRIES` times:

```bash
#!/bin/bash
MAX_RETRIES=3
DELAY=1
for i in $(seq 1 $MAX_RETRIES); do
  STATUS=$(curl -s -o response.png -w "%{http_code}" -X POST \
    "https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE" \
    -H "Content-Type: application/json" \
    -d '{"url": "https://scraping-sandbox.netlify.app/dashboard"}')
  if [ "$STATUS" = "200" ]; then
    echo "Success"
    exit 0
  fi
  echo "Attempt $i failed with status $STATUS, retrying in ${DELAY}s..."
  sleep $DELAY
  DELAY=$((DELAY * 2))
done
echo "Failed after $MAX_RETRIES attempts" && exit 1
```

    **2. Check the output**

    Each failed attempt prints `Attempt N failed with status 429, retrying in Ns...`. On success, `response.png` contains the screenshot and the script exits 0. If all retries are exhausted, the script exits 1.

    
    
**JavaScript:**

    

    **1. Send the request with retry**

    The helper checks `response.status === 429` explicitly. That means Browserless is over capacity and worth retrying. A 400 or 422 means bad input; retrying won't help, so those fall through to `return result` and you handle them in the caller:

```js
async function withRetry(fn, { retries = 3, baseDelay = 500 } = {}) {
  for (let attempt = 0; attempt <= retries; attempt++) {
    try {
      const result = await fn();
      if (result.status === 429) {
        if (attempt === retries) throw new Error('Browserless over capacity after retries');
        const delay = baseDelay * 2 ** attempt;
        console.warn(`Attempt ${attempt + 1} failed: 429. Retrying in ${delay}ms...`);
        await new Promise(r => setTimeout(r, delay));
        continue;
      }
      return result;
    } catch (err) {
      if (attempt === retries) throw err;
      const delay = baseDelay * 2 ** attempt;
      console.warn(`Attempt ${attempt + 1} failed: ${err.message}. Retrying in ${delay}ms...`);
      await new Promise(r => setTimeout(r, delay));
    }
  }
}

const response = await withRetry(() =>
  fetch('https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ url: 'https://scraping-sandbox.netlify.app/dashboard' }),
  })
);
const buffer = await response.arrayBuffer();
```

    **2. Check the output**

    Run with `node retry-backoff.mjs`. On a 429, you'll see `Attempt 1 failed: 429. Retrying in 500ms...` before the next attempt. On success, the response buffer is returned to the caller. If all retries are exhausted, the last error is thrown with the HTTP status.

    
    
**Python:**

    

    **1. Install dependencies**

```bash
pip install requests
```

    **2. Send the request with retry**

    The loop retries on both 429 and any other non-OK status. On the final attempt it calls `raise_for_status()` to surface the error with the HTTP status code:

```python
import time
import requests

def with_retry(fn, retries=3, base_delay=0.5):
    for attempt in range(retries + 1):
        response = fn()
        if response.status_code == 429 or not response.ok:
            if attempt == retries:
                response.raise_for_status()
            time.sleep(base_delay * (2 ** attempt))
            continue
        return response

response = with_retry(lambda: requests.post(
    'https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE',
    headers={'Content-Type': 'application/json'},
    json={'url': 'https://scraping-sandbox.netlify.app/dashboard'},
))

with open('screenshot.png', 'wb') as f:
    f.write(response.content)
```

    **3. Check the output**

    Run with `python retry_backoff.py`. Each failed attempt sleeps before the next try. On success, `screenshot.png` is written to disk. If all retries are exhausted, `raise_for_status()` raises an `HTTPError` with the final status code.

    
    
**Java:**

    

    **1. Send the request with retry**

    The loop doubles the base delay with a left shift (`1L << attempt`) to avoid floating-point math. A 429 is retried; anything else is returned to the caller for inspection:

```java
import java.net.URI;
import java.net.http.*;

String token = "YOUR_API_TOKEN_HERE";
String endpoint = "https://production-sfo.browserless.io/screenshot?token=" + token;

String body = "{\"url\": \"https://scraping-sandbox.netlify.app/dashboard\"}";

HttpClient client = HttpClient.newHttpClient();
int retries = 3;
long baseDelay = 500;

HttpResponse<byte[]> response = null;
for (int attempt = 0; attempt <= retries; attempt++) {
    HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create(endpoint))
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(body))
        .build();

    response = client.send(request, HttpResponse.BodyHandlers.ofByteArray());

    if (response.statusCode() == 429) {
        if (attempt == retries) throw new RuntimeException("Browserless over capacity after retries");
        Thread.sleep(baseDelay * (1L << attempt));
        continue;
    }
    break;
}

System.out.println("Status: " + response.statusCode());
System.out.println("Body length: " + response.body().length + " bytes");
```

    **2. Check the output**

    Run the class. Each 429 sleeps and retries. On success, you'll see `Status: 200, Body length: NNNNN bytes`. Confirm the byte count is non-zero before treating it as a valid screenshot.

    
    
**C#:**

    

    **1. Send the request with retry**

    `Task.Delay` with `Math.Pow(2, attempt)` gives the same exponential curve as the other languages. Non-429 failures are returned to the caller. Don't retry a 400:

```csharp
using System.Net.Http;
using System.Text;
using System.Text.Json;

string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"https://production-sfo.browserless.io/screenshot?token={token}";

var payload = JsonSerializer.Serialize(new { url = "https://scraping-sandbox.netlify.app/dashboard" });
int retries = 3;
int baseDelay = 500;

using HttpClient httpClient = new HttpClient();
HttpResponseMessage response = null;

for (int attempt = 0; attempt <= retries; attempt++)
{
    var content = new StringContent(payload, Encoding.UTF8, "application/json");
    response = await httpClient.PostAsync(endpoint, content);

    if ((int)response.StatusCode == 429)
    {
        if (attempt == retries) throw new Exception("Browserless over capacity after retries");
        await Task.Delay(baseDelay * (int)Math.Pow(2, attempt));
        continue;
    }
    break;
}

byte[] bytes = await response.Content.ReadAsByteArrayAsync();
Console.WriteLine($"Status: {(int)response.StatusCode}, Body: {bytes.Length} bytes");
```

    **2. Check the output**

    Run the program. Each 429 waits and retries. On success, you'll see `Status: 200, Body: NNNNN bytes`. Confirm the byte count is non-zero before treating it as a valid screenshot.

    
  

  
  
**Frameworks:**

  Framework connections go over WebSocket directly to Browserless, so a 429 or connection drop means the `connect()` call throws. Retry matters more here than in REST because a mid-session crash drops all in-progress work.

  The retry wraps the entire session (`connect()` through `browser.close()` in the finally block), not just the `connect()` call. If the browser crashes after connecting, wrapping only `connect()` would skip re-running the page logic and return a partial or empty result. Wrapping the full session means each retry starts clean.

  Not everything is worth retrying. If your callback throws because a selector wasn't found or a page returned a 404, retrying won't help. Inspect the error message to distinguish transient failures (connection refused, browser crashed, WebSocket closed) from logic errors, and re-throw logic errors immediately rather than burning through retries.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Connect and run with retry**

```js
import puppeteer from 'puppeteer-core';

const TRANSIENT = ['ECONNREFUSED', 'WebSocket', 'Protocol error', 'Target closed', 'net::'];

async function withRetry(fn, { retries = 3, baseDelay = 1000 } = {}) {
  for (let attempt = 0; attempt <= retries; attempt++) {
    try {
      return await fn();
    } catch (err) {
      const isTransient = TRANSIENT.some(s => err.message?.includes(s));
      if (attempt === retries || !isTransient) throw err;
      const delay = baseDelay * 2 ** attempt;
      console.warn(`Attempt ${attempt + 1} failed: ${err.message}. Retrying in ${delay}ms...`);
      await new Promise(r => setTimeout(r, delay));
    }
  }
}

const result = await withRetry(async () => {
  const browser = await puppeteer.connect({
    browserWSEndpoint: 'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE',
  });
  try {
    const page = await browser.newPage();
    await page.goto('https://scraping-sandbox.netlify.app/dashboard');
    return await page.title();
  } finally {
    // Always close to release the session even on error.
    await browser.close();
  }
});

console.log(result);
```

    **3. Check the output**

    Run with `node retry-backoff.mjs`. Each failed connection logs `Attempt N failed: <error>. Retrying in NNNNms...`. On success, the page title prints. If all retries are exhausted, the last error is thrown with its full stack.

    
    
**Playwright:**

    

    **1. Install dependencies**

```bash
npm install playwright-core
```

    **2. Connect and run with retry**

    The `withRetry` helper is identical to Puppeteer's. Playwright uses `connectOverCDP` instead of `connect`:

```js
import { chromium } from 'playwright-core';

const TRANSIENT = ['ECONNREFUSED', 'WebSocket', 'Protocol error', 'Target closed', 'net::'];

async function withRetry(fn, { retries = 3, baseDelay = 1000 } = {}) {
  for (let attempt = 0; attempt <= retries; attempt++) {
    try {
      return await fn();
    } catch (err) {
      const isTransient = TRANSIENT.some(s => err.message?.includes(s));
      if (attempt === retries || !isTransient) throw err;
      const delay = baseDelay * 2 ** attempt;
      console.warn(`Attempt ${attempt + 1} failed: ${err.message}. Retrying in ${delay}ms...`);
      await new Promise(r => setTimeout(r, delay));
    }
  }
}

const result = await withRetry(async () => {
  const browser = await chromium.connectOverCDP(
    'wss://production-sfo.browserless.io/chromium/playwright?token=YOUR_API_TOKEN_HERE'
  );
  try {
    const context = browser.contexts()[0] ?? await browser.newContext();
    const page = await context.newPage();
    await page.goto('https://scraping-sandbox.netlify.app/dashboard');
    return await page.title();
  } finally {
    // Always close to release the session even on error.
    await browser.close();
  }
});

console.log(result);
```

    **3. Check the output**

    Run with `node retry-backoff.mjs`. Each failed connection logs `Attempt N failed: <error>. Retrying in NNNNms...`. On success, the page title prints. If all retries are exhausted, the last error is thrown with its full stack.

    
    
**BAP:**

    **1. Install dependencies**

```bash
npm install @browserless.io/bap-ts
```

    **2. Connect with retries**

```typescript
import Browserless from '@browserless.io/bap-ts';

const browser = Browserless.connect({
  browserWSEndpoint: 'wss://production-sfo.browserless.io/chromium/bql',
  token: 'YOUR_API_TOKEN_HERE',
});

try {
  const page = await browser.newPage();
  await page.goto('https://scraping-sandbox.netlify.app/dashboard', { waitUntil: 'domContentLoaded' });

  const heading = await page.$eval('h1');
  console.log(heading);
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Wrap the block in your own retry loop and catch `TimeoutError` and `ConnectionError` separately — only the latter is worth reconnecting for.

    
  

  
  
**BQL:**

  

  BQL requests are HTTP, so the [REST API](/rest-apis/intro) retry pattern applies directly. Wrap your BQL POST in the same `withRetry` helper. Read-only operations like `goto` + `text` are safe to retry because they don't change server state. Mutations that write state (form submission, button clicks) need extra care: if Browserless executes the action and the response is dropped in transit, a retry will submit the form or fire the click a second time.

  **1. Write the mutation**

  Define a read-only mutation that navigates to a page and extracts a heading. Because neither operation changes state, it's safe to retry on any failure:

```graphql
mutation RetryExample {
  goto(url: "https://scraping-sandbox.netlify.app/dashboard", waitUntil: domContentLoaded) {
    status
  }
  heading: text(selector: "h1")
}
```

  **2. Run it**

  Paste into the [BQL IDE](https://account.browserless.io/bql) and click Run.

  **3. Check the output**

  A successful run returns the BQL response body. Confirm `goto.status` is `200` and `heading` is non-null. If the retry loop fired, you'll see retry log lines before this output appears:

```json
{
  "data": {
    "goto": { "status": 200 },
    "heading": "Dashboard"
  }
}
```

---

# Scrape structured data

URL: https://docs.browserless.io/examples/scrape

# Scrape structured data

Extract structured data from fully rendered JavaScript pages using Browserless.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

  
**AI Agent:**

  Use the [Browserless MCP server](https://github.com/browserless/browserless-mcp) to scrape structured data from a webpage from any MCP-compatible AI agent (Claude Desktop, Cursor, Windsurf, ChatGPT, etc.).

  **1. Connect the MCP server**

  Send this prompt to your AI agent to install the Browserless MCP server:

```text
Go to https://github.com/browserless/browserless-mcp/blob/main/install.md
and follow the instructions to install the Browserless MCP server
for my client.
```

  **2. Scrape a page**

  Use `browserless_smartscraper`. It extracts page content in one call with automatic bot-protection handling.

```text
Use the browserless_smartscraper tool to scrape the main content
of https://scraping-sandbox.netlify.app/products as markdown
```

  **3. Scrape after interaction**

  Use `browserless_agent`. Some pages paginate content that requires clicking through to load all items.

```text
Use the browserless_agent tool to navigate to
https://scraping-sandbox.netlify.app/products,
click the "Next" button to load additional pages of products,
then scrape the full page content as markdown
```

  
  
**REST API:**

  Use the `/scrape` REST endpoint to extract structured data from a page. No WebSocket connection needed.

  
    
**cURL:**

    

    **1. Build the request**

    Append your token to the scrape endpoint and specify the selectors you want to extract:

```bash
https://production-sfo.browserless.io/scrape?token=YOUR_API_TOKEN_HERE
```

    **2. Send the request**

```bash
curl -X POST \
  "https://production-sfo.browserless.io/scrape?token=YOUR_API_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://scraping-sandbox.netlify.app/products",
    "elements": [
      { "selector": "h1" },
      { "selector": ".product-title" }
    ]
  }'
```

    **3. Check the output**

    The response is JSON with a `data` array. Each item corresponds to one selector and includes matched elements with their text, HTML, dimensions, and position:

```json
{
  "data": [
    {
      "selector": "h1",
      "results": [
        {
          "attributes": [],
          "height": 48,
          "html": "Product Catalog",
          "left": 192,
          "text": "Product Catalog",
          "top": 120,
          "width": 816
        }
      ]
    },
    {
      "selector": ".product-title",
      "results": [
        {
          "attributes": [{ "name": "class", "value": "product-title" }],
          "height": 20,
          "html": "ProBass A100 Wireless Headphones",
          "left": 340,
          "text": "ProBass A100 Wireless Headphones",
          "top": 480,
          "width": 250
        },
        {
          "attributes": [{ "name": "class", "value": "product-title" }],
          "height": 20,
          "html": "Aura Smart Ring Gen 4",
          "left": 620,
          "text": "Aura Smart Ring Gen 4",
          "top": 480,
          "width": 250
        }
      ]
    }
  ]
}
```

    
    
**JavaScript:**

    

    **1. Send the request**

```js
const response = await fetch(
  'https://production-sfo.browserless.io/scrape?token=YOUR_API_TOKEN_HERE',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      url: 'https://scraping-sandbox.netlify.app/products',
      elements: [
        { selector: 'h1' },
        { selector: '.product-title' },
      ],
    }),
  }
);

const { data } = await response.json();
console.log(data);
```

    **2. Check the output**

    Run the script with `node scrape.mjs`. The extracted data is logged to the console as a structured JSON array.

    
    
**Python:**

    

    **1. Install dependencies**

```bash
pip install requests
```

    **2. Send the request**

```python
import requests

response = requests.post(
    'https://production-sfo.browserless.io/scrape?token=YOUR_API_TOKEN_HERE',
    json={
        'url': 'https://scraping-sandbox.netlify.app/products',
        'elements': [
            {'selector': 'h1'},
            {'selector': '.product-title'},
        ],
    },
)

data = response.json()['data']
print(data)
```

    **3. Check the output**

    Run the script with `python scrape.py`. The extracted data is printed as a list of selector results.

    
    
**Java:**

    

    **1. Add dependencies**

```xml
<!-- https://kong.github.io/unirest-java/ -->
<dependency>
  <groupId>com.konghq</groupId>
  <artifactId>unirest-java</artifactId>
  <version>3.14.5</version>
</dependency>
```

    **2. Send the request**

```java
import kong.unirest.HttpResponse;
import kong.unirest.Unirest;

String url = "https://production-sfo.browserless.io/scrape";
String token = "YOUR_API_TOKEN_HERE";
String endpoint = String.format("%s?token=%s", url, token);

HttpResponse response = Unirest.post(endpoint)
    .header("Content-Type", "application/json")
    .body("{\"url\": \"https://scraping-sandbox.netlify.app/products\", \"elements\": [{\"selector\": \"h1\"}, {\"selector\": \".product-title\"}]}")
    .asString();

System.out.println(response.getBody());
```

    **3. Check the output**

    Run the class. The response body is structured JSON with matched elements for each selector.

    
    
**C#:**

    

    **1. Send the request**

```csharp
using System.Net.Http;
using System.Text;
using System.Text.Json;

string url = "https://production-sfo.browserless.io/scrape";
string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"{url}?token={token}";

var payload = new
{
    url = "https://scraping-sandbox.netlify.app/products",
    elements = new[] { new { selector = "h1" }, new { selector = ".product-title" } },
};

using (HttpClient httpClient = new HttpClient())
{
    var jsonPayload = JsonSerializer.Serialize(payload);
    var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
    var response = await httpClient.PostAsync(endpoint, content);
    string responseBody = await response.Content.ReadAsStringAsync();
    Console.WriteLine(responseBody);
}
```

    **2. Check the output**

    Run the program. The response body is structured JSON with matched elements for each selector.

    
  

  
  
**Frameworks:**

  Use a browser connection to evaluate the fully rendered DOM and extract data with custom logic.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Connect and extract**

```js
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint: 'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE',
});

try {
  const page = await browser.newPage();
  await page.goto('https://scraping-sandbox.netlify.app/products', { waitUntil: 'networkidle2' });

  const data = await page.evaluate(() => ({
    heading: document.querySelector('h1')?.textContent,
    products: [...document.querySelectorAll('.product-title')].map((el) => el.textContent),
  }));

  console.log(data);
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Run the script with `node scrape.mjs`. The extracted data is logged to the console.

    
    
**Playwright:**

    

    **1. Install dependencies**

```bash
npm install playwright-core
```

    **2. Connect and extract**

```js
import { chromium } from 'playwright-core';

const browser = await chromium.connect(
  'wss://production-sfo.browserless.io/chromium/playwright?token=YOUR_API_TOKEN_HERE'
);

try {
  const page = await browser.newPage();
  await page.goto('https://scraping-sandbox.netlify.app/products', { waitUntil: 'networkidle' });

  const data = await page.evaluate(() => ({
    heading: document.querySelector('h1')?.textContent,
    products: [...document.querySelectorAll('.product-title')].map((el) => el.textContent),
  }));

  console.log(data);
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Run the script with `node scrape.mjs`. The extracted data is logged to the console.

    
    
**BAP:**

    **1. Install dependencies**

```bash
npm install @browserless.io/bap-ts
```

    **2. Connect and extract**

```typescript
import Browserless from '@browserless.io/bap-ts';

const browser = Browserless.connect({
  browserWSEndpoint: 'wss://production-sfo.browserless.io/chromium/bql',
  token: 'YOUR_API_TOKEN_HERE',
});

try {
  const page = await browser.newPage();
  await page.goto('https://scraping-sandbox.netlify.app/products', { waitUntil: 'domContentLoaded' });

  // mapSelector runs server-side and returns element properties for every match.
  const [heading] = await page.mapSelector('h1');
  const products = await page.mapSelector('.product-title');

  console.log({
    heading: heading?.innerText,
    products: products.map((product) => product.innerText),
  });
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Run the script with `node scrape.mjs`. The extracted data is logged to the console.

    
  

  
  
**BQL:**

  

  **1. Write the mutation**

  Navigate to the page and map elements to structured text using CSS selectors:

```graphql
mutation Scrape {
  goto(url: "https://scraping-sandbox.netlify.app/products", waitUntil: domContentLoaded) {
    status
  }

  heading: mapSelector(selector: "h1") {
    innerText
  }

  products: mapSelector(selector: ".product-title") {
    innerText
  }
}
```

  **2. Run it**

  Paste into the [BQL IDE](https://account.browserless.io/bql) and click Run.

  **3. Check the output**

  The response returns structured JSON with text for each matched element:

```json
{
  "data": {
    "goto": { "status": 200 },
    "heading": [{ "innerText": "Product Catalog" }],
    "products": [
      { "innerText": "ProBass A100 Wireless Headphones" },
      { "innerText": "Aura Smart Ring Gen 4" }
    ]
  }
}
```

  

## Next steps

  <a href="/examples/screenshot" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Take a Screenshot</h3>
      capture the page visually
    
  </a>
  <a href="/examples/forms" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Fill and Submit a Form</h3>
      automate form interactions before scraping
    
  </a>
  <a href="/examples/sessions" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Authenticated Sessions</h3>
      scrape pages that require login
    
  </a>

---

# Scrape Amazon product listings

URL: https://docs.browserless.io/examples/scrape-amazon

# Scrape Amazon product listings

Search Amazon for a keyword and extract product titles, prices, ratings, and links from the results page.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

Amazon aggressively blocks automated browsers with CAPTCHAs and fingerprinting. A plain HTTP request or standard headless browser returns a bot-detection page instead of product data. The examples below search for "wireless headphones" and route through stealth mode with a residential proxy to bypass Amazon's protections.

> **Selector stability**
> Amazon updates its markup frequently. If `.s-result-item` or nested selectors stop returning results, inspect the live page with browser DevTools to find the current element names.

  
**AI Agent:**

  Use the [Browserless MCP server](https://github.com/browserless/browserless-mcp) to scrape product listings from Amazon from any MCP-compatible AI agent (Claude Desktop, Cursor, Windsurf, ChatGPT, etc.).

  **1. Connect the MCP server**

  Send this prompt to your AI agent to install the Browserless MCP server:

```text
Go to https://github.com/browserless/browserless-mcp/blob/main/install.md
and follow the instructions to install the Browserless MCP server
for my client.
```

  **2. Scrape Amazon**

  Use `browserless_smartscraper`. It handles Amazon's bot protection automatically.

```text
Use the browserless_smartscraper tool to scrape product listings
from https://www.amazon.com/s?k=wireless+headphones
and return the results as markdown
```

  
  
**REST API:**

  Send the BQL mutation over HTTP to the stealth endpoint. No browser library or BQL IDE required.

  
    
**cURL:**

    

    **1. Send the request**

```bash
curl -X POST \
  "https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "mutation ScrapeAmazon { goto(url: \"https://www.amazon.com/s?k=wireless+headphones\", waitUntil: networkIdle) { status } waitForSelector(selector: \"div.s-result-item[data-asin]\", timeout: 15000) { time } products: mapSelector(selector: \"div.s-result-item[data-asin]\") { asin: attribute(name: \"data-asin\") { value } title: mapSelector(selector: \"h2 span\") { innerText } price: mapSelector(selector: \".a-price .a-offscreen\") { innerText } rating: mapSelector(selector: \".a-icon-alt\") { innerText } reviewCount: mapSelector(selector: \".a-size-base.s-underline-text\") { innerText } link: mapSelector(selector: \"h2 a\") { href: attribute(name: \"href\") { value } } } }",
    "variables": {}
  }'
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 2845 },
    "products": [
      {
        "asin": { "value": "B0BT35LT8S" },
        "title": [{ "innerText": "Sony WH-1000XM5 Wireless Noise Canceling Headphones" }],
        "price": [{ "innerText": "$278.00" }],
        "rating": [{ "innerText": "4.6 out of 5 stars" }],
        "reviewCount": [{ "innerText": "12,847" }],
        "link": [{ "href": { "value": "/dp/B0BT35LT8S" } }]
      },
      {
        "asin": { "value": "B09WX4GJ1G" },
        "title": [{ "innerText": "Apple AirPods Max Wireless Over-Ear Headphones" }],
        "price": [{ "innerText": "$449.99" }],
        "rating": [{ "innerText": "4.7 out of 5 stars" }],
        "reviewCount": [{ "innerText": "8,234" }],
        "link": [{ "href": { "value": "/dp/B09WX4GJ1G" } }]
      }
    ]
  }
}
```

    
    
**JavaScript:**

    

    **1. Send the request**

```js
const query = `mutation ScrapeAmazon {
  goto(url: "https://www.amazon.com/s?k=wireless+headphones", waitUntil: networkIdle) {
    status
  }
  waitForSelector(selector: "div.s-result-item[data-asin]", timeout: 15000) {
    time
  }
  products: mapSelector(selector: "div.s-result-item[data-asin]") {
    asin: attribute(name: "data-asin") { value }
    title: mapSelector(selector: "h2 span") { innerText }
    price: mapSelector(selector: ".a-price .a-offscreen") { innerText }
    rating: mapSelector(selector: ".a-icon-alt") { innerText }
    reviewCount: mapSelector(selector: ".a-size-base.s-underline-text") { innerText }
    link: mapSelector(selector: "h2 a") {
      href: attribute(name: "href") { value }
    }
  }
}`;

const response = await fetch(
  'https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ query, variables: {} }),
  }
);

const { data } = await response.json();
console.log(JSON.stringify(data.products, null, 2));
```

    **2. Check the output**

```json
[
  {
    "asin": { "value": "B0BT35LT8S" },
    "title": [{ "innerText": "Sony WH-1000XM5 Wireless Noise Canceling Headphones" }],
    "price": [{ "innerText": "$278.00" }],
    "rating": [{ "innerText": "4.6 out of 5 stars" }],
    "reviewCount": [{ "innerText": "12,847" }],
    "link": [{ "href": { "value": "/dp/B0BT35LT8S" } }]
  },
  {
    "asin": { "value": "B09WX4GJ1G" },
    "title": [{ "innerText": "Apple AirPods Max Wireless Over-Ear Headphones" }],
    "price": [{ "innerText": "$449.99" }],
    "rating": [{ "innerText": "4.7 out of 5 stars" }],
    "reviewCount": [{ "innerText": "8,234" }],
    "link": [{ "href": { "value": "/dp/B09WX4GJ1G" } }]
  }
]
```

    
    
**Python:**

    

    **1. Install dependencies**

```bash
pip install requests
```

    **2. Send the request**

```python
import requests

query = """
mutation ScrapeAmazon {
  goto(url: "https://www.amazon.com/s?k=wireless+headphones", waitUntil: networkIdle) {
    status
  }
  waitForSelector(selector: "div.s-result-item[data-asin]", timeout: 15000) {
    time
  }
  products: mapSelector(selector: "div.s-result-item[data-asin]") {
    asin: attribute(name: "data-asin") { value }
    title: mapSelector(selector: "h2 span") { innerText }
    price: mapSelector(selector: ".a-price .a-offscreen") { innerText }
    rating: mapSelector(selector: ".a-icon-alt") { innerText }
    reviewCount: mapSelector(selector: ".a-size-base.s-underline-text") { innerText }
    link: mapSelector(selector: "h2 a") {
      href: attribute(name: "href") { value }
    }
  }
}
"""

response = requests.post(
    'https://production-sfo.browserless.io/stealth/bql',
    params={
        'token': 'YOUR_API_TOKEN_HERE',
        'proxy': 'residential',
        'proxyCountry': 'us',
    },
    json={'query': query, 'variables': {}},
)

data = response.json()['data']
for product in data['products']:
    title = product['title'][0]['innerText']
    price = product['price'][0]['innerText'] if product['price'] else 'N/A'
    rating = product['rating'][0]['innerText'] if product['rating'] else 'N/A'
    print(f'{title} | {price} | {rating}')
```

    **3. Check the output**

```text
Sony WH-1000XM5 Wireless Noise Canceling Headphones | $278.00 | 4.6 out of 5 stars
Apple AirPods Max Wireless Over-Ear Headphones | $449.99 | 4.7 out of 5 stars
```

    
    
**Java:**

    

    **1. Send the request**

```java
import java.net.URI;
import java.net.http.*;

String token = "YOUR_API_TOKEN_HERE";
String endpoint = "https://production-sfo.browserless.io/stealth/bql?token=" + token
    + "&proxy=residential&proxyCountry=us";

String query = "mutation ScrapeAmazon {"
    + " goto(url: \\\"https://www.amazon.com/s?k=wireless+headphones\\\", waitUntil: networkIdle) { status }"
    + " waitForSelector(selector: \\\"div.s-result-item[data-asin]\\\", timeout: 15000) { time }"
    + " products: mapSelector(selector: \\\"div.s-result-item[data-asin]\\\") {"
    + "   asin: attribute(name: \\\"data-asin\\\") { value }"
    + "   title: mapSelector(selector: \\\"h2 span\\\") { innerText }"
    + "   price: mapSelector(selector: \\\".a-price .a-offscreen\\\") { innerText }"
    + "   rating: mapSelector(selector: \\\".a-icon-alt\\\") { innerText }"
    + "   reviewCount: mapSelector(selector: \\\".a-size-base.s-underline-text\\\") { innerText }"
    + "   link: mapSelector(selector: \\\"h2 a\\\") { href: attribute(name: \\\"href\\\") { value } }"
    + " }"
    + " }";

String payload = "{\"query\": \"" + query + "\", \"variables\": {}}";

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(endpoint))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(payload))
    .build();

HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 2845 },
    "products": [
      {
        "asin": { "value": "B0BT35LT8S" },
        "title": [{ "innerText": "Sony WH-1000XM5 Wireless Noise Canceling Headphones" }],
        "price": [{ "innerText": "$278.00" }],
        "rating": [{ "innerText": "4.6 out of 5 stars" }]
      }
    ]
  }
}
```

    
    
**C#:**

    

    **1. Send the request**

```csharp
using System.Net.Http;
using System.Text;
using System.Text.Json;

string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"https://production-sfo.browserless.io/stealth/bql?token={token}&proxy=residential&proxyCountry=us";

var payload = new
{
    query = @"mutation ScrapeAmazon {
      goto(url: ""https://www.amazon.com/s?k=wireless+headphones"", waitUntil: networkIdle) { status }
      waitForSelector(selector: ""div.s-result-item[data-asin]"", timeout: 15000) { time }
      products: mapSelector(selector: ""div.s-result-item[data-asin]"") {
        asin: attribute(name: ""data-asin"") { value }
        title: mapSelector(selector: ""h2 span"") { innerText }
        price: mapSelector(selector: "".a-price .a-offscreen"") { innerText }
        rating: mapSelector(selector: "".a-icon-alt"") { innerText }
        reviewCount: mapSelector(selector: "".a-size-base.s-underline-text"") { innerText }
        link: mapSelector(selector: ""h2 a"") {
          href: attribute(name: ""href"") { value }
        }
      }
    }",
    variables = new { },
};

using (HttpClient httpClient = new HttpClient())
{
    var content = new StringContent(
        JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
    var response = await httpClient.PostAsync(endpoint, content);
    string body = await response.Content.ReadAsStringAsync();
    Console.WriteLine(body);
}
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 2845 },
    "products": [
      {
        "asin": { "value": "B0BT35LT8S" },
        "title": [{ "innerText": "Sony WH-1000XM5 Wireless Noise Canceling Headphones" }],
        "price": [{ "innerText": "$278.00" }],
        "rating": [{ "innerText": "4.6 out of 5 stars" }]
      }
    ]
  }
}
```

    
  

  
  
**Frameworks:**

  Connect through stealth mode and a residential proxy so Amazon sees traffic from a real browser, then read product data from the rendered search results.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Connect and scrape**

```js
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint:
    'wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us',
});

try {
  const page = await browser.newPage();
  await page.goto('https://www.amazon.com/s?k=wireless+headphones', {
    waitUntil: 'networkidle2',
  });

  await page.waitForSelector('div.s-result-item[data-asin]');

  const products = await page.evaluate(() =>
    Array.from(document.querySelectorAll('div.s-result-item[data-asin]'))
      .filter((el) => el.getAttribute('data-asin'))
      .map((el) => ({
        asin: el.getAttribute('data-asin'),
        title: el.querySelector('h2 span')?.innerText?.trim() ?? '',
        price: el.querySelector('.a-price .a-offscreen')?.innerText?.trim() ?? '',
        rating: el.querySelector('.a-icon-alt')?.innerText?.trim() ?? '',
      }))
  );

  console.log(JSON.stringify(products, null, 2));
} finally {
  await browser.close();
}
```

    **3. Check the output**

    Run with `node scrape-amazon.mjs`. Each object has `asin`, `title`, `price`, and `rating` fields.

```json
[
  {
    "asin": "B0BT35LT8S",
    "title": "Sony WH-1000XM5 Wireless Noise Canceling Headphones",
    "price": "$278.00",
    "rating": "4.6 out of 5 stars"
  }
]
```

    
    
**Playwright:**

    

    **1. Install dependencies**

```bash
npm install playwright-core
```

    **2. Connect and scrape**

```js
import { chromium } from 'playwright-core';

const browser = await chromium.connectOverCDP(
  'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&stealth&proxy=residential&proxyCountry=us'
);

try {
  const context = browser.contexts()[0];
  const page = await context.newPage();
  await page.goto('https://www.amazon.com/s?k=wireless+headphones', {
    waitUntil: 'networkidle',
  });

  await page.waitForSelector('div.s-result-item[data-asin]');

  const products = await page.evaluate(() =>
    Array.from(document.querySelectorAll('div.s-result-item[data-asin]'))
      .filter((el) => el.getAttribute('data-asin'))
      .map((el) => ({
        asin: el.getAttribute('data-asin'),
        title: el.querySelector('h2 span')?.innerText?.trim() ?? '',
        price: el.querySelector('.a-price .a-offscreen')?.innerText?.trim() ?? '',
        rating: el.querySelector('.a-icon-alt')?.innerText?.trim() ?? '',
      }))
  );

  console.log(JSON.stringify(products, null, 2));
} finally {
  await browser.close();
}
```

    **3. Check the output**

    Run with `node scrape-amazon.mjs`. Each object has `asin`, `title`, `price`, and `rating` fields.

```json
[
  {
    "asin": "B0BT35LT8S",
    "title": "Sony WH-1000XM5 Wireless Noise Canceling Headphones",
    "price": "$278.00",
    "rating": "4.6 out of 5 stars"
  }
]
```

    
  

  
  
**BQL:**

  

  

  **1. Write the mutation**

  Navigate to Amazon's search results, wait for product cards to load, then extract titles, prices, ratings, and ASINs. We use `/stealth/bql` because Amazon's bot detection blocks standard headless browsers.

```graphql
mutation ScrapeAmazon {
  goto(url: "https://www.amazon.com/s?k=wireless+headphones", waitUntil: networkIdle) {
    status
  }
  waitForSelector(selector: "div.s-result-item[data-asin]", timeout: 15000) {
    time
  }
  products: mapSelector(selector: "div.s-result-item[data-asin]") {
    asin: attribute(name: "data-asin") { value }
    title: mapSelector(selector: "h2 span") { innerText }
    price: mapSelector(selector: ".a-price .a-offscreen") { innerText }
    rating: mapSelector(selector: ".a-icon-alt") { innerText }
    reviewCount: mapSelector(selector: ".a-size-base.s-underline-text") { innerText }
    link: mapSelector(selector: "h2 a") {
      href: attribute(name: "href") { value }
    }
  }
}
```

  **2. Run it**

  Paste into the [BQL IDE](https://account.browserless.io/bql) and click Run.

  **3. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 2845 },
    "products": [
      {
        "asin": { "value": "B0BT35LT8S" },
        "title": [{ "innerText": "Sony WH-1000XM5 Wireless Noise Canceling Headphones" }],
        "price": [{ "innerText": "$278.00" }],
        "rating": [{ "innerText": "4.6 out of 5 stars" }],
        "reviewCount": [{ "innerText": "12,847" }],
        "link": [{ "href": { "value": "/dp/B0BT35LT8S" } }]
      },
      {
        "asin": { "value": "B09WX4GJ1G" },
        "title": [{ "innerText": "Apple AirPods Max Wireless Over-Ear Headphones" }],
        "price": [{ "innerText": "$449.99" }],
        "rating": [{ "innerText": "4.7 out of 5 stars" }],
        "reviewCount": [{ "innerText": "8,234" }],
        "link": [{ "href": { "value": "/dp/B09WX4GJ1G" } }]
      }
    ]
  }
}
```

  

## Next steps

  <a href="/examples/scrape-etsy" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape Etsy Product Listings</h3>
      scrape another e-commerce platform with stealth mode
    
  </a>
  <a href="/examples/scrape" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape Structured Data</h3>
      extract data from other sites using the `/scrape` endpoint
    
  </a>
  <a href="/examples/cloudflare" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Solving Cloudflare Challenges</h3>
      bypass Cloudflare protection on other targets
    
  </a>

---

# Scrape Booking.com hotel listings

URL: https://docs.browserless.io/examples/scrape-booking

# Scrape Booking.com hotel listings

Pull hotel names, prices, ratings, and locations from Booking.com search results.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

Booking.com runs aggressive bot detection, including fingerprint checks, CAPTCHAs, and dynamic content that loads differently for automated requests. The examples below navigate directly to a pre-formed search URL and extract hotel listing data. All three tabs route through stealth mode and a residential proxy, which is what makes them work where plain requests don't.

> **Selector stability**
> Booking.com updates its markup regularly and may show a currency or location modal on first load. If selectors return empty results, check whether an overlay modal appeared and update the CSS selectors to match the current DOM structure.

  
**AI Agent:**

  Use the [Browserless MCP server](https://github.com/browserless/browserless-mcp) to scrape hotel listings from Booking.com from any MCP-compatible AI agent (Claude Desktop, Cursor, Windsurf, ChatGPT, etc.).

  **1. Connect the MCP server**

  Send this prompt to your AI agent to install the Browserless MCP server:

```text
Go to https://github.com/browserless/browserless-mcp/blob/main/install.md
and follow the instructions to install the Browserless MCP server
for my client.
```

  **2. Scrape Booking.com**

  Use `browserless_smartscraper`. Booking.com has bot protection and smartscraper handles it automatically.

```text
Use the browserless_smartscraper tool to scrape hotel listings
from https://www.booking.com/searchresults.html?ss=New+York
and return the results as markdown
```

  
  
**REST API:**

  Send the BQL mutation over HTTP to the stealth endpoint. No browser library or BQL IDE required.

  
    
**cURL:**

    

    **1. Send the request**

```bash
curl -X POST \
  "https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "mutation ScrapeBooking { goto(url: \"https://www.booking.com/searchresults.html?ss=New+York&checkin=2026-09-01&checkout=2026-09-02&group_adults=2\", waitUntil: networkIdle) { status } hotels: mapSelector(selector: \"[data-testid='\''property-card'\'']\") { name: mapSelector(selector: \"[data-testid='\''title'\'']\") { innerText } price: mapSelector(selector: \"[data-testid='\''price-and-discounted-price'\'']\") { innerText } rating: mapSelector(selector: \"[data-testid='\''review-score'\'']\") { innerText } location: mapSelector(selector: \"[data-testid='\''address'\'']\") { innerText } } }",
    "variables": {}
  }'
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "hotels": [
      {
        "name": [{ "innerText": "The Manhattan Hotel" }],
        "price": [{ "innerText": "$189" }],
        "rating": [{ "innerText": "8.5" }],
        "location": [{ "innerText": "Midtown Manhattan, New York" }]
      },
      {
        "name": [{ "innerText": "Brooklyn Heights Suites" }],
        "price": [{ "innerText": "$142" }],
        "rating": [{ "innerText": "7.9" }],
        "location": [{ "innerText": "Brooklyn Heights, New York" }]
      }
    ]
  }
}
```

    
    
**JavaScript:**

    

    **1. Send the request**

```js
const query = `mutation ScrapeBooking {
  goto(
    url: "https://www.booking.com/searchresults.html?ss=New+York&checkin=2026-09-01&checkout=2026-09-02&group_adults=2"
    waitUntil: networkIdle
  ) {
    status
  }
  hotels: mapSelector(selector: "[data-testid='property-card']") {
    name: mapSelector(selector: "[data-testid='title']") {
      innerText
    }
    price: mapSelector(selector: "[data-testid='price-and-discounted-price']") {
      innerText
    }
    rating: mapSelector(selector: "[data-testid='review-score']") {
      innerText
    }
    location: mapSelector(selector: "[data-testid='address']") {
      innerText
    }
  }
}`;

const response = await fetch(
  'https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ query, variables: {} }),
  }
);

const { data } = await response.json();
const hotels = data.hotels.map((hotel) => ({
  name: hotel.name?.[0]?.innerText ?? '',
  price: hotel.price?.[0]?.innerText ?? '',
  rating: hotel.rating?.[0]?.innerText ?? '',
  location: hotel.location?.[0]?.innerText ?? '',
}));
console.log(JSON.stringify(hotels, null, 2));
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "hotels": [
      {
        "name": [{ "innerText": "The Manhattan Hotel" }],
        "price": [{ "innerText": "$189" }],
        "rating": [{ "innerText": "8.5" }],
        "location": [{ "innerText": "Midtown Manhattan, New York" }]
      },
      {
        "name": [{ "innerText": "Brooklyn Heights Suites" }],
        "price": [{ "innerText": "$142" }],
        "rating": [{ "innerText": "7.9" }],
        "location": [{ "innerText": "Brooklyn Heights, New York" }]
      }
    ]
  }
}
```

    
    
**Python:**

    

    **1. Install dependencies**

```bash
pip install requests
```

    **2. Send the request**

```python
import requests

query = """
mutation ScrapeBooking {
  goto(
    url: "https://www.booking.com/searchresults.html?ss=New+York&checkin=2026-09-01&checkout=2026-09-02&group_adults=2"
    waitUntil: networkIdle
  ) {
    status
  }
  hotels: mapSelector(selector: "[data-testid='property-card']") {
    name: mapSelector(selector: "[data-testid='title']") {
      innerText
    }
    price: mapSelector(selector: "[data-testid='price-and-discounted-price']") {
      innerText
    }
    rating: mapSelector(selector: "[data-testid='review-score']") {
      innerText
    }
    location: mapSelector(selector: "[data-testid='address']") {
      innerText
    }
  }
}
"""

response = requests.post(
    'https://production-sfo.browserless.io/stealth/bql',
    params={
        'token': 'YOUR_API_TOKEN_HERE',
        'proxy': 'residential',
        'proxyCountry': 'us',
    },
    json={'query': query, 'variables': {}},
)

data = response.json()['data']
for hotel in data['hotels']:
    name = hotel['name'][0]['innerText'] if hotel['name'] else ''
    price = hotel['price'][0]['innerText'] if hotel['price'] else ''
    rating = hotel['rating'][0]['innerText'] if hotel['rating'] else ''
    location = hotel['location'][0]['innerText'] if hotel['location'] else ''
    print(f'{name} — {price} — {rating} — {location}')
```

    **3. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "hotels": [
      {
        "name": [{ "innerText": "The Manhattan Hotel" }],
        "price": [{ "innerText": "$189" }],
        "rating": [{ "innerText": "8.5" }],
        "location": [{ "innerText": "Midtown Manhattan, New York" }]
      },
      {
        "name": [{ "innerText": "Brooklyn Heights Suites" }],
        "price": [{ "innerText": "$142" }],
        "rating": [{ "innerText": "7.9" }],
        "location": [{ "innerText": "Brooklyn Heights, New York" }]
      }
    ]
  }
}
```

    
    
**Java:**

    

    **1. Send the request**

```java
import java.net.URI;
import java.net.http.*;

String token = "YOUR_API_TOKEN_HERE";
String endpoint = "https://production-sfo.browserless.io/stealth/bql?token=" + token
    + "&proxy=residential&proxyCountry=us";

String query = "mutation ScrapeBooking {"
    + " goto(url: \\\"https://www.booking.com/searchresults.html?ss=New+York&checkin=2026-09-01&checkout=2026-09-02&group_adults=2\\\","
    + " waitUntil: networkIdle) { status }"
    + " hotels: mapSelector(selector: \\\"[data-testid='property-card']\\\") {"
    + "   name: mapSelector(selector: \\\"[data-testid='title']\\\") { innerText }"
    + "   price: mapSelector(selector: \\\"[data-testid='price-and-discounted-price']\\\") { innerText }"
    + "   rating: mapSelector(selector: \\\"[data-testid='review-score']\\\") { innerText }"
    + "   location: mapSelector(selector: \\\"[data-testid='address']\\\") { innerText }"
    + " }"
    + " }";

String payload = "{\"query\": \"" + query + "\", \"variables\": {}}";

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(endpoint))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(payload))
    .build();

HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "hotels": [
      {
        "name": [{ "innerText": "The Manhattan Hotel" }],
        "price": [{ "innerText": "$189" }],
        "rating": [{ "innerText": "8.5" }],
        "location": [{ "innerText": "Midtown Manhattan, New York" }]
      },
      {
        "name": [{ "innerText": "Brooklyn Heights Suites" }],
        "price": [{ "innerText": "$142" }],
        "rating": [{ "innerText": "7.9" }],
        "location": [{ "innerText": "Brooklyn Heights, New York" }]
      }
    ]
  }
}
```

    
    
**C#:**

    

    **1. Send the request**

```csharp
using System.Net.Http;
using System.Text;
using System.Text.Json;

string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"https://production-sfo.browserless.io/stealth/bql?token={token}&proxy=residential&proxyCountry=us";

var payload = new
{
    query = @"mutation ScrapeBooking {
      goto(
        url: ""https://www.booking.com/searchresults.html?ss=New+York&checkin=2026-09-01&checkout=2026-09-02&group_adults=2""
        waitUntil: networkIdle
      ) { status }
      hotels: mapSelector(selector: ""[data-testid='property-card']"") {
        name: mapSelector(selector: ""[data-testid='title']"") { innerText }
        price: mapSelector(selector: ""[data-testid='price-and-discounted-price']"") { innerText }
        rating: mapSelector(selector: ""[data-testid='review-score']"") { innerText }
        location: mapSelector(selector: ""[data-testid='address']"") { innerText }
      }
    }",
    variables = new { },
};

using (HttpClient httpClient = new HttpClient())
{
    var content = new StringContent(
        JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
    var response = await httpClient.PostAsync(endpoint, content);
    string body = await response.Content.ReadAsStringAsync();
    Console.WriteLine(body);
}
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "hotels": [
      {
        "name": [{ "innerText": "The Manhattan Hotel" }],
        "price": [{ "innerText": "$189" }],
        "rating": [{ "innerText": "8.5" }],
        "location": [{ "innerText": "Midtown Manhattan, New York" }]
      },
      {
        "name": [{ "innerText": "Brooklyn Heights Suites" }],
        "price": [{ "innerText": "$142" }],
        "rating": [{ "innerText": "7.9" }],
        "location": [{ "innerText": "Brooklyn Heights, New York" }]
      }
    ]
  }
}
```

    
  

  
  
**Frameworks:**

  Connect through stealth mode and a residential proxy so Booking.com sees a real-looking browser session, then extract hotel data from the rendered listings.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Connect and scrape**

```js
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint:
    'wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us',
});

try {
  const page = await browser.newPage();
  await page.goto(
    'https://www.booking.com/searchresults.html?ss=New+York&checkin=2026-09-01&checkout=2026-09-02&group_adults=2',
    { waitUntil: 'networkidle2' }
  );

  // Wait for property cards to confirm the listings have rendered.
  await page.waitForSelector('[data-testid="property-card"]');

  const hotels = await page.evaluate(() =>
    Array.from(document.querySelectorAll('[data-testid="property-card"]')).map((card) => ({
      name: card.querySelector('[data-testid="title"]')?.innerText?.trim() ?? null,
      price: card.querySelector('[data-testid="price-and-discounted-price"]')?.innerText?.trim() ?? null,
      rating: card.querySelector('[data-testid="review-score"]')?.innerText?.trim() ?? null,
      location: card.querySelector('[data-testid="address"]')?.innerText?.trim() ?? null,
    }))
  );

  console.log(JSON.stringify(hotels, null, 2));
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Run with `node scrape-booking.mjs`. `null` values mean the selector wasn't matched. Increase the `waitForSelector` timeout or check whether a modal intercepted the page load.

```json
[
  {
    "name": "The Manhattan Hotel",
    "price": "$189",
    "rating": "8.5",
    "location": "Midtown Manhattan, New York"
  },
  {
    "name": "Brooklyn Heights Suites",
    "price": "$142",
    "rating": "7.9",
    "location": "Brooklyn Heights, New York"
  }
]
```

    
    
**Playwright:**

    

    **1. Install dependencies**

```bash
npm install playwright-core
```

    **2. Connect and scrape**

```js
import { chromium } from 'playwright-core';

const browser = await chromium.connectOverCDP(
  'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&stealth&proxy=residential&proxyCountry=us'
);

try {
  const context = browser.contexts()[0];
  const page = await context.newPage();
  await page.goto(
    'https://www.booking.com/searchresults.html?ss=New+York&checkin=2026-09-01&checkout=2026-09-02&group_adults=2',
    { waitUntil: 'networkidle' }
  );

  // Wait for property cards to confirm the listings have rendered.
  await page.waitForSelector('[data-testid="property-card"]');

  const hotels = await page.evaluate(() =>
    Array.from(document.querySelectorAll('[data-testid="property-card"]')).map((card) => ({
      name: card.querySelector('[data-testid="title"]')?.innerText?.trim() ?? null,
      price: card.querySelector('[data-testid="price-and-discounted-price"]')?.innerText?.trim() ?? null,
      rating: card.querySelector('[data-testid="review-score"]')?.innerText?.trim() ?? null,
      location: card.querySelector('[data-testid="address"]')?.innerText?.trim() ?? null,
    }))
  );

  console.log(JSON.stringify(hotels, null, 2));
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Run with `node scrape-booking.mjs`. `null` values mean the selector wasn't matched. Increase the `waitForSelector` timeout or check whether a modal intercepted the page load.

```json
[
  {
    "name": "The Manhattan Hotel",
    "price": "$189",
    "rating": "8.5",
    "location": "Midtown Manhattan, New York"
  },
  {
    "name": "Brooklyn Heights Suites",
    "price": "$142",
    "rating": "7.9",
    "location": "Brooklyn Heights, New York"
  }
]
```

    
  

  
  
**BQL:**

  

  **1. Write the mutation**

  Navigate to the search results URL and use nested `mapSelector` calls to pull name, price, rating, and location from each property card. We send this to `/stealth/bql` instead of the default `/bql` because Booking.com's bot detection blocks plain browser sessions before listings load.

```graphql
mutation ScrapeBooking {
  goto(
    url: "https://www.booking.com/searchresults.html?ss=New+York&checkin=2026-09-01&checkout=2026-09-02&group_adults=2"
    waitUntil: networkIdle
  ) {
    status
  }
  hotels: mapSelector(selector: "[data-testid='property-card']") {
    name: mapSelector(selector: "[data-testid='title']") {
      innerText
    }
    price: mapSelector(selector: "[data-testid='price-and-discounted-price']") {
      innerText
    }
    rating: mapSelector(selector: "[data-testid='review-score']") {
      innerText
    }
    location: mapSelector(selector: "[data-testid='address']") {
      innerText
    }
  }
}
```

  **2. Run it**

  Paste into the [BQL IDE](https://account.browserless.io/bql) and click Run.

  **3. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "hotels": [
      {
        "name": [{ "innerText": "The Manhattan Hotel" }],
        "price": [{ "innerText": "$189" }],
        "rating": [{ "innerText": "8.5" }],
        "location": [{ "innerText": "Midtown Manhattan, New York" }]
      },
      {
        "name": [{ "innerText": "Brooklyn Heights Suites" }],
        "price": [{ "innerText": "$142" }],
        "rating": [{ "innerText": "7.9" }],
        "location": [{ "innerText": "Brooklyn Heights, New York" }]
      }
    ]
  }
}
```

  

## Next steps

  <a href="/examples/scrape-glassdoor" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape Glassdoor Job Listings</h3>
      another stealth-mode scrape against aggressive bot detection
    
  </a>
  <a href="/examples/scrape-walmart" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape Walmart Product Listings</h3>
      extract product data from a bot-protected retail site
    
  </a>
  <a href="/examples/cloudflare" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Solving Cloudflare Challenges</h3>
      bypass Cloudflare's interstitial pages before scraping
    
  </a>

---

# Scrape Etsy product listings

URL: https://docs.browserless.io/examples/scrape-etsy

# Scrape Etsy product listings

Search Etsy for a keyword and extract product titles, prices, and links from the results page.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

  
**AI Agent:**

  Use the [Browserless MCP server](https://github.com/browserless/browserless-mcp) to scrape product listings from Etsy from any MCP-compatible AI agent (Claude Desktop, Cursor, Windsurf, ChatGPT, etc.).

  **1. Connect the MCP server**

  Send this prompt to your AI agent to install the Browserless MCP server:

```text
Go to https://github.com/browserless/browserless-mcp/blob/main/install.md
and follow the instructions to install the Browserless MCP server
for my client.
```

  **2. Scrape Etsy**

  Use `browserless_smartscraper`. Etsy has bot detection and smartscraper handles it automatically.

```text
Use the browserless_smartscraper tool to scrape product listings
from https://www.etsy.com/search?q=handmade+jewelry
and return the results as markdown
```

  
  
**REST API:**

  Use the `/scrape` REST endpoint to extract product listing data from Etsy search results.

  :::note
  Etsy uses bot detection and loads content dynamically. The `/scrape` endpoint may return incomplete results. For more reliable extraction, use the BQL tab with stealth mode and residential proxies.
  :::

  
    
**cURL:**

    

    **1. Send the request**

```bash
curl -X POST \
  "https://production-sfo.browserless.io/scrape?token=YOUR_API_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://www.etsy.com/search?q=candles",
    "elements": [
      { "selector": ".v2-listing-card h3" },
      { "selector": ".v2-listing-card .currency-value" }
    ]
  }'
```

    **2. Check the output**

    Each selector returns an array of matched elements with text, HTML, and position data:

```json
{
  "data": [
    {
      "selector": ".v2-listing-card h3",
      "results": [
        {
          "text": "Handmade Soy Candle, Vanilla Scent",
          "html": "Handmade Soy Candle, Vanilla Scent",
          "height": 20,
          "left": 32,
          "top": 410,
          "width": 280
        }
      ]
    },
    {
      "selector": ".v2-listing-card .currency-value",
      "results": [
        {
          "text": "24.99",
          "html": "24.99",
          "height": 16,
          "left": 32,
          "top": 440,
          "width": 60
        }
      ]
    }
  ]
}
```

    
    
**JavaScript:**

    

    **1. Send the request**

```js
const response = await fetch(
  'https://production-sfo.browserless.io/scrape?token=YOUR_API_TOKEN_HERE',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      url: 'https://www.etsy.com/search?q=candles',
      elements: [
        { selector: '.v2-listing-card h3' },
        { selector: '.v2-listing-card .currency-value' },
      ],
    }),
  }
);

const { data } = await response.json();
const titles = data[0].results.map((r) => r.text);
const prices = data[1].results.map((r) => r.text);
console.log(titles.map((title, i) => ({ title, price: prices[i] })));
```

    **2. Check the output**

    Run with `node scrape-etsy.mjs`. The array contains product titles paired with prices.

    
    
**Python:**

    

    **1. Install dependencies**

```bash
pip install requests
```

    **2. Send the request**

```python
import requests

response = requests.post(
    'https://production-sfo.browserless.io/scrape?token=YOUR_API_TOKEN_HERE',
    json={
        'url': 'https://www.etsy.com/search?q=candles',
        'elements': [
            {'selector': '.v2-listing-card h3'},
            {'selector': '.v2-listing-card .currency-value'},
        ],
    },
)

data = response.json()['data']
titles = [item['text'] for item in data[0]['results']]
prices = [item['text'] for item in data[1]['results']]
for title, price in zip(titles, prices):
    print(f'{title}: ${price}')
```

    **3. Check the output**

    Run with `python scrape_etsy.py`. Each line shows a product title and its price.

    
    
**Java:**

    

    **1. Send the request**

```java
import java.net.URI;
import java.net.http.*;

String token = "YOUR_API_TOKEN_HERE";
String endpoint = "https://production-sfo.browserless.io/scrape?token=" + token;

String payload = """
    {
      "url": "https://www.etsy.com/search?q=candles",
      "elements": [
        { "selector": ".v2-listing-card h3" },
        { "selector": ".v2-listing-card .currency-value" }
      ]
    }
    """;

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(endpoint))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(payload))
    .build();

HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
```

    **2. Check the output**

    Run the class. The response JSON contains product titles and prices.

    
    
**C#:**

    

    **1. Send the request**

```csharp
using System.Net.Http;
using System.Text;
using System.Text.Json;

string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"https://production-sfo.browserless.io/scrape?token={token}";

var payload = new
{
    url = "https://www.etsy.com/search?q=candles",
    elements = new[]
    {
        new { selector = ".v2-listing-card h3" },
        new { selector = ".v2-listing-card .currency-value" },
    },
};

using (HttpClient httpClient = new HttpClient())
{
    var jsonPayload = JsonSerializer.Serialize(payload);
    var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
    var response = await httpClient.PostAsync(endpoint, content);
    string responseBody = await response.Content.ReadAsStringAsync();
    Console.WriteLine(responseBody);
}
```

    **2. Check the output**

    Run the program. The response JSON contains matched product titles and prices.

    
  

  
  
**Frameworks:**

  Connect a headless browser to search Etsy and extract product data from the rendered DOM.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Connect and scrape**

```js
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint:
    'wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us',
});

try {
  const page = await browser.newPage();
  await page.goto('https://www.etsy.com/search?q=candles', {
    waitUntil: 'networkidle2',
  });

  const products = await page.evaluate(() =>
    Array.from(document.querySelectorAll('.v2-listing-card')).map((card) => ({
      title: card.querySelector('h3')?.innerText?.trim() ?? '',
      price: card.querySelector('.currency-value')?.innerText?.trim() ?? '',
      link: card.querySelector('a.listing-link')?.href ?? '',
    }))
  );

  console.log(products);
} finally {
  await browser.close();
}
```

    **3. Check the output**

    Run with `node scrape-etsy.mjs`. Each object has `title`, `price`, and `link` fields.

    
    
**Playwright:**

    

    **1. Install dependencies**

```bash
npm install playwright-core
```

    **2. Connect and scrape**

```js
import { chromium } from 'playwright-core';

const browser = await chromium.connectOverCDP(
  'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&stealth&proxy=residential&proxyCountry=us'
);

try {
  const context = browser.contexts()[0];
  const page = await context.newPage();
  await page.goto('https://www.etsy.com/search?q=candles', {
    waitUntil: 'networkidle',
  });

  const products = await page.evaluate(() =>
    Array.from(document.querySelectorAll('.v2-listing-card')).map((card) => ({
      title: card.querySelector('h3')?.innerText?.trim() ?? '',
      price: card.querySelector('.currency-value')?.innerText?.trim() ?? '',
      link: card.querySelector('a.listing-link')?.href ?? '',
    }))
  );

  console.log(products);
} finally {
  await browser.close();
}
```

    **3. Check the output**

    Run with `node scrape-etsy.mjs`. Each object has `title`, `price`, and `link` fields.

    
  

  
  
**BQL:**

  

  **1. Write the mutation**

  Navigate to Etsy, search for a keyword, then use `mapSelector` to extract structured data from each product card:

```graphql
mutation ScrapeEtsy {
  goto(url: "https://www.etsy.com", waitUntil: domContentLoaded) {
    status
  }

  fillSearch: type(
    selector: "input#global-enhancements-search-query"
    text: "candles"
  ) {
    time
  }

  submitSearch: click(selector: "button.wt-input-btn-group__btn") {
    time
  }

  waitForTimeout(time: 3000) {
    time
  }

  listings: mapSelector(selector: ".v2-listing-card") {
    title: mapSelector(selector: "h3", wait: true) {
      innerText
    }
    price: mapSelector(selector: ".currency-value", wait: true) {
      innerText
    }
    link: mapSelector(selector: "a.listing-link") {
      href: attribute(name: "href") {
        value
      }
    }
  }
}
```

  **2. Run it**

  Paste into the [BQL IDE](https://account.browserless.io/bql) and click Run.

  **3. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "fillSearch": { "time": 245 },
    "submitSearch": { "time": 112 },
    "waitForTimeout": { "time": 3000 },
    "listings": [
      {
        "title": [{ "innerText": "Handmade Soy Candle, Vanilla Scent" }],
        "price": [{ "innerText": "24.99" }],
        "link": [{ "href": { "value": "https://www.etsy.com/listing/123456789/handmade-soy-candle" } }]
      },
      {
        "title": [{ "innerText": "Beeswax Pillar Candle Set" }],
        "price": [{ "innerText": "18.50" }],
        "link": [{ "href": { "value": "https://www.etsy.com/listing/987654321/beeswax-pillar-candle-set" } }]
      }
    ]
  }
}
```

  

## Next steps

  <a href="/examples/scrape" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape Structured Data</h3>
      extract data from other sites using the `/scrape` endpoint
    
  </a>
  <a href="/examples/cloudflare" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Solving Cloudflare Challenges</h3>
      bypass Cloudflare protection on other targets
    
  </a>
  <a href="/examples/google-search" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Automate Google Search</h3>
      scrape search engine results with BQL
    
  </a>

---

# Scrape GitHub trending repos

URL: https://docs.browserless.io/examples/scrape-github-trending

# Scrape GitHub trending repos

Extract repository names, descriptions, languages, and star counts from GitHub's trending page.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

GitHub's trending page is server-rendered and doesn't require stealth mode or residential proxies. A standard Browserless session handles it without issues. The examples below extract today's trending repositories.

  
**AI Agent:**

  Use the [Browserless MCP server](https://github.com/browserless/browserless-mcp) to scrape trending repositories from GitHub from any MCP-compatible AI agent (Claude Desktop, Cursor, Windsurf, ChatGPT, etc.).

  **1. Connect the MCP server**

  Send this prompt to your AI agent to install the Browserless MCP server:

```text
Go to https://github.com/browserless/browserless-mcp/blob/main/install.md
and follow the instructions to install the Browserless MCP server
for my client.
```

  **2. Scrape GitHub trending**

  Use `browserless_smartscraper`. GitHub's trending page is straightforward to scrape.

```text
Use the browserless_smartscraper tool to scrape the trending repositories
from https://github.com/trending
and return the results as markdown
```

  
  
**REST API:**

  Send the BQL mutation over HTTP. GitHub's trending page doesn't need stealth mode, so we use the standard `/chromium/bql` endpoint.

  
    
**cURL:**

    

    **1. Send the request**

```bash
curl -X POST \
  "https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "mutation ScrapeGitHubTrending { goto(url: \"https://github.com/trending\", waitUntil: networkIdle) { status } waitForSelector(selector: \"article.Box-row\", timeout: 15000) { time } repos: mapSelector(selector: \"article.Box-row\") { name: mapSelector(selector: \"h2 a\") { innerText } description: mapSelector(selector: \"p\") { innerText } language: mapSelector(selector: \"[itemprop=programmingLanguage]\") { innerText } stars: mapSelector(selector: \"a[href*=stargazers]\") { innerText } forks: mapSelector(selector: \"a[href*=forks]\") { innerText } todayStars: mapSelector(selector: \"span.d-inline-block.float-sm-right\") { innerText } } }",
    "variables": {}
  }'
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 1245 },
    "repos": [
      {
        "name": [{ "innerText": "vercel / next.js" }],
        "description": [{ "innerText": "The React Framework" }],
        "language": [{ "innerText": "JavaScript" }],
        "stars": [{ "innerText": "128,456" }],
        "forks": [{ "innerText": "27,891" }],
        "todayStars": [{ "innerText": "245 stars today" }]
      },
      {
        "name": [{ "innerText": "denoland / deno" }],
        "description": [{ "innerText": "A modern runtime for JavaScript and TypeScript" }],
        "language": [{ "innerText": "Rust" }],
        "stars": [{ "innerText": "98,234" }],
        "forks": [{ "innerText": "5,412" }],
        "todayStars": [{ "innerText": "189 stars today" }]
      }
    ]
  }
}
```

    
    
**JavaScript:**

    

    **1. Send the request**

```js
const query = `mutation ScrapeGitHubTrending {
  goto(url: "https://github.com/trending", waitUntil: networkIdle) {
    status
  }
  waitForSelector(selector: "article.Box-row", timeout: 15000) {
    time
  }
  repos: mapSelector(selector: "article.Box-row") {
    name: mapSelector(selector: "h2 a") { innerText }
    description: mapSelector(selector: "p") { innerText }
    language: mapSelector(selector: "[itemprop=programmingLanguage]") { innerText }
    stars: mapSelector(selector: "a[href*=stargazers]") { innerText }
    forks: mapSelector(selector: "a[href*=forks]") { innerText }
    todayStars: mapSelector(selector: "span.d-inline-block.float-sm-right") { innerText }
  }
}`;

const response = await fetch(
  'https://production-sfo.browserless.io/chromium/bql?token=YOUR_API_TOKEN_HERE',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ query, variables: {} }),
  }
);

const { data } = await response.json();
console.log(JSON.stringify(data.repos, null, 2));
```

    **2. Check the output**

```json
[
  {
    "name": [{ "innerText": "vercel / next.js" }],
    "description": [{ "innerText": "The React Framework" }],
    "language": [{ "innerText": "JavaScript" }],
    "stars": [{ "innerText": "128,456" }],
    "forks": [{ "innerText": "27,891" }],
    "todayStars": [{ "innerText": "245 stars today" }]
  },
  {
    "name": [{ "innerText": "denoland / deno" }],
    "description": [{ "innerText": "A modern runtime for JavaScript and TypeScript" }],
    "language": [{ "innerText": "Rust" }],
    "stars": [{ "innerText": "98,234" }],
    "forks": [{ "innerText": "5,412" }],
    "todayStars": [{ "innerText": "189 stars today" }]
  }
]
```

    
    
**Python:**

    

    **1. Install dependencies**

```bash
pip install requests
```

    **2. Send the request**

```python
import requests

query = """
mutation ScrapeGitHubTrending {
  goto(url: "https://github.com/trending", waitUntil: networkIdle) {
    status
  }
  waitForSelector(selector: "article.Box-row", timeout: 15000) {
    time
  }
  repos: mapSelector(selector: "article.Box-row") {
    name: mapSelector(selector: "h2 a") { innerText }
    description: mapSelector(selector: "p") { innerText }
    language: mapSelector(selector: "[itemprop=programmingLanguage]") { innerText }
    stars: mapSelector(selector: "a[href*=stargazers]") { innerText }
    forks: mapSelector(selector: "a[href*=forks]") { innerText }
    todayStars: mapSelector(selector: "span.d-inline-block.float-sm-right") { innerText }
  }
}
"""

response = requests.post(
    'https://production-sfo.browserless.io/chromium/bql',
    params={'token': 'YOUR_API_TOKEN_HERE'},
    json={'query': query, 'variables': {}},
)

data = response.json()['data']
for repo in data['repos']:
    name = repo['name'][0]['innerText'].strip()
    desc = repo['description'][0]['innerText'].strip() if repo['description'] else ''
    lang = repo['language'][0]['innerText'] if repo['language'] else 'N/A'
    stars = repo['stars'][0]['innerText'].strip() if repo['stars'] else '0'
    print(f'{name} ({lang}) - {stars} stars - {desc}')
```

    **3. Check the output**

```text
vercel / next.js (JavaScript) - 128,456 stars - The React Framework
denoland / deno (Rust) - 98,234 stars - A modern runtime for JavaScript and TypeScript
```

    
    
**Java:**

    

    **1. Send the request**

```java
import java.net.URI;
import java.net.http.*;

String token = "YOUR_API_TOKEN_HERE";
String endpoint = "https://production-sfo.browserless.io/chromium/bql?token=" + token;

String query = "mutation ScrapeGitHubTrending {"
    + " goto(url: \\\"https://github.com/trending\\\", waitUntil: networkIdle) { status }"
    + " waitForSelector(selector: \\\"article.Box-row\\\", timeout: 15000) { time }"
    + " repos: mapSelector(selector: \\\"article.Box-row\\\") {"
    + "   name: mapSelector(selector: \\\"h2 a\\\") { innerText }"
    + "   description: mapSelector(selector: \\\"p\\\") { innerText }"
    + "   language: mapSelector(selector: \\\"[itemprop=programmingLanguage]\\\") { innerText }"
    + "   stars: mapSelector(selector: \\\"a[href*=stargazers]\\\") { innerText }"
    + " }"
    + " }";

String payload = "{\"query\": \"" + query + "\", \"variables\": {}}";

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(endpoint))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(payload))
    .build();

HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 1245 },
    "repos": [
      {
        "name": [{ "innerText": "vercel / next.js" }],
        "language": [{ "innerText": "JavaScript" }],
        "stars": [{ "innerText": "128,456" }]
      }
    ]
  }
}
```

    
    
**C#:**

    

    **1. Send the request**

```csharp
using System.Net.Http;
using System.Text;
using System.Text.Json;

string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"https://production-sfo.browserless.io/chromium/bql?token={token}";

var payload = new
{
    query = @"mutation ScrapeGitHubTrending {
      goto(url: ""https://github.com/trending"", waitUntil: networkIdle) { status }
      waitForSelector(selector: ""article.Box-row"", timeout: 15000) { time }
      repos: mapSelector(selector: ""article.Box-row"") {
        name: mapSelector(selector: ""h2 a"") { innerText }
        description: mapSelector(selector: ""p"") { innerText }
        language: mapSelector(selector: ""[itemprop=programmingLanguage]"") { innerText }
        stars: mapSelector(selector: ""a[href*=stargazers]"") { innerText }
        forks: mapSelector(selector: ""a[href*=forks]"") { innerText }
      }
    }",
    variables = new { },
};

using (HttpClient httpClient = new HttpClient())
{
    var content = new StringContent(
        JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
    var response = await httpClient.PostAsync(endpoint, content);
    string body = await response.Content.ReadAsStringAsync();
    Console.WriteLine(body);
}
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 1245 },
    "repos": [
      {
        "name": [{ "innerText": "vercel / next.js" }],
        "language": [{ "innerText": "JavaScript" }],
        "stars": [{ "innerText": "128,456" }]
      }
    ]
  }
}
```

    
  

  
  
**Frameworks:**

  Connect a headless browser and extract trending repository data from the rendered page. GitHub doesn't need stealth mode or proxies.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Connect and scrape**

```js
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint:
    'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE',
});

try {
  const page = await browser.newPage();
  await page.goto('https://github.com/trending', {
    waitUntil: 'networkidle2',
  });

  await page.waitForSelector('article.Box-row');

  const repos = await page.evaluate(() =>
    Array.from(document.querySelectorAll('article.Box-row')).map((row) => ({
      name: row.querySelector('h2 a')?.innerText?.replace(/\s+/g, ' ').trim() ?? '',
      description: row.querySelector('p')?.innerText?.trim() ?? '',
      language: row.querySelector('[itemprop="programmingLanguage"]')?.innerText?.trim() ?? '',
      stars: row.querySelector('a[href*="stargazers"]')?.innerText?.trim() ?? '',
    }))
  );

  console.log(JSON.stringify(repos, null, 2));
} finally {
  await browser.close();
}
```

    **3. Check the output**

    Run with `node scrape-github-trending.mjs`. Each object has `name`, `description`, `language`, and `stars` fields.

```json
[
  {
    "name": "vercel / next.js",
    "description": "The React Framework",
    "language": "JavaScript",
    "stars": "128,456"
  }
]
```

    
    
**Playwright:**

    

    **1. Install dependencies**

```bash
npm install playwright-core
```

    **2. Connect and scrape**

```js
import { chromium } from 'playwright-core';

const browser = await chromium.connectOverCDP(
  'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE'
);

try {
  const context = browser.contexts()[0];
  const page = await context.newPage();
  await page.goto('https://github.com/trending', {
    waitUntil: 'networkidle',
  });

  await page.waitForSelector('article.Box-row');

  const repos = await page.evaluate(() =>
    Array.from(document.querySelectorAll('article.Box-row')).map((row) => ({
      name: row.querySelector('h2 a')?.innerText?.replace(/\s+/g, ' ').trim() ?? '',
      description: row.querySelector('p')?.innerText?.trim() ?? '',
      language: row.querySelector('[itemprop="programmingLanguage"]')?.innerText?.trim() ?? '',
      stars: row.querySelector('a[href*="stargazers"]')?.innerText?.trim() ?? '',
    }))
  );

  console.log(JSON.stringify(repos, null, 2));
} finally {
  await browser.close();
}
```

    **3. Check the output**

    Run with `node scrape-github-trending.mjs`. Each object has `name`, `description`, `language`, and `stars` fields.

```json
[
  {
    "name": "vercel / next.js",
    "description": "The React Framework",
    "language": "JavaScript",
    "stars": "128,456"
  }
]
```

    
  

  
  
**BQL:**

  

  **1. Write the mutation**

  Navigate to GitHub's trending page and extract repo names, descriptions, languages, and star counts. GitHub doesn't need stealth mode, so this works on the standard `/chromium/bql` endpoint.

```graphql
mutation ScrapeGitHubTrending {
  goto(url: "https://github.com/trending", waitUntil: networkIdle) {
    status
  }
  waitForSelector(selector: "article.Box-row", timeout: 15000) {
    time
  }
  repos: mapSelector(selector: "article.Box-row") {
    name: mapSelector(selector: "h2 a") { innerText }
    description: mapSelector(selector: "p") { innerText }
    language: mapSelector(selector: "[itemprop=programmingLanguage]") { innerText }
    stars: mapSelector(selector: "a[href*=stargazers]") { innerText }
    forks: mapSelector(selector: "a[href*=forks]") { innerText }
    todayStars: mapSelector(selector: "span.d-inline-block.float-sm-right") { innerText }
  }
}
```

  **2. Run it**

  Paste into the [BQL IDE](https://account.browserless.io/bql) and click Run.

  **3. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 1245 },
    "repos": [
      {
        "name": [{ "innerText": "vercel / next.js" }],
        "description": [{ "innerText": "The React Framework" }],
        "language": [{ "innerText": "JavaScript" }],
        "stars": [{ "innerText": "128,456" }],
        "forks": [{ "innerText": "27,891" }],
        "todayStars": [{ "innerText": "245 stars today" }]
      },
      {
        "name": [{ "innerText": "denoland / deno" }],
        "description": [{ "innerText": "A modern runtime for JavaScript and TypeScript" }],
        "language": [{ "innerText": "Rust" }],
        "stars": [{ "innerText": "98,234" }],
        "forks": [{ "innerText": "5,412" }],
        "todayStars": [{ "innerText": "189 stars today" }]
      }
    ]
  }
}
```

  

## Next steps

  <a href="/examples/scrape-imdb" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape IMDb Movie Ratings</h3>
      extract movie data from another popular site
    
  </a>
  <a href="/examples/scrape" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape Structured Data</h3>
      use the `/scrape` endpoint for simpler extraction
    
  </a>
  <a href="/examples/google-search" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Automate Google Search</h3>
      pull search results with the `/stealth/bql` endpoint
    
  </a>

---

# Scrape Glassdoor job listings

URL: https://docs.browserless.io/examples/scrape-glassdoor

# Scrape Glassdoor job listings

Pull job titles, companies, locations, and salary estimates from Glassdoor search results.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

The examples below navigate to a pre-formed Glassdoor search URL and extract job listing data.

> **Selector stability**
> Glassdoor frequently updates its markup. If the selectors below stop returning results, inspect the page with browser DevTools and update the CSS selectors to match the current DOM structure.

  
**AI Agent:**

  Use the [Browserless MCP server](https://github.com/browserless/browserless-mcp) to scrape job listings from Glassdoor from any MCP-compatible AI agent (Claude Desktop, Cursor, Windsurf, ChatGPT, etc.).

  **1. Connect the MCP server**

  Send this prompt to your AI agent to install the Browserless MCP server:

```text
Go to https://github.com/browserless/browserless-mcp/blob/main/install.md
and follow the instructions to install the Browserless MCP server
for my client.
```

  **2. Scrape Glassdoor**

  Use `browserless_smartscraper`. Glassdoor has aggressive bot detection and smartscraper handles it automatically.

```text
Use the browserless_smartscraper tool to scrape job listings
from https://www.glassdoor.com/Job/new-york-jobs-SRCH_IL.0,8_IC1132348.htm
and return the results as markdown
```

  
  
**REST API:**

  Send the BQL mutation over HTTP to the stealth endpoint. No browser library or BQL IDE required.

  
    
**cURL:**

    **1. Send the request**

```bash
curl -X POST \
  "https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "mutation ScrapeGlassdoor { goto(url: \"https://www.glassdoor.com/Job/new-york-software-engineer-jobs-SRCH_IL.0,8_IC1132348_KO9,26.htm\", waitUntil: networkIdle) { status } jobs: mapSelector(selector: \"[data-test='\''jobListing'\'']\") { title: mapSelector(selector: \"a[data-test='\''job-title'\'']\") { innerText } company: mapSelector(selector: \"[data-test='\''employer-name'\'']\") { innerText } location: mapSelector(selector: \"[data-test='\''emp-location'\'']\") { innerText } salary: mapSelector(selector: \"[data-test='\''detailSalary'\'']\") { innerText } } }",
    "variables": {},
    "operationName": "ScrapeGlassdoor"
  }'
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "jobs": [
      {
        "title": [{ "innerText": "Software Engineer" }],
        "company": [{ "innerText": "TechCorp" }],
        "location": [{ "innerText": "New York, NY" }],
        "salary": [{ "innerText": "$120K – $180K (Glassdoor est.)" }]
      },
      {
        "title": [{ "innerText": "Senior Software Engineer" }],
        "company": [{ "innerText": "BuildCo" }],
        "location": [{ "innerText": "New York, NY (Remote)" }],
        "salary": [{ "innerText": "$150K – $220K (Glassdoor est.)" }]
      }
    ]
  }
}
```

    
    
**JavaScript:**

    **1. Send the request**

```js
const query = `mutation ScrapeGlassdoor {
  goto(
    url: "https://www.glassdoor.com/Job/new-york-software-engineer-jobs-SRCH_IL.0,8_IC1132348_KO9,26.htm"
    waitUntil: networkIdle
  ) {
    status
  }
  jobs: mapSelector(selector: "[data-test='jobListing']") {
    title: mapSelector(selector: "a[data-test='job-title']") {
      innerText
    }
    company: mapSelector(selector: "[data-test='employer-name']") {
      innerText
    }
    location: mapSelector(selector: "[data-test='emp-location']") {
      innerText
    }
    salary: mapSelector(selector: "[data-test='detailSalary']") {
      innerText
    }
  }
}`;

const response = await fetch(
  'https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ query, variables: {}, operationName: 'ScrapeGlassdoor' }),
  }
);

const { data } = await response.json();
const jobs = data.jobs.map((job) => ({
  title: job.title?.[0]?.innerText ?? '',
  company: job.company?.[0]?.innerText ?? '',
  location: job.location?.[0]?.innerText ?? '',
  salary: job.salary?.[0]?.innerText ?? '',
}));
console.log(JSON.stringify(jobs, null, 2));
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "jobs": [
      {
        "title": [{ "innerText": "Software Engineer" }],
        "company": [{ "innerText": "TechCorp" }],
        "location": [{ "innerText": "New York, NY" }],
        "salary": [{ "innerText": "$120K – $180K (Glassdoor est.)" }]
      },
      {
        "title": [{ "innerText": "Senior Software Engineer" }],
        "company": [{ "innerText": "BuildCo" }],
        "location": [{ "innerText": "New York, NY (Remote)" }],
        "salary": [{ "innerText": "$150K – $220K (Glassdoor est.)" }]
      }
    ]
  }
}
```

    
    
**Python:**

    **1. Install dependencies**

```bash
pip install requests
```

    **2. Send the request**

```python
import requests

query = """
mutation ScrapeGlassdoor {
  goto(
    url: "https://www.glassdoor.com/Job/new-york-software-engineer-jobs-SRCH_IL.0,8_IC1132348_KO9,26.htm"
    waitUntil: networkIdle
  ) {
    status
  }
  jobs: mapSelector(selector: "[data-test='jobListing']") {
    title: mapSelector(selector: "a[data-test='job-title']") {
      innerText
    }
    company: mapSelector(selector: "[data-test='employer-name']") {
      innerText
    }
    location: mapSelector(selector: "[data-test='emp-location']") {
      innerText
    }
    salary: mapSelector(selector: "[data-test='detailSalary']") {
      innerText
    }
  }
}
"""

response = requests.post(
    'https://production-sfo.browserless.io/stealth/bql',
    params={
        'token': 'YOUR_API_TOKEN_HERE',
        'proxy': 'residential',
        'proxyCountry': 'us',
    },
    json={'query': query, 'variables': {}, 'operationName': 'ScrapeGlassdoor'},
)

data = response.json()['data']
for job in data['jobs']:
    title = job['title'][0]['innerText'] if job['title'] else ''
    company = job['company'][0]['innerText'] if job['company'] else ''
    location = job['location'][0]['innerText'] if job['location'] else ''
    salary = job['salary'][0]['innerText'] if job['salary'] else ''
    print(f'{title} at {company} — {location} — {salary}')
```

    **3. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "jobs": [
      {
        "title": [{ "innerText": "Software Engineer" }],
        "company": [{ "innerText": "TechCorp" }],
        "location": [{ "innerText": "New York, NY" }],
        "salary": [{ "innerText": "$120K – $180K (Glassdoor est.)" }]
      },
      {
        "title": [{ "innerText": "Senior Software Engineer" }],
        "company": [{ "innerText": "BuildCo" }],
        "location": [{ "innerText": "New York, NY (Remote)" }],
        "salary": [{ "innerText": "$150K – $220K (Glassdoor est.)" }]
      }
    ]
  }
}
```

    
    
**Java:**

    **1. Send the request**

```java
import java.net.URI;
import java.net.http.*;

String token = "YOUR_API_TOKEN_HERE";
String endpoint = "https://production-sfo.browserless.io/stealth/bql?token="
    + token + "&proxy=residential&proxyCountry=us";

String query = """
    mutation ScrapeGlassdoor {
      goto(url: "https://www.glassdoor.com/Job/new-york-software-engineer-jobs-SRCH_IL.0,8_IC1132348_KO9,26.htm", waitUntil: networkIdle) { status }
      jobs: mapSelector(selector: "[data-test='jobListing']") {
        title: mapSelector(selector: "a[data-test='job-title']") { innerText }
        company: mapSelector(selector: "[data-test='employer-name']") { innerText }
        location: mapSelector(selector: "[data-test='emp-location']") { innerText }
        salary: mapSelector(selector: "[data-test='detailSalary']") { innerText }
      }
    }
    """;

String payload = "{\"query\": " + com.google.gson.JsonParser.parseString(query) + ", \"variables\": {}, \"operationName\": \"ScrapeGlassdoor\"}";

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(endpoint))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(payload))
    .build();

HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "jobs": [
      {
        "title": [{ "innerText": "Software Engineer" }],
        "company": [{ "innerText": "TechCorp" }],
        "location": [{ "innerText": "New York, NY" }],
        "salary": [{ "innerText": "$120K – $180K (Glassdoor est.)" }]
      },
      {
        "title": [{ "innerText": "Senior Software Engineer" }],
        "company": [{ "innerText": "BuildCo" }],
        "location": [{ "innerText": "New York, NY (Remote)" }],
        "salary": [{ "innerText": "$150K – $220K (Glassdoor est.)" }]
      }
    ]
  }
}
```

    
    
**C#:**

    **1. Send the request**

```csharp
using System.Net.Http;
using System.Text;
using System.Text.Json;

string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"https://production-sfo.browserless.io/stealth/bql?token={token}&proxy=residential&proxyCountry=us";

var payload = new
{
    query = @"mutation ScrapeGlassdoor {
      goto(url: ""https://www.glassdoor.com/Job/new-york-software-engineer-jobs-SRCH_IL.0,8_IC1132348_KO9,26.htm"", waitUntil: networkIdle) { status }
      jobs: mapSelector(selector: ""[data-test='jobListing']"") {
        title: mapSelector(selector: ""a[data-test='job-title']"") { innerText }
        company: mapSelector(selector: ""[data-test='employer-name']"") { innerText }
        location: mapSelector(selector: ""[data-test='emp-location']"") { innerText }
        salary: mapSelector(selector: ""[data-test='detailSalary']"") { innerText }
      }
    }",
    variables = new { },
    operationName = "ScrapeGlassdoor",
};

using (HttpClient httpClient = new HttpClient())
{
    var content = new StringContent(
        JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
    var response = await httpClient.PostAsync(endpoint, content);
    string body = await response.Content.ReadAsStringAsync();
    Console.WriteLine(body);
}
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "jobs": [
      {
        "title": [{ "innerText": "Software Engineer" }],
        "company": [{ "innerText": "TechCorp" }],
        "location": [{ "innerText": "New York, NY" }],
        "salary": [{ "innerText": "$120K – $180K (Glassdoor est.)" }]
      },
      {
        "title": [{ "innerText": "Senior Software Engineer" }],
        "company": [{ "innerText": "BuildCo" }],
        "location": [{ "innerText": "New York, NY (Remote)" }],
        "salary": [{ "innerText": "$150K – $220K (Glassdoor est.)" }]
      }
    ]
  }
}
```

    
  

  
  
**Frameworks:**

  Connect through stealth mode and a residential proxy so Glassdoor sees traffic that looks like it's coming from a real US-based browser, then extract job listing data from the fully rendered page.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Connect and scrape**

```js
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint:
    'wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us',
});

try {
  const page = await browser.newPage();
  await page.goto(
    'https://www.glassdoor.com/Job/new-york-software-engineer-jobs-SRCH_IL.0,8_IC1132348_KO9,26.htm',
    { waitUntil: 'networkidle2' }
  );

  const jobs = await page.evaluate(() =>
    Array.from(document.querySelectorAll('[data-test="jobListing"]')).map((card) => ({
      title: card.querySelector('a[data-test="job-title"]')?.innerText?.trim() ?? '',
      company: card.querySelector('[data-test="employer-name"]')?.innerText?.trim() ?? '',
      location: card.querySelector('[data-test="emp-location"]')?.innerText?.trim() ?? '',
      salary: card.querySelector('[data-test="detailSalary"]')?.innerText?.trim() ?? '',
    }))
  );

  console.log(JSON.stringify(jobs, null, 2));
} finally {
  await browser.close();
}
```

    **3. Check the output**

    Run with `node scrape-glassdoor.mjs`. You get one object per job card, each with `title`, `company`, `location`, and `salary` already grouped. No index-stitching required because we're selecting within each card element.

    
    
**Playwright:**

    

    **1. Install dependencies**

```bash
npm install playwright-core
```

    **2. Connect and scrape**

```js
import { chromium } from 'playwright-core';

const browser = await chromium.connectOverCDP(
  'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&stealth&proxy=residential&proxyCountry=us'
);

try {
  const context = browser.contexts()[0];
  const page = await context.newPage();
  await page.goto(
    'https://www.glassdoor.com/Job/new-york-software-engineer-jobs-SRCH_IL.0,8_IC1132348_KO9,26.htm',
    { waitUntil: 'networkidle' }
  );

  const jobs = await page.evaluate(() =>
    Array.from(document.querySelectorAll('[data-test="jobListing"]')).map((card) => ({
      title: card.querySelector('a[data-test="job-title"]')?.innerText?.trim() ?? '',
      company: card.querySelector('[data-test="employer-name"]')?.innerText?.trim() ?? '',
      location: card.querySelector('[data-test="emp-location"]')?.innerText?.trim() ?? '',
      salary: card.querySelector('[data-test="detailSalary"]')?.innerText?.trim() ?? '',
    }))
  );

  console.log(JSON.stringify(jobs, null, 2));
} finally {
  await browser.close();
}
```

    **3. Check the output**

    Run with `node scrape-glassdoor.mjs`. You get one object per job card, each with `title`, `company`, `location`, and `salary` already grouped. No index-stitching required because we're selecting within each card element.

    
  

  
  
**BQL:**

  

  **1. Write the mutation**

  Navigate directly to the Glassdoor search results URL and use `mapSelector` to pull structured data from each job card. We send this to the `/stealth/bql` endpoint (not the default `/bql`) because Glassdoor's bot detection will block a plain browser session before the page even loads.

```graphql
mutation ScrapeGlassdoor {
  goto(
    url: "https://www.glassdoor.com/Job/new-york-software-engineer-jobs-SRCH_IL.0,8_IC1132348_KO9,26.htm"
    waitUntil: networkIdle
  ) {
    status
  }

  jobs: mapSelector(selector: "[data-test='jobListing']") {
    title: mapSelector(selector: "a[data-test='job-title']") {
      innerText
    }
    company: mapSelector(selector: "[data-test='employer-name']") {
      innerText
    }
    location: mapSelector(selector: "[data-test='emp-location']") {
      innerText
    }
    salary: mapSelector(selector: "[data-test='detailSalary']") {
      innerText
    }
  }
}
```

  **2. Run it**

  Paste into the [BQL IDE](https://account.browserless.io/bql) and click Run.

  **3. Check the output**

  Each entry under `jobs` is already structured by card. `mapSelector` nests its results relative to each matched parent element, so you don't need to stitch fields together by index.

```json
{
  "data": {
    "goto": { "status": 200 },
    "jobs": [
      {
        "title": [{ "innerText": "Software Engineer" }],
        "company": [{ "innerText": "TechCorp" }],
        "location": [{ "innerText": "New York, NY" }],
        "salary": [{ "innerText": "$120K – $180K (Glassdoor est.)" }]
      },
      {
        "title": [{ "innerText": "Senior Software Engineer" }],
        "company": [{ "innerText": "BuildCo" }],
        "location": [{ "innerText": "New York, NY (Remote)" }],
        "salary": [{ "innerText": "$150K – $220K (Glassdoor est.)" }]
      }
    ]
  }
}
```

  

## Next steps

  <a href="/examples/scrape-etsy" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape Etsy Product Listings</h3>
      extract product data from another bot-protected marketplace
    
  </a>
  <a href="/examples/cloudflare" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Solving Cloudflare Challenges</h3>
      bypass Cloudflare protection on other targets
    
  </a>
  <a href="/examples/proxy" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Change Your Browser's IP Address Using Proxies</h3>
      control which IP address target sites see
    
  </a>

---

# Scrape Google Shopping results

URL: https://docs.browserless.io/examples/scrape-google-shopping

# Scrape Google Shopping results

Query Google Shopping and pull product names, prices, and links out of the results page.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

  
**AI Agent:**

  Use the [Browserless MCP server](https://github.com/browserless/browserless-mcp) to scrape product results from Google Shopping from any MCP-compatible AI agent (Claude Desktop, Cursor, Windsurf, ChatGPT, etc.).

  **1. Connect the MCP server**

  Send this prompt to your AI agent to install the Browserless MCP server:

```text
Go to https://github.com/browserless/browserless-mcp/blob/main/install.md
and follow the instructions to install the Browserless MCP server
for my client.
```

  **2. Scrape Google Shopping**

  Use `browserless_smartscraper`. Google has bot detection and smartscraper handles it automatically.

```text
Use the browserless_smartscraper tool to scrape product listings
from https://www.google.com/search?q=headphones&tbm=shop
and return the results as markdown
```

  
  
**REST API:**

  Send the product grid selectors to the `/scrape` endpoint and get back matched text and position data. No browser code required.

  :::note
  Google Shopping runs bot detection that can serve a CAPTCHA or return an empty grid to automated requests. If you get incomplete results here, switch to the BQL tab, which routes through stealth mode and residential proxies to avoid that detection.
  :::

  
    
**cURL:**

    

    **1. Send the request**

    `div.sh-dgr__grid-result h4` holds the product title; `.a8Pemb` holds the price Google renders inside each card:

```bash
curl -X POST \
  "https://production-sfo.browserless.io/scrape?token=YOUR_API_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://www.google.com/search?q=board+games&tbm=shop",
    "elements": [
      { "selector": "div.sh-dgr__grid-result h4" },
      { "selector": "div.sh-dgr__grid-result .a8Pemb" }
    ]
  }'
```

    **2. Check the output**

    The response groups results by selector. Each entry in `results` gives you the element's text, its rendered dimensions, and its position on the page. This is useful if you need to verify the right element matched:

```json
{
  "data": [
    {
      "selector": "div.sh-dgr__grid-result h4",
      "results": [
        {
          "text": "Catan Board Game",
          "html": "Catan Board Game",
          "height": 20,
          "left": 16,
          "top": 220,
          "width": 180
        }
      ]
    },
    {
      "selector": "div.sh-dgr__grid-result .a8Pemb",
      "results": [
        {
          "text": "$44.99",
          "html": "$44.99",
          "height": 16,
          "left": 16,
          "top": 280,
          "width": 60
        }
      ]
    }
  ]
}
```

    
    
**JavaScript:**

    

    **1. Send the request**

```js
const response = await fetch(
  'https://production-sfo.browserless.io/scrape?token=YOUR_API_TOKEN_HERE',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      url: 'https://www.google.com/search?q=board+games&tbm=shop',
      elements: [
        { selector: 'div.sh-dgr__grid-result h4' },
        { selector: 'div.sh-dgr__grid-result .a8Pemb' },
      ],
    }),
  }
);

const { data } = await response.json();
const titles = data[0].results.map((r) => r.text);
const prices = data[1].results.map((r) => r.text);
console.log(titles.map((title, i) => ({ title, price: prices[i] })));
```

    **2. Check the output**

    Run with `node scrape-google-shopping.mjs`. You'll get an array of objects where each item pairs a product name with its price. The zip-by-index pairing is intentional: the `/scrape` endpoint returns results grouped by selector, so `data[0].results[i]` and `data[1].results[i]` always correspond to the same product card.

    
    
**Python:**

    

    **1. Install dependencies**

```bash
pip install requests
```

    **2. Send the request**

```python
import requests

response = requests.post(
    'https://production-sfo.browserless.io/scrape?token=YOUR_API_TOKEN_HERE',
    json={
        'url': 'https://www.google.com/search?q=board+games&tbm=shop',
        'elements': [
            {'selector': 'div.sh-dgr__grid-result h4'},
            {'selector': 'div.sh-dgr__grid-result .a8Pemb'},
        ],
    },
)

data = response.json()['data']
titles = [item['text'] for item in data[0]['results']]
prices = [item['text'] for item in data[1]['results']]
for title, price in zip(titles, prices):
    print(f'{title}: {price}')
```

    **3. Check the output**

    Run with `python scrape_google_shopping.py`. Each printed line is a product name followed by its price. The `zip(titles, prices)` pairing is intentional: the `/scrape` endpoint returns results grouped by selector, so the i-th title and i-th price always come from the same product card.

    
    
**Java:**

    

    **1. Send the request**

```java
import java.net.URI;
import java.net.http.*;

String token = "YOUR_API_TOKEN_HERE";
String endpoint = "https://production-sfo.browserless.io/scrape?token=" + token;

String payload = """
    {
      "url": "https://www.google.com/search?q=board+games&tbm=shop",
      "elements": [
        { "selector": "div.sh-dgr__grid-result h4" },
        { "selector": "div.sh-dgr__grid-result .a8Pemb" }
      ]
    }
    """;

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(endpoint))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(payload))
    .build();

HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
```

    **2. Check the output**

    Compile and run the class. The response body is JSON with `data[0].results` holding product names and `data[1].results` holding prices.

    
    
**C#:**

    

    **1. Send the request**

```csharp
using System.Net.Http;
using System.Text;
using System.Text.Json;

string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"https://production-sfo.browserless.io/scrape?token={token}";

var payload = new
{
    url = "https://www.google.com/search?q=board+games&tbm=shop",
    elements = new[]
    {
        new { selector = "div.sh-dgr__grid-result h4" },
        new { selector = "div.sh-dgr__grid-result .a8Pemb" },
    },
};

using (HttpClient httpClient = new HttpClient())
{
    var jsonPayload = JsonSerializer.Serialize(payload);
    var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
    var response = await httpClient.PostAsync(endpoint, content);
    string responseBody = await response.Content.ReadAsStringAsync();
    Console.WriteLine(responseBody);
}
```

    **2. Check the output**

    Run the program. The response JSON has two `data` entries: the first holds matched product names, the second holds prices. Each is an array of `results` objects.

    
  

  
  
**Frameworks:**

  Connect a headless browser via CDP, navigate to the Google Shopping results page, and pull product data directly out of the rendered DOM. We use stealth mode and a US residential proxy here because Google Shopping checks browser fingerprints and IP reputation. A datacenter IP will usually hit a CAPTCHA.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Connect and scrape**

```js
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint:
    'wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us',
});

try {
  const page = await browser.newPage();
  await page.goto(
    'https://www.google.com/search?q=board+games&tbm=shop',
    { waitUntil: 'networkidle2' }
  );

  const products = await page.evaluate(() =>
    Array.from(document.querySelectorAll('div.sh-dgr__grid-result')).map((card) => ({
      title: card.querySelector('h4')?.innerText?.trim() ?? '',
      price: card.querySelector('.a8Pemb')?.innerText?.trim() ?? '',
      link: card.querySelector('a')?.href ?? '',
    }))
  );

  console.log(products);
} finally {
  await browser.close();
}
```

    **3. Check the output**

    Run with `node scrape-google-shopping.mjs`. Each object in the array has `title`, `price`, and `link`. The link is a full Google Shopping product URL you can follow to get merchant details.

    
    
**Playwright:**

    

    **1. Install dependencies**

```bash
npm install playwright-core
```

    **2. Connect and scrape**

```js
import { chromium } from 'playwright-core';

const browser = await chromium.connectOverCDP(
  'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&stealth&proxy=residential&proxyCountry=us'
);

try {
  const context = browser.contexts()[0];
  const page = await context.newPage();
  await page.goto(
    'https://www.google.com/search?q=board+games&tbm=shop',
    { waitUntil: 'networkidle' }
  );

  const products = await page.evaluate(() =>
    Array.from(document.querySelectorAll('div.sh-dgr__grid-result')).map((card) => ({
      title: card.querySelector('h4')?.innerText?.trim() ?? '',
      price: card.querySelector('.a8Pemb')?.innerText?.trim() ?? '',
      link: card.querySelector('a')?.href ?? '',
    }))
  );

  console.log(products);
} finally {
  await browser.close();
}
```

    **3. Check the output**

    Run with `node scrape-google-shopping.mjs`. Each object in the array has `title`, `price`, and `link`. The link is a full Google Shopping product URL you can follow to get merchant details.

    
  

  
  
**BQL:**

  

  **1. Write the mutation**

  Navigate to the Google Shopping URL, then use nested `mapSelector` calls to walk each product card and pull out title, price, and link in one pass. We use `wait: true` on the title and price selectors because Google Shopping renders those fields via JavaScript after the initial HTML loads. Without it you'd get empty strings:

```graphql
mutation ScrapeGoogleShopping {
  goto(url: "https://www.google.com/search?q=board+games&tbm=shop", waitUntil: domContentLoaded) {
    status
  }

  results: mapSelector(selector: "div.sh-dgr__grid-result") {
    title: mapSelector(selector: "h4", wait: true) {
      innerText
    }
    price: mapSelector(selector: ".a8Pemb", wait: true) {
      innerText
    }
    link: mapSelector(selector: "a") {
      href: attribute(name: "href") {
        value
      }
    }
  }
}
```

  **2. Run it**

  Paste into the [BQL IDE](https://account.browserless.io/bql) and click Run.

  **3. Check the output**

  Each entry in `results` corresponds to one product card. Title and price are arrays because `mapSelector` can match multiple child elements. In practice each card has one of each, so you'll read index `[0]`:

```json
{
  "data": {
    "goto": { "status": 200 },
    "results": [
      {
        "title": [{ "innerText": "Catan Board Game" }],
        "price": [{ "innerText": "$44.99" }],
        "link": [{ "href": { "value": "https://www.google.com/shopping/product/1/specs?q=board+games&prds=..." } }]
      },
      {
        "title": [{ "innerText": "Ticket to Ride Board Game" }],
        "price": [{ "innerText": "$54.99" }],
        "link": [{ "href": { "value": "https://www.google.com/shopping/product/2/specs?q=board+games&prds=..." } }]
      }
    ]
  }
}
```

  

## Next steps

  <a href="/examples/google-search" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Automate Google Search</h3>
      scrape Google web search results with BQL
    
  </a>
  <a href="/examples/scrape" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape Structured Data</h3>
      extract data from other sites using the `/scrape` endpoint
    
  </a>
  <a href="/examples/cloudflare" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Solving Cloudflare Challenges</h3>
      bypass bot detection on heavily protected targets
    
  </a>

---

# Scrape IMDb movie ratings

URL: https://docs.browserless.io/examples/scrape-imdb

# Scrape IMDb movie ratings

Extract movie titles, release years, and ratings from IMDb's Top 250 chart.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

IMDb's Top 250 page renders with JavaScript and is owned by Amazon, which applies bot-detection measures. The examples below extract the top-rated movies and route through stealth mode with a residential proxy.

> **Selector stability**
> IMDb updates its markup periodically. If selectors stop returning results, inspect the live page with browser DevTools to find the current element names.

  
**AI Agent:**

  Use the [Browserless MCP server](https://github.com/browserless/browserless-mcp) to scrape movie ratings from IMDb from any MCP-compatible AI agent (Claude Desktop, Cursor, Windsurf, ChatGPT, etc.).

  **1. Connect the MCP server**

  Send this prompt to your AI agent to install the Browserless MCP server:

```text
Go to https://github.com/browserless/browserless-mcp/blob/main/install.md
and follow the instructions to install the Browserless MCP server
for my client.
```

  **2. Scrape IMDb**

  Use `browserless_smartscraper`. It handles IMDb's dynamic content and bot protection automatically.

```text
Use the browserless_smartscraper tool to scrape the top 250 movies
from https://www.imdb.com/chart/top/
and return the results as markdown
```

  
  
**REST API:**

  Send the BQL mutation over HTTP to the stealth endpoint. No browser library or BQL IDE required.

  
    
**cURL:**

    

    **1. Send the request**

```bash
curl -X POST \
  "https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "mutation ScrapeIMDb { goto(url: \"https://www.imdb.com/chart/top/\", waitUntil: networkIdle) { status } waitForSelector(selector: \".ipc-metadata-list-summary-item\", timeout: 15000) { time } movies: mapSelector(selector: \".ipc-metadata-list-summary-item\") { title: mapSelector(selector: \".ipc-title__text\") { innerText } metadata: mapSelector(selector: \".cli-title-metadata span\") { innerText } rating: mapSelector(selector: \".ipc-rating-star--imdb\") { ratingLabel: attribute(name: \"aria-label\") { value } } } }",
    "variables": {}
  }'
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 2340 },
    "movies": [
      {
        "title": [{ "innerText": "1. The Shawshank Redemption" }],
        "metadata": [{ "innerText": "1994" }, { "innerText": "2h 22m" }],
        "rating": [{ "ratingLabel": { "value": "IMDb rating: 9.3" } }]
      },
      {
        "title": [{ "innerText": "2. The Godfather" }],
        "metadata": [{ "innerText": "1972" }, { "innerText": "2h 55m" }],
        "rating": [{ "ratingLabel": { "value": "IMDb rating: 9.2" } }]
      }
    ]
  }
}
```

    
    
**JavaScript:**

    

    **1. Send the request**

```js
const query = `mutation ScrapeIMDb {
  goto(url: "https://www.imdb.com/chart/top/", waitUntil: networkIdle) {
    status
  }
  waitForSelector(selector: ".ipc-metadata-list-summary-item", timeout: 15000) {
    time
  }
  movies: mapSelector(selector: ".ipc-metadata-list-summary-item") {
    title: mapSelector(selector: ".ipc-title__text") { innerText }
    metadata: mapSelector(selector: ".cli-title-metadata span") { innerText }
    rating: mapSelector(selector: ".ipc-rating-star--imdb") {
      ratingLabel: attribute(name: "aria-label") { value }
    }
  }
}`;

const response = await fetch(
  'https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ query, variables: {} }),
  }
);

const { data } = await response.json();
console.log(JSON.stringify(data.movies, null, 2));
```

    **2. Check the output**

```json
[
  {
    "title": [{ "innerText": "1. The Shawshank Redemption" }],
    "metadata": [{ "innerText": "1994" }, { "innerText": "2h 22m" }],
    "rating": [{ "ratingLabel": { "value": "IMDb rating: 9.3" } }]
  },
  {
    "title": [{ "innerText": "2. The Godfather" }],
    "metadata": [{ "innerText": "1972" }, { "innerText": "2h 55m" }],
    "rating": [{ "ratingLabel": { "value": "IMDb rating: 9.2" } }]
  }
]
```

    
    
**Python:**

    

    **1. Install dependencies**

```bash
pip install requests
```

    **2. Send the request**

```python
import requests

query = """
mutation ScrapeIMDb {
  goto(url: "https://www.imdb.com/chart/top/", waitUntil: networkIdle) {
    status
  }
  waitForSelector(selector: ".ipc-metadata-list-summary-item", timeout: 15000) {
    time
  }
  movies: mapSelector(selector: ".ipc-metadata-list-summary-item") {
    title: mapSelector(selector: ".ipc-title__text") { innerText }
    metadata: mapSelector(selector: ".cli-title-metadata span") { innerText }
    rating: mapSelector(selector: ".ipc-rating-star--imdb") {
      ratingLabel: attribute(name: "aria-label") { value }
    }
  }
}
"""

response = requests.post(
    'https://production-sfo.browserless.io/stealth/bql',
    params={
        'token': 'YOUR_API_TOKEN_HERE',
        'proxy': 'residential',
        'proxyCountry': 'us',
    },
    json={'query': query, 'variables': {}},
)

data = response.json()['data']
for movie in data['movies']:
    title = movie['title'][0]['innerText']
    year = movie['metadata'][0]['innerText'] if movie['metadata'] else 'N/A'
    rating = movie['rating'][0]['ratingLabel']['value'] if movie['rating'] else 'N/A'
    print(f'{title} ({year}) - {rating}')
```

    **3. Check the output**

```text
1. The Shawshank Redemption (1994) - IMDb rating: 9.3
2. The Godfather (1972) - IMDb rating: 9.2
```

    
    
**Java:**

    

    **1. Send the request**

```java
import java.net.URI;
import java.net.http.*;

String token = "YOUR_API_TOKEN_HERE";
String endpoint = "https://production-sfo.browserless.io/stealth/bql?token=" + token
    + "&proxy=residential&proxyCountry=us";

String query = "mutation ScrapeIMDb {"
    + " goto(url: \\\"https://www.imdb.com/chart/top/\\\", waitUntil: networkIdle) { status }"
    + " waitForSelector(selector: \\\".ipc-metadata-list-summary-item\\\", timeout: 15000) { time }"
    + " movies: mapSelector(selector: \\\".ipc-metadata-list-summary-item\\\") {"
    + "   title: mapSelector(selector: \\\".ipc-title__text\\\") { innerText }"
    + "   metadata: mapSelector(selector: \\\".cli-title-metadata span\\\") { innerText }"
    + "   rating: mapSelector(selector: \\\".ipc-rating-star--imdb\\\") { ratingLabel: attribute(name: \\\"aria-label\\\") { value } }"
    + " }"
    + " }";

String payload = "{\"query\": \"" + query + "\", \"variables\": {}}";

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(endpoint))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(payload))
    .build();

HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 2340 },
    "movies": [
      {
        "title": [{ "innerText": "1. The Shawshank Redemption" }],
        "metadata": [{ "innerText": "1994" }],
        "rating": [{ "ratingLabel": { "value": "IMDb rating: 9.3" } }]
      }
    ]
  }
}
```

    
    
**C#:**

    

    **1. Send the request**

```csharp
using System.Net.Http;
using System.Text;
using System.Text.Json;

string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"https://production-sfo.browserless.io/stealth/bql?token={token}&proxy=residential&proxyCountry=us";

var payload = new
{
    query = @"mutation ScrapeIMDb {
      goto(url: ""https://www.imdb.com/chart/top/"", waitUntil: networkIdle) { status }
      waitForSelector(selector: "".ipc-metadata-list-summary-item"", timeout: 15000) { time }
      movies: mapSelector(selector: "".ipc-metadata-list-summary-item"") {
        title: mapSelector(selector: "".ipc-title__text"") { innerText }
        metadata: mapSelector(selector: "".cli-title-metadata span"") { innerText }
        rating: mapSelector(selector: "".ipc-rating-star--imdb"") {
          ratingLabel: attribute(name: ""aria-label"") { value }
        }
      }
    }",
    variables = new { },
};

using (HttpClient httpClient = new HttpClient())
{
    var content = new StringContent(
        JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
    var response = await httpClient.PostAsync(endpoint, content);
    string body = await response.Content.ReadAsStringAsync();
    Console.WriteLine(body);
}
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 2340 },
    "movies": [
      {
        "title": [{ "innerText": "1. The Shawshank Redemption" }],
        "metadata": [{ "innerText": "1994" }],
        "rating": [{ "ratingLabel": { "value": "IMDb rating: 9.3" } }]
      }
    ]
  }
}
```

    
  

  
  
**Frameworks:**

  Connect through stealth mode and a residential proxy so IMDb sees traffic from a real browser, then extract movie data from the rendered chart.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Connect and scrape**

```js
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint:
    'wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us',
});

try {
  const page = await browser.newPage();
  await page.goto('https://www.imdb.com/chart/top/', {
    waitUntil: 'networkidle2',
  });

  await page.waitForSelector('.ipc-metadata-list-summary-item');

  const movies = await page.evaluate(() =>
    Array.from(document.querySelectorAll('.ipc-metadata-list-summary-item')).map((item) => ({
      title: item.querySelector('.ipc-title__text')?.innerText?.trim() ?? '',
      year: item.querySelector('.cli-title-metadata span')?.innerText?.trim() ?? '',
      rating: item.querySelector('.ipc-rating-star--imdb')?.getAttribute('aria-label') ?? '',
    }))
  );

  console.log(JSON.stringify(movies.slice(0, 10), null, 2));
} finally {
  await browser.close();
}
```

    **3. Check the output**

    Run with `node scrape-imdb.mjs`. Each object has `title`, `year`, and `rating` fields.

```json
[
  {
    "title": "1. The Shawshank Redemption",
    "year": "1994",
    "rating": "IMDb rating: 9.3"
  },
  {
    "title": "2. The Godfather",
    "year": "1972",
    "rating": "IMDb rating: 9.2"
  }
]
```

    
    
**Playwright:**

    

    **1. Install dependencies**

```bash
npm install playwright-core
```

    **2. Connect and scrape**

```js
import { chromium } from 'playwright-core';

const browser = await chromium.connectOverCDP(
  'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&stealth&proxy=residential&proxyCountry=us'
);

try {
  const context = browser.contexts()[0];
  const page = await context.newPage();
  await page.goto('https://www.imdb.com/chart/top/', {
    waitUntil: 'networkidle',
  });

  await page.waitForSelector('.ipc-metadata-list-summary-item');

  const movies = await page.evaluate(() =>
    Array.from(document.querySelectorAll('.ipc-metadata-list-summary-item')).map((item) => ({
      title: item.querySelector('.ipc-title__text')?.innerText?.trim() ?? '',
      year: item.querySelector('.cli-title-metadata span')?.innerText?.trim() ?? '',
      rating: item.querySelector('.ipc-rating-star--imdb')?.getAttribute('aria-label') ?? '',
    }))
  );

  console.log(JSON.stringify(movies.slice(0, 10), null, 2));
} finally {
  await browser.close();
}
```

    **3. Check the output**

    Run with `node scrape-imdb.mjs`. Each object has `title`, `year`, and `rating` fields.

```json
[
  {
    "title": "1. The Shawshank Redemption",
    "year": "1994",
    "rating": "IMDb rating: 9.3"
  },
  {
    "title": "2. The Godfather",
    "year": "1972",
    "rating": "IMDb rating: 9.2"
  }
]
```

    
  

  
  
**BQL:**

  

  **1. Write the mutation**

  Navigate to IMDb's Top 250 chart, wait for the list to render, then extract movie titles, years, and ratings. We use `/stealth/bql` because IMDb (Amazon-owned) applies bot detection that blocks standard headless browsers.

```graphql
mutation ScrapeIMDb {
  goto(url: "https://www.imdb.com/chart/top/", waitUntil: networkIdle) {
    status
  }
  waitForSelector(selector: ".ipc-metadata-list-summary-item", timeout: 15000) {
    time
  }
  movies: mapSelector(selector: ".ipc-metadata-list-summary-item") {
    title: mapSelector(selector: ".ipc-title__text") { innerText }
    metadata: mapSelector(selector: ".cli-title-metadata span") { innerText }
    rating: mapSelector(selector: ".ipc-rating-star--imdb") {
      ratingLabel: attribute(name: "aria-label") { value }
    }
  }
}
```

  **2. Run it**

  Paste into the [BQL IDE](https://account.browserless.io/bql) and click Run.

  **3. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 2340 },
    "movies": [
      {
        "title": [{ "innerText": "1. The Shawshank Redemption" }],
        "metadata": [{ "innerText": "1994" }, { "innerText": "2h 22m" }],
        "rating": [{ "ratingLabel": { "value": "IMDb rating: 9.3" } }]
      },
      {
        "title": [{ "innerText": "2. The Godfather" }],
        "metadata": [{ "innerText": "1972" }, { "innerText": "2h 55m" }],
        "rating": [{ "ratingLabel": { "value": "IMDb rating: 9.2" } }]
      }
    ]
  }
}
```

  

## Next steps

  <a href="/examples/scrape-github-trending" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape GitHub Trending Repos</h3>
      scrape trending repositories from GitHub
    
  </a>
  <a href="/examples/scrape-amazon" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape Amazon Product Listings</h3>
      scrape another Amazon-owned property with stealth mode
    
  </a>
  <a href="/examples/scrape" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape Structured Data</h3>
      extract data from other sites using the `/scrape` endpoint
    
  </a>

---

# Scrape Indeed job listings

URL: https://docs.browserless.io/examples/scrape-indeed-jobs

# Scrape Indeed job listings

Search Indeed for jobs and extract titles, companies, locations, salaries, and descriptions from the results.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

Indeed renders job listings with JavaScript and employs bot-detection measures that block standard headless browsers. The examples below search for "data scientist" jobs and route through stealth mode with a residential proxy.

> **Selector stability**
> Indeed updates its markup frequently. If `.job_seen_beacon` or nested selectors stop returning results, inspect the live page with browser DevTools to find the current element names.

  
**AI Agent:**

  Use the [Browserless MCP server](https://github.com/browserless/browserless-mcp) to scrape job listings from Indeed from any MCP-compatible AI agent (Claude Desktop, Cursor, Windsurf, ChatGPT, etc.).

  **1. Connect the MCP server**

  Send this prompt to your AI agent to install the Browserless MCP server:

```text
Go to https://github.com/browserless/browserless-mcp/blob/main/install.md
and follow the instructions to install the Browserless MCP server
for my client.
```

  **2. Scrape Indeed**

  Use `browserless_smartscraper`. It handles Indeed's dynamic content and bot protection automatically.

```text
Use the browserless_smartscraper tool to scrape job listings
from https://www.indeed.com/jobs?q=data+scientist&l=Remote
and return the results as markdown
```

  
  
**REST API:**

  Send the BQL mutation over HTTP to the stealth endpoint. No browser library or BQL IDE required.

  
    
**cURL:**

    

    **1. Send the request**

```bash
curl -X POST \
  "https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "mutation ScrapeIndeedJobs { goto(url: \"https://www.indeed.com/jobs?q=data+scientist&l=Remote\", waitUntil: networkIdle) { status } waitForSelector(selector: \".job_seen_beacon\", timeout: 15000) { time } jobs: mapSelector(selector: \".job_seen_beacon\") { title: mapSelector(selector: \".jobTitle a span\") { innerText } company: mapSelector(selector: \".companyName\") { innerText } location: mapSelector(selector: \".companyLocation\") { innerText } salary: mapSelector(selector: \".salary-snippet-container\") { innerText } snippet: mapSelector(selector: \".job-snippet\") { innerText } link: mapSelector(selector: \".jobTitle a\") { href: attribute(name: \"href\") { value } } } }",
    "variables": {}
  }'
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 2956 },
    "jobs": [
      {
        "title": [{ "innerText": "Senior Data Scientist" }],
        "company": [{ "innerText": "Meta" }],
        "location": [{ "innerText": "Remote" }],
        "salary": [{ "innerText": "$140,000 - $180,000 a year" }],
        "snippet": [{ "innerText": "Build and deploy ML models for content recommendations..." }],
        "link": [{ "href": { "value": "/rc/clk?jk=abc123" } }]
      },
      {
        "title": [{ "innerText": "Data Scientist" }],
        "company": [{ "innerText": "Spotify" }],
        "location": [{ "innerText": "Remote" }],
        "salary": [{ "innerText": "$120,000 - $155,000 a year" }],
        "snippet": [{ "innerText": "Analyze user listening patterns and build recommendation engines..." }],
        "link": [{ "href": { "value": "/rc/clk?jk=def456" } }]
      }
    ]
  }
}
```

    
    
**JavaScript:**

    

    **1. Send the request**

```js
const query = `mutation ScrapeIndeedJobs {
  goto(url: "https://www.indeed.com/jobs?q=data+scientist&l=Remote", waitUntil: networkIdle) {
    status
  }
  waitForSelector(selector: ".job_seen_beacon", timeout: 15000) {
    time
  }
  jobs: mapSelector(selector: ".job_seen_beacon") {
    title: mapSelector(selector: ".jobTitle a span") { innerText }
    company: mapSelector(selector: ".companyName") { innerText }
    location: mapSelector(selector: ".companyLocation") { innerText }
    salary: mapSelector(selector: ".salary-snippet-container") { innerText }
    snippet: mapSelector(selector: ".job-snippet") { innerText }
    link: mapSelector(selector: ".jobTitle a") {
      href: attribute(name: "href") { value }
    }
  }
}`;

const response = await fetch(
  'https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ query, variables: {} }),
  }
);

const { data } = await response.json();
console.log(JSON.stringify(data.jobs, null, 2));
```

    **2. Check the output**

```json
[
  {
    "title": [{ "innerText": "Senior Data Scientist" }],
    "company": [{ "innerText": "Meta" }],
    "location": [{ "innerText": "Remote" }],
    "salary": [{ "innerText": "$140,000 - $180,000 a year" }],
    "snippet": [{ "innerText": "Build and deploy ML models for content recommendations..." }],
    "link": [{ "href": { "value": "/rc/clk?jk=abc123" } }]
  },
  {
    "title": [{ "innerText": "Data Scientist" }],
    "company": [{ "innerText": "Spotify" }],
    "location": [{ "innerText": "Remote" }],
    "salary": [{ "innerText": "$120,000 - $155,000 a year" }],
    "snippet": [{ "innerText": "Analyze user listening patterns and build recommendation engines..." }],
    "link": [{ "href": { "value": "/rc/clk?jk=def456" } }]
  }
]
```

    
    
**Python:**

    

    **1. Install dependencies**

```bash
pip install requests
```

    **2. Send the request**

```python
import requests

query = """
mutation ScrapeIndeedJobs {
  goto(url: "https://www.indeed.com/jobs?q=data+scientist&l=Remote", waitUntil: networkIdle) {
    status
  }
  waitForSelector(selector: ".job_seen_beacon", timeout: 15000) {
    time
  }
  jobs: mapSelector(selector: ".job_seen_beacon") {
    title: mapSelector(selector: ".jobTitle a span") { innerText }
    company: mapSelector(selector: ".companyName") { innerText }
    location: mapSelector(selector: ".companyLocation") { innerText }
    salary: mapSelector(selector: ".salary-snippet-container") { innerText }
    snippet: mapSelector(selector: ".job-snippet") { innerText }
    link: mapSelector(selector: ".jobTitle a") {
      href: attribute(name: "href") { value }
    }
  }
}
"""

response = requests.post(
    'https://production-sfo.browserless.io/stealth/bql',
    params={
        'token': 'YOUR_API_TOKEN_HERE',
        'proxy': 'residential',
        'proxyCountry': 'us',
    },
    json={'query': query, 'variables': {}},
)

data = response.json()['data']
for job in data['jobs']:
    title = job['title'][0]['innerText']
    company = job['company'][0]['innerText']
    location = job['location'][0]['innerText']
    salary = job['salary'][0]['innerText'] if job['salary'] else 'Not listed'
    print(f'{title} at {company} ({location}) - {salary}')
```

    **3. Check the output**

```text
Senior Data Scientist at Meta (Remote) - $140,000 - $180,000 a year
Data Scientist at Spotify (Remote) - $120,000 - $155,000 a year
```

    
    
**Java:**

    

    **1. Send the request**

```java
import java.net.URI;
import java.net.http.*;

String token = "YOUR_API_TOKEN_HERE";
String endpoint = "https://production-sfo.browserless.io/stealth/bql?token=" + token
    + "&proxy=residential&proxyCountry=us";

String query = "mutation ScrapeIndeedJobs {"
    + " goto(url: \\\"https://www.indeed.com/jobs?q=data+scientist&l=Remote\\\", waitUntil: networkIdle) { status }"
    + " waitForSelector(selector: \\\".job_seen_beacon\\\", timeout: 15000) { time }"
    + " jobs: mapSelector(selector: \\\".job_seen_beacon\\\") {"
    + "   title: mapSelector(selector: \\\".jobTitle a span\\\") { innerText }"
    + "   company: mapSelector(selector: \\\".companyName\\\") { innerText }"
    + "   location: mapSelector(selector: \\\".companyLocation\\\") { innerText }"
    + "   salary: mapSelector(selector: \\\".salary-snippet-container\\\") { innerText }"
    + "   snippet: mapSelector(selector: \\\".job-snippet\\\") { innerText }"
    + " }"
    + " }";

String payload = "{\"query\": \"" + query + "\", \"variables\": {}}";

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(endpoint))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(payload))
    .build();

HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 2956 },
    "jobs": [
      {
        "title": [{ "innerText": "Senior Data Scientist" }],
        "company": [{ "innerText": "Meta" }],
        "location": [{ "innerText": "Remote" }]
      }
    ]
  }
}
```

    
    
**C#:**

    

    **1. Send the request**

```csharp
using System.Net.Http;
using System.Text;
using System.Text.Json;

string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"https://production-sfo.browserless.io/stealth/bql?token={token}&proxy=residential&proxyCountry=us";

var payload = new
{
    query = @"mutation ScrapeIndeedJobs {
      goto(url: ""https://www.indeed.com/jobs?q=data+scientist&l=Remote"", waitUntil: networkIdle) { status }
      waitForSelector(selector: "".job_seen_beacon"", timeout: 15000) { time }
      jobs: mapSelector(selector: "".job_seen_beacon"") {
        title: mapSelector(selector: "".jobTitle a span"") { innerText }
        company: mapSelector(selector: "".companyName"") { innerText }
        location: mapSelector(selector: "".companyLocation"") { innerText }
        salary: mapSelector(selector: "".salary-snippet-container"") { innerText }
        snippet: mapSelector(selector: "".job-snippet"") { innerText }
      }
    }",
    variables = new { },
};

using (HttpClient httpClient = new HttpClient())
{
    var content = new StringContent(
        JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
    var response = await httpClient.PostAsync(endpoint, content);
    string body = await response.Content.ReadAsStringAsync();
    Console.WriteLine(body);
}
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 2956 },
    "jobs": [
      {
        "title": [{ "innerText": "Senior Data Scientist" }],
        "company": [{ "innerText": "Meta" }],
        "location": [{ "innerText": "Remote" }]
      }
    ]
  }
}
```

    
  

  
  
**Frameworks:**

  Connect through stealth mode and a residential proxy so Indeed sees traffic from a real browser, then extract job data from the rendered results.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Connect and scrape**

```js
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint:
    'wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us',
});

try {
  const page = await browser.newPage();
  await page.goto('https://www.indeed.com/jobs?q=data+scientist&l=Remote', {
    waitUntil: 'networkidle2',
  });

  await page.waitForSelector('.job_seen_beacon');

  const jobs = await page.evaluate(() =>
    Array.from(document.querySelectorAll('.job_seen_beacon')).map((card) => ({
      title: card.querySelector('.jobTitle a span')?.innerText?.trim() ?? '',
      company: card.querySelector('.companyName')?.innerText?.trim() ?? '',
      location: card.querySelector('.companyLocation')?.innerText?.trim() ?? '',
      salary: card.querySelector('.salary-snippet-container')?.innerText?.trim() ?? '',
      link: card.querySelector('.jobTitle a')?.href ?? '',
    }))
  );

  console.log(JSON.stringify(jobs, null, 2));
} finally {
  await browser.close();
}
```

    **3. Check the output**

    Run with `node scrape-indeed-jobs.mjs`. Each object has `title`, `company`, `location`, `salary`, and `link` fields.

```json
[
  {
    "title": "Senior Data Scientist",
    "company": "Meta",
    "location": "Remote",
    "salary": "$140,000 - $180,000 a year",
    "link": "https://www.indeed.com/rc/clk?jk=abc123"
  }
]
```

    
    
**Playwright:**

    

    **1. Install dependencies**

```bash
npm install playwright-core
```

    **2. Connect and scrape**

```js
import { chromium } from 'playwright-core';

const browser = await chromium.connectOverCDP(
  'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&stealth&proxy=residential&proxyCountry=us'
);

try {
  const context = browser.contexts()[0];
  const page = await context.newPage();
  await page.goto('https://www.indeed.com/jobs?q=data+scientist&l=Remote', {
    waitUntil: 'networkidle',
  });

  await page.waitForSelector('.job_seen_beacon');

  const jobs = await page.evaluate(() =>
    Array.from(document.querySelectorAll('.job_seen_beacon')).map((card) => ({
      title: card.querySelector('.jobTitle a span')?.innerText?.trim() ?? '',
      company: card.querySelector('.companyName')?.innerText?.trim() ?? '',
      location: card.querySelector('.companyLocation')?.innerText?.trim() ?? '',
      salary: card.querySelector('.salary-snippet-container')?.innerText?.trim() ?? '',
      link: card.querySelector('.jobTitle a')?.href ?? '',
    }))
  );

  console.log(JSON.stringify(jobs, null, 2));
} finally {
  await browser.close();
}
```

    **3. Check the output**

    Run with `node scrape-indeed-jobs.mjs`. Each object has `title`, `company`, `location`, `salary`, and `link` fields.

```json
[
  {
    "title": "Senior Data Scientist",
    "company": "Meta",
    "location": "Remote",
    "salary": "$140,000 - $180,000 a year",
    "link": "https://www.indeed.com/rc/clk?jk=abc123"
  }
]
```

    
  

  
  
**BQL:**

  

  **1. Write the mutation**

  Navigate to Indeed's job search results, wait for listings to render, then extract job details. We use `/stealth/bql` because Indeed's bot detection blocks standard headless browsers.

```graphql
mutation ScrapeIndeedJobs {
  goto(url: "https://www.indeed.com/jobs?q=data+scientist&l=Remote", waitUntil: networkIdle) {
    status
  }
  waitForSelector(selector: ".job_seen_beacon", timeout: 15000) {
    time
  }
  jobs: mapSelector(selector: ".job_seen_beacon") {
    title: mapSelector(selector: ".jobTitle a span") { innerText }
    company: mapSelector(selector: ".companyName") { innerText }
    location: mapSelector(selector: ".companyLocation") { innerText }
    salary: mapSelector(selector: ".salary-snippet-container") { innerText }
    snippet: mapSelector(selector: ".job-snippet") { innerText }
    link: mapSelector(selector: ".jobTitle a") {
      href: attribute(name: "href") { value }
    }
  }
}
```

  **2. Run it**

  Paste into the [BQL IDE](https://account.browserless.io/bql) and click Run.

  **3. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 2956 },
    "jobs": [
      {
        "title": [{ "innerText": "Senior Data Scientist" }],
        "company": [{ "innerText": "Meta" }],
        "location": [{ "innerText": "Remote" }],
        "salary": [{ "innerText": "$140,000 - $180,000 a year" }],
        "snippet": [{ "innerText": "Build and deploy ML models for content recommendations..." }],
        "link": [{ "href": { "value": "/rc/clk?jk=abc123" } }]
      },
      {
        "title": [{ "innerText": "Data Scientist" }],
        "company": [{ "innerText": "Spotify" }],
        "location": [{ "innerText": "Remote" }],
        "salary": [{ "innerText": "$120,000 - $155,000 a year" }],
        "snippet": [{ "innerText": "Analyze user listening patterns and build recommendation engines..." }],
        "link": [{ "href": { "value": "/rc/clk?jk=def456" } }]
      }
    ]
  }
}
```

  

## Next steps

  <a href="/examples/scrape-linkedin-jobs" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape LinkedIn Job Listings</h3>
      scrape another job board with stealth mode
    
  </a>
  <a href="/examples/scrape-glassdoor" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape Glassdoor Job Listings</h3>
      stealth-mode scraping against aggressive bot detection
    
  </a>
  <a href="/examples/cloudflare" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Solving Cloudflare Challenges</h3>
      bypass Cloudflare's interstitial pages before scraping
    
  </a>

---

# Scrape LinkedIn job listings

URL: https://docs.browserless.io/examples/scrape-linkedin-jobs

# Scrape LinkedIn job listings

Extract job titles, company names, locations, and posting dates from LinkedIn's public job search results.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

LinkedIn's public job board (`linkedin.com/jobs/search`) renders with JavaScript and has aggressive bot detection. A standard headless browser gets blocked or served a login wall. The examples below search for "software engineer" jobs and route through stealth mode with a residential proxy.

> **Selector stability**
> LinkedIn updates its markup frequently. If `.base-card` or related selectors stop returning results, inspect the live page with browser DevTools to find the current class names.

  
**AI Agent:**

  Use the [Browserless MCP server](https://github.com/browserless/browserless-mcp) to scrape job listings from LinkedIn from any MCP-compatible AI agent (Claude Desktop, Cursor, Windsurf, ChatGPT, etc.).

  **1. Connect the MCP server**

  Send this prompt to your AI agent to install the Browserless MCP server:

```text
Go to https://github.com/browserless/browserless-mcp/blob/main/install.md
and follow the instructions to install the Browserless MCP server
for my client.
```

  **2. Scrape LinkedIn jobs**

  Use `browserless_smartscraper`. It handles LinkedIn's dynamic content and bot protection automatically.

```text
Use the browserless_smartscraper tool to scrape job listings
from https://www.linkedin.com/jobs/search/?keywords=software+engineer&location=United+States
and return the results as markdown
```

  
  
**REST API:**

  Send the BQL mutation over HTTP to the stealth endpoint. No browser library or BQL IDE required.

  
    
**cURL:**

    

    **1. Send the request**

```bash
curl -X POST \
  "https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "mutation ScrapeLinkedInJobs { goto(url: \"https://www.linkedin.com/jobs/search/?keywords=software+engineer&location=United+States\", waitUntil: networkIdle) { status } waitForSelector(selector: \".base-card\", timeout: 15000) { time } jobs: mapSelector(selector: \".base-card\") { title: mapSelector(selector: \".base-search-card__title\") { innerText } company: mapSelector(selector: \".base-search-card__subtitle a\") { innerText } location: mapSelector(selector: \".job-search-card__location\") { innerText } posted: mapSelector(selector: \"time\") { innerText } link: mapSelector(selector: \"a.base-card__full-link\") { href: attribute(name: \"href\") { value } } } }",
    "variables": {}
  }'
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 3876 },
    "jobs": [
      {
        "title": [{ "innerText": "Senior Software Engineer" }],
        "company": [{ "innerText": "Google" }],
        "location": [{ "innerText": "Mountain View, CA" }],
        "posted": [{ "innerText": "2 days ago" }],
        "link": [{ "href": { "value": "https://www.linkedin.com/jobs/view/1234567890" } }]
      },
      {
        "title": [{ "innerText": "Software Engineer II" }],
        "company": [{ "innerText": "Microsoft" }],
        "location": [{ "innerText": "Redmond, WA" }],
        "posted": [{ "innerText": "1 week ago" }],
        "link": [{ "href": { "value": "https://www.linkedin.com/jobs/view/9876543210" } }]
      }
    ]
  }
}
```

    
    
**JavaScript:**

    

    **1. Send the request**

```js
const query = `mutation ScrapeLinkedInJobs {
  goto(url: "https://www.linkedin.com/jobs/search/?keywords=software+engineer&location=United+States", waitUntil: networkIdle) {
    status
  }
  waitForSelector(selector: ".base-card", timeout: 15000) {
    time
  }
  jobs: mapSelector(selector: ".base-card") {
    title: mapSelector(selector: ".base-search-card__title") { innerText }
    company: mapSelector(selector: ".base-search-card__subtitle a") { innerText }
    location: mapSelector(selector: ".job-search-card__location") { innerText }
    posted: mapSelector(selector: "time") { innerText }
    link: mapSelector(selector: "a.base-card__full-link") {
      href: attribute(name: "href") { value }
    }
  }
}`;

const response = await fetch(
  'https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ query, variables: {} }),
  }
);

const { data } = await response.json();
console.log(JSON.stringify(data.jobs, null, 2));
```

    **2. Check the output**

```json
[
  {
    "title": [{ "innerText": "Senior Software Engineer" }],
    "company": [{ "innerText": "Google" }],
    "location": [{ "innerText": "Mountain View, CA" }],
    "posted": [{ "innerText": "2 days ago" }],
    "link": [{ "href": { "value": "https://www.linkedin.com/jobs/view/1234567890" } }]
  },
  {
    "title": [{ "innerText": "Full Stack Developer" }],
    "company": [{ "innerText": "Microsoft" }],
    "location": [{ "innerText": "Redmond, WA" }],
    "posted": [{ "innerText": "5 days ago" }],
    "link": [{ "href": { "value": "https://www.linkedin.com/jobs/view/9876543210" } }]
  }
]
```

    
    
**Python:**

    

    **1. Install dependencies**

```bash
pip install requests
```

    **2. Send the request**

```python
import requests

query = """
mutation ScrapeLinkedInJobs {
  goto(url: "https://www.linkedin.com/jobs/search/?keywords=software+engineer&location=United+States", waitUntil: networkIdle) {
    status
  }
  waitForSelector(selector: ".base-card", timeout: 15000) {
    time
  }
  jobs: mapSelector(selector: ".base-card") {
    title: mapSelector(selector: ".base-search-card__title") { innerText }
    company: mapSelector(selector: ".base-search-card__subtitle a") { innerText }
    location: mapSelector(selector: ".job-search-card__location") { innerText }
    posted: mapSelector(selector: "time") { innerText }
    link: mapSelector(selector: "a.base-card__full-link") {
      href: attribute(name: "href") { value }
    }
  }
}
"""

response = requests.post(
    'https://production-sfo.browserless.io/stealth/bql',
    params={
        'token': 'YOUR_API_TOKEN_HERE',
        'proxy': 'residential',
        'proxyCountry': 'us',
    },
    json={'query': query, 'variables': {}},
)

data = response.json()['data']
for job in data['jobs']:
    title = job['title'][0]['innerText']
    company = job['company'][0]['innerText']
    location = job['location'][0]['innerText']
    print(f'{title} at {company} ({location})')
```

    **3. Check the output**

```text
Senior Software Engineer at Google (Mountain View, CA)
Full Stack Developer at Microsoft (Redmond, WA)
```

    
    
**Java:**

    

    **1. Send the request**

```java
import java.net.URI;
import java.net.http.*;

String token = "YOUR_API_TOKEN_HERE";
String endpoint = "https://production-sfo.browserless.io/stealth/bql?token=" + token
    + "&proxy=residential&proxyCountry=us";

String query = "mutation ScrapeLinkedInJobs {"
    + " goto(url: \\\"https://www.linkedin.com/jobs/search/?keywords=software+engineer&location=United+States\\\", waitUntil: networkIdle) { status }"
    + " waitForSelector(selector: \\\".base-card\\\", timeout: 15000) { time }"
    + " jobs: mapSelector(selector: \\\".base-card\\\") {"
    + "   title: mapSelector(selector: \\\".base-search-card__title\\\") { innerText }"
    + "   company: mapSelector(selector: \\\".base-search-card__subtitle a\\\") { innerText }"
    + "   location: mapSelector(selector: \\\".job-search-card__location\\\") { innerText }"
    + "   posted: mapSelector(selector: \\\"time\\\") { innerText }"
    + " }"
    + " }";

String payload = "{\"query\": \"" + query + "\", \"variables\": {}}";

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(endpoint))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(payload))
    .build();

HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 3876 },
    "jobs": [
      {
        "title": [{ "innerText": "Senior Software Engineer" }],
        "company": [{ "innerText": "Google" }],
        "location": [{ "innerText": "Mountain View, CA" }]
      }
    ]
  }
}
```

    
    
**C#:**

    

    **1. Send the request**

```csharp
using System.Net.Http;
using System.Text;
using System.Text.Json;

string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"https://production-sfo.browserless.io/stealth/bql?token={token}&proxy=residential&proxyCountry=us";

var payload = new
{
    query = @"mutation ScrapeLinkedInJobs {
      goto(url: ""https://www.linkedin.com/jobs/search/?keywords=software+engineer&location=United+States"", waitUntil: networkIdle) { status }
      waitForSelector(selector: "".base-card"", timeout: 15000) { time }
      jobs: mapSelector(selector: "".base-card"") {
        title: mapSelector(selector: "".base-search-card__title"") { innerText }
        company: mapSelector(selector: "".base-search-card__subtitle a"") { innerText }
        location: mapSelector(selector: "".job-search-card__location"") { innerText }
        posted: mapSelector(selector: ""time"") { innerText }
        link: mapSelector(selector: ""a.base-card__full-link"") {
          href: attribute(name: ""href"") { value }
        }
      }
    }",
    variables = new { },
};

using (HttpClient httpClient = new HttpClient())
{
    var content = new StringContent(
        JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
    var response = await httpClient.PostAsync(endpoint, content);
    string body = await response.Content.ReadAsStringAsync();
    Console.WriteLine(body);
}
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 3876 },
    "jobs": [
      {
        "title": [{ "innerText": "Senior Software Engineer" }],
        "company": [{ "innerText": "Google" }],
        "location": [{ "innerText": "Mountain View, CA" }]
      }
    ]
  }
}
```

    
  

  
  
**Frameworks:**

  Connect through stealth mode and a residential proxy so LinkedIn sees traffic from a real browser, then extract job data from the rendered search results.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Connect and scrape**

```js
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint:
    'wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us',
});

try {
  const page = await browser.newPage();
  await page.goto('https://www.linkedin.com/jobs/search/?keywords=software+engineer&location=United+States', {
    waitUntil: 'networkidle2',
  });

  await page.waitForSelector('.base-card');

  const jobs = await page.evaluate(() =>
    Array.from(document.querySelectorAll('.base-card')).map((card) => ({
      title: card.querySelector('.base-search-card__title')?.innerText?.trim() ?? '',
      company: card.querySelector('.base-search-card__subtitle a')?.innerText?.trim() ?? '',
      location: card.querySelector('.job-search-card__location')?.innerText?.trim() ?? '',
      posted: card.querySelector('time')?.innerText?.trim() ?? '',
      link: card.querySelector('a.base-card__full-link')?.href ?? '',
    }))
  );

  console.log(JSON.stringify(jobs, null, 2));
} finally {
  await browser.close();
}
```

    **3. Check the output**

    Run with `node scrape-linkedin-jobs.mjs`. Each object has `title`, `company`, `location`, `posted`, and `link` fields.

```json
[
  {
    "title": "Senior Software Engineer",
    "company": "Google",
    "location": "Mountain View, CA",
    "posted": "2 days ago",
    "link": "https://www.linkedin.com/jobs/view/1234567890"
  }
]
```

    
    
**Playwright:**

    

    **1. Install dependencies**

```bash
npm install playwright-core
```

    **2. Connect and scrape**

```js
import { chromium } from 'playwright-core';

const browser = await chromium.connectOverCDP(
  'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&stealth&proxy=residential&proxyCountry=us'
);

try {
  const context = browser.contexts()[0];
  const page = await context.newPage();
  await page.goto('https://www.linkedin.com/jobs/search/?keywords=software+engineer&location=United+States', {
    waitUntil: 'networkidle',
  });

  await page.waitForSelector('.base-card');

  const jobs = await page.evaluate(() =>
    Array.from(document.querySelectorAll('.base-card')).map((card) => ({
      title: card.querySelector('.base-search-card__title')?.innerText?.trim() ?? '',
      company: card.querySelector('.base-search-card__subtitle a')?.innerText?.trim() ?? '',
      location: card.querySelector('.job-search-card__location')?.innerText?.trim() ?? '',
      posted: card.querySelector('time')?.innerText?.trim() ?? '',
      link: card.querySelector('a.base-card__full-link')?.href ?? '',
    }))
  );

  console.log(JSON.stringify(jobs, null, 2));
} finally {
  await browser.close();
}
```

    **3. Check the output**

    Run with `node scrape-linkedin-jobs.mjs`. Each object has `title`, `company`, `location`, `posted`, and `link` fields.

```json
[
  {
    "title": "Senior Software Engineer",
    "company": "Google",
    "location": "Mountain View, CA",
    "posted": "2 days ago",
    "link": "https://www.linkedin.com/jobs/view/1234567890"
  }
]
```

    
  

  
  
**BQL:**

  

  **1. Write the mutation**

  Navigate to LinkedIn's public job search, wait for cards to render, then extract job details from each listing. We use `/stealth/bql` because LinkedIn's bot detection blocks standard headless browsers.

```graphql
mutation ScrapeLinkedInJobs {
  goto(url: "https://www.linkedin.com/jobs/search/?keywords=software+engineer&location=United+States", waitUntil: networkIdle) {
    status
  }
  waitForSelector(selector: ".base-card", timeout: 15000) {
    time
  }
  jobs: mapSelector(selector: ".base-card") {
    title: mapSelector(selector: ".base-search-card__title") { innerText }
    company: mapSelector(selector: ".base-search-card__subtitle a") { innerText }
    location: mapSelector(selector: ".job-search-card__location") { innerText }
    posted: mapSelector(selector: "time") { innerText }
    link: mapSelector(selector: "a.base-card__full-link") {
      href: attribute(name: "href") { value }
    }
  }
}
```

  **2. Run it**

  Paste into the [BQL IDE](https://account.browserless.io/bql) and click Run.

  **3. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 3876 },
    "jobs": [
      {
        "title": [{ "innerText": "Senior Software Engineer" }],
        "company": [{ "innerText": "Google" }],
        "location": [{ "innerText": "Mountain View, CA" }],
        "posted": [{ "innerText": "2 days ago" }],
        "link": [{ "href": { "value": "https://www.linkedin.com/jobs/view/1234567890" } }]
      },
      {
        "title": [{ "innerText": "Software Engineer II" }],
        "company": [{ "innerText": "Microsoft" }],
        "location": [{ "innerText": "Redmond, WA" }],
        "posted": [{ "innerText": "1 week ago" }],
        "link": [{ "href": { "value": "https://www.linkedin.com/jobs/view/9876543210" } }]
      }
    ]
  }
}
```

  

## Next steps

  <a href="/examples/scrape-indeed-jobs" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape Indeed Job Listings</h3>
      scrape another job board with similar techniques
    
  </a>
  <a href="/examples/scrape-glassdoor" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape Glassdoor Job Listings</h3>
      stealth-mode scraping against aggressive bot detection
    
  </a>
  <a href="/examples/google-search" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Automate Google Search</h3>
      pull search results using the same `/stealth/bql` endpoint
    
  </a>

---

# Scrape OpenTable restaurants

URL: https://docs.browserless.io/examples/scrape-opentable

# Scrape OpenTable restaurants

Search OpenTable for restaurants and extract names, ratings, cuisines, and price ranges from the results.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

OpenTable renders its search results with JavaScript and uses bot-detection measures, so a plain HTTP request won't return listing data. The examples below search for Italian restaurants and route through stealth mode with a residential proxy.

> **Selector stability**
> OpenTable updates its markup periodically. If selectors stop returning results, inspect the live page with browser DevTools to find the current element and attribute names.

  
**AI Agent:**

  Use the [Browserless MCP server](https://github.com/browserless/browserless-mcp) to scrape restaurant listings from OpenTable from any MCP-compatible AI agent (Claude Desktop, Cursor, Windsurf, ChatGPT, etc.).

  **1. Connect the MCP server**

  Send this prompt to your AI agent to install the Browserless MCP server:

```text
Go to https://github.com/browserless/browserless-mcp/blob/main/install.md
and follow the instructions to install the Browserless MCP server
for my client.
```

  **2. Scrape OpenTable**

  Use `browserless_smartscraper`. It handles OpenTable's dynamic content and bot protection automatically.

```text
Use the browserless_smartscraper tool to scrape restaurant listings
from https://www.opentable.com/s?term=italian&covers=2
and return the results as markdown
```

  
  
**REST API:**

  Send the BQL mutation over HTTP to the stealth endpoint. No browser library or BQL IDE required.

  
    
**cURL:**

    

    **1. Send the request**

```bash
curl -X POST \
  "https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "mutation ScrapeOpenTable { goto(url: \"https://www.opentable.com/s?term=italian&covers=2\", waitUntil: networkIdle) { status } waitForSelector(selector: \"[data-test=restaurant-card]\", timeout: 15000) { time } restaurants: mapSelector(selector: \"[data-test=restaurant-card]\") { name: mapSelector(selector: \"h2\") { innerText } rating: mapSelector(selector: \"[data-test=rating-score]\") { innerText } cuisine: mapSelector(selector: \"[data-test=cuisine]\") { innerText } priceRange: mapSelector(selector: \"[data-test=price-range]\") { innerText } bookingsToday: mapSelector(selector: \"[data-test=bookings-today]\") { innerText } } }",
    "variables": {}
  }'
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 3412 },
    "restaurants": [
      {
        "name": [{ "innerText": "Carbone" }],
        "rating": [{ "innerText": "4.8" }],
        "cuisine": [{ "innerText": "Italian" }],
        "priceRange": [{ "innerText": "$$$$" }],
        "bookingsToday": [{ "innerText": "Booked 42 times today" }]
      },
      {
        "name": [{ "innerText": "L'Artusi" }],
        "rating": [{ "innerText": "4.7" }],
        "cuisine": [{ "innerText": "Italian" }],
        "priceRange": [{ "innerText": "$$$" }],
        "bookingsToday": [{ "innerText": "Booked 28 times today" }]
      }
    ]
  }
}
```

    
    
**JavaScript:**

    

    **1. Send the request**

```js
const query = `mutation ScrapeOpenTable {
  goto(url: "https://www.opentable.com/s?term=italian&covers=2", waitUntil: networkIdle) {
    status
  }
  waitForSelector(selector: "[data-test=restaurant-card]", timeout: 15000) {
    time
  }
  restaurants: mapSelector(selector: "[data-test=restaurant-card]") {
    name: mapSelector(selector: "h2") { innerText }
    rating: mapSelector(selector: "[data-test=rating-score]") { innerText }
    cuisine: mapSelector(selector: "[data-test=cuisine]") { innerText }
    priceRange: mapSelector(selector: "[data-test=price-range]") { innerText }
    bookingsToday: mapSelector(selector: "[data-test=bookings-today]") { innerText }
  }
}`;

const response = await fetch(
  'https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ query, variables: {} }),
  }
);

const { data } = await response.json();
console.log(JSON.stringify(data.restaurants, null, 2));
```

    **2. Check the output**

```json
[
  {
    "name": [{ "innerText": "Carbone" }],
    "rating": [{ "innerText": "4.8" }],
    "cuisine": [{ "innerText": "Italian" }],
    "priceRange": [{ "innerText": "$$$$" }],
    "bookingsToday": [{ "innerText": "Booked 42 times today" }]
  },
  {
    "name": [{ "innerText": "L'Artusi" }],
    "rating": [{ "innerText": "4.7" }],
    "cuisine": [{ "innerText": "Italian" }],
    "priceRange": [{ "innerText": "$$$" }],
    "bookingsToday": [{ "innerText": "Booked 28 times today" }]
  }
]
```

    
    
**Python:**

    

    **1. Install dependencies**

```bash
pip install requests
```

    **2. Send the request**

```python
import requests

query = """
mutation ScrapeOpenTable {
  goto(url: "https://www.opentable.com/s?term=italian&covers=2", waitUntil: networkIdle) {
    status
  }
  waitForSelector(selector: "[data-test=restaurant-card]", timeout: 15000) {
    time
  }
  restaurants: mapSelector(selector: "[data-test=restaurant-card]") {
    name: mapSelector(selector: "h2") { innerText }
    rating: mapSelector(selector: "[data-test=rating-score]") { innerText }
    cuisine: mapSelector(selector: "[data-test=cuisine]") { innerText }
    priceRange: mapSelector(selector: "[data-test=price-range]") { innerText }
    bookingsToday: mapSelector(selector: "[data-test=bookings-today]") { innerText }
  }
}
"""

response = requests.post(
    'https://production-sfo.browserless.io/stealth/bql',
    params={
        'token': 'YOUR_API_TOKEN_HERE',
        'proxy': 'residential',
        'proxyCountry': 'us',
    },
    json={'query': query, 'variables': {}},
)

data = response.json()['data']
for r in data['restaurants']:
    name = r['name'][0]['innerText']
    rating = r['rating'][0]['innerText']
    cuisine = r['cuisine'][0]['innerText']
    price = r['priceRange'][0]['innerText']
    print(f'{name} | {rating} stars | {cuisine} | {price}')
```

    **3. Check the output**

```text
Carbone | 4.8 stars | Italian | $$$$
L'Artusi | 4.7 stars | Italian | $$$
```

    
    
**Java:**

    

    **1. Send the request**

```java
import java.net.URI;
import java.net.http.*;

String token = "YOUR_API_TOKEN_HERE";
String endpoint = "https://production-sfo.browserless.io/stealth/bql?token=" + token
    + "&proxy=residential&proxyCountry=us";

String query = "mutation ScrapeOpenTable {"
    + " goto(url: \\\"https://www.opentable.com/s?term=italian&covers=2\\\", waitUntil: networkIdle) { status }"
    + " waitForSelector(selector: \\\"[data-test=restaurant-card]\\\", timeout: 15000) { time }"
    + " restaurants: mapSelector(selector: \\\"[data-test=restaurant-card]\\\") {"
    + "   name: mapSelector(selector: \\\"h2\\\") { innerText }"
    + "   rating: mapSelector(selector: \\\"[data-test=rating-score]\\\") { innerText }"
    + "   cuisine: mapSelector(selector: \\\"[data-test=cuisine]\\\") { innerText }"
    + "   priceRange: mapSelector(selector: \\\"[data-test=price-range]\\\") { innerText }"
    + "   bookingsToday: mapSelector(selector: \\\"[data-test=bookings-today]\\\") { innerText }"
    + " }"
    + " }";

String payload = "{\"query\": \"" + query + "\", \"variables\": {}}";

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(endpoint))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(payload))
    .build();

HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 3412 },
    "restaurants": [
      {
        "name": [{ "innerText": "Carbone" }],
        "rating": [{ "innerText": "4.8" }],
        "cuisine": [{ "innerText": "Italian" }],
        "priceRange": [{ "innerText": "$$$$" }],
        "bookingsToday": [{ "innerText": "Booked 42 times today" }]
      }
    ]
  }
}
```

    
    
**C#:**

    

    **1. Send the request**

```csharp
using System.Net.Http;
using System.Text;
using System.Text.Json;

string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"https://production-sfo.browserless.io/stealth/bql?token={token}&proxy=residential&proxyCountry=us";

var payload = new
{
    query = @"mutation ScrapeOpenTable {
      goto(url: ""https://www.opentable.com/s?term=italian&covers=2"", waitUntil: networkIdle) { status }
      waitForSelector(selector: ""[data-test=restaurant-card]"", timeout: 15000) { time }
      restaurants: mapSelector(selector: ""[data-test=restaurant-card]"") {
        name: mapSelector(selector: ""h2"") { innerText }
        rating: mapSelector(selector: ""[data-test=rating-score]"") { innerText }
        cuisine: mapSelector(selector: ""[data-test=cuisine]"") { innerText }
        priceRange: mapSelector(selector: ""[data-test=price-range]"") { innerText }
        bookingsToday: mapSelector(selector: ""[data-test=bookings-today]"") { innerText }
      }
    }",
    variables = new { },
};

using (HttpClient httpClient = new HttpClient())
{
    var content = new StringContent(
        JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
    var response = await httpClient.PostAsync(endpoint, content);
    string body = await response.Content.ReadAsStringAsync();
    Console.WriteLine(body);
}
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 3412 },
    "restaurants": [
      {
        "name": [{ "innerText": "Carbone" }],
        "rating": [{ "innerText": "4.8" }],
        "cuisine": [{ "innerText": "Italian" }],
        "priceRange": [{ "innerText": "$$$$" }],
        "bookingsToday": [{ "innerText": "Booked 42 times today" }]
      }
    ]
  }
}
```

    
  

  
  
**Frameworks:**

  Connect through stealth mode and a residential proxy so OpenTable sees traffic from a real browser, then extract restaurant data from the rendered page.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Connect and scrape**

```js
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint:
    'wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us',
});

try {
  const page = await browser.newPage();
  await page.goto('https://www.opentable.com/s?term=italian&covers=2', {
    waitUntil: 'networkidle2',
  });

  await page.waitForSelector('[data-test="restaurant-card"]');

  const restaurants = await page.evaluate(() =>
    Array.from(document.querySelectorAll('[data-test="restaurant-card"]')).map((card) => ({
      name: card.querySelector('h2')?.innerText?.trim() ?? '',
      rating: card.querySelector('[data-test="rating-score"]')?.innerText?.trim() ?? '',
      cuisine: card.querySelector('[data-test="cuisine"]')?.innerText?.trim() ?? '',
      priceRange: card.querySelector('[data-test="price-range"]')?.innerText?.trim() ?? '',
    }))
  );

  console.log(JSON.stringify(restaurants, null, 2));
} finally {
  await browser.close();
}
```

    **3. Check the output**

    Run with `node scrape-opentable.mjs`. Each object has `name`, `rating`, `cuisine`, and `priceRange` fields.

```json
[
  {
    "name": "Carbone",
    "rating": "4.8",
    "cuisine": "Italian",
    "priceRange": "$$$$"
  },
  {
    "name": "L'Artusi",
    "rating": "4.7",
    "cuisine": "Italian",
    "priceRange": "$$$"
  }
]
```

    
    
**Playwright:**

    

    **1. Install dependencies**

```bash
npm install playwright-core
```

    **2. Connect and scrape**

```js
import { chromium } from 'playwright-core';

const browser = await chromium.connectOverCDP(
  'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&stealth&proxy=residential&proxyCountry=us'
);

try {
  const context = browser.contexts()[0];
  const page = await context.newPage();
  await page.goto('https://www.opentable.com/s?term=italian&covers=2', {
    waitUntil: 'networkidle',
  });

  await page.waitForSelector('[data-test="restaurant-card"]');

  const restaurants = await page.evaluate(() =>
    Array.from(document.querySelectorAll('[data-test="restaurant-card"]')).map((card) => ({
      name: card.querySelector('h2')?.innerText?.trim() ?? '',
      rating: card.querySelector('[data-test="rating-score"]')?.innerText?.trim() ?? '',
      cuisine: card.querySelector('[data-test="cuisine"]')?.innerText?.trim() ?? '',
      priceRange: card.querySelector('[data-test="price-range"]')?.innerText?.trim() ?? '',
    }))
  );

  console.log(JSON.stringify(restaurants, null, 2));
} finally {
  await browser.close();
}
```

    **3. Check the output**

    Run with `node scrape-opentable.mjs`. Each object has `name`, `rating`, `cuisine`, and `priceRange` fields.

```json
[
  {
    "name": "Carbone",
    "rating": "4.8",
    "cuisine": "Italian",
    "priceRange": "$$$$"
  },
  {
    "name": "L'Artusi",
    "rating": "4.7",
    "cuisine": "Italian",
    "priceRange": "$$$"
  }
]
```

    
  

  
  
**BQL:**

  

  **1. Write the mutation**

  Navigate to OpenTable's search results, wait for restaurant cards to render, then extract structured data from each listing. We use `/stealth/bql` because OpenTable's bot detection blocks plain browser sessions.

```graphql
mutation ScrapeOpenTable {
  goto(url: "https://www.opentable.com/s?term=italian&covers=2", waitUntil: networkIdle) {
    status
  }
  waitForSelector(selector: "[data-test=restaurant-card]", timeout: 15000) {
    time
  }
  restaurants: mapSelector(selector: "[data-test=restaurant-card]") {
    name: mapSelector(selector: "h2") { innerText }
    rating: mapSelector(selector: "[data-test=rating-score]") { innerText }
    cuisine: mapSelector(selector: "[data-test=cuisine]") { innerText }
    priceRange: mapSelector(selector: "[data-test=price-range]") { innerText }
    bookingsToday: mapSelector(selector: "[data-test=bookings-today]") { innerText }
  }
}
```

  **2. Run it**

  Paste into the [BQL IDE](https://account.browserless.io/bql) and click Run.

  **3. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 3412 },
    "restaurants": [
      {
        "name": [{ "innerText": "Carbone" }],
        "rating": [{ "innerText": "4.8" }],
        "cuisine": [{ "innerText": "Italian" }],
        "priceRange": [{ "innerText": "$$$$" }],
        "bookingsToday": [{ "innerText": "Booked 42 times today" }]
      },
      {
        "name": [{ "innerText": "L'Artusi" }],
        "rating": [{ "innerText": "4.7" }],
        "cuisine": [{ "innerText": "Italian" }],
        "priceRange": [{ "innerText": "$$$" }],
        "bookingsToday": [{ "innerText": "Booked 28 times today" }]
      }
    ]
  }
}
```

  

## Next steps

  <a href="/examples/scrape-yelp" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape Yelp Restaurant Listings</h3>
      another restaurant/business listing scrape with stealth mode
    
  </a>
  <a href="/examples/scrape-glassdoor" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape Glassdoor Job Listings</h3>
      stealth-mode scraping against aggressive bot detection
    
  </a>
  <a href="/examples/cloudflare" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Solving Cloudflare Challenges</h3>
      bypass Cloudflare's interstitial pages before scraping
    
  </a>

---

# Scrape Realtor.com property listings

URL: https://docs.browserless.io/examples/scrape-realtor

# Scrape Realtor.com property listings

Search Realtor.com for homes and extract prices, addresses, bed/bath counts, and square footage from the results.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

Realtor.com renders listings with JavaScript and uses bot-detection measures. The examples below search for homes in San Francisco and route through stealth mode with a residential proxy.

> **Selector stability**
> Realtor.com updates its markup periodically. If selectors stop returning results, inspect the live page with browser DevTools to find the current element and attribute names.

  
**AI Agent:**

  Use the [Browserless MCP server](https://github.com/browserless/browserless-mcp) to scrape property listings from Realtor.com from any MCP-compatible AI agent (Claude Desktop, Cursor, Windsurf, ChatGPT, etc.).

  **1. Connect the MCP server**

  Send this prompt to your AI agent to install the Browserless MCP server:

```text
Go to https://github.com/browserless/browserless-mcp/blob/main/install.md
and follow the instructions to install the Browserless MCP server
for my client.
```

  **2. Scrape Realtor.com**

  Use `browserless_smartscraper`. It handles Realtor.com's dynamic content and bot protection automatically.

```text
Use the browserless_smartscraper tool to scrape property listings
from https://www.realtor.com/realestateandhomes-search/San-Francisco_CA
and return the results as markdown
```

  
  
**REST API:**

  Send the BQL mutation over HTTP to the stealth endpoint. No browser library or BQL IDE required.

  
    
**cURL:**

    

    **1. Send the request**

```bash
curl -X POST \
  "https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "mutation ScrapeRealtor { goto(url: \"https://www.realtor.com/realestateandhomes-search/San-Francisco_CA\", waitUntil: networkIdle) { status } waitForSelector(selector: \"[data-testid=card-content]\", timeout: 15000) { time } properties: mapSelector(selector: \"[data-testid=card-content]\") { price: mapSelector(selector: \"[data-testid=card-price]\") { innerText } address: mapSelector(selector: \"[data-testid=card-address]\") { innerText } beds: mapSelector(selector: \"[data-testid=property-meta-beds] span\") { innerText } baths: mapSelector(selector: \"[data-testid=property-meta-baths] span\") { innerText } sqft: mapSelector(selector: \"[data-testid=property-meta-sqft] span\") { innerText } status: mapSelector(selector: \"[data-testid=card-description]\") { innerText } } }",
    "variables": {}
  }'
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 3654 },
    "properties": [
      {
        "price": [{ "innerText": "$1,295,000" }],
        "address": [{ "innerText": "123 Valencia St, San Francisco, CA 94103" }],
        "beds": [{ "innerText": "3" }],
        "baths": [{ "innerText": "2" }],
        "sqft": [{ "innerText": "1,450" }],
        "status": [{ "innerText": "For Sale" }]
      },
      {
        "price": [{ "innerText": "$875,000" }],
        "address": [{ "innerText": "456 Guerrero St, San Francisco, CA 94110" }],
        "beds": [{ "innerText": "2" }],
        "baths": [{ "innerText": "1" }],
        "sqft": [{ "innerText": "980" }],
        "status": [{ "innerText": "For Sale" }]
      }
    ]
  }
}
```

    
    
**JavaScript:**

    

    **1. Send the request**

```js
const query = `mutation ScrapeRealtor {
  goto(url: "https://www.realtor.com/realestateandhomes-search/San-Francisco_CA", waitUntil: networkIdle) {
    status
  }
  waitForSelector(selector: "[data-testid=card-content]", timeout: 15000) {
    time
  }
  properties: mapSelector(selector: "[data-testid=card-content]") {
    price: mapSelector(selector: "[data-testid=card-price]") { innerText }
    address: mapSelector(selector: "[data-testid=card-address]") { innerText }
    beds: mapSelector(selector: "[data-testid=property-meta-beds] span") { innerText }
    baths: mapSelector(selector: "[data-testid=property-meta-baths] span") { innerText }
    sqft: mapSelector(selector: "[data-testid=property-meta-sqft] span") { innerText }
  }
}`;

const response = await fetch(
  'https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ query, variables: {} }),
  }
);

const { data } = await response.json();
console.log(JSON.stringify(data.properties, null, 2));
```

    **2. Check the output**

```json
[
  {
    "price": [{ "innerText": "$1,295,000" }],
    "address": [{ "innerText": "123 Valencia St, San Francisco, CA 94103" }],
    "beds": [{ "innerText": "3" }],
    "baths": [{ "innerText": "2" }],
    "sqft": [{ "innerText": "1,450" }]
  },
  {
    "price": [{ "innerText": "$875,000" }],
    "address": [{ "innerText": "456 Mission St, San Francisco, CA 94105" }],
    "beds": [{ "innerText": "2" }],
    "baths": [{ "innerText": "1" }],
    "sqft": [{ "innerText": "1,100" }]
  }
]
```

    
    
**Python:**

    

    **1. Install dependencies**

```bash
pip install requests
```

    **2. Send the request**

```python
import requests

query = """
mutation ScrapeRealtor {
  goto(url: "https://www.realtor.com/realestateandhomes-search/San-Francisco_CA", waitUntil: networkIdle) {
    status
  }
  waitForSelector(selector: "[data-testid=card-content]", timeout: 15000) {
    time
  }
  properties: mapSelector(selector: "[data-testid=card-content]") {
    price: mapSelector(selector: "[data-testid=card-price]") { innerText }
    address: mapSelector(selector: "[data-testid=card-address]") { innerText }
    beds: mapSelector(selector: "[data-testid=property-meta-beds] span") { innerText }
    baths: mapSelector(selector: "[data-testid=property-meta-baths] span") { innerText }
    sqft: mapSelector(selector: "[data-testid=property-meta-sqft] span") { innerText }
  }
}
"""

response = requests.post(
    'https://production-sfo.browserless.io/stealth/bql',
    params={
        'token': 'YOUR_API_TOKEN_HERE',
        'proxy': 'residential',
        'proxyCountry': 'us',
    },
    json={'query': query, 'variables': {}},
)

data = response.json()['data']
for prop in data['properties']:
    price = prop['price'][0]['innerText']
    address = prop['address'][0]['innerText']
    beds = prop['beds'][0]['innerText'] if prop['beds'] else '?'
    baths = prop['baths'][0]['innerText'] if prop['baths'] else '?'
    sqft = prop['sqft'][0]['innerText'] if prop['sqft'] else '?'
    print(f'{address} | {price} | {beds}bd/{baths}ba | {sqft} sqft')
```

    **3. Check the output**

```text
123 Valencia St, San Francisco, CA 94103 | $1,295,000 | 3bd/2ba | 1,450 sqft
456 Mission St, San Francisco, CA 94105 | $875,000 | 2bd/1ba | 1,100 sqft
```

    
    
**Java:**

    

    **1. Send the request**

```java
import java.net.URI;
import java.net.http.*;

String token = "YOUR_API_TOKEN_HERE";
String endpoint = "https://production-sfo.browserless.io/stealth/bql?token=" + token
    + "&proxy=residential&proxyCountry=us";

String query = "mutation ScrapeRealtor {"
    + " goto(url: \\\"https://www.realtor.com/realestateandhomes-search/San-Francisco_CA\\\", waitUntil: networkIdle) { status }"
    + " waitForSelector(selector: \\\"[data-testid=card-content]\\\", timeout: 15000) { time }"
    + " properties: mapSelector(selector: \\\"[data-testid=card-content]\\\") {"
    + "   price: mapSelector(selector: \\\"[data-testid=card-price]\\\") { innerText }"
    + "   address: mapSelector(selector: \\\"[data-testid=card-address]\\\") { innerText }"
    + "   beds: mapSelector(selector: \\\"[data-testid=property-meta-beds] span\\\") { innerText }"
    + "   baths: mapSelector(selector: \\\"[data-testid=property-meta-baths] span\\\") { innerText }"
    + "   sqft: mapSelector(selector: \\\"[data-testid=property-meta-sqft] span\\\") { innerText }"
    + " }"
    + " }";

String payload = "{\"query\": \"" + query + "\", \"variables\": {}}";

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(endpoint))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(payload))
    .build();

HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 3654 },
    "properties": [
      {
        "price": [{ "innerText": "$1,295,000" }],
        "address": [{ "innerText": "123 Valencia St, San Francisco, CA 94103" }]
      }
    ]
  }
}
```

    
    
**C#:**

    

    **1. Send the request**

```csharp
using System.Net.Http;
using System.Text;
using System.Text.Json;

string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"https://production-sfo.browserless.io/stealth/bql?token={token}&proxy=residential&proxyCountry=us";

var payload = new
{
    query = @"mutation ScrapeRealtor {
      goto(url: ""https://www.realtor.com/realestateandhomes-search/San-Francisco_CA"", waitUntil: networkIdle) { status }
      waitForSelector(selector: ""[data-testid=card-content]"", timeout: 15000) { time }
      properties: mapSelector(selector: ""[data-testid=card-content]"") {
        price: mapSelector(selector: ""[data-testid=card-price]"") { innerText }
        address: mapSelector(selector: ""[data-testid=card-address]"") { innerText }
        beds: mapSelector(selector: ""[data-testid=property-meta-beds] span"") { innerText }
        baths: mapSelector(selector: ""[data-testid=property-meta-baths] span"") { innerText }
        sqft: mapSelector(selector: ""[data-testid=property-meta-sqft] span"") { innerText }
      }
    }",
    variables = new { },
};

using (HttpClient httpClient = new HttpClient())
{
    var content = new StringContent(
        JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
    var response = await httpClient.PostAsync(endpoint, content);
    string body = await response.Content.ReadAsStringAsync();
    Console.WriteLine(body);
}
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 3654 },
    "properties": [
      {
        "price": [{ "innerText": "$1,295,000" }],
        "address": [{ "innerText": "123 Valencia St, San Francisco, CA 94103" }]
      }
    ]
  }
}
```

    
  

  
  
**Frameworks:**

  Connect through stealth mode and a residential proxy so Realtor.com sees traffic from a real browser, then extract property data from the rendered listings.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Connect and scrape**

```js
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint:
    'wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us',
});

try {
  const page = await browser.newPage();
  await page.goto('https://www.realtor.com/realestateandhomes-search/San-Francisco_CA', {
    waitUntil: 'networkidle2',
  });

  await page.waitForSelector('[data-testid="card-content"]');

  const properties = await page.evaluate(() =>
    Array.from(document.querySelectorAll('[data-testid="card-content"]')).map((card) => ({
      price: card.querySelector('[data-testid="card-price"]')?.innerText?.trim() ?? '',
      address: card.querySelector('[data-testid="card-address"]')?.innerText?.trim() ?? '',
      beds: card.querySelector('[data-testid="property-meta-beds"] span')?.innerText?.trim() ?? '',
      baths: card.querySelector('[data-testid="property-meta-baths"] span')?.innerText?.trim() ?? '',
      sqft: card.querySelector('[data-testid="property-meta-sqft"] span')?.innerText?.trim() ?? '',
    }))
  );

  console.log(JSON.stringify(properties, null, 2));
} finally {
  await browser.close();
}
```

    **3. Check the output**

    Run with `node scrape-realtor.mjs`. Each object has `price`, `address`, `beds`, `baths`, and `sqft` fields.

```json
[
  {
    "price": "$1,295,000",
    "address": "123 Valencia St, San Francisco, CA 94103",
    "beds": "3",
    "baths": "2",
    "sqft": "1,450"
  }
]
```

    
    
**Playwright:**

    

    **1. Install dependencies**

```bash
npm install playwright-core
```

    **2. Connect and scrape**

```js
import { chromium } from 'playwright-core';

const browser = await chromium.connectOverCDP(
  'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&stealth&proxy=residential&proxyCountry=us'
);

try {
  const context = browser.contexts()[0];
  const page = await context.newPage();
  await page.goto('https://www.realtor.com/realestateandhomes-search/San-Francisco_CA', {
    waitUntil: 'networkidle',
  });

  await page.waitForSelector('[data-testid="card-content"]');

  const properties = await page.evaluate(() =>
    Array.from(document.querySelectorAll('[data-testid="card-content"]')).map((card) => ({
      price: card.querySelector('[data-testid="card-price"]')?.innerText?.trim() ?? '',
      address: card.querySelector('[data-testid="card-address"]')?.innerText?.trim() ?? '',
      beds: card.querySelector('[data-testid="property-meta-beds"] span')?.innerText?.trim() ?? '',
      baths: card.querySelector('[data-testid="property-meta-baths"] span')?.innerText?.trim() ?? '',
      sqft: card.querySelector('[data-testid="property-meta-sqft"] span')?.innerText?.trim() ?? '',
    }))
  );

  console.log(JSON.stringify(properties, null, 2));
} finally {
  await browser.close();
}
```

    **3. Check the output**

    Run with `node scrape-realtor.mjs`. Each object has `price`, `address`, `beds`, `baths`, and `sqft` fields.

```json
[
  {
    "price": "$1,295,000",
    "address": "123 Valencia St, San Francisco, CA 94103",
    "beds": "3",
    "baths": "2",
    "sqft": "1,450"
  }
]
```

    
  

  
  
**BQL:**

  

  **1. Write the mutation**

  Navigate to Realtor.com's search results, wait for property cards to load, then extract prices, addresses, and property details. We use `/stealth/bql` because Realtor.com's bot detection blocks standard headless browsers.

```graphql
mutation ScrapeRealtor {
  goto(url: "https://www.realtor.com/realestateandhomes-search/San-Francisco_CA", waitUntil: networkIdle) {
    status
  }
  waitForSelector(selector: "[data-testid=card-content]", timeout: 15000) {
    time
  }
  properties: mapSelector(selector: "[data-testid=card-content]") {
    price: mapSelector(selector: "[data-testid=card-price]") { innerText }
    address: mapSelector(selector: "[data-testid=card-address]") { innerText }
    beds: mapSelector(selector: "[data-testid=property-meta-beds] span") { innerText }
    baths: mapSelector(selector: "[data-testid=property-meta-baths] span") { innerText }
    sqft: mapSelector(selector: "[data-testid=property-meta-sqft] span") { innerText }
    status: mapSelector(selector: "[data-testid=card-description]") { innerText }
  }
}
```

  **2. Run it**

  Paste into the [BQL IDE](https://account.browserless.io/bql) and click Run.

  **3. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 3654 },
    "properties": [
      {
        "price": [{ "innerText": "$1,295,000" }],
        "address": [{ "innerText": "123 Valencia St, San Francisco, CA 94103" }],
        "beds": [{ "innerText": "3" }],
        "baths": [{ "innerText": "2" }],
        "sqft": [{ "innerText": "1,450" }],
        "status": [{ "innerText": "For Sale" }]
      },
      {
        "price": [{ "innerText": "$875,000" }],
        "address": [{ "innerText": "456 Guerrero St, San Francisco, CA 94110" }],
        "beds": [{ "innerText": "2" }],
        "baths": [{ "innerText": "1" }],
        "sqft": [{ "innerText": "980" }],
        "status": [{ "innerText": "For Sale" }]
      }
    ]
  }
}
```

  

## Next steps

  <a href="/examples/scrape-amazon" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape Amazon Product Listings</h3>
      scrape another major commercial site with stealth mode
    
  </a>
  <a href="/examples/scrape" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape Structured Data</h3>
      extract data from sites using the `/scrape` endpoint
    
  </a>
  <a href="/examples/cloudflare" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Solving Cloudflare Challenges</h3>
      bypass Cloudflare's interstitial pages before scraping
    
  </a>

---

# Scrape Reddit posts

URL: https://docs.browserless.io/examples/scrape-reddit

# Scrape Reddit posts

Extract post titles, scores, comment counts, and links from any subreddit.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

Reddit's feed is fully JavaScript-rendered and detects automated requests, so a plain HTTP fetch returns no useful data. If you only need basic post data, Reddit's JSON API (`reddit.com/r/{subreddit}.json`) is simpler and doesn't require a browser. Use the browser approach when you need data the API doesn't expose, want to avoid API rate limits, or are scraping user profiles and comment threads.

The examples below target `r/programming` and route through stealth mode and a residential proxy to bypass Reddit's fingerprinting and rate limiting.

> **Selector stability**
> Reddit updates its markup periodically. If `shreddit-post` stops returning results or attributes come back `null`, inspect the live page with browser DevTools to find the current element and attribute names.

  
**AI Agent:**

  Use the [Browserless MCP server](https://github.com/browserless/browserless-mcp) to scrape posts from Reddit from any MCP-compatible AI agent (Claude Desktop, Cursor, Windsurf, ChatGPT, etc.).

  **1. Connect the MCP server**

  Send this prompt to your AI agent to install the Browserless MCP server:

```text
Go to https://github.com/browserless/browserless-mcp/blob/main/install.md
and follow the instructions to install the Browserless MCP server
for my client.
```

  **2. Scrape Reddit**

  Use `browserless_smartscraper`. It handles Reddit's dynamic content and bot protection automatically.

```text
Use the browserless_smartscraper tool to scrape the top posts
from https://www.reddit.com/r/programming
and return the results as markdown
```

  
  
**REST API:**

  Send the BQL mutation over HTTP to the stealth endpoint. No browser library or BQL IDE required.

  
    
**cURL:**

    

    **1. Send the request**

```bash
curl -X POST \
  "https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "mutation ScrapeRedditPosts { goto(url: \"https://www.reddit.com/r/programming/\", waitUntil: networkIdle) { status } waitForSelector(selector: \"shreddit-post\", timeout: 15000) { time } posts: mapSelector(selector: \"shreddit-post\") { title: attribute(name: \"post-title\") { value } score: attribute(name: \"score\") { value } commentCount: attribute(name: \"comment-count\") { value } permalink: attribute(name: \"permalink\") { value } } }",
    "variables": {}
  }'
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 2103 },
    "posts": [
      {
        "title": { "value": "Show HN: I built a static site generator in Go" },
        "score": { "value": "1847" },
        "commentCount": { "value": "143" },
        "permalink": { "value": "/r/programming/comments/abc123/show_hn_i_built_a_static_site_generator" }
      },
      {
        "title": { "value": "Why I still use Vim in 2025" },
        "score": { "value": "923" },
        "commentCount": { "value": "312" },
        "permalink": { "value": "/r/programming/comments/def456/why_i_still_use_vim_in_2025" }
      }
    ]
  }
}
```

    
    
**JavaScript:**

    

    **1. Send the request**

```js
const query = `mutation ScrapeRedditPosts {
  goto(url: "https://www.reddit.com/r/programming/", waitUntil: networkIdle) {
    status
  }
  waitForSelector(selector: "shreddit-post", timeout: 15000) {
    time
  }
  posts: mapSelector(selector: "shreddit-post") {
    title: attribute(name: "post-title") { value }
    score: attribute(name: "score") { value }
    commentCount: attribute(name: "comment-count") { value }
    permalink: attribute(name: "permalink") { value }
  }
}`;

const response = await fetch(
  'https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ query, variables: {} }),
  }
);

const { data } = await response.json();
console.log(JSON.stringify(data.posts, null, 2));
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 2103 },
    "posts": [
      {
        "title": { "value": "Show HN: I built a static site generator in Go" },
        "score": { "value": "1847" },
        "commentCount": { "value": "143" },
        "permalink": { "value": "/r/programming/comments/abc123/show_hn_i_built_a_static_site_generator" }
      },
      {
        "title": { "value": "Why I still use Vim in 2025" },
        "score": { "value": "923" },
        "commentCount": { "value": "312" },
        "permalink": { "value": "/r/programming/comments/def456/why_i_still_use_vim_in_2025" }
      }
    ]
  }
}
```

    
    
**Python:**

    

    **1. Install dependencies**

```bash
pip install requests
```

    **2. Send the request**

```python
import requests

query = """
mutation ScrapeRedditPosts {
  goto(url: "https://www.reddit.com/r/programming/", waitUntil: networkIdle) {
    status
  }
  waitForSelector(selector: "shreddit-post", timeout: 15000) {
    time
  }
  posts: mapSelector(selector: "shreddit-post") {
    title: attribute(name: "post-title") { value }
    score: attribute(name: "score") { value }
    commentCount: attribute(name: "comment-count") { value }
    permalink: attribute(name: "permalink") { value }
  }
}
"""

response = requests.post(
    'https://production-sfo.browserless.io/stealth/bql',
    params={
        'token': 'YOUR_API_TOKEN_HERE',
        'proxy': 'residential',
        'proxyCountry': 'us',
    },
    json={'query': query, 'variables': {}},
)

data = response.json()['data']
for post in data['posts']:
    title = post['title']['value']
    score = post['score']['value']
    comments = post['commentCount']['value']
    permalink = post['permalink']['value']
    print(f'{title} | {score} pts | {comments} comments | reddit.com{permalink}')
```

    **3. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 2103 },
    "posts": [
      {
        "title": { "value": "Show HN: I built a static site generator in Go" },
        "score": { "value": "1847" },
        "commentCount": { "value": "143" },
        "permalink": { "value": "/r/programming/comments/abc123/show_hn_i_built_a_static_site_generator" }
      },
      {
        "title": { "value": "Why I still use Vim in 2025" },
        "score": { "value": "923" },
        "commentCount": { "value": "312" },
        "permalink": { "value": "/r/programming/comments/def456/why_i_still_use_vim_in_2025" }
      }
    ]
  }
}
```

    
    
**Java:**

    

    **1. Send the request**

```java
import java.net.URI;
import java.net.http.*;

String token = "YOUR_API_TOKEN_HERE";
String endpoint = "https://production-sfo.browserless.io/stealth/bql?token=" + token
    + "&proxy=residential&proxyCountry=us";

String query = "mutation ScrapeRedditPosts {"
    + " goto(url: \\\"https://www.reddit.com/r/programming/\\\", waitUntil: networkIdle) { status }"
    + " waitForSelector(selector: \\\"shreddit-post\\\", timeout: 15000) { time }"
    + " posts: mapSelector(selector: \\\"shreddit-post\\\") {"
    + "   title: attribute(name: \\\"post-title\\\") { value }"
    + "   score: attribute(name: \\\"score\\\") { value }"
    + "   commentCount: attribute(name: \\\"comment-count\\\") { value }"
    + "   permalink: attribute(name: \\\"permalink\\\") { value }"
    + " }"
    + " }";

String payload = "{\"query\": \"" + query + "\", \"variables\": {}}";

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(endpoint))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(payload))
    .build();

HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 2103 },
    "posts": [
      {
        "title": { "value": "Show HN: I built a static site generator in Go" },
        "score": { "value": "1847" },
        "commentCount": { "value": "143" },
        "permalink": { "value": "/r/programming/comments/abc123/show_hn_i_built_a_static_site_generator" }
      },
      {
        "title": { "value": "Why I still use Vim in 2025" },
        "score": { "value": "923" },
        "commentCount": { "value": "312" },
        "permalink": { "value": "/r/programming/comments/def456/why_i_still_use_vim_in_2025" }
      }
    ]
  }
}
```

    
    
**C#:**

    

    **1. Send the request**

```csharp
using System.Net.Http;
using System.Text;
using System.Text.Json;

string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"https://production-sfo.browserless.io/stealth/bql?token={token}&proxy=residential&proxyCountry=us";

var payload = new
{
    query = @"mutation ScrapeRedditPosts {
      goto(url: ""https://www.reddit.com/r/programming/"", waitUntil: networkIdle) { status }
      waitForSelector(selector: ""shreddit-post"", timeout: 15000) { time }
      posts: mapSelector(selector: ""shreddit-post"") {
        title: attribute(name: ""post-title"") { value }
        score: attribute(name: ""score"") { value }
        commentCount: attribute(name: ""comment-count"") { value }
        permalink: attribute(name: ""permalink"") { value }
      }
    }",
    variables = new { },
};

using (HttpClient httpClient = new HttpClient())
{
    var content = new StringContent(
        JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
    var response = await httpClient.PostAsync(endpoint, content);
    string body = await response.Content.ReadAsStringAsync();
    Console.WriteLine(body);
}
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 2103 },
    "posts": [
      {
        "title": { "value": "Show HN: I built a static site generator in Go" },
        "score": { "value": "1847" },
        "commentCount": { "value": "143" },
        "permalink": { "value": "/r/programming/comments/abc123/show_hn_i_built_a_static_site_generator" }
      },
      {
        "title": { "value": "Why I still use Vim in 2025" },
        "score": { "value": "923" },
        "commentCount": { "value": "312" },
        "permalink": { "value": "/r/programming/comments/def456/why_i_still_use_vim_in_2025" }
      }
    ]
  }
}
```

    
  

  
  
**Frameworks:**

  Connect through stealth mode and a residential proxy so Reddit sees traffic from what looks like a real browser, then read post data directly from the rendered feed.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Connect and scrape**

```js
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint:
    'wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us',
});

try {
  const page = await browser.newPage();
  await page.goto('https://www.reddit.com/r/programming/', {
    waitUntil: 'networkidle2',
  });

  // Wait for posts to render before reading attributes.
  await page.waitForSelector('shreddit-post');

  const posts = await page.evaluate(() =>
    Array.from(document.querySelectorAll('shreddit-post')).map((post) => ({
      title: post.getAttribute('post-title'),
      score: post.getAttribute('score'),
      commentCount: post.getAttribute('comment-count'),
      permalink: post.getAttribute('permalink'),
    }))
  );

  console.log(JSON.stringify(posts, null, 2));
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Run with `node scrape-reddit.mjs`. An empty array means posts didn't render in time. Increase the `waitForSelector` timeout or check whether a consent banner intercepted the page.

```json
[
  {
    "title": "Show HN: I built a static site generator in Go",
    "score": "1847",
    "commentCount": "143",
    "permalink": "/r/programming/comments/abc123/show_hn_i_built_a_static_site_generator"
  },
  {
    "title": "Why I still use Vim in 2025",
    "score": "923",
    "commentCount": "312",
    "permalink": "/r/programming/comments/def456/why_i_still_use_vim_in_2025"
  }
]
```

    
    
**Playwright:**

    

    **1. Install dependencies**

```bash
npm install playwright-core
```

    **2. Connect and scrape**

```js
import { chromium } from 'playwright-core';

const browser = await chromium.connectOverCDP(
  'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&stealth&proxy=residential&proxyCountry=us'
);

try {
  const context = browser.contexts()[0];
  const page = await context.newPage();
  await page.goto('https://www.reddit.com/r/programming/', {
    waitUntil: 'networkidle',
  });

  // Wait for posts to render before reading attributes.
  await page.waitForSelector('shreddit-post');

  const posts = await page.evaluate(() =>
    Array.from(document.querySelectorAll('shreddit-post')).map((post) => ({
      title: post.getAttribute('post-title'),
      score: post.getAttribute('score'),
      commentCount: post.getAttribute('comment-count'),
      permalink: post.getAttribute('permalink'),
    }))
  );

  console.log(JSON.stringify(posts, null, 2));
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Run with `node scrape-reddit.mjs`. An empty array means posts didn't render in time. Increase the `waitForSelector` timeout or check whether a consent banner intercepted the page.

```json
[
  {
    "title": "Show HN: I built a static site generator in Go",
    "score": "1847",
    "commentCount": "143",
    "permalink": "/r/programming/comments/abc123/show_hn_i_built_a_static_site_generator"
  },
  {
    "title": "Why I still use Vim in 2025",
    "score": "923",
    "commentCount": "312",
    "permalink": "/r/programming/comments/def456/why_i_still_use_vim_in_2025"
  }
]
```

    
  

  
  
**BQL:**

  

  **1. Write the mutation**

  Navigate to the subreddit, wait for posts to appear, then read title, score, comment count, and permalink from each post element's attributes. We send this to `/stealth/bql` instead of the default `/bql` because Reddit's bot detection blocks plain browser sessions.

```graphql
mutation ScrapeRedditPosts {
  goto(url: "https://www.reddit.com/r/programming/", waitUntil: networkIdle) {
    status
  }
  waitForSelector(selector: "shreddit-post", timeout: 15000) {
    time
  }
  posts: mapSelector(selector: "shreddit-post") {
    title: attribute(name: "post-title") { value }
    score: attribute(name: "score") { value }
    commentCount: attribute(name: "comment-count") { value }
    permalink: attribute(name: "permalink") { value }
  }
}
```

  **2. Run it**

  Paste into the [BQL IDE](https://account.browserless.io/bql) and click Run.

  **3. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 2103 },
    "posts": [
      {
        "title": { "value": "Show HN: I built a static site generator in Go" },
        "score": { "value": "1847" },
        "commentCount": { "value": "143" },
        "permalink": { "value": "/r/programming/comments/abc123/show_hn_i_built_a_static_site_generator" }
      },
      {
        "title": { "value": "Why I still use Vim in 2025" },
        "score": { "value": "923" },
        "commentCount": { "value": "312" },
        "permalink": { "value": "/r/programming/comments/def456/why_i_still_use_vim_in_2025" }
      }
    ]
  }
}
```

  

## Next steps

  <a href="/examples/scrape-glassdoor" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape Glassdoor Job Listings</h3>
      another stealth-mode scrape against aggressive bot detection
    
  </a>
  <a href="/examples/google-search" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Automate Google Search</h3>
      pull search results using the same `/stealth/bql` endpoint
    
  </a>
  <a href="/examples/cloudflare" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Solving Cloudflare Challenges</h3>
      bypass Cloudflare's interstitial pages before scraping
    
  </a>

---

# Scrape Ticketmaster events

URL: https://docs.browserless.io/examples/scrape-ticketmaster

# Scrape Ticketmaster events

Search Ticketmaster for events and extract names, dates, venues, and price ranges from the results.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

Ticketmaster renders its search results with JavaScript and uses bot-detection measures. A standard headless browser gets blocked or served incomplete data. The examples below search for concerts and route through stealth mode with a residential proxy.

> **Selector stability**
> Ticketmaster updates its markup frequently. If selectors stop returning results, inspect the live page with browser DevTools to find the current element names.

  
**AI Agent:**

  Use the [Browserless MCP server](https://github.com/browserless/browserless-mcp) to scrape event listings from Ticketmaster from any MCP-compatible AI agent (Claude Desktop, Cursor, Windsurf, ChatGPT, etc.).

  **1. Connect the MCP server**

  Send this prompt to your AI agent to install the Browserless MCP server:

```text
Go to https://github.com/browserless/browserless-mcp/blob/main/install.md
and follow the instructions to install the Browserless MCP server
for my client.
```

  **2. Scrape Ticketmaster**

  Use `browserless_smartscraper`. It handles Ticketmaster's dynamic content and bot protection automatically.

```text
Use the browserless_smartscraper tool to scrape event listings
from https://www.ticketmaster.com/search?q=concerts
and return the results as markdown
```

  
  
**REST API:**

  Send the BQL mutation over HTTP to the stealth endpoint. No browser library or BQL IDE required.

  
    
**cURL:**

    

    **1. Send the request**

```bash
curl -X POST \
  "https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "mutation ScrapeTicketmaster { goto(url: \"https://www.ticketmaster.com/search?q=concerts\", waitUntil: networkIdle) { status } waitForSelector(selector: \"[data-testid=search-event-card]\", timeout: 15000) { time } events: mapSelector(selector: \"[data-testid=search-event-card]\") { name: mapSelector(selector: \"[data-testid=event-name]\") { innerText } date: mapSelector(selector: \"[data-testid=event-date]\") { innerText } venue: mapSelector(selector: \"[data-testid=event-venue]\") { innerText } price: mapSelector(selector: \"[data-testid=event-price]\") { innerText } link: mapSelector(selector: \"a\") { href: attribute(name: \"href\") { value } } } }",
    "variables": {}
  }'
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 3890 },
    "events": [
      {
        "name": [{ "innerText": "Taylor Swift | The Eras Tour" }],
        "date": [{ "innerText": "Aug 15, 2025 7:00 PM" }],
        "venue": [{ "innerText": "SoFi Stadium - Inglewood, CA" }],
        "price": [{ "innerText": "From $249" }],
        "link": [{ "href": { "value": "/event/abc123" } }]
      },
      {
        "name": [{ "innerText": "Beyonce - Renaissance World Tour" }],
        "date": [{ "innerText": "Sep 20, 2025 8:00 PM" }],
        "venue": [{ "innerText": "MetLife Stadium - East Rutherford, NJ" }],
        "price": [{ "innerText": "From $189" }],
        "link": [{ "href": { "value": "/event/def456" } }]
      }
    ]
  }
}
```

    
    
**JavaScript:**

    

    **1. Send the request**

```js
const query = `mutation ScrapeTicketmaster {
  goto(url: "https://www.ticketmaster.com/search?q=concerts", waitUntil: networkIdle) {
    status
  }
  waitForSelector(selector: "[data-testid=search-event-card]", timeout: 15000) {
    time
  }
  events: mapSelector(selector: "[data-testid=search-event-card]") {
    name: mapSelector(selector: "[data-testid=event-name]") { innerText }
    date: mapSelector(selector: "[data-testid=event-date]") { innerText }
    venue: mapSelector(selector: "[data-testid=event-venue]") { innerText }
    price: mapSelector(selector: "[data-testid=event-price]") { innerText }
    link: mapSelector(selector: "a") {
      href: attribute(name: "href") { value }
    }
  }
}`;

const response = await fetch(
  'https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ query, variables: {} }),
  }
);

const { data } = await response.json();
console.log(JSON.stringify(data.events, null, 2));
```

    **2. Check the output**

```json
[
  {
    "name": [{ "innerText": "Taylor Swift | The Eras Tour" }],
    "date": [{ "innerText": "Aug 15, 2025 7:00 PM" }],
    "venue": [{ "innerText": "SoFi Stadium - Inglewood, CA" }],
    "price": [{ "innerText": "From $249" }]
  },
  {
    "name": [{ "innerText": "Beyoncé | Renaissance World Tour" }],
    "date": [{ "innerText": "Sep 20, 2025 8:00 PM" }],
    "venue": [{ "innerText": "MetLife Stadium - East Rutherford, NJ" }],
    "price": [{ "innerText": "From $175" }]
  }
]
```

    
    
**Python:**

    

    **1. Install dependencies**

```bash
pip install requests
```

    **2. Send the request**

```python
import requests

query = """
mutation ScrapeTicketmaster {
  goto(url: "https://www.ticketmaster.com/search?q=concerts", waitUntil: networkIdle) {
    status
  }
  waitForSelector(selector: "[data-testid=search-event-card]", timeout: 15000) {
    time
  }
  events: mapSelector(selector: "[data-testid=search-event-card]") {
    name: mapSelector(selector: "[data-testid=event-name]") { innerText }
    date: mapSelector(selector: "[data-testid=event-date]") { innerText }
    venue: mapSelector(selector: "[data-testid=event-venue]") { innerText }
    price: mapSelector(selector: "[data-testid=event-price]") { innerText }
    link: mapSelector(selector: "a") {
      href: attribute(name: "href") { value }
    }
  }
}
"""

response = requests.post(
    'https://production-sfo.browserless.io/stealth/bql',
    params={
        'token': 'YOUR_API_TOKEN_HERE',
        'proxy': 'residential',
        'proxyCountry': 'us',
    },
    json={'query': query, 'variables': {}},
)

data = response.json()['data']
for event in data['events']:
    name = event['name'][0]['innerText']
    date = event['date'][0]['innerText']
    venue = event['venue'][0]['innerText']
    price = event['price'][0]['innerText'] if event['price'] else 'See site'
    print(f'{name} | {date} | {venue} | {price}')
```

    **3. Check the output**

```text
Taylor Swift | The Eras Tour | Aug 15, 2025 7:00 PM | SoFi Stadium - Inglewood, CA | From $249
Beyoncé | Renaissance World Tour | Sep 20, 2025 8:00 PM | MetLife Stadium - East Rutherford, NJ | From $175
```

    
    
**Java:**

    

    **1. Send the request**

```java
import java.net.URI;
import java.net.http.*;

String token = "YOUR_API_TOKEN_HERE";
String endpoint = "https://production-sfo.browserless.io/stealth/bql?token=" + token
    + "&proxy=residential&proxyCountry=us";

String query = "mutation ScrapeTicketmaster {"
    + " goto(url: \\\"https://www.ticketmaster.com/search?q=concerts\\\", waitUntil: networkIdle) { status }"
    + " waitForSelector(selector: \\\"[data-testid=search-event-card]\\\", timeout: 15000) { time }"
    + " events: mapSelector(selector: \\\"[data-testid=search-event-card]\\\") {"
    + "   name: mapSelector(selector: \\\"[data-testid=event-name]\\\") { innerText }"
    + "   date: mapSelector(selector: \\\"[data-testid=event-date]\\\") { innerText }"
    + "   venue: mapSelector(selector: \\\"[data-testid=event-venue]\\\") { innerText }"
    + "   price: mapSelector(selector: \\\"[data-testid=event-price]\\\") { innerText }"
    + " }"
    + " }";

String payload = "{\"query\": \"" + query + "\", \"variables\": {}}";

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(endpoint))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(payload))
    .build();

HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 3890 },
    "events": [
      {
        "name": [{ "innerText": "Taylor Swift | The Eras Tour" }],
        "date": [{ "innerText": "Aug 15, 2025 7:00 PM" }]
      }
    ]
  }
}
```

    
    
**C#:**

    

    **1. Send the request**

```csharp
using System.Net.Http;
using System.Text;
using System.Text.Json;

string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"https://production-sfo.browserless.io/stealth/bql?token={token}&proxy=residential&proxyCountry=us";

var payload = new
{
    query = @"mutation ScrapeTicketmaster {
      goto(url: ""https://www.ticketmaster.com/search?q=concerts"", waitUntil: networkIdle) { status }
      waitForSelector(selector: ""[data-testid=search-event-card]"", timeout: 15000) { time }
      events: mapSelector(selector: ""[data-testid=search-event-card]"") {
        name: mapSelector(selector: ""[data-testid=event-name]"") { innerText }
        date: mapSelector(selector: ""[data-testid=event-date]"") { innerText }
        venue: mapSelector(selector: ""[data-testid=event-venue]"") { innerText }
        price: mapSelector(selector: ""[data-testid=event-price]"") { innerText }
        link: mapSelector(selector: ""a"") {
          href: attribute(name: ""href"") { value }
        }
      }
    }",
    variables = new { },
};

using (HttpClient httpClient = new HttpClient())
{
    var content = new StringContent(
        JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
    var response = await httpClient.PostAsync(endpoint, content);
    string body = await response.Content.ReadAsStringAsync();
    Console.WriteLine(body);
}
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 3890 },
    "events": [
      {
        "name": [{ "innerText": "Taylor Swift | The Eras Tour" }],
        "date": [{ "innerText": "Aug 15, 2025 7:00 PM" }]
      }
    ]
  }
}
```

    
  

  
  
**Frameworks:**

  Connect through stealth mode and a residential proxy so Ticketmaster sees traffic from a real browser, then extract event data from the rendered search results.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Connect and scrape**

```js
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint:
    'wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us',
});

try {
  const page = await browser.newPage();
  await page.goto('https://www.ticketmaster.com/search?q=concerts', {
    waitUntil: 'networkidle2',
  });

  await page.waitForSelector('[data-testid="search-event-card"]');

  const events = await page.evaluate(() =>
    Array.from(document.querySelectorAll('[data-testid="search-event-card"]')).map((card) => ({
      name: card.querySelector('[data-testid="event-name"]')?.innerText?.trim() ?? '',
      date: card.querySelector('[data-testid="event-date"]')?.innerText?.trim() ?? '',
      venue: card.querySelector('[data-testid="event-venue"]')?.innerText?.trim() ?? '',
      price: card.querySelector('[data-testid="event-price"]')?.innerText?.trim() ?? '',
      link: card.querySelector('a')?.href ?? '',
    }))
  );

  console.log(JSON.stringify(events, null, 2));
} finally {
  await browser.close();
}
```

    **3. Check the output**

    Run with `node scrape-ticketmaster.mjs`. Each object has `name`, `date`, `venue`, `price`, and `link` fields.

```json
[
  {
    "name": "Taylor Swift | The Eras Tour",
    "date": "Aug 15, 2025 7:00 PM",
    "venue": "SoFi Stadium - Inglewood, CA",
    "price": "From $249",
    "link": "https://www.ticketmaster.com/event/abc123"
  }
]
```

    
    
**Playwright:**

    

    **1. Install dependencies**

```bash
npm install playwright-core
```

    **2. Connect and scrape**

```js
import { chromium } from 'playwright-core';

const browser = await chromium.connectOverCDP(
  'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&stealth&proxy=residential&proxyCountry=us'
);

try {
  const context = browser.contexts()[0];
  const page = await context.newPage();
  await page.goto('https://www.ticketmaster.com/search?q=concerts', {
    waitUntil: 'networkidle',
  });

  await page.waitForSelector('[data-testid="search-event-card"]');

  const events = await page.evaluate(() =>
    Array.from(document.querySelectorAll('[data-testid="search-event-card"]')).map((card) => ({
      name: card.querySelector('[data-testid="event-name"]')?.innerText?.trim() ?? '',
      date: card.querySelector('[data-testid="event-date"]')?.innerText?.trim() ?? '',
      venue: card.querySelector('[data-testid="event-venue"]')?.innerText?.trim() ?? '',
      price: card.querySelector('[data-testid="event-price"]')?.innerText?.trim() ?? '',
      link: card.querySelector('a')?.href ?? '',
    }))
  );

  console.log(JSON.stringify(events, null, 2));
} finally {
  await browser.close();
}
```

    **3. Check the output**

    Run with `node scrape-ticketmaster.mjs`. Each object has `name`, `date`, `venue`, `price`, and `link` fields.

```json
[
  {
    "name": "Taylor Swift | The Eras Tour",
    "date": "Aug 15, 2025 7:00 PM",
    "venue": "SoFi Stadium - Inglewood, CA",
    "price": "From $249",
    "link": "https://www.ticketmaster.com/event/abc123"
  }
]
```

    
  

  
  
**BQL:**

  

  **1. Write the mutation**

  Navigate to Ticketmaster's search results, wait for event cards to render, then extract event details. We use `/stealth/bql` because Ticketmaster's bot detection blocks standard headless browsers.

```graphql
mutation ScrapeTicketmaster {
  goto(url: "https://www.ticketmaster.com/search?q=concerts", waitUntil: networkIdle) {
    status
  }
  waitForSelector(selector: "[data-testid=search-event-card]", timeout: 15000) {
    time
  }
  events: mapSelector(selector: "[data-testid=search-event-card]") {
    name: mapSelector(selector: "[data-testid=event-name]") { innerText }
    date: mapSelector(selector: "[data-testid=event-date]") { innerText }
    venue: mapSelector(selector: "[data-testid=event-venue]") { innerText }
    price: mapSelector(selector: "[data-testid=event-price]") { innerText }
    link: mapSelector(selector: "a") {
      href: attribute(name: "href") { value }
    }
  }
}
```

  **2. Run it**

  Paste into the [BQL IDE](https://account.browserless.io/bql) and click Run.

  **3. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 3890 },
    "events": [
      {
        "name": [{ "innerText": "Taylor Swift | The Eras Tour" }],
        "date": [{ "innerText": "Aug 15, 2025 7:00 PM" }],
        "venue": [{ "innerText": "SoFi Stadium - Inglewood, CA" }],
        "price": [{ "innerText": "From $249" }],
        "link": [{ "href": { "value": "/event/abc123" } }]
      },
      {
        "name": [{ "innerText": "Beyonce - Renaissance World Tour" }],
        "date": [{ "innerText": "Sep 20, 2025 8:00 PM" }],
        "venue": [{ "innerText": "MetLife Stadium - East Rutherford, NJ" }],
        "price": [{ "innerText": "From $189" }],
        "link": [{ "href": { "value": "/event/def456" } }]
      }
    ]
  }
}
```

  

## Next steps

  <a href="/examples/scrape-amazon" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape Amazon Product Listings</h3>
      scrape another major commercial site with stealth mode
    
  </a>
  <a href="/examples/scrape-glassdoor" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape Glassdoor Job Listings</h3>
      stealth-mode scraping against aggressive bot detection
    
  </a>
  <a href="/examples/cloudflare" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Solving Cloudflare Challenges</h3>
      bypass Cloudflare's interstitial pages before scraping
    
  </a>

---

# Scrape Walmart product listings

URL: https://docs.browserless.io/examples/scrape-walmart

# Scrape Walmart product listings

Search Walmart and extract product names, prices, ratings, and review counts.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

> **Selector stability**
> Walmart updates its markup regularly. If selectors stop returning results, open the page in browser DevTools and inspect the current DOM structure to update them.

  
**AI Agent:**

  Use the [Browserless MCP server](https://github.com/browserless/browserless-mcp) to scrape product listings from Walmart from any MCP-compatible AI agent (Claude Desktop, Cursor, Windsurf, ChatGPT, etc.).

  **1. Connect the MCP server**

  Send this prompt to your AI agent to install the Browserless MCP server:

```text
Go to https://github.com/browserless/browserless-mcp/blob/main/install.md
and follow the instructions to install the Browserless MCP server
for my client.
```

  **2. Scrape Walmart**

  Use `browserless_smartscraper`. Walmart has aggressive bot detection and smartscraper handles it automatically.

```text
Use the browserless_smartscraper tool to scrape product listings
from https://www.walmart.com/search?q=laptop
and return the results as markdown
```

  
  
**REST API:**

  Send the BQL mutation over HTTP to the stealth endpoint. No browser library or BQL IDE required.

  
    
**cURL:**

    **1. Send the request**

```bash
curl -X POST \
  "https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "mutation ScrapeWalmart { goto(url: \"https://www.walmart.com/search?q=iphone\", waitUntil: networkIdle) { status } waitForTimeout(time: 3000) { time } products: mapSelector(selector: \"[data-item-id]\") { name: mapSelector(selector: \"[itemprop='\''name'\'']\") { innerText } price: mapSelector(selector: \"[itemprop='\''price'\'']\") { innerText } rating: mapSelector(selector: \"span[itemprop='\''ratingValue'\'']\") { innerText } reviewCount: mapSelector(selector: \"[itemprop='\''ratingCount'\'']\") { innerText } link: mapSelector(selector: \"a[href*='\''\/ip\/'\'']\") { href: attribute(name: \"href\") { value } } } }",
    "variables": {},
    "operationName": "ScrapeWalmart"
  }'
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForTimeout": { "time": 3000 },
    "products": [
      {
        "name": [{ "innerText": "Apple iPhone 15, 128GB, Black - Unlocked" }],
        "price": [{ "innerText": "$699.00" }],
        "rating": [{ "innerText": "4.6" }],
        "reviewCount": [{ "innerText": "2847" }],
        "link": [{ "href": { "value": "https://www.walmart.com/ip/Apple-iPhone-15/1752657432" } }]
      },
      {
        "name": [{ "innerText": "Apple iPhone 14, 128GB, Midnight - Unlocked" }],
        "price": [{ "innerText": "$499.00" }],
        "rating": [{ "innerText": "4.5" }],
        "reviewCount": [{ "innerText": "5120" }],
        "link": [{ "href": { "value": "https://www.walmart.com/ip/Apple-iPhone-14/633586395" } }]
      }
    ]
  }
}
```

    
    
**JavaScript:**

    **1. Send the request**

```js
const query = `mutation ScrapeWalmart {
  goto(url: "https://www.walmart.com/search?q=iphone", waitUntil: networkIdle) {
    status
  }
  waitForTimeout(time: 3000) {
    time
  }
  products: mapSelector(selector: "[data-item-id]") {
    name: mapSelector(selector: "[itemprop='name']") {
      innerText
    }
    price: mapSelector(selector: "[itemprop='price']") {
      innerText
    }
    rating: mapSelector(selector: "span[itemprop='ratingValue']") {
      innerText
    }
    reviewCount: mapSelector(selector: "[itemprop='ratingCount']") {
      innerText
    }
    link: mapSelector(selector: "a[href*='/ip/']") {
      href: attribute(name: "href") {
        value
      }
    }
  }
}`;

const response = await fetch(
  'https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ query, variables: {}, operationName: 'ScrapeWalmart' }),
  }
);

const { data } = await response.json();
const products = data.products.map((p) => ({
  name: p.name?.[0]?.innerText ?? '',
  price: p.price?.[0]?.innerText ?? '',
  rating: p.rating?.[0]?.innerText ?? '',
  reviewCount: p.reviewCount?.[0]?.innerText ?? '',
  link: p.link?.[0]?.href?.value ?? '',
}));
console.log(JSON.stringify(products, null, 2));
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForTimeout": { "time": 3000 },
    "products": [
      {
        "name": [{ "innerText": "Apple iPhone 15, 128GB, Black - Unlocked" }],
        "price": [{ "innerText": "$699.00" }],
        "rating": [{ "innerText": "4.6" }],
        "reviewCount": [{ "innerText": "2847" }],
        "link": [{ "href": { "value": "https://www.walmart.com/ip/Apple-iPhone-15/1752657432" } }]
      },
      {
        "name": [{ "innerText": "Apple iPhone 14, 128GB, Midnight - Unlocked" }],
        "price": [{ "innerText": "$499.00" }],
        "rating": [{ "innerText": "4.5" }],
        "reviewCount": [{ "innerText": "5120" }],
        "link": [{ "href": { "value": "https://www.walmart.com/ip/Apple-iPhone-14/633586395" } }]
      }
    ]
  }
}
```

    
    
**Python:**

    **1. Install dependencies**

```bash
pip install requests
```

    **2. Send the request**

```python
import requests

query = """
mutation ScrapeWalmart {
  goto(url: "https://www.walmart.com/search?q=iphone", waitUntil: networkIdle) {
    status
  }
  waitForTimeout(time: 3000) {
    time
  }
  products: mapSelector(selector: "[data-item-id]") {
    name: mapSelector(selector: "[itemprop='name']") {
      innerText
    }
    price: mapSelector(selector: "[itemprop='price']") {
      innerText
    }
    rating: mapSelector(selector: "span[itemprop='ratingValue']") {
      innerText
    }
    reviewCount: mapSelector(selector: "[itemprop='ratingCount']") {
      innerText
    }
    link: mapSelector(selector: "a[href*='/ip/']") {
      href: attribute(name: "href") {
        value
      }
    }
  }
}
"""

response = requests.post(
    'https://production-sfo.browserless.io/stealth/bql',
    params={
        'token': 'YOUR_API_TOKEN_HERE',
        'proxy': 'residential',
        'proxyCountry': 'us',
    },
    json={'query': query, 'variables': {}, 'operationName': 'ScrapeWalmart'},
)

data = response.json()['data']
for product in data['products']:
    name = product['name'][0]['innerText'] if product['name'] else ''
    price = product['price'][0]['innerText'] if product['price'] else ''
    rating = product['rating'][0]['innerText'] if product['rating'] else ''
    print(f'{name} — {price} — ★{rating}')
```

    **3. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForTimeout": { "time": 3000 },
    "products": [
      {
        "name": [{ "innerText": "Apple iPhone 15, 128GB, Black - Unlocked" }],
        "price": [{ "innerText": "$699.00" }],
        "rating": [{ "innerText": "4.6" }],
        "reviewCount": [{ "innerText": "2847" }],
        "link": [{ "href": { "value": "https://www.walmart.com/ip/Apple-iPhone-15/1752657432" } }]
      },
      {
        "name": [{ "innerText": "Apple iPhone 14, 128GB, Midnight - Unlocked" }],
        "price": [{ "innerText": "$499.00" }],
        "rating": [{ "innerText": "4.5" }],
        "reviewCount": [{ "innerText": "5120" }],
        "link": [{ "href": { "value": "https://www.walmart.com/ip/Apple-iPhone-14/633586395" } }]
      }
    ]
  }
}
```

    
    
**Java:**

    **1. Send the request**

```java
import java.net.URI;
import java.net.http.*;

String token = "YOUR_API_TOKEN_HERE";
String endpoint = "https://production-sfo.browserless.io/stealth/bql?token="
    + token + "&proxy=residential&proxyCountry=us";

String query = """
    mutation ScrapeWalmart {
      goto(url: "https://www.walmart.com/search?q=iphone", waitUntil: networkIdle) { status }
      waitForTimeout(time: 3000) { time }
      products: mapSelector(selector: "[data-item-id]") {
        name: mapSelector(selector: "[itemprop='name']") { innerText }
        price: mapSelector(selector: "[itemprop='price']") { innerText }
        rating: mapSelector(selector: "span[itemprop='ratingValue']") { innerText }
        reviewCount: mapSelector(selector: "[itemprop='ratingCount']") { innerText }
        link: mapSelector(selector: "a[href*='/ip/']") { href: attribute(name: "href") { value } }
      }
    }
    """;

String payload = "{\"query\": " + com.google.gson.JsonParser.parseString(query) + ", \"variables\": {}, \"operationName\": \"ScrapeWalmart\"}";

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(endpoint))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(payload))
    .build();

HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForTimeout": { "time": 3000 },
    "products": [
      {
        "name": [{ "innerText": "Apple iPhone 15, 128GB, Black - Unlocked" }],
        "price": [{ "innerText": "$699.00" }],
        "rating": [{ "innerText": "4.6" }],
        "reviewCount": [{ "innerText": "2847" }],
        "link": [{ "href": { "value": "https://www.walmart.com/ip/Apple-iPhone-15/1752657432" } }]
      },
      {
        "name": [{ "innerText": "Apple iPhone 14, 128GB, Midnight - Unlocked" }],
        "price": [{ "innerText": "$499.00" }],
        "rating": [{ "innerText": "4.5" }],
        "reviewCount": [{ "innerText": "5120" }],
        "link": [{ "href": { "value": "https://www.walmart.com/ip/Apple-iPhone-14/633586395" } }]
      }
    ]
  }
}
```

    
    
**C#:**

    **1. Send the request**

```csharp
using System.Net.Http;
using System.Text;
using System.Text.Json;

string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"https://production-sfo.browserless.io/stealth/bql?token={token}&proxy=residential&proxyCountry=us";

var payload = new
{
    query = @"mutation ScrapeWalmart {
      goto(url: ""https://www.walmart.com/search?q=iphone"", waitUntil: networkIdle) { status }
      waitForTimeout(time: 3000) { time }
      products: mapSelector(selector: ""[data-item-id]"") {
        name: mapSelector(selector: ""[itemprop='name']"") { innerText }
        price: mapSelector(selector: ""[itemprop='price']"") { innerText }
        rating: mapSelector(selector: ""span[itemprop='ratingValue']"") { innerText }
        reviewCount: mapSelector(selector: ""[itemprop='ratingCount']"") { innerText }
        link: mapSelector(selector: ""a[href*='/ip/']"") { href: attribute(name: ""href"") { value } }
      }
    }",
    variables = new { },
    operationName = "ScrapeWalmart",
};

using (HttpClient httpClient = new HttpClient())
{
    var content = new StringContent(
        JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
    var response = await httpClient.PostAsync(endpoint, content);
    string body = await response.Content.ReadAsStringAsync();
    Console.WriteLine(body);
}
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForTimeout": { "time": 3000 },
    "products": [
      {
        "name": [{ "innerText": "Apple iPhone 15, 128GB, Black - Unlocked" }],
        "price": [{ "innerText": "$699.00" }],
        "rating": [{ "innerText": "4.6" }],
        "reviewCount": [{ "innerText": "2847" }],
        "link": [{ "href": { "value": "https://www.walmart.com/ip/Apple-iPhone-15/1752657432" } }]
      },
      {
        "name": [{ "innerText": "Apple iPhone 14, 128GB, Midnight - Unlocked" }],
        "price": [{ "innerText": "$499.00" }],
        "rating": [{ "innerText": "4.5" }],
        "reviewCount": [{ "innerText": "5120" }],
        "link": [{ "href": { "value": "https://www.walmart.com/ip/Apple-iPhone-14/633586395" } }]
      }
    ]
  }
}
```

    
  

  
  
**Frameworks:**

  Connect through stealth mode and a residential proxy. This makes the browser fingerprint and IP look like a real US visitor, which is what gets past Walmart's bot detection. Then extract product data from the rendered page.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Connect and scrape**

```js
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint:
    'wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us',
});

try {
  const page = await browser.newPage();
  await page.goto('https://www.walmart.com/search?q=iphone', {
    waitUntil: 'networkidle2',
  });

  const products = await page.evaluate(() =>
    Array.from(document.querySelectorAll('[data-item-id]')).map((card) => ({
      name: card.querySelector('[itemprop="name"]')?.innerText?.trim() ?? '',
      price: card.querySelector('[itemprop="price"]')?.getAttribute('content')
        ?? card.querySelector('[itemprop="price"]')?.innerText?.trim() ?? '',
      rating: card.querySelector('span[itemprop="ratingValue"]')?.innerText?.trim() ?? '',
      reviewCount: card.querySelector('[itemprop="ratingCount"]')?.innerText?.trim() ?? '',
      link: card.querySelector('a[href*="/ip/"]')?.href ?? '',
    }))
  );

  console.log(JSON.stringify(products, null, 2));
} finally {
  await browser.close();
}
```

    **3. Check the output**

    Run with `node scrape-walmart.mjs`. Each object in the array contains `name`, `price`, `rating`, `reviewCount`, and `link` for one product.

    
    
**Playwright:**

    

    **1. Install dependencies**

```bash
npm install playwright-core
```

    **2. Connect and scrape**

```js
import { chromium } from 'playwright-core';

const browser = await chromium.connectOverCDP(
  'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&stealth&proxy=residential&proxyCountry=us'
);

try {
  const context = browser.contexts()[0];
  const page = await context.newPage();
  await page.goto('https://www.walmart.com/search?q=iphone', {
    waitUntil: 'networkidle',
  });

  const products = await page.evaluate(() =>
    Array.from(document.querySelectorAll('[data-item-id]')).map((card) => ({
      name: card.querySelector('[itemprop="name"]')?.innerText?.trim() ?? '',
      price: card.querySelector('[itemprop="price"]')?.getAttribute('content')
        ?? card.querySelector('[itemprop="price"]')?.innerText?.trim() ?? '',
      rating: card.querySelector('span[itemprop="ratingValue"]')?.innerText?.trim() ?? '',
      reviewCount: card.querySelector('[itemprop="ratingCount"]')?.innerText?.trim() ?? '',
      link: card.querySelector('a[href*="/ip/"]')?.href ?? '',
    }))
  );

  console.log(JSON.stringify(products, null, 2));
} finally {
  await browser.close();
}
```

    **3. Check the output**

    Run with `node scrape-walmart.mjs`. Each object in the array contains `name`, `price`, `rating`, `reviewCount`, and `link` for one product.

    
  

  
  
**BQL:**

  

  **1. Write the mutation**

  Navigate to the Walmart search results URL and use `mapSelector` to pull structured data out of each product card. We use the stealth endpoint here because the plain BQL endpoint doesn't apply browser fingerprint masking. The `waitForTimeout` gives JS-rendered content time to settle after `networkIdle` fires.

```graphql
mutation ScrapeWalmart {
  goto(url: "https://www.walmart.com/search?q=iphone", waitUntil: networkIdle) {
    status
  }

  waitForTimeout(time: 3000) {
    time
  }

  products: mapSelector(selector: "[data-item-id]") {
    name: mapSelector(selector: "[itemprop='name']") {
      innerText
    }
    price: mapSelector(selector: "[itemprop='price']") {
      innerText
    }
    rating: mapSelector(selector: "span[itemprop='ratingValue']") {
      innerText
    }
    reviewCount: mapSelector(selector: "[itemprop='ratingCount']") {
      innerText
    }
    link: mapSelector(selector: "a[href*='/ip/']") {
      href: attribute(name: "href") {
        value
      }
    }
  }
}
```

  **2. Run it**

  Paste into the [BQL IDE](https://account.browserless.io/bql) and click Run.

  **3. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForTimeout": { "time": 3000 },
    "products": [
      {
        "name": [{ "innerText": "Apple iPhone 15, 128GB, Black - Unlocked" }],
        "price": [{ "innerText": "$699.00" }],
        "rating": [{ "innerText": "4.6" }],
        "reviewCount": [{ "innerText": "2847" }],
        "link": [{ "href": { "value": "https://www.walmart.com/ip/Apple-iPhone-15/1752657432" } }]
      },
      {
        "name": [{ "innerText": "Apple iPhone 14, 128GB, Midnight - Unlocked" }],
        "price": [{ "innerText": "$499.00" }],
        "rating": [{ "innerText": "4.5" }],
        "reviewCount": [{ "innerText": "5120" }],
        "link": [{ "href": { "value": "https://www.walmart.com/ip/Apple-iPhone-14/633586395" } }]
      }
    ]
  }
}
```

  

## Next steps

  <a href="/examples/scrape-etsy" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape Etsy Product Listings</h3>
      extract product data from another bot-protected marketplace
    
  </a>
  <a href="/examples/scrape-glassdoor" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape Glassdoor Job Listings</h3>
      apply the same stealth pattern to job listings
    
  </a>
  <a href="/examples/proxy" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Change Your Browser's IP Address Using Proxies</h3>
      control which IP address target sites see
    
  </a>

---

# Scrape Yelp business listings

URL: https://docs.browserless.io/examples/scrape-yelp

# Scrape Yelp business listings

Search Yelp for businesses and extract names, ratings, review counts, categories, and price ranges.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

Yelp renders its search results with JavaScript and has bot-detection measures. The examples below search for pizza in New York and route through stealth mode with a residential proxy.

> **Selector stability**
> Yelp updates its markup periodically. If selectors stop returning results, inspect the live page with browser DevTools to find the current element and attribute names.

  
**AI Agent:**

  Use the [Browserless MCP server](https://github.com/browserless/browserless-mcp) to scrape business listings from Yelp from any MCP-compatible AI agent (Claude Desktop, Cursor, Windsurf, ChatGPT, etc.).

  **1. Connect the MCP server**

  Send this prompt to your AI agent to install the Browserless MCP server:

```text
Go to https://github.com/browserless/browserless-mcp/blob/main/install.md
and follow the instructions to install the Browserless MCP server
for my client.
```

  **2. Scrape Yelp**

  Use `browserless_smartscraper`. It handles Yelp's dynamic content and bot protection automatically.

```text
Use the browserless_smartscraper tool to scrape business listings
from https://www.yelp.com/search?find_desc=pizza&find_loc=New+York%2C+NY
and return the results as markdown
```

  
  
**REST API:**

  Send the BQL mutation over HTTP to the stealth endpoint. No browser library or BQL IDE required.

  
    
**cURL:**

    

    **1. Send the request**

```bash
curl -X POST \
  "https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "mutation ScrapeYelp { goto(url: \"https://www.yelp.com/search?find_desc=pizza&find_loc=New+York%2C+NY\", waitUntil: networkIdle) { status } waitForSelector(selector: \"[data-testid=serp-ia-card]\", timeout: 15000) { time } businesses: mapSelector(selector: \"[data-testid=serp-ia-card]\") { name: mapSelector(selector: \"a[class*=businessName] span\") { innerText } rating: mapSelector(selector: \"[aria-label*=star]\") { ratingLabel: attribute(name: \"aria-label\") { value } } reviewCount: mapSelector(selector: \"span[class*=reviewCount]\") { innerText } categories: mapSelector(selector: \"a[class*=categoryLink]\") { innerText } priceRange: mapSelector(selector: \"span[class*=priceRange]\") { innerText } } }",
    "variables": {}
  }'
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 3210 },
    "businesses": [
      {
        "name": [{ "innerText": "Joe's Pizza" }],
        "rating": [{ "ratingLabel": { "value": "4.5 star rating" } }],
        "reviewCount": [{ "innerText": "1,247" }],
        "categories": [{ "innerText": "Pizza" }],
        "priceRange": [{ "innerText": "$" }]
      },
      {
        "name": [{ "innerText": "Di Fara Pizza" }],
        "rating": [{ "ratingLabel": { "value": "4.0 star rating" } }],
        "reviewCount": [{ "innerText": "3,891" }],
        "categories": [{ "innerText": "Pizza" }],
        "priceRange": [{ "innerText": "$$" }]
      }
    ]
  }
}
```

    
    
**JavaScript:**

    

    **1. Send the request**

```js
const query = `mutation ScrapeYelp {
  goto(url: "https://www.yelp.com/search?find_desc=pizza&find_loc=New+York%2C+NY", waitUntil: networkIdle) {
    status
  }
  waitForSelector(selector: "[data-testid=serp-ia-card]", timeout: 15000) {
    time
  }
  businesses: mapSelector(selector: "[data-testid=serp-ia-card]") {
    name: mapSelector(selector: "a[class*=businessName] span") { innerText }
    rating: mapSelector(selector: "[aria-label*=star]") {
      ratingLabel: attribute(name: "aria-label") { value }
    }
    reviewCount: mapSelector(selector: "span[class*=reviewCount]") { innerText }
    categories: mapSelector(selector: "a[class*=categoryLink]") { innerText }
    priceRange: mapSelector(selector: "span[class*=priceRange]") { innerText }
  }
}`;

const response = await fetch(
  'https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ query, variables: {} }),
  }
);

const { data } = await response.json();
console.log(JSON.stringify(data.businesses, null, 2));
```

    **2. Check the output**

```json
[
  {
    "name": [{ "innerText": "Joe's Pizza" }],
    "rating": [{ "ratingLabel": { "value": "4.5 star rating" } }],
    "reviewCount": [{ "innerText": "1,247" }],
    "categories": [{ "innerText": "Pizza" }],
    "priceRange": [{ "innerText": "$" }]
  },
  {
    "name": [{ "innerText": "Di Fara Pizza" }],
    "rating": [{ "ratingLabel": { "value": "4.0 star rating" } }],
    "reviewCount": [{ "innerText": "3,891" }],
    "categories": [{ "innerText": "Pizza" }],
    "priceRange": [{ "innerText": "$$" }]
  }
]
```

    
    
**Python:**

    

    **1. Install dependencies**

```bash
pip install requests
```

    **2. Send the request**

```python
import requests

query = """
mutation ScrapeYelp {
  goto(url: "https://www.yelp.com/search?find_desc=pizza&find_loc=New+York%2C+NY", waitUntil: networkIdle) {
    status
  }
  waitForSelector(selector: "[data-testid=serp-ia-card]", timeout: 15000) {
    time
  }
  businesses: mapSelector(selector: "[data-testid=serp-ia-card]") {
    name: mapSelector(selector: "a[class*=businessName] span") { innerText }
    rating: mapSelector(selector: "[aria-label*=star]") {
      ratingLabel: attribute(name: "aria-label") { value }
    }
    reviewCount: mapSelector(selector: "span[class*=reviewCount]") { innerText }
    categories: mapSelector(selector: "a[class*=categoryLink]") { innerText }
    priceRange: mapSelector(selector: "span[class*=priceRange]") { innerText }
  }
}
"""

response = requests.post(
    'https://production-sfo.browserless.io/stealth/bql',
    params={
        'token': 'YOUR_API_TOKEN_HERE',
        'proxy': 'residential',
        'proxyCountry': 'us',
    },
    json={'query': query, 'variables': {}},
)

data = response.json()['data']
for biz in data['businesses']:
    name = biz['name'][0]['innerText']
    rating = biz['rating'][0]['ratingLabel']['value']
    reviews = biz['reviewCount'][0]['innerText']
    print(f'{name} | {rating} | {reviews} reviews')
```

    **3. Check the output**

```text
Joe's Pizza | 4.5 star rating | 1,247 reviews
Di Fara Pizza | 4.0 star rating | 3,891 reviews
```

    
    
**Java:**

    

    **1. Send the request**

```java
import java.net.URI;
import java.net.http.*;

String token = "YOUR_API_TOKEN_HERE";
String endpoint = "https://production-sfo.browserless.io/stealth/bql?token=" + token
    + "&proxy=residential&proxyCountry=us";

String query = "mutation ScrapeYelp {"
    + " goto(url: \\\"https://www.yelp.com/search?find_desc=pizza&find_loc=New+York%2C+NY\\\", waitUntil: networkIdle) { status }"
    + " waitForSelector(selector: \\\"[data-testid=serp-ia-card]\\\", timeout: 15000) { time }"
    + " businesses: mapSelector(selector: \\\"[data-testid=serp-ia-card]\\\") {"
    + "   name: mapSelector(selector: \\\"a[class*=businessName] span\\\") { innerText }"
    + "   rating: mapSelector(selector: \\\"[aria-label*=star]\\\") { ratingLabel: attribute(name: \\\"aria-label\\\") { value } }"
    + "   reviewCount: mapSelector(selector: \\\"span[class*=reviewCount]\\\") { innerText }"
    + "   categories: mapSelector(selector: \\\"a[class*=categoryLink]\\\") { innerText }"
    + "   priceRange: mapSelector(selector: \\\"span[class*=priceRange]\\\") { innerText }"
    + " }"
    + " }";

String payload = "{\"query\": \"" + query + "\", \"variables\": {}}";

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(endpoint))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(payload))
    .build();

HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 3210 },
    "businesses": [
      {
        "name": [{ "innerText": "Joe's Pizza" }],
        "rating": [{ "ratingLabel": { "value": "4.5 star rating" } }]
      }
    ]
  }
}
```

    
    
**C#:**

    

    **1. Send the request**

```csharp
using System.Net.Http;
using System.Text;
using System.Text.Json;

string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"https://production-sfo.browserless.io/stealth/bql?token={token}&proxy=residential&proxyCountry=us";

var payload = new
{
    query = @"mutation ScrapeYelp {
      goto(url: ""https://www.yelp.com/search?find_desc=pizza&find_loc=New+York%2C+NY"", waitUntil: networkIdle) { status }
      waitForSelector(selector: ""[data-testid=serp-ia-card]"", timeout: 15000) { time }
      businesses: mapSelector(selector: ""[data-testid=serp-ia-card]"") {
        name: mapSelector(selector: ""a[class*=businessName] span"") { innerText }
        rating: mapSelector(selector: ""[aria-label*=star]"") {
          ratingLabel: attribute(name: ""aria-label"") { value }
        }
        reviewCount: mapSelector(selector: ""span[class*=reviewCount]"") { innerText }
        categories: mapSelector(selector: ""a[class*=categoryLink]"") { innerText }
        priceRange: mapSelector(selector: ""span[class*=priceRange]"") { innerText }
      }
    }",
    variables = new { },
};

using (HttpClient httpClient = new HttpClient())
{
    var content = new StringContent(
        JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
    var response = await httpClient.PostAsync(endpoint, content);
    string body = await response.Content.ReadAsStringAsync();
    Console.WriteLine(body);
}
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 3210 },
    "businesses": [
      {
        "name": [{ "innerText": "Joe's Pizza" }],
        "rating": [{ "ratingLabel": { "value": "4.5 star rating" } }]
      }
    ]
  }
}
```

    
  

  
  
**Frameworks:**

  Connect through stealth mode and a residential proxy so Yelp sees traffic from a real browser, then extract business data from the rendered search results.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Connect and scrape**

```js
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint:
    'wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE&proxy=residential&proxyCountry=us',
});

try {
  const page = await browser.newPage();
  await page.goto('https://www.yelp.com/search?find_desc=pizza&find_loc=New+York%2C+NY', {
    waitUntil: 'networkidle2',
  });

  await page.waitForSelector('[data-testid="serp-ia-card"]');

  const businesses = await page.evaluate(() =>
    Array.from(document.querySelectorAll('[data-testid="serp-ia-card"]')).map((card) => ({
      name: card.querySelector('a[class*="businessName"] span')?.innerText?.trim() ?? '',
      rating: card.querySelector('[aria-label*="star"]')?.getAttribute('aria-label') ?? '',
      reviewCount: card.querySelector('span[class*="reviewCount"]')?.innerText?.trim() ?? '',
      categories: card.querySelector('a[class*="categoryLink"]')?.innerText?.trim() ?? '',
      priceRange: card.querySelector('span[class*="priceRange"]')?.innerText?.trim() ?? '',
    }))
  );

  console.log(JSON.stringify(businesses, null, 2));
} finally {
  await browser.close();
}
```

    **3. Check the output**

    Run with `node scrape-yelp.mjs`. Each object has `name`, `rating`, `reviewCount`, `categories`, and `priceRange` fields.

```json
[
  {
    "name": "Joe's Pizza",
    "rating": "4.5 star rating",
    "reviewCount": "1,247",
    "categories": "Pizza",
    "priceRange": "$"
  }
]
```

    
    
**Playwright:**

    

    **1. Install dependencies**

```bash
npm install playwright-core
```

    **2. Connect and scrape**

```js
import { chromium } from 'playwright-core';

const browser = await chromium.connectOverCDP(
  'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&stealth&proxy=residential&proxyCountry=us'
);

try {
  const context = browser.contexts()[0];
  const page = await context.newPage();
  await page.goto('https://www.yelp.com/search?find_desc=pizza&find_loc=New+York%2C+NY', {
    waitUntil: 'networkidle',
  });

  await page.waitForSelector('[data-testid="serp-ia-card"]');

  const businesses = await page.evaluate(() =>
    Array.from(document.querySelectorAll('[data-testid="serp-ia-card"]')).map((card) => ({
      name: card.querySelector('a[class*="businessName"] span')?.innerText?.trim() ?? '',
      rating: card.querySelector('[aria-label*="star"]')?.getAttribute('aria-label') ?? '',
      reviewCount: card.querySelector('span[class*="reviewCount"]')?.innerText?.trim() ?? '',
      categories: card.querySelector('a[class*="categoryLink"]')?.innerText?.trim() ?? '',
      priceRange: card.querySelector('span[class*="priceRange"]')?.innerText?.trim() ?? '',
    }))
  );

  console.log(JSON.stringify(businesses, null, 2));
} finally {
  await browser.close();
}
```

    **3. Check the output**

    Run with `node scrape-yelp.mjs`. Each object has `name`, `rating`, `reviewCount`, `categories`, and `priceRange` fields.

```json
[
  {
    "name": "Joe's Pizza",
    "rating": "4.5 star rating",
    "reviewCount": "1,247",
    "categories": "Pizza",
    "priceRange": "$"
  }
]
```

    
  

  
  
**BQL:**

  

  **1. Write the mutation**

  Navigate to Yelp's search results, wait for business cards to load, then extract structured data from each listing. We use `/stealth/bql` because Yelp's bot detection blocks standard headless browsers.

```graphql
mutation ScrapeYelp {
  goto(url: "https://www.yelp.com/search?find_desc=pizza&find_loc=New+York%2C+NY", waitUntil: networkIdle) {
    status
  }
  waitForSelector(selector: "[data-testid=serp-ia-card]", timeout: 15000) {
    time
  }
  businesses: mapSelector(selector: "[data-testid=serp-ia-card]") {
    name: mapSelector(selector: "a[class*=businessName] span") { innerText }
    rating: mapSelector(selector: "[aria-label*=star]") {
      ratingLabel: attribute(name: "aria-label") { value }
    }
    reviewCount: mapSelector(selector: "span[class*=reviewCount]") { innerText }
    categories: mapSelector(selector: "a[class*=categoryLink]") { innerText }
    priceRange: mapSelector(selector: "span[class*=priceRange]") { innerText }
  }
}
```

  **2. Run it**

  Paste into the [BQL IDE](https://account.browserless.io/bql) and click Run.

  **3. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 3210 },
    "businesses": [
      {
        "name": [{ "innerText": "Joe's Pizza" }],
        "rating": [{ "ratingLabel": { "value": "4.5 star rating" } }],
        "reviewCount": [{ "innerText": "1,247" }],
        "categories": [{ "innerText": "Pizza" }],
        "priceRange": [{ "innerText": "$" }]
      },
      {
        "name": [{ "innerText": "Di Fara Pizza" }],
        "rating": [{ "ratingLabel": { "value": "4.0 star rating" } }],
        "reviewCount": [{ "innerText": "3,891" }],
        "categories": [{ "innerText": "Pizza" }],
        "priceRange": [{ "innerText": "$$" }]
      }
    ]
  }
}
```

  

## Next steps

  <a href="/examples/scrape-opentable" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape OpenTable Restaurants</h3>
      scrape another restaurant/dining platform with stealth mode
    
  </a>
  <a href="/examples/google-search" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Automate Google Search</h3>
      pull search results using the same `/stealth/bql` endpoint
    
  </a>
  <a href="/examples/cloudflare" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Solving Cloudflare Challenges</h3>
      bypass Cloudflare's interstitial pages before scraping
    
  </a>

---

# Scrape YouTube video data

URL: https://docs.browserless.io/examples/scrape-youtube

# Scrape YouTube video data

Extract title, channel, view count, likes, and upload date from a YouTube video URL.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

YouTube renders all video metadata via JavaScript after the initial page load. A plain HTTP fetch returns almost no data. The examples below wait for the page to settle before reading, and route through stealth mode because YouTube actively detects plain browser automation.

> **Selector stability**
> YouTube's DOM selectors change periodically. If any field returns `null` or an empty string, inspect the live page with browser DevTools to find the updated selector.

  
**AI Agent:**

  Use the [Browserless MCP server](https://github.com/browserless/browserless-mcp) to scrape video data from YouTube from any MCP-compatible AI agent (Claude Desktop, Cursor, Windsurf, ChatGPT, etc.).

  **1. Connect the MCP server**

  Send this prompt to your AI agent to install the Browserless MCP server:

```text
Go to https://github.com/browserless/browserless-mcp/blob/main/install.md
and follow the instructions to install the Browserless MCP server
for my client.
```

  **2. Scrape YouTube**

  Use `browserless_smartscraper`. YouTube has bot detection and smartscraper handles it automatically.

```text
Use the browserless_smartscraper tool to scrape video information
from https://www.youtube.com/watch?v=dQw4w9WgXcQ
and return the results as markdown
```

  
  
**REST API:**

  Send the BQL mutation over HTTP to the stealth endpoint. No browser library or BQL IDE required.

  
    
**cURL:**

    **1. Send the request**

```bash
curl -X POST \
  "https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "mutation ScrapeYouTubeVideo { goto(url: \"https://www.youtube.com/watch?v=dQw4w9WgXcQ\", waitUntil: networkIdle) { status } waitForSelector(selector: \"h1.ytd-watch-metadata\", timeout: 10000) { time } title: text(selector: \"h1.ytd-watch-metadata yt-formatted-string\") channel: text(selector: \"ytd-channel-name yt-formatted-string a\") views: text(selector: \".ytd-video-view-count-renderer .view-count\") likes: text(selector: \"ytd-segmented-like-dislike-button-renderer yt-formatted-string\") uploadDate: text(selector: \"#info-strings yt-formatted-string\") }",
    "variables": {}
  }'
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 1243 },
    "title": "Rick Astley - Never Gonna Give You Up (Official Music Video)",
    "channel": "Rick Astley",
    "views": "1,234,567,890 views",
    "likes": "16M",
    "uploadDate": "Oct 24, 2009"
  }
}
```

    
    
**JavaScript:**

    **1. Send the request**

```js
const query = `mutation ScrapeYouTubeVideo {
  goto(url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", waitUntil: networkIdle) {
    status
  }
  waitForSelector(selector: "h1.ytd-watch-metadata", timeout: 10000) {
    time
  }
  title: text(selector: "h1.ytd-watch-metadata yt-formatted-string")
  channel: text(selector: "ytd-channel-name yt-formatted-string a")
  views: text(selector: ".ytd-video-view-count-renderer .view-count")
  likes: text(selector: "ytd-segmented-like-dislike-button-renderer yt-formatted-string")
  uploadDate: text(selector: "#info-strings yt-formatted-string")
}`;

const response = await fetch(
  'https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ query, variables: {} }),
  }
);

const { data } = await response.json();
console.log(JSON.stringify(data, null, 2));
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 1243 },
    "title": "Rick Astley - Never Gonna Give You Up (Official Music Video)",
    "channel": "Rick Astley",
    "views": "1,234,567,890 views",
    "likes": "16M",
    "uploadDate": "Oct 24, 2009"
  }
}
```

    
    
**Python:**

    **1. Install dependencies**

```bash
pip install requests
```

    **2. Send the request**

```python
import requests

query = """
mutation ScrapeYouTubeVideo {
  goto(url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", waitUntil: networkIdle) {
    status
  }
  waitForSelector(selector: "h1.ytd-watch-metadata", timeout: 10000) {
    time
  }
  title: text(selector: "h1.ytd-watch-metadata yt-formatted-string")
  channel: text(selector: "ytd-channel-name yt-formatted-string a")
  views: text(selector: ".ytd-video-view-count-renderer .view-count")
  likes: text(selector: "ytd-segmented-like-dislike-button-renderer yt-formatted-string")
  uploadDate: text(selector: "#info-strings yt-formatted-string")
}
"""

response = requests.post(
    'https://production-sfo.browserless.io/stealth/bql',
    params={'token': 'YOUR_API_TOKEN_HERE'},
    json={'query': query, 'variables': {}},
)

print(response.json())
```

    **3. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 1243 },
    "title": "Rick Astley - Never Gonna Give You Up (Official Music Video)",
    "channel": "Rick Astley",
    "views": "1,234,567,890 views",
    "likes": "16M",
    "uploadDate": "Oct 24, 2009"
  }
}
```

    
    
**Java:**

    **1. Send the request**

```java
import java.net.URI;
import java.net.http.*;

String token = "YOUR_API_TOKEN_HERE";
String endpoint = "https://production-sfo.browserless.io/stealth/bql?token=" + token;

String query = "mutation ScrapeYouTubeVideo {"
    + " goto(url: \\\"https://www.youtube.com/watch?v=dQw4w9WgXcQ\\\", waitUntil: networkIdle) { status }"
    + " waitForSelector(selector: \\\"h1.ytd-watch-metadata\\\", timeout: 10000) { time }"
    + " title: text(selector: \\\"h1.ytd-watch-metadata yt-formatted-string\\\")"
    + " channel: text(selector: \\\"ytd-channel-name yt-formatted-string a\\\")"
    + " views: text(selector: \\\".ytd-video-view-count-renderer .view-count\\\")"
    + " likes: text(selector: \\\"ytd-segmented-like-dislike-button-renderer yt-formatted-string\\\")"
    + " uploadDate: text(selector: \\\"#info-strings yt-formatted-string\\\")"
    + " }";

String payload = "{\"query\": \"" + query + "\", \"variables\": {}}";

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(endpoint))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(payload))
    .build();

HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 1243 },
    "title": "Rick Astley - Never Gonna Give You Up (Official Music Video)",
    "channel": "Rick Astley",
    "views": "1,234,567,890 views",
    "likes": "16M",
    "uploadDate": "Oct 24, 2009"
  }
}
```

    
    
**C#:**

    **1. Send the request**

```csharp
using System.Net.Http;
using System.Text;
using System.Text.Json;

string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"https://production-sfo.browserless.io/stealth/bql?token={token}";

var payload = new
{
    query = @"mutation ScrapeYouTubeVideo {
      goto(url: ""https://www.youtube.com/watch?v=dQw4w9WgXcQ"", waitUntil: networkIdle) { status }
      waitForSelector(selector: ""h1.ytd-watch-metadata"", timeout: 10000) { time }
      title: text(selector: ""h1.ytd-watch-metadata yt-formatted-string"")
      channel: text(selector: ""ytd-channel-name yt-formatted-string a"")
      views: text(selector: "".ytd-video-view-count-renderer .view-count"")
      likes: text(selector: ""ytd-segmented-like-dislike-button-renderer yt-formatted-string"")
      uploadDate: text(selector: ""#info-strings yt-formatted-string"")
    }",
    variables = new { },
};

using (HttpClient httpClient = new HttpClient())
{
    var content = new StringContent(
        JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
    var response = await httpClient.PostAsync(endpoint, content);
    string body = await response.Content.ReadAsStringAsync();
    Console.WriteLine(body);
}
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 1243 },
    "title": "Rick Astley - Never Gonna Give You Up (Official Music Video)",
    "channel": "Rick Astley",
    "views": "1,234,567,890 views",
    "likes": "16M",
    "uploadDate": "Oct 24, 2009"
  }
}
```

    
  

  
  
**Frameworks:**

  Connect through stealth mode so the browser looks like a real Chrome session, then wait for the title element before reading. YouTube's metadata loads after the initial HTML, so without `waitForSelector` you'd get empty fields.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Connect and scrape**

```js
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint: 'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE',
});

try {
  const page = await browser.newPage();
  await page.goto('https://www.youtube.com/watch?v=dQw4w9WgXcQ', {
    waitUntil: 'networkidle2',
  });

  // Wait for the title to confirm the video metadata has rendered.
  await page.waitForSelector('h1.ytd-watch-metadata');

  // Batch all reads into one evaluate() call to avoid multiple round-trips.
  const data = await page.evaluate(() => ({
    title: document.querySelector('h1.ytd-watch-metadata yt-formatted-string')?.textContent?.trim() ?? null,
    channel: document.querySelector('ytd-channel-name yt-formatted-string a')?.textContent?.trim() ?? null,
    views: document.querySelector('.ytd-video-view-count-renderer .view-count')?.textContent?.trim() ?? null,
    likes: document.querySelector('ytd-segmented-like-dislike-button-renderer yt-formatted-string')?.textContent?.trim() ?? null,
    uploadDate: document.querySelector('#info-strings yt-formatted-string')?.textContent?.trim() ?? null,
  }));

  console.log(JSON.stringify(data, null, 2));
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Run with `node scrape-youtube.mjs`. `null` values mean the element wasn't present when the script ran. Increase the `waitForSelector` timeout or check whether a consent banner appeared.

```json
{
  "title": "Rick Astley - Never Gonna Give You Up (Official Music Video)",
  "channel": "Rick Astley",
  "views": "1,234,567,890 views",
  "likes": "16M",
  "uploadDate": "Oct 24, 2009"
}
```

    
    
**Playwright:**

    

    **1. Install dependencies**

```bash
npm install playwright-core
```

    **2. Connect and scrape**

```js
import { chromium } from 'playwright-core';

const browser = await chromium.connect(
  'wss://production-sfo.browserless.io/chromium/playwright?token=YOUR_API_TOKEN_HERE'
);

try {
  const page = await browser.newPage();
  await page.goto('https://www.youtube.com/watch?v=dQw4w9WgXcQ', {
    waitUntil: 'networkidle',
  });

  // Wait for the title to confirm the video metadata has rendered.
  await page.waitForSelector('h1.ytd-watch-metadata');

  // Batch all reads into one evaluate() call to avoid multiple round-trips.
  const data = await page.evaluate(() => ({
    title: document.querySelector('h1.ytd-watch-metadata yt-formatted-string')?.textContent?.trim() ?? null,
    channel: document.querySelector('ytd-channel-name yt-formatted-string a')?.textContent?.trim() ?? null,
    views: document.querySelector('.ytd-video-view-count-renderer .view-count')?.textContent?.trim() ?? null,
    likes: document.querySelector('ytd-segmented-like-dislike-button-renderer yt-formatted-string')?.textContent?.trim() ?? null,
    uploadDate: document.querySelector('#info-strings yt-formatted-string')?.textContent?.trim() ?? null,
  }));

  console.log(JSON.stringify(data, null, 2));
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Run with `node scrape-youtube.mjs`. `null` values mean the element wasn't present when the script ran. Increase the `waitForSelector` timeout or check whether a consent banner appeared.

```json
{
  "title": "Rick Astley - Never Gonna Give You Up (Official Music Video)",
  "channel": "Rick Astley",
  "views": "1,234,567,890 views",
  "likes": "16M",
  "uploadDate": "Oct 24, 2009"
}
```

    
    
**BAP:**

    **1. Install dependencies**

```bash
npm install @browserless.io/bap-ts
```

    **2. Connect and extract**

```typescript
import Browserless from '@browserless.io/bap-ts';

const browser = Browserless.connect({
  browserWSEndpoint: 'wss://production-sfo.browserless.io/chromium/bql',
  token: 'YOUR_API_TOKEN_HERE',
});

try {
  const page = await browser.newPage();
  await page.goto('https://www.youtube.com/watch?v=dQw4w9WgXcQ', { waitUntil: 'networkIdle' });
  await page.waitForSelector('h1.ytd-watch-metadata', { timeout: 10000 });

  console.log({
    title: await page.$eval('h1.ytd-watch-metadata yt-formatted-string'),
    channel: await page.$eval('ytd-channel-name yt-formatted-string a'),
    views: await page.$eval('.ytd-video-view-count-renderer .view-count'),
    likes: await page.$eval('ytd-segmented-like-dislike-button-renderer yt-formatted-string'),
    uploadDate: await page.$eval('#info-strings yt-formatted-string'),
  });
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Each `$eval` is one operation on the same connection, so the five fields cost one round trip each and no extra sessions.

    
  

  
  
**BQL:**

  

  **1. Write the mutation**

  Navigate to the video URL and use `waitForSelector` to block until the title element appears, then read each metadata field. We send this to `/stealth/bql` instead of the default `/bql` because YouTube's bot detection blocks a plain browser session before the page fully loads.

```graphql
mutation ScrapeYouTubeVideo {
  goto(url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", waitUntil: networkIdle) {
    status
  }
  waitForSelector(selector: "h1.ytd-watch-metadata", timeout: 10000) {
    time
  }
  title: text(selector: "h1.ytd-watch-metadata yt-formatted-string")
  channel: text(selector: "ytd-channel-name yt-formatted-string a")
  views: text(selector: ".ytd-video-view-count-renderer .view-count")
  likes: text(selector: "ytd-segmented-like-dislike-button-renderer yt-formatted-string")
  uploadDate: text(selector: "#info-strings yt-formatted-string")
}
```

  **2. Run it**

  Paste into the [BQL IDE](https://account.browserless.io/bql) and click Run.

  **3. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForSelector": { "time": 1243 },
    "title": "Rick Astley - Never Gonna Give You Up (Official Music Video)",
    "channel": "Rick Astley",
    "views": "1,234,567,890 views",
    "likes": "16M",
    "uploadDate": "Oct 24, 2009"
  }
}
```

  

## Next steps

  <a href="/examples/scrape-glassdoor" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape Glassdoor Job Listings</h3>
      another stealth-mode scrape against aggressive bot detection
    
  </a>
  <a href="/examples/google-search" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Automate Google Search</h3>
      pull search results using the same `/stealth/bql` endpoint
    
  </a>
  <a href="/examples/cloudflare" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Solving Cloudflare Challenges</h3>
      bypass Cloudflare's interstitial pages before scraping
    
  </a>

---

# Scrape Zillow agent listings

URL: https://docs.browserless.io/examples/scrape-zillow

# Scrape Zillow agent listings

Pull agent names and profile URLs from a Zillow agent directory page as structured JSON.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

Zillow blocks most automated requests without stealth mode and residential proxies. The examples below navigate to the New York agent directory and extract agent data from the rendered page.

> **Selector stability**
> Zillow updates its markup regularly. If selectors stop returning results, inspect the current DOM in browser DevTools and update them.

  
**AI Agent:**

  Use the [Browserless MCP server](https://github.com/browserless/browserless-mcp) to scrape Zillow agent listings and reviews from any MCP-compatible AI agent (Claude Desktop, Cursor, Windsurf, ChatGPT, etc.).

  **1. Connect the MCP server**

  Send this prompt to your AI agent to install the Browserless MCP server:

```text
Go to https://github.com/browserless/browserless-mcp/blob/main/install.md
and follow the instructions to install the Browserless MCP server
for my client.
```

  **2. Scrape Zillow**

  Use `browserless_smartscraper`. Zillow has aggressive bot detection and smartscraper handles it automatically.

```text
Use the browserless_smartscraper tool to scrape agent listings
from https://www.zillow.com/professionals/real-estate-agent-reviews/
and return the results as markdown
```

  
  
**REST API:**

  Send the BQL mutation over HTTP to the stealth endpoint. No browser library or BQL IDE required.

  
    
**cURL:**

    **1. Send the request**

```bash
curl -X POST \
  "https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE&proxy=residential&proxySticky=true&proxyCountry=us&humanlike=true&blockAds=true" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "mutation ScrapeZillow { goto(url: \"https://www.zillow.com/professionals/real-estate-agent-reviews/new-york-ny/ny/\", waitUntil: networkIdle) { status } waitForTimeout(time: 3000) { time } agents: mapSelector(selector: \"a[href^='\''\/profile\/'\'']\" ) { name: mapSelector(selector: \"span, h3\") { innerText } profileUrl: attribute(name: \"href\") { value } } }",
    "variables": {},
    "operationName": "ScrapeZillow"
  }'
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForTimeout": { "time": 3000 },
    "agents": [
      {
        "name": [{ "innerText": "Jane Smith" }],
        "profileUrl": { "value": "/profile/janesmith/" }
      },
      {
        "name": [{ "innerText": "John Doe" }],
        "profileUrl": { "value": "/profile/johndoe/" }
      }
    ]
  }
}
```

    
    
**JavaScript:**

    **1. Send the request**

```js
const query = `mutation ScrapeZillow {
  goto(
    url: "https://www.zillow.com/professionals/real-estate-agent-reviews/new-york-ny/ny/"
    waitUntil: networkIdle
  ) {
    status
  }
  waitForTimeout(time: 3000) {
    time
  }
  agents: mapSelector(selector: "a[href^='/profile/']") {
    name: mapSelector(selector: "span, h3") {
      innerText
    }
    profileUrl: attribute(name: "href") {
      value
    }
  }
}`;

const response = await fetch(
  'https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE&proxy=residential&proxySticky=true&proxyCountry=us&humanlike=true&blockAds=true',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ query, variables: {}, operationName: 'ScrapeZillow' }),
  }
);

const { data } = await response.json();
const agents = data.agents.map((a) => ({
  name: a.name?.[0]?.innerText ?? '',
  profileUrl: 'https://www.zillow.com' + (a.profileUrl?.value ?? ''),
}));
console.log(JSON.stringify(agents, null, 2));
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForTimeout": { "time": 3000 },
    "agents": [
      {
        "name": [{ "innerText": "Jane Smith" }],
        "profileUrl": { "value": "/profile/janesmith/" }
      },
      {
        "name": [{ "innerText": "John Doe" }],
        "profileUrl": { "value": "/profile/johndoe/" }
      }
    ]
  }
}
```

    
    
**Python:**

    **1. Install dependencies**

```bash
pip install requests
```

    **2. Send the request**

```python
import requests

query = """
mutation ScrapeZillow {
  goto(
    url: "https://www.zillow.com/professionals/real-estate-agent-reviews/new-york-ny/ny/"
    waitUntil: networkIdle
  ) {
    status
  }
  waitForTimeout(time: 3000) {
    time
  }
  agents: mapSelector(selector: "a[href^='/profile/']") {
    name: mapSelector(selector: "span, h3") {
      innerText
    }
    profileUrl: attribute(name: "href") {
      value
    }
  }
}
"""

response = requests.post(
    'https://production-sfo.browserless.io/stealth/bql',
    params={
        'token': 'YOUR_API_TOKEN_HERE',
        'proxy': 'residential',
        'proxySticky': 'true',
        'proxyCountry': 'us',
        'humanlike': 'true',
        'blockAds': 'true',
    },
    json={'query': query, 'variables': {}, 'operationName': 'ScrapeZillow'},
)

data = response.json()['data']
for agent in data['agents']:
    name = agent['name'][0]['innerText'] if agent['name'] else ''
    profile_url = 'https://www.zillow.com' + (agent['profileUrl']['value'] if agent['profileUrl'] else '')
    print(f'{name}: {profile_url}')
```

    **3. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForTimeout": { "time": 3000 },
    "agents": [
      {
        "name": [{ "innerText": "Jane Smith" }],
        "profileUrl": { "value": "/profile/janesmith/" }
      },
      {
        "name": [{ "innerText": "John Doe" }],
        "profileUrl": { "value": "/profile/johndoe/" }
      }
    ]
  }
}
```

    
    
**Java:**

    **1. Send the request**

```java
import java.net.URI;
import java.net.http.*;

String token = "YOUR_API_TOKEN_HERE";
String endpoint = "https://production-sfo.browserless.io/stealth/bql?token=" + token
    + "&proxy=residential&proxySticky=true&proxyCountry=us&humanlike=true&blockAds=true";

String query = """
    mutation ScrapeZillow {
      goto(url: "https://www.zillow.com/professionals/real-estate-agent-reviews/new-york-ny/ny/", waitUntil: networkIdle) { status }
      waitForTimeout(time: 3000) { time }
      agents: mapSelector(selector: "a[href^='/profile/']") {
        name: mapSelector(selector: "span, h3") { innerText }
        profileUrl: attribute(name: "href") { value }
      }
    }
    """;

String payload = "{\"query\": " + com.google.gson.JsonParser.parseString(query) + ", \"variables\": {}, \"operationName\": \"ScrapeZillow\"}";

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(endpoint))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(payload))
    .build();

HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForTimeout": { "time": 3000 },
    "agents": [
      {
        "name": [{ "innerText": "Jane Smith" }],
        "profileUrl": { "value": "/profile/janesmith/" }
      },
      {
        "name": [{ "innerText": "John Doe" }],
        "profileUrl": { "value": "/profile/johndoe/" }
      }
    ]
  }
}
```

    
    
**C#:**

    **1. Send the request**

```csharp
using System.Net.Http;
using System.Text;
using System.Text.Json;

string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"https://production-sfo.browserless.io/stealth/bql?token={token}&proxy=residential&proxySticky=true&proxyCountry=us&humanlike=true&blockAds=true";

var payload = new
{
    query = @"mutation ScrapeZillow {
      goto(url: ""https://www.zillow.com/professionals/real-estate-agent-reviews/new-york-ny/ny/"", waitUntil: networkIdle) { status }
      waitForTimeout(time: 3000) { time }
      agents: mapSelector(selector: ""a[href^='/profile/']"") {
        name: mapSelector(selector: ""span, h3"") { innerText }
        profileUrl: attribute(name: ""href"") { value }
      }
    }",
    variables = new { },
    operationName = "ScrapeZillow",
};

using (HttpClient httpClient = new HttpClient())
{
    var content = new StringContent(
        JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
    var response = await httpClient.PostAsync(endpoint, content);
    string body = await response.Content.ReadAsStringAsync();
    Console.WriteLine(body);
}
```

    **2. Check the output**

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForTimeout": { "time": 3000 },
    "agents": [
      {
        "name": [{ "innerText": "Jane Smith" }],
        "profileUrl": { "value": "/profile/janesmith/" }
      },
      {
        "name": [{ "innerText": "John Doe" }],
        "profileUrl": { "value": "/profile/johndoe/" }
      }
    ]
  }
}
```

    
  

  
  
**Frameworks:**

  Connect through stealth mode and a residential proxy to get past Zillow's bot detection, then extract agent data from the rendered page.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Connect and scrape**

```js
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint:
    'wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE&proxy=residential&proxySticky=true&proxyCountry=us',
});

try {
  const page = await browser.newPage();
  await page.goto(
    'https://www.zillow.com/professionals/real-estate-agent-reviews/new-york-ny/ny/',
    { waitUntil: 'networkidle2' }
  );

  const agents = await page.evaluate(() =>
    Array.from(document.querySelectorAll('a[href^="/profile/"]')).map((link) => ({
      name: link.innerText?.trim() ?? '',
      profileUrl: 'https://www.zillow.com' + link.getAttribute('href'),
    }))
  );

  console.log(JSON.stringify(agents, null, 2));
} finally {
  await browser.close();
}
```

    **3. Check the output**

    Run with `node scrape-zillow.mjs`. You'll get a JSON array where each object has a `name` string and a `profileUrl` that's already absolute. The code prepends `https://www.zillow.com` to the relative `href` before returning it.

    
    
**Playwright:**

    

    **1. Install dependencies**

```bash
npm install playwright-core
```

    **2. Connect and scrape**

```js
import { chromium } from 'playwright-core';

const browser = await chromium.connectOverCDP(
  'wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE&proxy=residential&proxySticky=true&proxyCountry=us'
);

try {
  const context = browser.contexts()[0];
  const page = await context.newPage();
  await page.goto(
    'https://www.zillow.com/professionals/real-estate-agent-reviews/new-york-ny/ny/',
    { waitUntil: 'networkidle' }
  );

  const agents = await page.evaluate(() =>
    Array.from(document.querySelectorAll('a[href^="/profile/"]')).map((link) => ({
      name: link.innerText?.trim() ?? '',
      profileUrl: 'https://www.zillow.com' + link.getAttribute('href'),
    }))
  );

  console.log(JSON.stringify(agents, null, 2));
} finally {
  await browser.close();
}
```

    **3. Check the output**

    Run with `node scrape-zillow.mjs`. You'll get a JSON array where each object has a `name` string and a `profileUrl` that's already absolute. The code prepends `https://www.zillow.com` to the relative `href` before returning it.

    
  

  
  
**BQL:**

  

  **1. Write the mutation**

  Navigate to the Zillow agent directory and use `mapSelector` to pull each agent's name and profile URL. We run this against the stealth endpoint with `humanlike` and `blockAds` enabled. `humanlike` triggers behavioral simulation (mouse movement, realistic timing) so the session looks less like a bot; `blockAds` blocks third-party trackers that can fingerprint automated sessions and trigger detection. The `waitForTimeout` is there because the agent grid renders client-side. Without it, the DOM may not have populated yet when the selectors run.

```graphql
mutation ScrapeZillow {
  goto(
    url: "https://www.zillow.com/professionals/real-estate-agent-reviews/new-york-ny/ny/"
    waitUntil: networkIdle
  ) {
    status
  }

  waitForTimeout(time: 3000) {
    time
  }

  agents: mapSelector(selector: "a[href^='/profile/']") {
    name: mapSelector(selector: "span, h3") {
      innerText
    }
    profileUrl: attribute(name: "href") {
      value
    }
  }
}
```

  **2. Run it**

  Paste into the [BQL IDE](https://account.browserless.io/bql) and click Run.

  **3. Check the output**

  Each agent entry in `data.agents` has a `name` array (text nodes from the matched child selectors) and a `profileUrl` object with the relative path. Prepend `https://www.zillow.com` to get the full URL.

```json
{
  "data": {
    "goto": { "status": 200 },
    "waitForTimeout": { "time": 3000 },
    "agents": [
      {
        "name": [{ "innerText": "Jane Smith" }],
        "profileUrl": { "value": "/profile/janesmith/" }
      },
      {
        "name": [{ "innerText": "John Doe" }],
        "profileUrl": { "value": "/profile/johndoe/" }
      }
    ]
  }
}
```

  

## Next steps

  <a href="/examples/scrape-glassdoor" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape Glassdoor Job Listings</h3>
      apply the same stealth pattern to job listings
    
  </a>
  <a href="/examples/scrape-walmart" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape Walmart Product Listings</h3>
      extract product data from another bot-protected site
    
  </a>
  <a href="/examples/proxy" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Change Your Browser's IP Address Using Proxies</h3>
      control which IP address target sites see
    
  </a>

---

# Record a browser session

URL: https://docs.browserless.io/examples/screen-recording

# Record a browser session

Capture a video recording of a browser session (navigations, clicks, and interactions) as a `.webm` file using the Browserless recording API.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

Recording uses CDP commands `Browserless.startRecording` and `Browserless.stopRecording`. Three parameters are required in the WebSocket URL: `headless=false` (the recording extension captures a visible window), `stealth` (recording is not available on the standard `/chrome` route), and `record=true`. The video dimensions match the viewport when recording starts. Set the viewport before calling `startRecording`.

  
**Puppeteer:**

  

  **1. Install dependencies**

```bash
npm install puppeteer-core
```

  **2. Connect, record, and save**

```js
import fs from 'fs';
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint:
    'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&headless=false&stealth&record=true',
});

try {
  const page = await browser.newPage();

  // Set viewport before starting — dimensions are fixed for the entire recording.
  await page.setViewport({ width: 1280, height: 720 });

  const cdp = await page.createCDPSession();
  await cdp.send('Browserless.startRecording');

  await page.goto('https://scraping-sandbox.netlify.app/login', { waitUntil: 'networkidle2' });
  await new Promise(r => setTimeout(r, 2000));
  await page.goto('https://scraping-sandbox.netlify.app/contact-us', { waitUntil: 'networkidle2' });
  await new Promise(r => setTimeout(r, 2000));

  // base64 encoding is required — CDP can't transfer raw binary over its text protocol.
  const { value } = await cdp.send('Browserless.stopRecording', { encoding: 'base64' });

  fs.writeFileSync('recording.webm', Buffer.from(value, 'base64'));
  console.log('Recording saved to recording.webm');
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

  **3. Check the output**

  Run with `node record.mjs`. The file `recording.webm` contains the full browser session video.

  
  
**Playwright:**

  All Playwright languages must reuse the existing context and page from `connectOverCDP` — creating new ones starts a fresh session that isn't wired to the recording.

  
    
**JavaScript:**

    

    **1. Install dependencies**

```bash
npm install playwright-core
```

    **2. Connect, record, and save**

```js
import fs from 'fs';
import { chromium } from 'playwright-core';

const browser = await chromium.connectOverCDP(
  'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&headless=false&stealth&record=true'
);

try {
  const context = browser.contexts()[0];
  const page = context.pages()[0];

  // Set viewport before starting — dimensions are fixed for the entire recording.
  await page.setViewportSize({ width: 1280, height: 720 });

  const cdp = await page.context().newCDPSession(page);
  await cdp.send('Browserless.startRecording');

  await page.goto('https://scraping-sandbox.netlify.app/login', { waitUntil: 'networkidle' });
  await new Promise(r => setTimeout(r, 2000));
  await page.goto('https://scraping-sandbox.netlify.app/contact-us', { waitUntil: 'networkidle' });
  await new Promise(r => setTimeout(r, 2000));

  // base64 encoding is required — CDP can't transfer raw binary over its text protocol.
  const { value } = await cdp.send('Browserless.stopRecording', { encoding: 'base64' });

  fs.writeFileSync('recording.webm', Buffer.from(value, 'base64'));
  console.log('Recording saved to recording.webm');
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Run with `node record.mjs`. The file `recording.webm` contains the full browser session video.

    
    
**Python:**

    

    **1. Install dependencies**

```bash
pip install playwright
playwright install chromium
```

    **2. Connect, record, and save**

```python
import base64
import time
from playwright.sync_api import sync_playwright

TOKEN = 'YOUR_API_TOKEN_HERE'
WS_ENDPOINT = f'wss://production-sfo.browserless.io?token={TOKEN}&headless=false&stealth&record=true'

with sync_playwright() as playwright:
    browser = playwright.chromium.connect_over_cdp(WS_ENDPOINT)
    try:
        # Reuse the existing context and page — new ones won't be wired to the recording.
        context = browser.contexts[0]
        page = context.pages[0]

        # Set viewport before starting — dimensions are fixed for the entire recording.
        page.set_viewport_size({'width': 1280, 'height': 720})

        cdp_session = context.new_cdp_session(page)
        cdp_session.send('Browserless.startRecording')

        page.goto('https://scraping-sandbox.netlify.app/login')
        time.sleep(5)
        page.goto('https://scraping-sandbox.netlify.app/contact-us')
        time.sleep(5)

        # base64 encoding is required — CDP can't transfer raw binary over its text protocol.
        response = cdp_session.send('Browserless.stopRecording', {'encoding': 'base64'})

        with open('recording.webm', 'wb') as f:
            f.write(base64.b64decode(response['value']))

        print('Recording saved to recording.webm')
    finally:
        # Always close to release the session even on error.
        browser.close()
```

    **3. Check the output**

    Run with `python record.py`. The file `recording.webm` contains the full browser session video.

    
    
**Java:**

    

    **1. Install dependencies**

    Add the Playwright dependency to your `pom.xml`:

```xml
<dependency>
  <groupId>com.microsoft.playwright</groupId>
  <artifactId>playwright</artifactId>
  <version>1.44.0</version>
</dependency>
```

    **2. Connect, record, and save**

```java
import com.microsoft.playwright.*;
import java.nio.file.*;
import java.util.Base64;
import java.util.Map;

public class ScreenRecording {
    public static void main(String[] args) throws Exception {
        String TOKEN = "YOUR_API_TOKEN_HERE";
        String WS_ENDPOINT = String.format(
            "wss://production-sfo.browserless.io?token=%s&headless=false&stealth&record=true",
            TOKEN
        );

        try (Playwright playwright = Playwright.create()) {
            Browser browser = playwright.chromium().connectOverCDP(WS_ENDPOINT);
            try {
                // Reuse the existing context and page — new ones won't be wired to the recording.
                BrowserContext context = browser.contexts().get(0);
                Page page = context.pages().get(0);

                // Set viewport before starting — dimensions are fixed for the entire recording.
                page.setViewportSize(1280, 720);

                CDPSession cdpSession = context.newCDPSession(page);
                cdpSession.send("Browserless.startRecording", null);

                page.navigate("https://scraping-sandbox.netlify.app/login");
                Thread.sleep(5000);
                page.navigate("https://scraping-sandbox.netlify.app/contact-us");
                Thread.sleep(5000);

                // base64 encoding is required — CDP can't transfer raw binary over its text protocol.
                var response = cdpSession.send(
                    "Browserless.stopRecording",
                    Map.of("encoding", "base64")
                );

                byte[] data = Base64.getDecoder().decode(response.get("value").getAsString());
                Files.write(Paths.get("recording.webm"), data);

                System.out.println("Recording saved to recording.webm");
            } finally {
                // Always close to release the session even on error.
                browser.close();
            }
        }
    }
}
```

    **3. Check the output**

    Compile and run with `mvn exec:java`. The file `recording.webm` contains the full browser session video.

    
    
**C#:**

    

    **1. Install dependencies**

```bash
dotnet add package Microsoft.Playwright
playwright install chromium
```

    **2. Connect, record, and save**

```csharp
using Microsoft.Playwright;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;

class ScreenRecording
{
    public static async Task Main()
    {
        const string TOKEN = "YOUR_API_TOKEN_HERE";
        string WS_ENDPOINT = $"wss://production-sfo.browserless.io?token={TOKEN}&headless=false&stealth&record=true";

        using var playwright = await Playwright.CreateAsync();
        var browser = await playwright.Chromium.ConnectOverCDPAsync(WS_ENDPOINT);

        try
        {
            // Reuse the existing context and page — new ones won't be wired to the recording.
            var context = browser.Contexts[0];
            var page = context.Pages[0];

            // Set viewport before starting — dimensions are fixed for the entire recording.
            await page.SetViewportSizeAsync(1280, 720);

            var cdpSession = await context.NewCDPSessionAsync(page);
            await cdpSession.SendAsync("Browserless.startRecording");

            await page.GotoAsync("https://scraping-sandbox.netlify.app/login");
            await Task.Delay(5000);
            await page.GotoAsync("https://scraping-sandbox.netlify.app/contact-us");
            await Task.Delay(5000);

            // base64 encoding is required — CDP can't transfer raw binary over its text protocol.
            var response = await cdpSession.SendAsync(
                "Browserless.stopRecording",
                new Dictionary<string, object> { ["encoding"] = "base64" }
            );

            var encoded = response?.GetProperty("value").GetString()
                ?? throw new InvalidOperationException("stopRecording returned no value.");
            byte[] data = Convert.FromBase64String(encoded);
            await File.WriteAllBytesAsync("recording.webm", data);

            Console.WriteLine("Recording saved to recording.webm");
        }
        finally
        {
            // Always close to release the session even on error.
            await browser.CloseAsync();
        }
    }
}
```

    **3. Check the output**

    Run with `dotnet run`. The file `recording.webm` contains the full browser session video.

    
  

  

## Notes

- The video is encoded as WebM (VP8). Convert to MP4 with `ffmpeg -i recording.webm recording.mp4` if needed.
- Do not change the viewport size after calling `startRecording`. The dimensions are fixed when recording begins.
- For DOM-based session replay (not video), use `replay=true` in the connection URL instead. Recordings are uploaded to the Browserless dashboard automatically.

## Next steps

  <a href="/examples/screenshot" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Take a Screenshot</h3>
      capture a static image instead of a video
    
  </a>
  <a href="/examples/concurrent-sessions" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Run Concurrent Browser Sessions</h3>
      record multiple sessions in parallel
    
  </a>

---

# Take a screenshot

URL: https://docs.browserless.io/examples/screenshot

# Take a screenshot

Capture any webpage as a PNG or JPEG using Browserless.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

  
**AI Agent:**

  Use the [Browserless MCP server](https://github.com/browserless/browserless-mcp) to take a screenshot from any MCP-compatible AI agent (Claude Desktop, Cursor, Windsurf, ChatGPT, etc.).

  **1. Connect the MCP server**

  Send this prompt to your AI agent to install the Browserless MCP server:

```text
Go to https://github.com/browserless/browserless-mcp/blob/main/install.md
and follow the instructions to install the Browserless MCP server
for my client.
```

  **2. Take a screenshot**

  Use `browserless_smartscraper`. It captures the page in one call with automatic bot-protection handling.

```text
Use the browserless_smartscraper tool to take a full-page
screenshot of https://scraping-sandbox.netlify.app/receipt and save it as screenshot.png
```

  
  
**REST API:**

  Use the `/screenshot` REST endpoint to capture a page as a PNG or JPEG. No WebSocket connection needed.

  
    
**cURL:**

    

    **1. Build the request**

    Append your token to the screenshot endpoint:

```bash
https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE
```

    **2. Send the request**

```bash
curl -X POST \
  "https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE" \
  -H 'Cache-Control: no-cache' \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://scraping-sandbox.netlify.app/receipt",
    "options": { "fullPage": true, "type": "png" }
  }' \
  --output screenshot.png
```

    **3. Check the output**

    The response is a raw PNG binary written to `screenshot.png` in the current directory.

    
    
**JavaScript:**

    

    **1. Send the request**

```js
import fs from 'fs';

const response = await fetch(
  'https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE',
  {
    method: 'POST',
    headers: {
      'Cache-Control': 'no-cache',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      url: 'https://scraping-sandbox.netlify.app/receipt',
      options: { fullPage: true, type: 'png' },
    }),
  }
);

const buffer = await response.arrayBuffer();
fs.writeFileSync('screenshot.png', Buffer.from(buffer));
```

    **2. Check the output**

    Run the script with `node screenshot.mjs`. The file `screenshot.png` will appear in the current directory.

    
    
**Python:**

    

    **1. Install dependencies**

```bash
pip install requests
```

    **2. Send the request**

```python
import requests

response = requests.post(
    'https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE',
    headers={'Cache-Control': 'no-cache', 'Content-Type': 'application/json'},
    json={'url': 'https://scraping-sandbox.netlify.app/receipt', 'options': {'fullPage': True, 'type': 'png'}},
)

with open('screenshot.png', 'wb') as f:
    f.write(response.content)
```

    **3. Check the output**

    Run the script with `python screenshot.py`. The file `screenshot.png` will appear in the current directory.

    
    
**Java:**

    

    **1. Send the request**

```java
import java.io.*;
import java.net.URI;
import java.net.http.*;

String token = "YOUR_API_TOKEN_HERE";
String endpoint = "https://production-sfo.browserless.io/screenshot?token=" + token;

String jsonData = """
    {
        "url": "https://scraping-sandbox.netlify.app/receipt",
        "options": { "fullPage": true, "type": "png" }
    }
""";

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(endpoint))
    .header("Cache-Control", "no-cache")
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(jsonData))
    .build();

HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofInputStream());
try (FileOutputStream fos = new FileOutputStream("screenshot.png")) {
    response.body().transferTo(fos);
}
System.out.println("Screenshot saved as screenshot.png");
```

    **2. Check the output**

    Run the class. The file `screenshot.png` will appear in the current directory.

    
    
**C#:**

    

    **1. Send the request**

```csharp
using System.IO;
using System.Net.Http;
using System.Text;
using System.Text.Json;

string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"https://production-sfo.browserless.io/screenshot?token={token}";

var payload = new
{
    url = "https://scraping-sandbox.netlify.app/receipt",
    options = new { fullPage = true, type = "png" },
};

using (HttpClient httpClient = new HttpClient())
{
    var request = new HttpRequestMessage(HttpMethod.Post, endpoint);
    request.Headers.Add("Cache-Control", "no-cache");
    request.Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");

    var response = await httpClient.SendAsync(request);
    var contentStream = await response.Content.ReadAsStreamAsync();
    using (var fileStream = new FileStream("screenshot.png", FileMode.Create, FileAccess.Write))
    {
        await contentStream.CopyToAsync(fileStream);
    }
    Console.WriteLine("Screenshot saved as screenshot.png");
}
```

    **2. Check the output**

    Run the program. The file `screenshot.png` will appear in the current directory.

    
  

  
  
**Frameworks:**

  Use a browser connection when you need to interact with the page or wait for dynamic content before capturing.

  

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Connect and capture**

```js
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint: 'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE',
});

try {
  const page = await browser.newPage();
  await page.goto('https://scraping-sandbox.netlify.app/receipt', { waitUntil: 'networkidle2' });
  await page.screenshot({ path: 'screenshot.png', fullPage: true });
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Run the script with `node screenshot.mjs`. The file `screenshot.png` will appear in the current directory.

    
    
**Playwright:**

    

    **1. Install dependencies**

```bash
npm install playwright-core
```

    **2. Connect and capture**

```js
import { chromium } from 'playwright-core';

const browser = await chromium.connect(
  'wss://production-sfo.browserless.io/chromium/playwright?token=YOUR_API_TOKEN_HERE'
);

try {
  const page = await browser.newPage();
  await page.goto('https://scraping-sandbox.netlify.app/receipt', { waitUntil: 'networkidle' });
  await page.screenshot({ path: 'screenshot.png', fullPage: true });
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Run the script with `node screenshot.mjs`. The file `screenshot.png` will appear in the current directory.

    
    
**BAP:**

    **1. Install dependencies**

```bash
npm install @browserless.io/bap-ts
```

    **2. Connect and capture**

```typescript
import Browserless from '@browserless.io/bap-ts';

const browser = Browserless.connect({
  browserWSEndpoint: 'wss://production-sfo.browserless.io/chromium/bql',
  token: 'YOUR_API_TOKEN_HERE',
});

try {
  const page = await browser.newPage();
  await page.goto('https://scraping-sandbox.netlify.app/receipt', { waitUntil: 'networkIdle' });
  await page.screenshot({ path: 'screenshot.png', fullPage: true });
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Check the output**

    Run the script with `node screenshot.mjs`. The file `screenshot.png` will appear in the current directory.

    
  

  
  
**BQL:**

  

  **1. Write the mutation**

  Navigate to the URL and capture a screenshot in one operation:

```graphql
mutation Screenshot {
  goto(url: "https://scraping-sandbox.netlify.app/receipt") {
    status
  }

  screenshot(omitBackground: true) {
    base64
  }
}
```

  **2. Run it**

  Paste into the [BQL IDE](https://account.browserless.io/bql) and click Run.

  **3. Check the output**

  The response includes a `base64`-encoded PNG. Each example above decodes it and saves it as `screenshot.png`.

```json
{
  "data": {
    "goto": { "status": 200 },
    "screenshot": { "base64": "iVBORw0KGgoAAAANSUhEUgAA..." }
  }
}
```

  

## Next steps

  <a href="/examples/pdf" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Generate a PDF</h3>
      export the same page as a document
    
  </a>
  <a href="/examples/scrape" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape Structured Data</h3>
      extract data instead of capturing images
    
  </a>

---

# Log in and reuse sessions

URL: https://docs.browserless.io/examples/sessions

# Log in and reuse sessions

Log in once and reuse that authenticated state across many browser sessions without re-entering credentials.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Steps

Authenticated sessions work in two phases: first you **save** the login state into a named profile, then you **reuse** that profile across future sessions.

  
**REST API:**

  Use the REST API to manage browser sessions without a persistent connection.

  
    
**cURL:**

    

    **1. Create a profile session**

    Start a profile-creation session. The response contains a WebSocket URL to connect a CDP client for the login flow:

```bash
curl -X POST \
  "https://production-sfo.browserless.io/profile?token=YOUR_API_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{ "name": "my-profile" }'
```

    The response includes a `connect` WebSocket URL and a `stop` URL:

```json
{
  "id": "<session-id>",
  "name": "my-profile",
  "connect": "wss://production-sfo.browserless.io/session/connect/<id>?token=...",
  "stop": "https://production-sfo.browserless.io/session/<id>?token=..."
}
```

    **2. Log in and save the profile**

    Use a CDP client (Puppeteer or Playwright) to connect to the `connect` URL, complete the login, and call `Browserless.saveProfile`. See the Puppeteer tab for a complete code example.

    **3. Use the saved profile**

    Once saved, append `?profile=my-profile` to any Browserless request to start with the authenticated state pre-loaded:

```bash
curl -X POST \
  "https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE&profile=my-profile" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://app.example.com/dashboard" }' \
  --output dashboard.png
```

    
    
**JavaScript:**

    

    **1. Create a profile session**

```js
const TOKEN = 'YOUR_API_TOKEN_HERE';
const ORIGIN = 'https://production-sfo.browserless.io';

const session = await fetch(`${ORIGIN}/profile?token=${TOKEN}`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'my-profile' }),
}).then((r) => r.json());

console.log(session.connect);
// Use session.connect with the Frameworks tab to log in and save.
```

    **2. Reuse the saved profile**

```js
import fs from 'fs';

const response = await fetch(
  `${ORIGIN}/screenshot?token=${TOKEN}&profile=my-profile`,
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ url: 'https://app.example.com/dashboard' }),
  }
);

const buffer = await response.arrayBuffer();
await fs.promises.writeFile('dashboard.png', Buffer.from(buffer));
console.log('Screenshot saved.');
```

    
    
**Python:**

    

    **1. Install dependencies**

```bash
pip install playwright
```

    **2. Log in and save the profile**

    Profile creation requires a CDP client. Use the Puppeteer or Playwright tab for the save step, then reuse the profile in Python.

    **3. Reuse the saved profile**

```python
from playwright.sync_api import sync_playwright

TOKEN = "YOUR_API_TOKEN_HERE"
WS_ENDPOINT = (
    f"wss://production-sfo.browserless.io/chromium/playwright"
    f"?token={TOKEN}&profile=my-profile"
)

with sync_playwright() as playwright:
    # connect_over_cdp with /chromium/playwright exposes the profile-loaded context.
    browser = playwright.chromium.connect_over_cdp(WS_ENDPOINT)
    try:
        context = browser.contexts[0]
        page = context.pages[0]
        page.goto("https://app.example.com/dashboard")  # already logged in
        print("Title:", page.title())
    finally:
        # Always close to release the session even on error.
        browser.close()
```

    
    
**Java:**

    

    **1. Add dependencies**

```xml
<dependency>
  <groupId>com.microsoft.playwright</groupId>
  <artifactId>playwright</artifactId>
  <version>1.44.0</version>
</dependency>
```

    **2. Create a profile session**

```java
import java.net.http.*;
import java.net.URI;
import java.nio.file.*;

HttpClient client = HttpClient.newHttpClient();

HttpRequest sessionRequest = HttpRequest.newBuilder()
    .uri(URI.create("https://production-sfo.browserless.io/profile?token=YOUR_API_TOKEN_HERE"))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString("{\"name\": \"my-profile\"}"))
    .build();
HttpResponse sessionResponse = client.send(sessionRequest, HttpResponse.BodyHandlers.ofString());
System.out.println(sessionResponse.body());
// Parse session.connect and use the Playwright Java tab to log in and save.
```

    **3. Reuse the saved profile**

```java
HttpRequest screenshotRequest = HttpRequest.newBuilder()
    .uri(URI.create("https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE&profile=my-profile"))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString("{\"url\": \"https://app.example.com/dashboard\"}"))
    .build();
HttpResponse<byte[]> screenshotResponse = client.send(screenshotRequest, HttpResponse.BodyHandlers.ofByteArray());
Files.write(Path.of("dashboard.png"), screenshotResponse.body());
System.out.println("Screenshot saved.");
```

    
    
**C#:**

    

    **1. Create a profile session**

```csharp
using System.Net.Http;
using System.Text;

using var client = new HttpClient();

var sessionBody = new StringContent("{\"name\": \"my-profile\"}", Encoding.UTF8, "application/json");
var sessionResponse = await client.PostAsync(
    "https://production-sfo.browserless.io/profile?token=YOUR_API_TOKEN_HERE",
    sessionBody
);
Console.WriteLine(await sessionResponse.Content.ReadAsStringAsync());
// Parse session.connect and use the Playwright C# tab to log in and save.
```

    **2. Reuse the saved profile**

```csharp
var screenshotBody = new StringContent("{\"url\": \"https://app.example.com/dashboard\"}", Encoding.UTF8, "application/json");
var screenshotResponse = await client.PostAsync(
    "https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE&profile=my-profile",
    screenshotBody
);
var bytes = await screenshotResponse.Content.ReadAsByteArrayAsync();
await File.WriteAllBytesAsync("dashboard.png", bytes);
Console.WriteLine("Screenshot saved.");
```

    
    
**Go:**

    

    **1. Create a profile session**

```go
package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
)

func main() {
    client := &http.Client{}

    sessionBody := bytes.NewBufferString(`{"name": "my-profile"}`)
    sessionReq, _ := http.NewRequest("POST",
        "https://production-sfo.browserless.io/profile?token=YOUR_API_TOKEN_HERE",
        sessionBody,
    )
    sessionReq.Header.Set("Content-Type", "application/json")
    sessionResp, _ := client.Do(sessionReq)
    defer sessionResp.Body.Close()

    var session map[string]string
    json.NewDecoder(sessionResp.Body).Decode(&session)
    fmt.Println(session["connect"])
    // Use session["connect"] with the Frameworks tab to log in and save.
```

    **2. Reuse the saved profile**

```go
    screenshotBody := bytes.NewBufferString(`{"url": "https://app.example.com/dashboard"}`)
    screenshotReq, _ := http.NewRequest("POST",
        "https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE&profile=my-profile",
        screenshotBody,
    )
    screenshotReq.Header.Set("Content-Type", "application/json")
    screenshotResp, _ := client.Do(screenshotReq)
    defer screenshotResp.Body.Close()
    data, _ := io.ReadAll(screenshotResp.Body)
    os.WriteFile("dashboard.png", data, 0644)
    fmt.Println("Screenshot saved.")
}
```

    
    
**PHP:**

    

    **1. Create a profile session**

```php
<?php
$ch = curl_init('https://production-sfo.browserless.io/profile?token=YOUR_API_TOKEN_HERE');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => '{"name": "my-profile"}',
    CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
    CURLOPT_RETURNTRANSFER => true,
]);
$session = json_decode(curl_exec($ch), true);
curl_close($ch);
// Use $session['connect'] with the Frameworks tab to log in and save.
```

    **2. Reuse the saved profile**

```php
$ch2 = curl_init('https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE&profile=my-profile');
curl_setopt_array($ch2, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => '{"url": "https://app.example.com/dashboard"}',
    CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
    CURLOPT_RETURNTRANSFER => true,
]);
file_put_contents('dashboard.png', curl_exec($ch2));
curl_close($ch2);
echo "Screenshot saved.\n";
```

    
    
**Ruby:**

    

    **1. Create a profile session**

```ruby
require 'net/http'
require 'json'
require 'uri'

uri = URI('https://production-sfo.browserless.io/profile?token=YOUR_API_TOKEN_HERE')
response = Net::HTTP.post(uri, '{"name": "my-profile"}', 'Content-Type' => 'application/json')
session = JSON.parse(response.body)
puts session['connect']
# Use session['connect'] with the Frameworks tab to log in and save.
```

    **2. Reuse the saved profile**

```ruby
uri2 = URI('https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE&profile=my-profile')
response2 = Net::HTTP.post(uri2, '{"url": "https://app.example.com/dashboard"}', 'Content-Type' => 'application/json')
File.binwrite('dashboard.png', response2.body)
puts 'Screenshot saved.'
```

    
  

  
  
**Frameworks:**

  Use a browser connection to manage sessions directly via Puppeteer or Playwright.

  
    
**Puppeteer:**

    

    **1. Install dependencies**

```bash
npm install puppeteer-core
```

    **2. Log in and save the profile**

```js
import puppeteer from 'puppeteer-core';

const TOKEN = 'YOUR_API_TOKEN_HERE';
const ORIGIN = 'https://production-sfo.browserless.io';

const session = await fetch(`${ORIGIN}/profile?token=${TOKEN}`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'my-profile' }),
}).then((r) => r.json());

const browser = await puppeteer.connect({ browserWSEndpoint: session.connect });

try {
  const page = await browser.newPage();

  await page.goto('https://app.example.com/login');
  await page.type('#email', 'user@example.com');
  await page.type('#password', process.env.PASSWORD);
  await page.click("button[type='submit']");
  await page.waitForNavigation();

  const cdp = await page.createCDPSession();
  const result = await cdp.send('Browserless.saveProfile', { name: 'my-profile' });
  console.log(result);
  // { ok: true, profileId: '<id>', name: 'my-profile', cookieCount: 12, originCount: 1 }
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    **3. Reuse the saved profile**

```js
import puppeteer from 'puppeteer-core';

const browser = await puppeteer.connect({
  browserWSEndpoint:
    'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&profile=my-profile',
});

try {
  const page = await browser.newPage();
  await page.goto('https://app.example.com/dashboard'); // already logged in
  console.log('Title:', await page.title());
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

    
    
**Playwright:**

    
      
**JavaScript:**

      

      **1. Install dependencies**

```bash
npm install playwright-core
```

      **2. Log in and save the profile**

```js
import { chromium } from 'playwright-core';

const TOKEN = 'YOUR_API_TOKEN_HERE';
const ORIGIN = 'https://production-sfo.browserless.io';

const session = await fetch(`${ORIGIN}/profile?token=${TOKEN}`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'my-profile' }),
}).then((r) => r.json());

const browser = await chromium.connectOverCDP(session.connect);

try {
  const context = browser.contexts()[0];
  const page = await context.newPage();

  await page.goto('https://app.example.com/login');
  await page.fill('#email', 'user@example.com');
  await page.fill('#password', process.env.PASSWORD);
  await page.click("button[type='submit']");
  await page.waitForURL('**/dashboard');

  const cdpSession = await context.newCDPSession(page);
  const result = await cdpSession.send('Browserless.saveProfile', { name: 'my-profile' });
  console.log(result);
  // { ok: true, profileId: '<id>', name: 'my-profile', cookieCount: 12, originCount: 1 }.
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

      **3. Reuse the saved profile**

```js
import { chromium } from 'playwright-core';

const browser = await chromium.connect(
  `wss://production-sfo.browserless.io/chromium/playwright?token=YOUR_API_TOKEN_HERE&profile=my-profile`
);

try {
  // connect() uses Playwright native mode — browser.newPage() creates a page in the
  // profile-loaded default context without needing to access contexts() directly.
  const page = await browser.newPage();
  await page.goto('https://app.example.com/dashboard'); // already logged in.
  console.log('Title:', await page.title());
} finally {
  // Always close to release the session even on error.
  await browser.close();
}
```

      
      
**Python:**

      

      **1. Install dependencies**

```bash
pip install requests playwright
```

      **2. Log in and save the profile**

```python
import requests
from playwright.sync_api import sync_playwright

TOKEN = 'YOUR_API_TOKEN_HERE'
ORIGIN = 'https://production-sfo.browserless.io'

session = requests.post(
    f'{ORIGIN}/profile?token={TOKEN}',
    json={'name': 'my-profile'},
).json()

with sync_playwright() as playwright:
    # connect_over_cdp gives access to CDP sessions needed for Browserless.saveProfile.
    browser = playwright.chromium.connect_over_cdp(session['connect'])
    try:
        context = browser.contexts[0]
        page = context.new_page()

        page.goto('https://app.example.com/login')
        page.fill('#email', 'user@example.com')
        page.fill('#password', 'YOUR_PASSWORD')
        page.click("button[type='submit']")
        page.wait_for_url('**/dashboard')

        cdp = page.context.new_cdp_session(page)
        result = cdp.send('Browserless.saveProfile', {'name': 'my-profile'})
        print(result)
        # {'ok': True, 'profileId': '<id>', 'name': 'my-profile', 'cookieCount': 12, 'originCount': 1}
    finally:
        # Always close to release the session even on error.
        browser.close()
```

      **3. Reuse the saved profile**

```python
from playwright.sync_api import sync_playwright

WS_REUSE = (
    'wss://production-sfo.browserless.io/chromium/playwright'
    '?token=YOUR_API_TOKEN_HERE&profile=my-profile'
)

with sync_playwright() as playwright:
    browser = playwright.chromium.connect_over_cdp(WS_REUSE)
    try:
        context = browser.contexts[0]
        page = context.pages[0]
        page.goto('https://app.example.com/dashboard')  # already logged in.
        print('Title:', page.title())
    finally:
        # Always close to release the session even on error.
        browser.close()
```

      
      
**Java:**

      

      **1. Add dependencies**

```xml
<dependency>
  <groupId>com.microsoft.playwright</groupId>
  <artifactId>playwright</artifactId>
  <version>1.44.0</version>
</dependency>
<!-- Add Gson or Jackson for JSON parsing -->
<dependency>
  <groupId>com.google.code.gson</groupId>
  <artifactId>gson</artifactId>
  <version>2.10.1</version>
</dependency>
```

      **2. Log in and save the profile**

```java
import com.microsoft.playwright.*;
import com.google.gson.*;
import java.net.http.*;
import java.net.URI;

public class SaveSession {
    public static void main(String[] args) throws Exception {
        String token = "YOUR_API_TOKEN_HERE";
        String origin = "https://production-sfo.browserless.io";

        HttpClient http = HttpClient.newHttpClient();
        HttpRequest req = HttpRequest.newBuilder()
            .uri(URI.create(origin + "/profile?token=" + token))
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString("{\"name\": \"my-profile\"}"))
            .build();
        String body = http.send(req, HttpResponse.BodyHandlers.ofString()).body();
        String wsEndpoint = JsonParser.parseString(body)
            .getAsJsonObject().get("connect").getAsString();

        try (Playwright playwright = Playwright.create()) {
            Browser browser = playwright.chromium().connectOverCDP(wsEndpoint);
            try {
                BrowserContext context = browser.contexts().get(0);
                Page page = context.newPage();

                page.navigate("https://app.example.com/login");
                page.fill("#email", "user@example.com");
                page.fill("#password", System.getenv("PASSWORD"));
                page.click("button[type='submit']");
                page.waitForURL("**/dashboard");

                // connectOverCDP gives access to CDP sessions needed for Browserless.saveProfile.
                CDPSession cdp = page.context().newCDPSession(page);
                JsonElement result = cdp.send("Browserless.saveProfile", Map.of("name", "my-profile"));
                System.out.println(result);
                // {"ok":true,"profileId":"<id>","name":"my-profile","cookieCount":12,"originCount":1}
            } finally {
                // Always close to release the session even on error.
                browser.close();
            }
        }
    }
}
```

      **3. Reuse the saved profile**

```java
String wsReuse = "wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&profile=my-profile";

try (Playwright playwright = Playwright.create()) {
    Browser browser = playwright.chromium().connectOverCDP(wsReuse);
    try {
        BrowserContext context = browser.contexts().get(0);
        Page page = context.pages().get(0);
        page.navigate("https://app.example.com/dashboard"); // already logged in.
        System.out.println("Title: " + page.title());
    } finally {
        // Always close to release the session even on error.
        browser.close();
    }
}
```

      
      
**C#:**

      

      **1. Add dependencies**

```bash
dotnet add package Microsoft.Playwright
```

      **2. Log in and save the profile**

```csharp
using Microsoft.Playwright;
using System.Net.Http;
using System.Text;
using System.Text.Json;

const string Token = "YOUR_API_TOKEN_HERE";
const string Origin = "https://production-sfo.browserless.io";

using var http = new HttpClient();
var sessionBody = new StringContent("{\"name\": \"my-profile\"}", Encoding.UTF8, "application/json");
var sessionResp = await http.PostAsync($"{Origin}/profile?token={Token}", sessionBody);
var sessionJson = JsonDocument.Parse(await sessionResp.Content.ReadAsStringAsync());
var wsEndpoint = sessionJson.RootElement.GetProperty("connect").GetString();

using var playwright = await Playwright.CreateAsync();
var browser = await playwright.Chromium.ConnectOverCDPAsync(wsEndpoint);

try
{
    var context = browser.Contexts[0];
    var page = await context.NewPageAsync();

    await page.GotoAsync("https://app.example.com/login");
    await page.FillAsync("#email", "user@example.com");
    await page.FillAsync("#password", Environment.GetEnvironmentVariable("PASSWORD"));
    await page.ClickAsync("button[type='submit']");
    await page.WaitForURLAsync("**/dashboard");

    // ConnectOverCDP gives access to CDP sessions needed for Browserless.saveProfile.
    var cdp = await page.Context.NewCDPSessionAsync(page);
    var result = await cdp.SendAsync(
        "Browserless.saveProfile",
        new Dictionary<string, object> { ["name"] = "my-profile" }
    );
    Console.WriteLine(result);
    // { ok: true, profileId: '<id>', name: 'my-profile', cookieCount: 12, originCount: 1 }
}
finally
{
    // Always close to release the session even on error.
    await browser.CloseAsync();
}
```

      **3. Reuse the saved profile**

```csharp
using var playwright = await Playwright.CreateAsync();
var browser = await playwright.Chromium.ConnectOverCDPAsync(
    "wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&profile=my-profile"
);

try
{
    var context = browser.Contexts[0];
    var page = context.Pages[0];
    await page.GotoAsync("https://app.example.com/dashboard"); // already logged in.
    Console.WriteLine($"Title: {await page.TitleAsync()}");
}
finally
{
    // Always close to release the session even on error.
    await browser.CloseAsync();
}
```

      
    

    
  

  

## Next steps

  <a href="/examples/scrape" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape Structured Data</h3>
      extract data from authenticated pages
    
  </a>
  <a href="/examples/forms" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Fill and Submit a Form</h3>
      automate interactions in logged-in flows
    
  </a>
  <a href="/examples/screenshot" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Take a Screenshot</h3>
      capture authenticated pages as images
    
  </a>

---

# Account Tools

URL: https://docs.browserless.io/mcp/account-tools

# Account Tools

The Browserless MCP server exposes five read-only tools that report on the account behind your API token: plan and billing, unit consumption, Browserless-side request logs, sessions and replays, and saved authentication profiles. Use them to let your AI assistant answer questions like "what plan am I on", "why did my run fail", or "which login profiles already exist" without leaving the conversation.

All five are stateless: one call in, one result out. None of them changes account state, stops a session, or returns API token values.

- Set up the [Browserless MCP Server](/mcp/browserless-mcp-server/setup) in your MCP client (Claude Desktop, Cursor, VS Code, Windsurf, Claude Code, etc.)

## `browserless_account`

Reads the account behind the current API token: plan, unit balance, billing period, payment status, and the names of the account's API keys. Token values are never returned; create or rotate keys in the dashboard.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `action` | string | Yes | Which part of the account to read: `billing` returns the plan, unit balance, and billing period; `keys` lists the account API keys by name and id |

Key ids returned by `action: "keys"` can be passed as `apiKeyId` to `browserless_logs` or `apiKeyIds` to `browserless_usage` to scope those reads to one key.

**Example prompts:**

> What plan am I on, and how many units are left this period?

> List my API keys and tell me which ones are revoked.

## `browserless_usage`

Reads request and unit consumption for the account: successes, errors, timeouts, queueing, rejections, peak concurrency, CAPTCHAs solved, proxy usage, and billed units for the current period. For per-request detail on failures, use `browserless_logs` instead.

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `timeframe` | string | No | `"day"` | Window the request counts cover: `hour`, `day`, or `week` |
| `apiKeyIds` | string[] | No | — | Restrict the numbers to specific API keys, by id (max 50). Omit for the whole account. Get ids from `browserless_account` with `action: "keys"` |

**Example prompts:**

> How much have I used this week?

> Why is my bill high? Break down usage by my production API key.

## `browserless_logs`

Reads Browserless's own record of the account's recent requests: what was attempted, whether it failed, why it stopped, how long it took, and what it cost. This is the tool for diagnosing a run that failed on the Browserless side rather than in your own code. How far back you can query depends on the account plan; the server reports the limit if a range is refused.

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `startTime` | string | No | plan window | Inclusive RFC 3339 start time. Omit to let the plan decide how far back to look |
| `endTime` | string | No | now | Exclusive RFC 3339 end time |
| `limit` | number | No | `50` | Maximum entries to return (1-100) |
| `requestId` | string | No | — | Return only entries for one request id |
| `url` | string | No | — | Filter by the target URL of the request |
| `eventNames` | string[] | No | — | Filter by lifecycle event name, e.g. `request.failed`, `bql.*.failed` (max 20) |
| `outcome` | string | No | — | Filter by request outcome, e.g. `failed` or `succeeded` |
| `apiKeyId` | string | No | — | Restrict to one API key, by id |
| `endpoint` | string | No | — | Filter by endpoint, e.g. `/chromium/bql` or `/screenshot` |
| `category` | string | No | — | Filter by failure category, e.g. `browserless_refused`, `browserless_killed`, `target_error` |
| `reason` | string | No | — | Filter by the specific failure reason within a category |
| `levels` | string[] | No | all | Severity levels to include: `TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`, `FATAL` |
| `order` | string | No | `"desc"` | Timestamp order: `asc` or `desc` (newest first) |
| `cursor` | string | No | — | Opaque cursor returned as `nextCursor` by the previous page |

**Example prompts:**

> Show me every request that failed in the last hour and why.

> Pull the logs for request id abc123 so we can see where it stopped.

## `browserless_sessions`

Inspects the sessions on the account: browsers running right now, persistent sessions saved on dedicated workers, recorded session replays, and 1Password credential integrations. Read-only; it never stops a session. The `replay` action downloads one recording and returns a self-contained playable [rrweb](https://www.rrweb.io/) page that needs no network access to render.

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `action` | string | Yes | — | Which session data to read: `active` (browsers running right now), `persistent` (saved sessions on dedicated workers, running or not), `replays` (list recorded session replays), `replay` (download one replay as a playable page; needs `sessionId`), `integrations` (1Password credential integrations) |
| `sessionId` | string | No | — | Which replay to download, for `action: "replay"`. Get ids from `action: "replays"` |
| `limit` | number | No | — | Maximum rows to return (max 50). Applies to every action |
| `skip` | number | No | `0` | Rows to skip, for paging through `active` or `integrations` |
| `page` | number | No | `1` | Page number for `replays` (1-based) |
| `search` | string | No | — | Filter `replays` by website or session id |

**Example prompts:**

> What browser sessions are running on my account right now?

> Find the replay of my last checkout run and play it back.

## `browserless_profiles`

Lists the authentication profiles saved for the current token. A profile is a saved logged-in browser state (cookies plus storage) that other tools can replay by passing its name as `profile`. Call it before a task that needs the browser to start signed in, to discover which profiles exist and pick one by name. Returns each profile's name, cookie and origin counts, and last-used time.

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `limit` | number | No | `100` | Maximum number of profiles to return (max 1000) |
| `offset` | number | No | `0` | Number of profiles to skip, for pagination |

Profiles are created by running an authenticated session with `createProfile`; the [Browser Agent](/mcp/browser-agent) walks through this when a task needs a login.

**Example prompt:**

> Which login profiles do I have saved, and when was each last used?

## FAQ & Troubleshooting

<details>
<summary>Why does <code>browserless_usage</code> show zero requests but a non-zero unit count?</summary>

The per-request counters and the billed-unit ledger come from different sources. On shared cloud-unit plans the request counters can be empty while the unit ledger still records consumption, so the billed-units section is the number that answers "how much have I used". Scope by `apiKeyIds` if you need to attribute usage to a specific key.

</details>

<details>
<summary>Why was my <code>browserless_logs</code> time range rejected?</summary>

The log window you can query is plan-dependent. If your `startTime` reaches further back than the plan allows, the server refuses the range and reports the limit in the error. Retry with a `startTime` inside that window, or omit it to get the widest range your plan supports.

</details>

<details>
<summary>Can these tools change my account, or leak my API tokens?</summary>

No. All five tools are read-only: they never modify plans or keys, never stop a session, and never return API token values. `browserless_account` returns key names and ids only; create or rotate the actual tokens in the [account dashboard](https://browserless.io/account/).

</details>

## Next steps

  <a href="/mcp/rest-api-tools" className="next-step-card-link">
    
      <h3 className="next-step-card-title">REST API Tools</h3>
      the stateless scraping, export, search, and audit tools
    
  </a>
  <a href="/mcp/browser-agent" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Browser Agent</h3>
      the stateful, multi-turn browser automation tool
    
  </a>

---

# Browser Agent

URL: https://docs.browserless.io/mcp/browser-agent

# Browser Agent

The `browserless_agent` tool turns any MCP-compatible AI assistant into a browser-using agent. It exposes a **snapshot-based** protocol so the model can reason about what's on the page and drive it step by step. Sessions are **one-shot by default**; opt in on the first call to keep a **browser session** across tool calls.

Use it when a single scrape isn't enough — filling forms, logging in, clicking through multi-step flows, paginating search results, interacting with consent dialogs, or any task that needs the browser's state to survive between turns.

- Set up the [Browserless MCP Server](/mcp/browserless-mcp-server/setup) in your MCP client (Claude Desktop, Cursor, VS Code, Windsurf, Claude Code, etc.)

## How it works

The agent follows the classic **ReAct** loop — **Reason → Act → Observe**:

1. **goto** — navigate to a URL
2. **snapshot** — capture every interactive and informational element on the page (buttons, links, inputs, headings, images with alt text) as a compact list with stable selectors
3. **plan** — the model reads the snapshot and decides what to do
4. **act** — `click`, `type`, `select`, `scroll`, etc., using selectors taken directly from the snapshot
5. **re-snapshot** whenever the page changes (navigation, form submission)
6. Repeat until the task is done, then **close**

The browser keeps cookies, local storage, and navigation history across commands. For this loop to span tool calls, set `keepSessionAlive: true` on the first call, then pass the returned `sessionId` on later calls. That keeps the same browser state available — you can log in once and keep interacting with authenticated pages.

## Session lifetime

This contract applies to MCP server **v1.33.0 and later**. Earlier versions kept sessions alive by default. Check your server version when upgrading a self-hosted or pinned client.

These are top-level `browserless_agent` parameters, alongside `commands`:

| Parameter | Type | Behavior |
|-----------|------|----------|
| `sessionId` | Optional string | Pass the ID returned by a kept-alive call to continue that browser. Omitting it starts a new browser, not a continuation. |
| `keepSessionAlive` | Optional boolean, no schema default | Omitted on a new session: one-shot. Set `true` on the **first call** for multi-step work. On later calls, echoing `sessionId` keeps it alive without repeating the flag. Explicit `false` force-closes even when continuing. |

| `sessionId` | `keepSessionAlive` | After a successful command batch and download drain |
|-------------|--------------------|----------------------------------------------------|
| Omitted | Omitted | Close the browser and free its concurrency slot. |
| Omitted | `true` | Keep the new browser for later calls. |
| Returned ID | Omitted or `true` | Keep that browser for further calls. |
| Omitted or returned ID | `false` | Close the browser, including a continued session. |

**Exceptions:** `keepSessionAlive` is ignored for profile creation (`createProfile`) and attached sessions (`attachSessionId`), whose lifetimes are managed separately. If download collection fails, the session is retained for recovery instead of being auto-closed: retry `getDownloads` with the `sessionId` supplied in the error, then close the session when finished. Kept sessions still have the unchanged **15-minute idle backstop**; keeping a session alive is not indefinite retention.

### One-shot example

When one batch finishes the task, omit both lifetime parameters. This call reads the page and automatically closes after the batch and download drain succeed:

```json
{
  "rationale": "Reading the example page",
  "commands": [
    { "method": "goto", "params": { "url": "https://example.com" } },
    { "method": "text" }
  ]
}
```

### Multi-call example

If you need to inspect a snapshot before deciding what to do next, opt in **before that first call finishes**. When unsure whether more calls are needed, keep the session alive.

**1. Start and inspect:**

```json
{
  "rationale": "Inspecting the example page",
  "keepSessionAlive": true,
  "commands": [
    { "method": "goto", "params": { "url": "https://example.com" } },
    { "method": "snapshot" }
  ]
}
```

**2. Continue:** replace `SESSION_ID_FROM_FIRST_CALL` with the returned ID. No `keepSessionAlive` flag is needed; echoing the ID keeps the browser alive.

```json
{
  "rationale": "Reading the page text",
  "sessionId": "SESSION_ID_FROM_FIRST_CALL",
  "commands": [{ "method": "text" }]
}
```

**3. Finish and force-close:** explicit `false` overrides the keep-alive implied by `sessionId`.

```json
{
  "rationale": "Reporting completion",
  "sessionId": "SESSION_ID_FROM_FIRST_CALL",
  "keepSessionAlive": false,
  "commands": [
    {
      "method": "reportOutcome",
      "params": { "success": true, "reason": "completed" }
    }
  ]
}
```

You can also end a kept session with an explicit `close` command, optionally preceded by `reportOutcome`.

> **Hard-coded multi-call clients**
> Update scripts that open a browser and expect to reuse it later: set `keepSessionAlive: true` on the **first** call, then echo the returned `sessionId` on every continuation. Passing the ID only on the second call is too late if the first call was one-shot; it cannot restore the closed browser's page, login, or progress.

## Available methods

### Navigation

| Method | Purpose |
|--------|---------|
| `goto { url, waitUntil? }` | Navigate to a URL. Defaults to `waitUntil: "domcontentloaded"`. |
| `back { waitUntil? }` | Go back in browser history. |
| `forward { waitUntil? }` | Go forward in browser history. |
| `reload { waitUntil? }` | Reload the current page. |

### Observation

| Method | Purpose |
|--------|---------|
| `snapshot { maxElements?, full?, targetId? }` | Capture the page as a list of interactive + informational elements with selectors. The first snapshot is complete; later ones return only what changed since the previous snapshot. Pass `full: true` to get the complete element list again. `targetId` snapshots a background tab without switching to it. |
| `text { selector? }` | Extract the text of a specific element (or the whole page). |
| `html { selector? }` | Get the raw HTML of a section (or the whole page). |
| `screenshot { fullPage?, selector?, clip?, type?, quality?, toDisk? }` | Capture the page, an element, or a region as an image. `selector`, `clip`, and `fullPage` are mutually exclusive. `toDisk: true` saves the file and returns a file reference instead of inline image data (see [Files](#files) for how references work per transport). |
| `evaluate { content }` | Run arbitrary JavaScript in the page context (IIFE syntax). |

### Interaction

| Method | Purpose |
|--------|---------|
| `click { selector }` | Click an element. |
| `type { selector, text }` | Type text into an input. |
| `select { selector, value }` | Choose an option in a `<select>`. |
| `checkbox { selector, checked? }` | Toggle a checkbox (preferred over `click` for checkboxes). |
| `hover { selector }` | Hover over an element. |
| `scroll { selector?, direction? }` | Scroll the page or a specific element. |

### Secrets and CAPTCHAs

| Method | Purpose |
|--------|---------|
| `loadSecret { ref, selector? }` | Inject a credential from a secrets vault (for example an `op://` reference) into an input. The value is resolved server-side and typed into the field, so it never enters the model's context. Use this instead of `type` for vault-managed usernames and passwords. |
| `solve { type?, timeout?, wait? }` | Solve a CAPTCHA on the page. Auto-detects the type when omitted; supports Cloudflare, reCAPTCHA (v2/v3), GeeTest, DataDome, Akamai, and more. Experimental and cloud-only: self-hosted Enterprise deployments lack the solver backend. |

### Files

| Method | Purpose |
|--------|---------|
| `uploadFile { selector, files }` | Attach files to an `<input type="file">`. Each file comes from a local `path` (stdio mode), a download `handle`, or inline base64 `content`. Combined size is capped (10MB default, 50MB max). |
| `getDownloads` | List the files the page has downloaded, returned as file references you can pass back to `uploadFile` or save locally. |

File references are transport-specific. In stdio (local) mode they're plain filesystem paths. Over HTTP each file comes with two references: a single-use download URL for saving the bytes, and a `browserless-download://<id>` handle for `uploadFile`. The download URL is consumed on first use, so don't pass it as an upload handle; use the `browserless-download://` handle instead.

### Tabs

The session isn't limited to a single page. Sites that open results in new tabs (external links, OAuth popups, print views) stay reachable:

| Method | Purpose |
|--------|---------|
| `getTabs` | List every open tab with its `targetId`, URL, and title. |
| `switchTab { targetId }` | Make another tab the active one. |
| `createTab { url?, activate?, waitUntil? }` | Open a new tab. Pass `activate: false` to open it in the background and keep the current tab active. `waitUntil` applies only when activating; defaults to `"domcontentloaded"`. |
| `closeTab { targetId }` | Close a specific tab. |

### Waiting

| Method | Purpose |
|--------|---------|
| `waitForSelector { selector, timeout? }` | Wait for a DOM element to appear. |
| `waitForNavigation { timeout? }` | Wait for a page navigation to complete. |
| `waitForTimeout { time }` | Wait for a fixed duration (ms). |
| `waitForRequest { url?, method?, timeout? }` | Wait for a network request matching a URL glob. |
| `waitForResponse { url?, statuses?, timeout? }` | Wait for a network response matching a URL glob and/or status codes. |

### Session

| Method | Purpose |
|--------|---------|
| `liveURL { timeout?, interactable?, quality?, type?, resizable? }` | Open a shareable live view of the browser that a human can watch or interact with. |
| `close` | End the browser session. |

### Reporting

| Method | Purpose |
|--------|---------|
| `reportOutcome { success, reason? }` | Optionally report the task verdict before `close`. `success` is a boolean; `reason` is optional and must be one of `completed`, `blocked_by_site`, `captcha`, `login_required`, `timeout`, or `other`. The last report wins and does not replace the page result returned by a batch. |

## Batching

When multiple actions share the same page state (for example filling a form), they can be batched into a single tool call using the `commands` array. This is significantly faster than invoking the tool once per step.

Continue a browser kept alive by an earlier call, using selectors from its snapshot:

```json
{
  "sessionId": "SESSION_ID_FROM_FIRST_CALL",
  "commands": [
    { "method": "type", "params": { "selector": "input#email", "text": "user@example.com" } },
    { "method": "type", "params": { "selector": "input#password", "text": "secret" } },
    { "method": "click", "params": { "selector": "input#remember-me" } },
    { "method": "click", "params": { "selector": "button[type='submit']" } }
  ]
}
```

Commands run sequentially, so `goto` can be followed by `text` or `snapshot` in the same batch, as in the examples above. Batch interactions that use the same page state, but don't reuse selectors from an old snapshot after navigation or a form submission. Capture a fresh snapshot first; if you need another tool call to decide the next interaction, keep the session alive as shown in the [multi-call example](#multi-call-example).

## Error recovery

The agent protocol returns structured errors with codes the model can reason about:

| Code | Meaning | Typical recovery |
|------|---------|------------------|
| `SELECTOR_NOT_FOUND` | Element didn't exist or wasn't found in time | Try a deep selector (`< selector`) — the element may be in shadow DOM. Otherwise re-snapshot. |
| `NAVIGATION_TIMEOUT` | Page didn't reach the requested ready state | Retry with a longer timeout or `waitUntil: "domcontentloaded"`. |
| `TIMEOUT` | Generic operation timeout | Retry with a longer timeout. |
| `BROWSER_CRASHED` | Session is gone | The MCP client transparently reconnects — the agent just re-navigates. |
| `INVALID_PARAMS` | Bad arguments | Not retryable. |
| `RATE_LIMITED` | Too many requests | Wait and retry. |

When a selector isn't found, the error response also includes a **fresh snapshot** so the model can re-plan without an extra round trip.

## When to use the agent vs. Smart Scraper

- **Use `browserless_smartscraper`** when you need the content of a single page. It picks the best strategy (direct fetch → proxy → headless browser → CAPTCHA solving) and returns the content in your preferred format.
- **Use `browserless_agent`** when the task requires **multiple turns, state, or interaction** — logging in, filling forms, paginating, navigating menus, reacting to what's on the page.

## Example tasks

Ask your AI assistant:

> Find a French ratatouille recipe on Allrecipes with a 4-star rating or higher and at least 15 reviews. Note the variety of vegetables included and the overall cooking time.

> Open Google Flights, search for a round-trip flight from New York to Tokyo departing March 10, 2026 and returning March 24, 2026, and list the five cheapest options.

> Search for a local text-to-speech repository on GitHub updated in the last 10 days with at least 200 stars and summarize its main objective.

> Go to FedEx. Calculate the shipping rates for a 10-pound package from Miami, FL to San Francisco, CA, and summarize prices and delivery dates.

## FAQ & Troubleshooting

<details>
<summary>How is this different from the <code>/agent/run</code> REST endpoint?</summary>

They split the reasoning differently. With `browserless_agent`, **your** AI model does the thinking: it reads snapshots, plans, and drives the browser step by step through MCP tool calls. With [`/agent/run`](/rest-apis/agent-run), **Browserless's** managed agent does the thinking: you POST a task in natural language, it browses autonomously, and you poll for the structured result. Use this tool when you already have an MCP-connected assistant in the loop; use `/agent/run` when you want to submit a task from any HTTP client and collect the answer.

</details>

<details>
<summary>What is the Browserless browser agent MCP tool?</summary>

The browser agent tool gives AI models direct control of a browser session, allowing them to navigate, click, type, and extract information from web pages autonomously through natural language instructions.

</details>

<details>
<summary>How does the browser agent differ from REST API tools?</summary>

REST API tools call individual Browserless endpoints for specific tasks. The browser agent provides a live browser session where the AI can perform multi-step interactions, making it better suited for complex, dynamic workflows.

</details>

<details>
<summary>Can the browser agent handle login flows?</summary>

Yes. The browser agent can navigate to login pages, fill in credentials, handle multi-factor authentication prompts, and maintain session state across subsequent page interactions.

For sites you log into repeatedly, use [Authenticated Profiles](/baas/features/authenticated-profiles) to save the login once and reuse it across sessions instead of logging in every time.

</details>

## Next steps

  <a href="/ai-integrations/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">AI integrations overview</h3>
      all supported AI frameworks and tools
    
  </a>
  <a href="/browserql/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BQL overview</h3>
      use BrowserQL for advanced browser automation
    
  </a>

---

# Browserless Docs MCP

URL: https://docs.browserless.io/mcp/browserless-docs-mcp

# Browserless Docs MCP

Connect to Browserless documentation directly from your IDE or AI tool. This is a read-only documentation server. For browser automation capabilities via MCP, see the [Browserless MCP Server](/mcp/overview).

- An MCP-compatible IDE or AI tool (Cursor, Claude Desktop, VS Code, etc.)

## Setup

Server URL: `https://browserless-docs.mcp.kapa.ai`

Setup steps vary depending on which AI assistant you're using.

**Cursor:**

Add the following to your `.cursor/mcp.json` file:

```json
{
  "mcpServers": {
    "browserless-docs": {
      "type": "http",
      "url": "https://browserless-docs.mcp.kapa.ai"
    }
  }
}
```

For more information, see the [Cursor MCP documentation](https://cursor.com/docs/context/mcp#using-mcpjson).

**VS Code:**

Prerequisites: VS Code 1.102+ with GitHub Copilot enabled.

Create an `mcp.json` file in your workspace `.vscode` folder:

```json title=".vscode/mcp.json"
{
  "servers": {
    "browserless-docs": {
      "type": "http",
      "url": "https://browserless-docs.mcp.kapa.ai"
    }
  }
}
```

For more details, see the [VS Code MCP documentation](https://code.visualstudio.com/docs/copilot/customization/mcp-servers).

**Claude Code:**

Run the following command in your terminal:

```bash
claude mcp add browserless-docs https://browserless-docs.mcp.kapa.ai
```

For more information, see the [Claude Code MCP documentation](https://docs.anthropic.com/en/docs/claude-code/mcp).

**ChatGPT Desktop:**

If your ChatGPT Desktop build exposes remote MCP or custom connector support, you can use the same server URL:

1. Open ChatGPT Desktop.
2. Go to **Settings > Features**.
3. Enable **Developer mode**.
4. Navigate to **Settings > MCP Servers**.
5. Click **Add Server** and enter:
   - Name: `browserless-docs`
   - URL: `https://browserless-docs.mcp.kapa.ai`

For current setup guidance, see the [OpenAI platform docs](https://platform.openai.com/docs).

**Claude Desktop:**

Add to your Claude Desktop config file:

**macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`

**Windows:** `%APPDATA%\Claude\claude_desktop_config.json`

```json
{
  "mcpServers": {
    "browserless-docs": {
      "type": "http",
      "url": "https://browserless-docs.mcp.kapa.ai"
    }
  }
}
```

Restart Claude Desktop for changes to take effect.

For more details, see the [Claude Desktop documentation](https://support.claude.com/en/articles/11175166-getting-started-with-custom-connectors-using-remote-mcp).

**Other:**

MCP is an open protocol supported by many clients. Use the server URL `https://browserless-docs.mcp.kapa.ai` and refer to your client's documentation for setup instructions.

Most clients accept the standard MCP JSON configuration format:

```json
{
  "mcpServers": {
    "browserless-docs": {
      "url": "https://browserless-docs.mcp.kapa.ai"
    }
  }
}
```

## What you can do

Once connected, you can ask context-aware questions about Browserless from within your editor. For example:

- "How do I connect to Browserless with Puppeteer?"
- "What are the available stealth options?"
- "How do I solve CAPTCHAs with BrowserQL?"

For a full overview of AI and agent integration options, see the [AI & Agent Integrations guide](/ai-integrations/start). To get started with Browserless itself, check the [Quickstart guide](/baas/quick-start).

## FAQ & Troubleshooting

<details>
<summary>How do I connect my agent to Browserless?</summary>

Pass your Browserless WebSocket URL (`wss://production-sfo.browserless.io/chromium?token=YOUR_TOKEN`) as the browser endpoint in your agent's configuration.

</details>

<details>
<summary>My agent is getting blocked by bot detection</summary>

Use the [Browserless Agent](/mcp/browser-agent) instead of a plain Puppeteer/Playwright connection — it runs on Browserless's managed, stealth-enabled browser infrastructure with residential proxies built in, so most bot detection is handled without extra configuration.

</details>

## Next steps

  <a href="/ai-integrations/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">AI integrations overview</h3>
      all supported AI frameworks and tools
    
  </a>
  <a href="/browserql/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BQL overview</h3>
      use BrowserQL for advanced browser automation
    
  </a>

---

# Connector

URL: https://docs.browserless.io/mcp/browserless-mcp-server/connector

# Connector

The **Connector** is a separate MCP endpoint for AI assistants and app-directory listings. It gives an assistant a cloud browser to navigate pages, read, fill forms, capture screenshots, run Lighthouse audits, search the web, and export pages.

For broader browser automation tasks, use the [full MCP server](/mcp/browserless-mcp-server/setup) instead.

- A Browserless account
- An MCP-compatible client (Claude, ChatGPT, etc.)

## Endpoint

```bash
https://mcp.browserless.io/mcp/connector
```

The full server remains available at `https://mcp.browserless.io/mcp`.

## Authentication

Authentication is identical to the full server. OAuth discovery is served at the domain root and shared by both endpoints, so signing in once works for either.

| Method | Best for |
|--------|----------|
| **OAuth** (Browserless account login) | Clients that support OAuth — no token needed |
| **`Authorization` header** | Clients that support custom headers |

  
**OAuth:**

Sign in with your Browserless account when the client prompts you. No token to copy or manage. OAuth is enabled on the Connector at `https://mcp.browserless.io/mcp/connector` with no extra configuration.

  
  
**API Token:**

Copy your token from the [account dashboard](https://browserless.io/account/), then send it in the `Authorization` header:

`Authorization: Bearer YOUR_API_TOKEN_HERE`

  

## Tools

The Connector exposes **five** tools.

### `browserless_agent`

Drives a real browser across multiple steps (navigate, read, click, type, screenshot, work across tabs) to complete a task you direct. See the [Browser Agent](/mcp/browser-agent) page for the full command reference.

**Example prompts:**

> Go to https://example.com and tell me the page heading and where its link goes.

> Open the docs page at &lt;url&gt;, then take a screenshot of the page.

### `browserless_search`

Searches the web and returns web, news, or image results.

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `query` | string | Yes | — | The search query |
| `limit` | number | No | `10` | Maximum results (max 100) |
| `sources` | string[] | No | `["web"]` | `web`, `news`, `images` |
| `tbs` | string | No | — | Time filter: `day`, `week`, `month`, `year` |
| `lang` / `country` / `location` | string | No | — | Locale / geo hints |

**Example prompt:**

> Search the web for the latest documentation on the Model Context Protocol.

### `browserless_export`

Fetches a URL and returns its content in the native format (HTML, PDF, or image), auto-detecting the content type.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `url` | string | Yes | The URL to export (http or https) |
| `gotoOptions` | object | No | Navigation options (`waitUntil`, `timeout`, `referer`) |
| `bestAttempt` | boolean | No | Proceed even if awaited events time out |
| `waitForTimeout` | number | No | Milliseconds to wait after load before exporting |

**Example prompt:**

> Export the page at https://example.com.

### `browserless_performance`

Runs a Lighthouse audit on a URL and returns scores and metrics.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `url` | string | Yes | The URL to audit (http or https) |
| `categories` | string[] | No | `accessibility`, `best-practices`, `performance`, `pwa`, `seo` — omit for all |
| `budgets` | object[] | No | Lighthouse performance budgets |

**Example prompt:**

> Run a Lighthouse performance audit on https://example.com and list the top issues.

### `browserless_skill`

Loads an on-demand agent skill: concise guidance the agent uses when a page needs special handling. The agent also auto-loads the relevant skill mid-task; this tool is the explicit fallback.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `id` | string | Yes | One of: `shadow-dom`, `cookie-consent`, `modals`, `snapshot-misses`, `dynamic-content`, `screenshots`, `vision-fallback`, `tabs` |

## Scope

The Connector focuses on user-directed automation on content you point it at. Anything beyond that — single-shot scraping, site crawling, sitemap discovery, custom code, or proxy control — lives on the [full MCP server](/mcp/browserless-mcp-server/setup). Not part of the Connector:

- **Tools:** `browserless_smartscraper`, `browserless_crawl`, `browserless_map`, `browserless_function`, and `browserless_profiles`.
- **Agent commands:** arbitrary code execution (`evaluate`), CAPTCHA solving, credential/profile loading, and file transfer (`uploadFile` / `getDownloads`).
- **Parameters:** `proxy` routing, authentication `profile` / `createProfile` (agent, export, performance), bulk-asset `includeResources` (export), and per-result `scrapeOptions` (search).

The Connector operates only on content you direct it to. It does not bypass access controls or bot protection, solve CAPTCHAs, or route around IP/geo restrictions.

---

# Setup

URL: https://docs.browserless.io/mcp/browserless-mcp-server/setup

# Browserless MCP Server

The Browserless MCP server gives AI assistants full browser automation through the [Model Context Protocol](https://modelcontextprotocol.io/). Connect any MCP-compatible client (Claude, Perplexity, [Cursor](https://cursor.com/marketplace/browserless), VS Code, Windsurf, and more) to scrape, search, crawl, audit, and run custom browser code with no infrastructure to manage.

- A Browserless account - either an API token from your [account dashboard](https://browserless.io/account/), or OAuth sign-in
- An MCP-compatible client (Claude Desktop, Cursor, VS Code, Windsurf, etc.)

## Hosted Server

Browserless provides a hosted MCP server ready to use:

```bash
https://mcp.browserless.io/mcp
```

No installation or environment variables required. See [Authentication](#authentication) for how to connect.

## Authentication

The hosted server supports three authentication methods:

| Method | Best for |
|--------|----------|
| **OAuth** (Browserless account login) | Clients that support OAuth — no token needed |
| **`Authorization` header** | Clients that support custom headers |
| **`token` query parameter** | URL-only clients (e.g. Claude.ai custom connectors) |

When multiple methods are present, they are evaluated in this order: `Authorization` header (plain API token) → `token` query parameter → OAuth JWT.

  
**OAuth:**

OAuth connects through your Browserless account, so you don't need to copy or manage an API token. When you connect from a supported client such as Claude Desktop or Cursor, the client opens a browser window for you to sign in.

OAuth is enabled on the hosted server at `https://mcp.browserless.io/mcp` with no extra configuration needed.

  
  
**API Token:**

API token authentication works with clients that accept custom headers or URL query parameters. Copy your token from the [account dashboard](https://browserless.io/account/), then pass it using one of these methods:

**Header (recommended):** `Authorization: Bearer YOUR_API_TOKEN_HERE`

**Query parameter:** `?token=YOUR_API_TOKEN_HERE`

  

## Client Setup

> **Tip**
> Clients that support OAuth can connect without a token - the server will prompt you to sign in with your Browserless account.

  
**Claude:**

#### Claude.ai

Claude.ai supports MCP servers via custom connectors. Since the connector form only accepts a URL, pass your token as a query parameter:

1. Go to **Customize > Connectors** in Claude.ai.
2. Click **Add custom connector**.
3. Enter a name (e.g., `Browserless`) and the following URL:

```bash
https://mcp.browserless.io/mcp?token=YOUR_API_TOKEN_HERE
```

4. Click **Add**.

#### Claude Desktop

Add to your `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "browserless": {
      "type": "http",
      "url": "https://mcp.browserless.io/mcp",
      "headers": {
        "Authorization": "Bearer YOUR_API_TOKEN_HERE"
      }
    }
  }
}
```

#### Claude Code

Run in your terminal:

```bash
claude mcp add --transport http browserless https://mcp.browserless.io/mcp \
  --header "Authorization: Bearer YOUR_API_TOKEN_HERE"
```

For more information, see the [Claude Code MCP documentation](https://docs.anthropic.com/en/docs/claude-code/mcp).

  
  
**Cursor:**

Add to your Cursor MCP settings:

```json
{
  "mcpServers": {
    "browserless": {
      "type": "http",
      "url": "https://mcp.browserless.io/mcp",
      "headers": {
        "Authorization": "Bearer YOUR_API_TOKEN_HERE"
      }
    }
  }
}
```

  
  
**VS Code:**

Add to your VS Code settings (`settings.json`):

```json
{
  "mcp": {
    "servers": {
      "browserless": {
        "type": "http",
        "url": "https://mcp.browserless.io/mcp",
        "headers": {
          "Authorization": "Bearer YOUR_API_TOKEN_HERE"
        }
      }
    }
  }
}
```

  
  
**Windsurf:**

Add to your Windsurf MCP configuration:

```json
{
  "mcpServers": {
    "browserless": {
      "type": "http",
      "url": "https://mcp.browserless.io/mcp",
      "headers": {
        "Authorization": "Bearer YOUR_API_TOKEN_HERE"
      }
    }
  }
}
```

  
  
**Gemini CLI:**

Add to your `~/.gemini/settings.json` (or project-scoped `.gemini/settings.json`):

```json
{
  "mcpServers": {
    "browserless": {
      "httpUrl": "https://mcp.browserless.io/mcp",
      "headers": {
        "Authorization": "Bearer YOUR_API_TOKEN_HERE"
      }
    }
  }
}
```

For more information, see the [Gemini CLI MCP documentation](https://geminicli.com/docs/cli/tutorials/mcp-setup/).

  
  
**Codex:**

The Browserless hosted server supports OAuth. Run in your terminal and follow the login prompt:

```bash
codex mcp add browserless --url https://mcp.browserless.io/mcp
```

To use an API token instead of OAuth, add to your `~/.codex/config.toml`:

```toml
[mcp_servers.browserless]
url = "https://mcp.browserless.io/mcp"
bearer_token_env_var = "BROWSERLESS_TOKEN"
```

Set `BROWSERLESS_TOKEN` in your environment before starting Codex.

You can also add it through the UI: **Settings → MCP Servers → + Add Server → Streamable HTTP**, then enter `https://mcp.browserless.io/mcp` as the URL.

For more information, see the [Codex MCP documentation](https://developers.openai.com/codex/mcp).

  
  
**Perplexity:**

Perplexity supports hosted MCP servers through custom remote connectors. Custom connectors require a paid Perplexity plan and may need to be enabled by your organization administrator.

1. Open **Profile → All settings → Connectors** in Perplexity.
2. Click **+ Custom connector**, then select **Remote**.
3. Enter `Browserless` as the name and `https://mcp.browserless.io/mcp` as the MCP server URL.
4. Expand **Advanced**, select **API Key**, and paste your Browserless API token.
5. Select **Streamable HTTP** as the transport, acknowledge the custom connector warning, and click **Add**.

After adding the connector, enable Browserless from the **+** menu in a Perplexity thread before asking it to use Browserless tools.

  
  
**Hermes Agent:**

Put your token in `~/.hermes/.env` so it stays out of the config file:

```bash
# Add to ~/.hermes/.env
BROWSERLESS_API_TOKEN=YOUR_API_TOKEN_HERE
```

Then reference it from `~/.hermes/config.yaml` — Hermes expands `${VAR}` in server entries from `.env` or the process environment:

```yaml
mcp_servers:
  browserless:
    url: "https://mcp.browserless.io/mcp"
    headers:
      Authorization: "Bearer ${BROWSERLESS_API_TOKEN}"
```

Reload with `/reload-mcp` if Hermes is already running. Hermes prefixes the tools with the server name (`mcp__browserless__browserless_agent`, and so on). See the [Hermes Agent guide](/ai-integrations/hermes-agent) for verification steps and tool filtering.

  
  
**opencode:**

Export your token, then add the server to `opencode.json` in your project root or `~/.config/opencode/opencode.json`:

```bash

```

```json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "browserless": {
      "type": "remote",
      "url": "https://mcp.browserless.io/mcp",
      "headers": {
        "Authorization": "Bearer {env:BROWSERLESS_API_TOKEN}"
      }
    }
  }
}
```

opencode substitutes `{env:VAR}` but not `$VAR` or `${VAR}`, so the shell-style forms reach the server as literal text. Verify with `opencode mcp list`, and restart opencode after editing the config. Tools are namespaced as `<server>_<tool>`, so they read `browserless_browserless_agent` and so on. See the [opencode guide](/ai-integrations/opencode) for OAuth sign-in and the Playwright MCP alternative.

  

## Run Locally

> **Requirements**
> [Node.js](https://nodejs.org/) 24 or newer.

Prefer not to use the hosted server? The MCP server is published to npm as [`@browserless.io/mcp`](https://www.npmjs.com/package/@browserless.io/mcp), so you can run it locally with [`npx`](https://docs.npmjs.com/cli/commands/npx) — no cloning, building, or Docker required. This is useful for pinning a specific server version or pointing at a [self-hosted Browserless instance](/baas/start) — including fully air-gapped setups, but only when that self-hosted instance also runs inside the isolated network. On its own, running locally with `npx` still calls the Browserless cloud for browser execution.

Running locally still calls the Browserless cloud (or your self-hosted instance) for the actual browser work. Only the MCP server process runs on your machine.

### stdio (MCP clients)

For desktop MCP clients, point the server config at `npx` and pass your token as an environment variable. The server starts in **stdio** mode by default, which is what local clients expect:

  
**Claude:**

#### Claude Desktop

Add to your `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "browserless": {
      "command": "npx",
      "args": ["-y", "@browserless.io/mcp"],
      "env": {
        "BROWSERLESS_TOKEN": "YOUR_API_TOKEN_HERE"
      }
    }
  }
}
```

#### Claude Code

Run in your terminal:

```bash
claude mcp add browserless --env BROWSERLESS_TOKEN=YOUR_API_TOKEN_HERE \
  -- npx -y @browserless.io/mcp
```

  
  
**Cursor:**

Add to your Cursor MCP settings:

```json
{
  "mcpServers": {
    "browserless": {
      "command": "npx",
      "args": ["-y", "@browserless.io/mcp"],
      "env": {
        "BROWSERLESS_TOKEN": "YOUR_API_TOKEN_HERE"
      }
    }
  }
}
```

  
  
**VS Code:**

Add to your VS Code settings (`settings.json`):

```json
{
  "mcp": {
    "servers": {
      "browserless": {
        "command": "npx",
        "args": ["-y", "@browserless.io/mcp"],
        "env": {
          "BROWSERLESS_TOKEN": "YOUR_API_TOKEN_HERE"
        }
      }
    }
  }
}
```

  
  
**Gemini CLI:**

Add to your `~/.gemini/settings.json` (or project-scoped `.gemini/settings.json`):

```json
{
  "mcpServers": {
    "browserless": {
      "command": "npx",
      "args": ["-y", "@browserless.io/mcp"],
      "env": {
        "BROWSERLESS_TOKEN": "YOUR_API_TOKEN_HERE"
      }
    }
  }
}
```

  
  
**Codex:**

Run in your terminal:

```bash
codex mcp add browserless --env BROWSERLESS_TOKEN=YOUR_API_TOKEN_HERE \
  -- npx -y @browserless.io/mcp
```

Or add to your `~/.codex/config.toml`:

```toml
[mcp_servers.browserless]
command = "npx"
args = ["-y", "@browserless.io/mcp"]

[mcp_servers.browserless.env]
BROWSERLESS_TOKEN = "YOUR_API_TOKEN_HERE"
```

  

### HTTP (httpStream)

To run the server as a long-lived HTTP endpoint (for example to share one instance across clients) set `TRANSPORT=httpStream`:

```bash
TRANSPORT=httpStream PORT=8080 BROWSERLESS_TOKEN=YOUR_API_TOKEN_HERE \
  npx -y @browserless.io/mcp
```

Then point your MCP client at `http://localhost:8080/mcp`, using the same header or query-parameter auth as the hosted server.

### Environment variables

| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `BROWSERLESS_TOKEN` | Yes | — | Your Browserless API token |
| `BROWSERLESS_API_URL` | No | `https://production-sfo.browserless.io` | API endpoint (set this to point at a [self-hosted instance](/baas/start) or a different [region](#regional-endpoints)) |
| `TRANSPORT` | No | `stdio` | Transport type: `stdio` or `httpStream` |
| `PORT` | No | `8080` | HTTP server port (only used with `httpStream`) |
| `BROWSERLESS_TIMEOUT` | No | `30000` | Request timeout in milliseconds |
| `BROWSERLESS_MAX_RETRIES` | No | `3` | Max retry attempts for failed requests |
| `BROWSERLESS_CACHE_TTL` | No | `60000` | Cache TTL in milliseconds (`0` to disable) |

## Regional Endpoints

By default, the hosted MCP server connects to the **US West (San Francisco)** Browserless region. To use a different region, pass the endpoint as a header or query parameter:

| Region | Endpoint |
|--------|----------|
| US West — San Francisco (default) | `https://production-sfo.browserless.io` |
| Europe — London | `https://production-lon.browserless.io` |
| Europe — Amsterdam | `https://production-ams.browserless.io` |

**Using the `x-browserless-api-url` header** (for clients that support headers):

```json
{
  "mcpServers": {
    "browserless": {
      "type": "http",
      "url": "https://mcp.browserless.io/mcp",
      "headers": {
        "Authorization": "Bearer YOUR_API_TOKEN_HERE",
        "x-browserless-api-url": "https://production-sfo.browserless.io"
      }
    }
  }
}
```

**Using the `browserlessUrl` query parameter** (for URL-only clients like Claude.ai):

```bash
https://mcp.browserless.io/mcp?token=YOUR_API_TOKEN_HERE&browserlessUrl=https://production-sfo.browserless.io
```

## Tools

The MCP server exposes fourteen tools to your AI assistant, split across three reference pages:

### [Browser Agent](/mcp/browser-agent)

A single **stateful** tool, `browserless_agent`, that can drive a browser session across turns. In MCP server v1.33.0+, sessions are one-shot by default. For multi-step work, set `keepSessionAlive: true` on the first call, then echo the returned `sessionId` on later calls. The model can then navigate, snapshot, click, type, and react within one browser context. See [session lifetime and examples](/mcp/browser-agent#session-lifetime).

### [REST API Tools](/mcp/rest-api-tools)

Eight **stateless** tools: one call in, one result out. Seven wrap individual Browserless REST endpoints; the eighth, `browserless_skill`, loads on-demand agent guidance:

| Tool | Purpose |
|------|---------|
| [`browserless_smartscraper`](/mcp/rest-api-tools#browserless_smartscraper) | Scrape any URL with cascading strategies (HTTP → proxy → headless → CAPTCHA solving) |
| [`browserless_function`](/mcp/rest-api-tools#browserless_function) | Run custom Puppeteer JavaScript on the Browserless cloud |
| [`browserless_export`](/mcp/rest-api-tools#browserless_export) | Export a page as HTML, PDF, image, or full offline ZIP |
| [`browserless_search`](/mcp/rest-api-tools#browserless_search) | Web/news/image search with optional scraping of each result |
| [`browserless_map`](/mcp/rest-api-tools#browserless_map) | Discover all URLs on a site via sitemap and link extraction |
| [`browserless_performance`](/mcp/rest-api-tools#browserless_performance) | Run a Lighthouse audit (accessibility, performance, SEO, PWA, best practices) |
| [`browserless_crawl`](/mcp/rest-api-tools#browserless_crawl) | Crawl a site from a seed URL and scrape every discovered page |
| [`browserless_skill`](/mcp/rest-api-tools#browserless_skill) | Load an on-demand agent skill or discover site-specific recipes |

### [Account Tools](/mcp/account-tools)

Five **read-only** tools that report on the account behind your API token. None of them changes account state or returns API token values:

| Tool | Purpose |
|------|---------|
| [`browserless_account`](/mcp/account-tools#browserless_account) | Plan, unit balance, billing period, and API key names |
| [`browserless_usage`](/mcp/account-tools#browserless_usage) | Request counts and billed units for the account or specific keys |
| [`browserless_logs`](/mcp/account-tools#browserless_logs) | Browserless-side request logs for diagnosing failed runs |
| [`browserless_sessions`](/mcp/account-tools#browserless_sessions) | Running browsers, persistent sessions, replays, and 1Password integrations |
| [`browserless_profiles`](/mcp/account-tools#browserless_profiles) | Saved authentication profiles other tools reuse by name |

## FAQ & Troubleshooting

<details>
<summary>My client fails with an SSE error like <code>Non-200 status code (400)</code></summary>

The hosted server speaks the Streamable HTTP transport only; it doesn't support the legacy SSE transport. In clients that let you pick (n8n's MCP Client node, some SDKs), select **Streamable HTTP** rather than SSE, point it at `https://mcp.browserless.io/mcp`, and authenticate with an `Authorization: Bearer YOUR_API_TOKEN_HERE` header or the `?token=` query parameter.

</details>

<details>
<summary>How much do MCP sessions cost?</summary>

Each MCP tool call opens a browser session that consumes units based on session time, plus proxy traffic and CAPTCHA solving if used. See [Unit Consumption](/overview/unit-consumption#mcp-session-cost) for the full breakdown.

</details>

## Next steps

  <a href="/mcp/rest-api-tools" className="next-step-card-link">
    
      <h3 className="next-step-card-title">REST API Tools</h3>
      the eight stateless MCP tools
    
  </a>
  <a href="/mcp/browser-agent" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Browser Agent</h3>
      the stateful, multi-turn browserless_agent tool
    
  </a>

---

# MCP Overview

URL: https://docs.browserless.io/mcp/overview

# MCP Overview

The [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) is an open standard that lets AI assistants connect to external tools and data sources. Browserless provides two MCP servers, each designed for a different use case.

## Browserless MCP Server

**The primary MCP server for browser automation.** Connect Claude Desktop, Cursor, VS Code, Windsurf, or any MCP-compatible client and give your AI assistant the ability to scrape pages, take screenshots, generate PDFs, download files, and run custom Puppeteer code. All powered by Browserless's managed, stealth-enabled browser infrastructure.

**Use this when you need your AI to interact with the live web**: scraping content, extracting data, exporting pages, or executing browser-side JavaScript.

Key capabilities:

- **Browser Agent** — a stateful, snapshot-driven browser your AI can navigate and interact with across turns (logins, multi-step flows, form filling)
- **Smart Scraper** — cascading scrape strategies (HTTP fetch, proxy, headless browser, CAPTCHA solving) selected automatically
- **Custom code execution** — run arbitrary Puppeteer JavaScript on the Browserless cloud
- **File downloads** — trigger and retrieve browser-initiated downloads
- **Page export** — export any URL as HTML, PDF, or a full offline ZIP archive
- **Lighthouse performance audits** — run accessibility, SEO, performance, PWA, and best-practices audits on any URL
- **Website crawling** — crawl an entire site from a seed URL with configurable depth, path filters, and scrape options

[Get started with the Browserless MCP Server &rarr;](/mcp/browserless-mcp-server/setup)

## Browserless Docs MCP

A lightweight, read-only MCP server that gives your AI assistant direct access to the Browserless documentation. No API token required.

**Use this when you need your AI to answer questions about Browserless**: how to configure connections, which API endpoints to call, how to set up proxies, or any other documentation lookup.

Key capabilities:

- **Context-aware answers** — ask questions about Browserless APIs, BrowserQL, configuration, and more
- **Zero configuration** — no API token or authentication needed
- **Works everywhere** — supports Cursor, VS Code, Claude Desktop, ChatGPT Desktop, Claude Code, and any MCP-compatible client

[Get started with the Browserless Docs MCP &rarr;](/mcp/browserless-docs-mcp)

Most users will want the **Browserless MCP Server** for its full browser automation capabilities. Add the **Browserless Docs MCP** alongside it if you also want your AI assistant to have instant access to Browserless documentation while coding.

## Using Claude Code?

If you specifically use [Claude Code](https://docs.anthropic.com/en/docs/claude-code), there is also a dedicated [**Browserless Claude Code Plugin**](/ai-integrations/claude-code-plugin). It provides a slash-command interface (`/browserless:smart-scrape`, `/browserless:screenshot`, etc.) backed by the Browserless REST APIs rather than MCP. Install it from inside Claude Code:

```bash
/plugin marketplace add browserless/claude-plugin
/plugin install browserless@browserless
```

## FAQ & Troubleshooting

<details>
<summary>What is MCP in the context of Browserless?</summary>

MCP (Model Context Protocol) lets AI agents interact with Browserless through a standardized interface. The Browserless MCP server exposes browser automation tools that AI models can call directly.

</details>

<details>
<summary>Which AI agents work with the Browserless MCP server?</summary>

The Browserless MCP server works with any MCP-compatible client, including Claude Desktop, Claude Code, Cursor, Windsurf, and other AI tools that support the Model Context Protocol standard.

</details>

<details>
<summary>Do I need a Browserless account to use the MCP server?</summary>

The Browserless MCP Server requires an API token for browser automation tasks. Sign up for a free account at browserless.io to get your token. The Docs MCP server, which provides documentation access only, does not require a Browserless token.

</details>

## Next steps

  <a href="/ai-integrations/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">AI integrations overview</h3>
      all supported AI frameworks and tools
    
  </a>
  <a href="/browserql/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BQL overview</h3>
      use BrowserQL for advanced browser automation
    
  </a>

---

# REST API Tools

URL: https://docs.browserless.io/mcp/rest-api-tools

# REST API Tools

The Browserless MCP server exposes eight stateless tools. Seven wrap the corresponding [Browserless REST APIs](/rest-apis/smart-scrape), each mapping to a single endpoint and returning its result in one call; the eighth, `browserless_skill`, loads on-demand agent skills. Use them when the AI just needs content, a file, or a one-off result without holding browser state.

Looking for file downloads? The standalone download tool has been removed. Downloads now go through the [Browser Agent](/mcp/browser-agent)'s `getDownloads` command.

For the stateful, multi-turn tool, see the [Browser Agent](/mcp/browser-agent) page. For reading account data (plan, usage, request logs, sessions, saved profiles), see [Account Tools](/mcp/account-tools).

- Set up the [Browserless MCP Server](/mcp/browserless-mcp-server/setup) in your MCP client (Claude Desktop, Cursor, VS Code, Windsurf, Claude Code, etc.)

## `browserless_smartscraper`

Scrapes any webpage using cascading strategies — HTTP fetch, proxy, headless browser, and CAPTCHA solving — automatically selecting the best approach. Wraps the [/smart-scrape API](/rest-apis/smart-scrape).

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `url` | string | Yes | — | The URL to scrape (http or https) |
| `formats` | string[] | No | `["markdown"]` | Output formats: `markdown`, `html`, `screenshot`, `pdf`, `links` |
| `timeout` | number | No | `30000` | Request timeout in milliseconds |

**Output formats:**

- **`markdown`** — Page content converted to clean Markdown (default)
- **`html`** — Raw HTML of the page
- **`screenshot`** — Full-page screenshot as a PNG image
- **`pdf`** — PDF rendering of the page
- **`links`** — All links found on the page

**Example prompts:**

> Scrape https://browserless.io and summarize the content.

> Extract all links from https://browserless.io.

## `browserless_function`

Executes custom Puppeteer JavaScript code on the Browserless cloud. Your function receives a Puppeteer `page` object and optional `context` data, and returns `{ data, type }` to control the response payload and Content-Type. Wraps the [/function API](/rest-apis/function).

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `code` | string | Yes | JavaScript (ESM) code to execute. The default export receives `{ page, context }` and should return `{ data, type }` |
| `context` | object | No | Optional context object passed to the function |
| `timeout` | number | No | Request timeout in milliseconds |

**Example prompts**

> Load script.js and run it with the context `{ "url": "https://example.com" }`.

> Run a Puppeteer script that opens https://browserless.io, waits for the hero to render, and returns the visible text.

## `browserless_export`

Exports a webpage by URL in its native format (HTML, PDF, image, etc.). Set `includeResources` to bundle all page assets into a ZIP archive for offline use. Wraps the [/export API](/rest-apis/export).

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `url` | string | Yes | The URL to export (http or https) |
| `gotoOptions` | object | No | Puppeteer `Page.goto()` options (`waitUntil`, `timeout`, `referer`) |
| `bestAttempt` | boolean | No | When true, proceed even if awaited events fail or timeout |
| `includeResources` | boolean | No | Bundle all linked resources (CSS, JS, images) into a ZIP file |
| `waitForTimeout` | number | No | Milliseconds to wait after page load before exporting |
| `timeout` | number | No | Request timeout in milliseconds |

**Example prompt:**

> Export `https://example.com` as a full offline ZIP with all assets.

## `browserless_search`

Searches the web using Browserless and optionally scrapes each result. Performs web searches via SearXNG and can return results from web, news, or images. Optionally scrape each result URL to get markdown, HTML, links, or screenshots. Wraps the [/search API](/rest-apis/search).

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `query` | string | Yes | — | The search query string |
| `limit` | number | No | `10` | Maximum number of results to return (capped by [plan limits](/rest-apis/search#plan-limits)) |
| `lang` | string | No | `"en"` | Language code for search results |
| `country` | string | No | — | Country code for geo-targeted results |
| `location` | string | No | — | Location string for geo-targeted results |
| `tbs` | string | No | — | Time-based filter: `day`, `week`, `month`, `year` |
| `sources` | string[] | No | `["web"]` | Search sources: `web`, `news`, `images` |
| `categories` | string[] | No | — | Filter by categories: `github`, `research`, `pdf` |
| `scrapeOptions` | object | No | — | Options for scraping each search result (see below) |
| `timeout` | number | No | `30000` | Request timeout in milliseconds |

**Scrape options** (optional sub-object on `scrapeOptions`):

| Parameter | Type | Description |
|-----------|------|-------------|
| `formats` | string[] | Output formats for scraped content: `markdown`, `html`, `links`, `screenshot` |
| `onlyMainContent` | boolean | Extract only the main content using Readability |
| `includeTags` | string[] | Only include content from these HTML tags |
| `excludeTags` | string[] | Exclude content from these HTML tags |

**Example prompt:**

> Search for "headless browser automation" and summarize the top 5 results.

> Find the latest news about Puppeteer and return markdown for each result.

## `browserless_map`

Discovers and maps all URLs on a website using Browserless. Crawls a site via sitemaps and link extraction to find all pages. Returns a list of URLs with optional titles and descriptions. Use the `search` parameter to order results by relevance to a query. Wraps the [/map API](/rest-apis/map).

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `url` | string | Yes | — | The base URL to start mapping from (http or https) |
| `search` | string | No | — | Search query to order results by relevance |
| `limit` | number | No | `100` | Maximum number of links to return (max: 5000) |
| `sitemap` | string | No | `"include"` | Sitemap handling: `include`, `skip`, `only` |
| `includeSubdomains` | boolean | No | `true` | Include URLs from subdomains |
| `ignoreQueryParameters` | boolean | No | `true` | Exclude URLs with query parameters |
| `timeout` | number | No | `30000` | Request timeout in milliseconds |

**Example prompt:**

> Map all the pages on https://browserless.io and list them.

## `browserless_performance`

Runs a [Lighthouse](https://developer.chrome.com/docs/lighthouse/overview) performance audit on any URL via the Browserless [/performance API](/rest-apis/performance). Returns scores and detailed metrics for accessibility, best practices, performance, PWA, and SEO. Optionally filter by category or supply performance budgets.

> **Note**
> Audits can take 30 s–120 s depending on the site.

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `url` | string | Yes | — | The URL to audit (http or https) |
| `categories` | string[] | No | all | Lighthouse categories: `accessibility`, `best-practices`, `performance`, `pwa`, `seo` |
| `budgets` | object[] | No | — | [Lighthouse performance budgets](https://developer.chrome.com/docs/lighthouse/performance/performance-budgets) array |
| `timeout` | number | No | — | Request timeout in milliseconds |

**Example prompt:**

> Run a Lighthouse audit on https://browserless.io and show me the scores.

> Audit https://example.com for accessibility and SEO only.

## `browserless_crawl`

Crawls a website starting from a seed URL and scrapes every discovered page using the Browserless [/crawl API](/rest-apis/crawl). Follows links up to a configurable depth and supports sitemap discovery, path filtering, subdomain handling, and custom scrape options. Returns scraped content (markdown, HTML, or raw text) for each page along with metadata. Note: Crawl is in beta and only available on Cloud plans — see [pricing](https://www.browserless.io/pricing) or [contact us](https://www.browserless.io/contact/) to confirm access.

> **BETA**
> The Crawl API is in beta and only available for Cloud plans. Parameters and response shapes may change in future releases. [Contact us](https://www.browserless.io/contact/) to enable access on your account.

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `url` | string | Yes | — | The URL to crawl (http or https) |
| `limit` | number | No | `100` | Maximum pages to crawl (max: 10 000) |
| `maxDepth` | number | No | `5` | Maximum link-follow depth from the root URL |
| `maxRetries` | number | No | `1` | Retry attempts per failed page |
| `allowExternalLinks` | boolean | No | `false` | Follow links to external domains |
| `allowSubdomains` | boolean | No | `false` | Follow links to subdomains |
| `sitemap` | string | No | `"auto"` | Sitemap handling: `auto`, `force`, `skip` |
| `includePaths` | string[] | No | — | Regex patterns for URL paths to include |
| `excludePaths` | string[] | No | — | Regex patterns for URL paths to exclude |
| `delay` | number | No | `200` | Delay between requests in milliseconds |
| `scrapeOptions` | object | No | — | Per-page scrape settings (see below) |
| `waitForCompletion` | boolean | No | `true` | Wait for crawl to finish; if `false`, returns immediately with a crawl ID |
| `pollInterval` | number | No | `5000` | Polling interval in ms when waiting for completion |
| `maxWaitTime` | number | No | `300000` | Maximum wait time in ms (default 5 minutes) |
| `timeout` | number | No | `30000` | HTTP request timeout in milliseconds |

**Scrape options** (optional sub-object on `scrapeOptions`):

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `formats` | string[] | `["markdown"]` | Output formats: `markdown`, `html`, `rawText` |
| `onlyMainContent` | boolean | `true` | Extract only the main content using Readability |
| `includeTags` | string[] | — | HTML tag selectors to include |
| `excludeTags` | string[] | — | HTML tag selectors to exclude |
| `waitFor` | number | `0` | Time in ms to wait after page load before scraping |
| `headers` | object | — | Custom HTTP headers to send with each request |
| `timeout` | number | — | Navigation timeout in milliseconds |

**Example prompt:**

> Crawl https://example.com up to 3 levels deep and summarize each page.

## `browserless_skill`

Loads an on-demand agent skill, or lists site-specific recipes for a host. Skills are concise guidance the [Browser Agent](/mcp/browser-agent) uses when a page needs special handling. The agent auto-loads the relevant skill mid-task when its heuristics fire; this tool is the explicit fallback.

Provide at least one parameter. `id` alone loads a skill, `site` alone lists a host's recipes, and a call with neither is rejected.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `id` | string | One of `id`/`site` | The skill to load: an in-house skill id (`shadow-dom`, `cookie-consent`, `modals`, `snapshot-misses`, `dynamic-content`, `screenshots`, `vision-fallback`, `tabs`, `autonomous-login`, `captchas`, `file-transfers`) or a site recipe id (`host/slug`) returned by a `site` lookup |
| `site` | string | One of `id`/`site` | A host (e.g. `ebay.com`). Lists any site-specific recipes tuned for that host |

**Example prompt:**

> Check for site-specific recipes for ebay.com before you start.

## Resources

The MCP server also exposes these resources that your AI assistant can read:

| Resource | Description |
|----------|-------------|
| `browserless://api-docs` | Smart Scraper API documentation and parameter reference |
| `browserless://status` | Live status of the Browserless API connection |

## Prompt Templates

Built-in prompt templates help your AI assistant use the tools effectively:

| Prompt | Description |
|--------|-------------|
| `scrape-url` | Scrape a webpage and summarize its content |
| `extract-content` | Extract specific information from a webpage using custom instructions |

## Further reading

- [Browserless MCP Server](/mcp/browserless-mcp-server/setup) – connection setup, authentication, and regional endpoints
- [Browser Agent](/mcp/browser-agent) – the stateful, multi-turn `browserless_agent` tool
- [REST APIs](/rest-apis/smart-scrape) – direct REST API access without MCP

## FAQ & Troubleshooting

<details>
<summary>What REST API tools are available through the MCP server?</summary>

The MCP server exposes Browserless REST endpoints as tools that AI agents can call with natural language parameters: content extraction, screenshots, PDF generation, scraping, and unblocking.

</details>

<details>
<summary>Can I use the REST API tools for web scraping?</summary>

Yes. The content and scrape tools let AI agents extract rendered HTML or structured data from any URL. The agent can then process, summarize, or analyze the extracted content in the same conversation.

</details>

<details>
<summary>Do REST API tools support stealth mode?</summary>

Yes. You can configure the MCP server to use stealth routes by default, or specify stealth parameters per-request. This applies browser fingerprint mitigations when scraping protected sites.

</details>

## Next steps

  <a href="/ai-integrations/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">AI integrations overview</h3>
      all supported AI frameworks and tools
    
  </a>
  <a href="/browserql/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BQL overview</h3>
      use BrowserQL for advanced browser automation
    
  </a>

---

# /active

URL: https://docs.browserless.io/open-api/active

Returns a simple "204" HTTP code, with no response, indicating that the service itself is up and running.
Useful for liveliness probes or other external checks.

Request

### Responses

- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /agent/run/*

URL: https://docs.browserless.io/open-api/agent-run

Cancel a pending/running agent run (→ stopped, aborting the in-flight turn) or delete a terminal one.

Request

### Query Parameters
- `token` (string) — The API token for authenticating the request.

### Responses

- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /agent/run/*/continue

URL: https://docs.browserless.io/open-api/agent-run-continue

Continue a multi-turn agent run with a follow-up task. Not enabled in v1.

Request

### Request Body (`application/json`)
- `query` (string) **(required)** — Follow-up task for the next turn.

### Responses

- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /agent/run/*

URL: https://docs.browserless.io/open-api/agent-run-get-1

Poll an agent run: its status, the latest turn result and streamed step rationales, and the full turn transcript.

Request

### Query Parameters
- `token` (string) — The API token for authenticating the request.
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.

### Responses

- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /agent/run

URL: https://docs.browserless.io/open-api/agent-run-get-2

List the caller's agent runs, newest first, with cursor pagination.

Request

### Query Parameters
- `cursor` (string) — Opaque pagination cursor from a previous response.
- `limit` (number) — Page size, 1-100.
- `status` (string) — Filter by run status.
- `token` (string) — The API token for authenticating the request.
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.

### Responses

- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /agent/run

URL: https://docs.browserless.io/open-api/agent-run-post-3

Start an asynchronous AI agent run: a managed agent drives a real browser against a natural-language task and returns a structured JSON result. Returns a run ID to poll via GET /agent/run/\{id\}.

Request

### Query Parameters
- `token` (string) — The API token for authenticating the request.
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.

### Request Body (`application/json`)
- `query` (string) **(required)** — Natural-language task for the agent. Minimum 10 characters.
- `timeout` (number) — Per-turn timeout in ms, clamped to [10000, 900000] (15-min hard cap).
- `webhook` (object) — Optional webhook POSTed the result envelope on terminal states.
  - `url` (string) **(required)**
  - `events` (`failed` | `succeeded`[])
- `responseSchema` (object) — Best-effort JSON schema for the agent's final answer.

- `maxSteps` (number) — Soft cap on browser tool calls. Default 60, hard cap 90.
- `allowedDomains` (string[]) — Navigation allowlist.
- `startUrl` (string) — First navigation target.
- `proxy` (`datacenter` | `residential`) — Route browsing through a residential or datacenter proxy. Best-effort like
- `proxyCountry` (string) — Two-letter country code for the proxy exit (e.g. `US`).
- `stealth` (boolean) — Ask the agent to use stealth / anti-bot-detection mode.
- `solveCaptchas` (boolean) — Ask the agent to solve CAPTCHAs it encounters.
- `profile` (string) — Name of a saved authentication profile to reuse.
- `replay` (boolean) — Opt in to recording the run as a session replay, listed in the account
- `formats` (`html` | `links` | `markdown` | `screenshot`[]) — Page captures to return with the result, taken from the agent's current

### Responses

- **200**:  — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /browser/*

URL: https://docs.browserless.io/open-api/browser

Terminates a browser instance by browserId. When browser ownership enforcement
is enabled, the browser must belong to the authenticated user.

Request

### Responses

- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /browser/*/live

URL: https://docs.browserless.io/open-api/browser-live

Creates a view-only live URL for a currently-running browser, identified by
browserId. Hosted authenticated Cloud plans, including Free, can use this
route. Explicitly disabled and unlicensed deployments remain ineligible.

Request

### Responses

- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /chrome

URL: https://docs.browserless.io/open-api/chrome

Launch and connect to Chromium with a library like puppeteer or others that work over chrome-devtools-protocol.

Request

### Query Parameters
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `integrations` (string)
- `launch` (object | string) — Launch options, which can be either an object
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.
- `record` (boolean)
- `replay` (boolean)
- `solveCaptchas` (boolean)
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier

### Responses

- **101**: Indicates successful WebSocket upgrade.
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /chrome/bql?(/*)

URL: https://docs.browserless.io/open-api/chrome-bql-connect

WebSocket endpoint for BrowserQL. Connect to establish a persistent browser
session, then send multiple GraphQL operations as JSON messages. The browser
and page state persist across all messages for the connection duration.

Messages use the same JSON format as the HTTP POST endpoint:
Send: \{ "query": "mutation \{ goto(url: \"..\") \{ status \} \}", "variables": \{\} \}
Receive: \{ "data": \{ "goto": \{ "status": 200 \} \} \}

Request

### Query Parameters
- `acceptInsecureCerts` (boolean) — Whether to ignore TLS certificate errors (e.g. self-signed or untrusted
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `blockConsentModals` (boolean)
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `deviceScaleFactor` (number) — Device pixel ratio for the desktop screen above (allowed: 1 or 1.25).
- `deviceSlot` (number) — Pin this session to one of your identity's N stable device slots (0-based,
- `emulatedDevice` (string) — Device model for the mobile persona (emulationOs=android): a device slug such
- `emulationOs` (`android` | `linux` | `macos` | `windows`) — Spoof OS for the session: 'windows', 'macos', or 'linux'. When set, the
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `humanlike` (boolean)
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `launch` (object | string) — Launch options for the browser, either as a JSON object or a JSON string.
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.
- `replay` (boolean)
- `screen` (string) — Desktop screen resolution as WIDTHxHEIGHT (e.g. '1920x1080'). Sets the
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier

### Responses

- **101**: Indicates successful WebSocket upgrade.
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /chrome/bql?(/*)

URL: https://docs.browserless.io/open-api/chrome-bql-execute

Parses and executes BrowserQL requests, powered by the BrowserQL Editor or by other API integrations. See the BrowserQL Editor for more documentation on this API.

Request

### Query Parameters
- `acceptInsecureCerts` (boolean) — Whether to ignore TLS certificate errors (e.g. self-signed or untrusted
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `blockConsentModals` (boolean) — Whether to automatically block cookie consent modals and popups.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `deviceScaleFactor` (number) — Device pixel ratio for the desktop screen above (allowed: 1 or 1.25).
- `deviceSlot` (number) — Pin this session to one of your identity's N stable device slots (0-based,
- `emulatedDevice` (string) — Device model for the mobile persona (emulationOs=android): a device slug such
- `emulationOs` (`android` | `linux` | `macos` | `windows`) — Spoof OS for the session: 'windows', 'macos', or 'linux'. When set, the
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `humanlike` (boolean) — Whether to enable human-like behavior for interactions.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `launch` (object | string) — Launch options for the browser, either as a JSON object or a JSON string.
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.
- `replay` (boolean) — Whether to enable session recording for replay.
- `screen` (string) — Desktop screen resolution as WIDTHxHEIGHT (e.g. '1920x1080'). Sets the
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier

### Request Body (`application/json`)
- `query` (string) **(required)** — The BrowserQL query string to execute.
- `operationName` (string) — The name of the operation to execute if the query contains multiple operations.
- `variables` (object) — Variables to pass to the BrowserQL query.

### Responses

- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /chrome/content

URL: https://docs.browserless.io/open-api/chrome-content

Given a `url` or `html` field, loads the page in Chrome and returns the fully rendered HTML.

Request

### Query Parameters
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `launch` (object | string)
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.

### Request Body (`application/json`)
- `addScriptTag` (object[])
- `addStyleTag` (object[])
- `authenticate` (object | null) — Credentials for HTTP authentication. Contains `username` and `password` properties.
- `bestAttempt` (boolean) — When bestAttempt is set to true, browserless attempt to proceed
- `cookies` (object[])
- `emulateMediaType` (string)
- `gotoOptions` (object) — Options to configure the page navigation, such as `timeout` and `waitUntil`.
  - `referer` (string) — If provided, it will take preference over the referer header value set by
  - `referrerPolicy` (string) — If provided, it will take preference over the referer-policy header value
  - `timeout` (number) — Maximum wait time in milliseconds. Pass 0 to disable the timeout.
  - `waitUntil` (`domcontentloaded` | `load` | `networkidle0` | `networkidle2`[] | `domcontentloaded` | `load` | `networkidle0` | `networkidle2`) — When to consider waiting succeeds. Given an array of event strings, waiting
  - `signal` (object) — A signal object that allows you to cancel the call.
- `html` (string)
- `rejectRequestPattern` (string[])
- `rejectResourceTypes` (`cspviolationreport` | `document` | `eventsource` | `fedcm` | `fetch` | `font` | `image` | `manifest` | `media` | `other` | `ping` | `prefetch` | `preflight` | `script` | `signedexchange` | `stylesheet` | `texttrack` | `websocket` | `xhr`[])
- `requestInterceptors` (object[])
- `setExtraHTTPHeaders` (object)
- `setJavaScriptEnabled` (boolean) — Whether or not to allow JavaScript to run on the page.
- `url` (string)
- `userAgent` (object)
  - `userAgent` (string)
  - `userAgentMetadata` (object) — Used to specify User Agent Client Hints to emulate. See https://wicg.github.io/ua-client-hints
  - `platform` (string)
- `viewport` (object | null) — The viewport dimensions and settings for the page.
- `waitForEvent` (object)
  - `event` (string) **(required)**
  - `timeout` (number)
- `waitForFunction` (object)
  - `fn` (string) **(required)** — The function, or statement, to be evaluated in browser context
  - `polling` (string,number) — An interval at which the pageFunction is executed, defaults to raf.
  - `timeout` (number) — Maximum time to wait for in milliseconds. Defaults to 30000 (30 seconds).
- `waitForSelector` (object)
  - `hidden` (boolean)
  - `selector` (string) **(required)**
  - `timeout` (number)
  - `visible` (boolean)
- `waitForTimeout` (number)

### Responses

- **200**: An HTML payload of the website or HTML after JavaScript — Content: `text/html`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /chrome/download

URL: https://docs.browserless.io/open-api/chrome-download

Runs a Puppeteer or Playwright script in Chrome and returns any files the browser downloaded during execution.
the execution of puppeteer code, which is ran inside context of the browser.
Browserless sets up a blank page, a fresh download directory, injects your puppeteer code, and then executes it.
You can load external libraries via the "import" syntax, and import ESM-style modules
that are written for execution inside of the browser. Once your script is finished, any
downloaded files from Chromium are returned back with the appropriate content-type header.

Request

### Query Parameters
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `launch` (object | string) — Launch options, which can be either an object
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.

### Request Body (`application/json`)
- `code` (string) **(required)**
- `context` (object)

### Request Body (`application/javascript`)
Type: string

### Responses

- **200**: Responses are determined by the returned value of the downloads — Content: `*/*`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /chrome/export

URL: https://docs.browserless.io/open-api/chrome-export

Exports a webpage to a PDF or image format. This API is useful for generating reports, screenshots, or PDFs of web pages.

Request

### Query Parameters
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `launch` (object | string) — Launch options for the browser, either as a JSON object or a JSON string.
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier

### Request Body (`application/json`)
- `bestAttempt` (boolean) — When bestAttempt is set to true, browserless will attempt to proceed
- `url` (string) **(required)** — The URL of the site you want to archive.
- `gotoOptions` (object) — An optional goto parameter object for considering when the page is done loading.
  - `referer` (string) — If provided, it will take preference over the referer header value set by
  - `referrerPolicy` (string) — If provided, it will take preference over the referer-policy header value
  - `timeout` (number) — Maximum wait time in milliseconds. Pass 0 to disable the timeout.
  - `waitUntil` (`domcontentloaded` | `load` | `networkidle0` | `networkidle2`[] | `domcontentloaded` | `load` | `networkidle0` | `networkidle2`) — When to consider waiting succeeds. Given an array of event strings, waiting
  - `signal` (object) — A signal object that allows you to cancel the call.
- `waitForEvent` (object) — Options for waiting for a specific event to be fired on the page.
  - `event` (string) **(required)**
  - `timeout` (number)
- `waitForFunction` (object) — Options for waiting for a JavaScript function to execute.
  - `fn` (string) **(required)** — The function, or statement, to be evaluated in browser context
  - `polling` (string,number) — An interval at which the pageFunction is executed, defaults to raf.
  - `timeout` (number) — Maximum time to wait for in milliseconds. Defaults to 30000 (30 seconds).
- `waitForSelector` (object) — Options for waiting for a specific CSS selector to appear on the page.
  - `hidden` (boolean)
  - `selector` (string) **(required)**
  - `timeout` (number)
  - `visible` (boolean)
- `waitForTimeout` (number) — The amount of time in milliseconds to wait before proceeding.
- `headers` (object) — An object containing additional HTTP headers to send with every request.
- `includeResources` (boolean) — Whether to include all linked resources (images, CSS, JS) in a zip file.

### Responses

- **200**:  — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /chrome/function

URL: https://docs.browserless.io/open-api/chrome-function

Executes a Puppeteer or Playwright script in the Chrome browser context and returns the result.
Browserless sets up a blank page, injects your puppeteer code, and runs it.
You can optionally load external libraries via the "import" module that are meant for browser usage.
Values returned from the function are checked and an appropriate content-type and response is sent back
to your HTTP call.

Request

### Query Parameters
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `launch` (object | string) — Launch options, which can be either an object
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.

### Request Body (`application/json`)
- `code` (string) **(required)**
- `context` (object)

### Request Body (`application/javascript`)
Type: string

### Responses

- **200**: Responses are determined by the returned value of the function — Content: `*/*`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /chrome/live/*

URL: https://docs.browserless.io/open-api/chrome-live

Websocket back-end that powers the live session experience.

Request

### Responses

- **101**: Indicates successful WebSocket upgrade.
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /chrome/pdf

URL: https://docs.browserless.io/open-api/chrome-pdf

Returns a PDF binary from a `url` or `html` payload (Chrome, legacy).
"url" or "html" payload in your request. Many options exist for
injecting cookies, request interceptors, user-agents and waiting for
selectors, timers and more.

Request

### Query Parameters
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `launch` (object | string) — Launch options, which can be either an object
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.

### Request Body (`application/json`)
- `addScriptTag` (object[])
- `addStyleTag` (object[])
- `authenticate` (object | null) — Credentials for HTTP authentication. Contains `username` and `password` properties.
- `bestAttempt` (boolean) — When bestAttempt is set to true, browserless attempt to proceed
- `cookies` (object[])
- `emulateMediaType` (string)
- `gotoOptions` (object) — Options to configure the page navigation, such as `timeout` and `waitUntil`.
  - `referer` (string) — If provided, it will take preference over the referer header value set by
  - `referrerPolicy` (string) — If provided, it will take preference over the referer-policy header value
  - `timeout` (number) — Maximum wait time in milliseconds. Pass 0 to disable the timeout.
  - `waitUntil` (`domcontentloaded` | `load` | `networkidle0` | `networkidle2`[] | `domcontentloaded` | `load` | `networkidle0` | `networkidle2`) — When to consider waiting succeeds. Given an array of event strings, waiting
  - `signal` (object) — A signal object that allows you to cancel the call.
- `html` (string)
- `options` (object) — PDF generation options based on Puppeteer's PDFOptions interface.
  - `scale` (number) — Scales the rendering of the web page. Amount must be between `0.1` and `2`.
  - `displayHeaderFooter` (boolean) — Whether to show the header and footer.
  - `headerTemplate` (string) — HTML template for the print header. Should be valid HTML with the following
  - `footerTemplate` (string) — HTML template for the print footer. Has the same constraints and support
  - `printBackground` (boolean) — Set to `true` to print background graphics.
  - `landscape` (boolean) — Whether to print in landscape orientation.
  - `pageRanges` (string) — Paper ranges to print, e.g. `1-5, 8, 11-13`.
  - `format` (`A0` | `A1` | `A2` | `A3` | `A4` | `A5` | `A6` | `LEDGER` | `LEGAL` | `LETTER` | `Ledger` | `Legal` | `Letter` | `TABLOID` | `Tabloid` | `a0` | `a1` | `a2` | `a3` | `a4` | `a5` | `a6` | `ledger` | `legal` | `letter` | `tabloid`) — All the valid paper format types when printing a PDF.
  - `width` (string,number) — Sets the width of paper. You can pass in a number or a string with a unit.
  - `height` (string,number) — Sets the height of paper. You can pass in a number or a string with a unit.
  - `preferCSSPageSize` (boolean) — Give any CSS `@page` size declared in the page priority over what is
  - `margin` (object) — Set the PDF margins.
  - `omitBackground` (boolean) — Hides default white background and allows generating pdfs with transparency.
  - `tagged` (boolean) — Generate tagged (accessible) PDF.
  - `outline` (boolean) — Generate document outline.
  - `timeout` (number) — Timeout in milliseconds. Pass `0` to disable timeout.
  - `waitForFonts` (boolean) — If true, waits for `document.fonts.ready` to resolve. This might require
  - `fullPage` (boolean)
- `rejectRequestPattern` (string[])
- `rejectResourceTypes` (`cspviolationreport` | `document` | `eventsource` | `fedcm` | `fetch` | `font` | `image` | `manifest` | `media` | `other` | `ping` | `prefetch` | `preflight` | `script` | `signedexchange` | `stylesheet` | `texttrack` | `websocket` | `xhr`[])
- `requestInterceptors` (object[])
- `setExtraHTTPHeaders` (object)
- `setJavaScriptEnabled` (boolean)
- `url` (string)
- `userAgent` (object)
  - `userAgent` (string)
  - `userAgentMetadata` (object) — Used to specify User Agent Client Hints to emulate. See https://wicg.github.io/ua-client-hints
  - `platform` (string)
- `viewport` (object | null) — The viewport dimensions and settings for the page.
- `waitForEvent` (object)
  - `event` (string) **(required)**
  - `timeout` (number)
- `waitForFunction` (object)
  - `fn` (string) **(required)** — The function, or statement, to be evaluated in browser context
  - `polling` (string,number) — An interval at which the pageFunction is executed, defaults to raf.
  - `timeout` (number) — Maximum time to wait for in milliseconds. Defaults to 30000 (30 seconds).
- `waitForSelector` (object)
  - `hidden` (boolean)
  - `selector` (string) **(required)**
  - `timeout` (number)
  - `visible` (boolean)
- `waitForTimeout` (number)

### Responses

- **200**: Responds with an application/pdf content-type and a binary PDF — Content: `application/pdf`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /chrome/performance

URL: https://docs.browserless.io/open-api/chrome-performance

Run lighthouse performance audits with a supplied "url" in your JSON payload.

Request

### Query Parameters
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `launch` (object | string) — Launch options, which can be either an object
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.

### Request Body (`application/json`)
- `budgets` (object[])
- `config` (object)

- `url` (string) **(required)**

### Responses

- **200**: The response of the lighthouse stats. Response objects are — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /chrome/playwright

URL: https://docs.browserless.io/open-api/chrome-playwright

Connect to Chromium with any playwright style library.

Request

### Query Parameters
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `launch` (object | string) — Launch options, which can be either an object
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.

### Responses

- **101**: Indicates successful WebSocket upgrade.
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /chrome/scrape

URL: https://docs.browserless.io/open-api/chrome-scrape

Returns text, HTML, and metadata from a given list of CSS selectors against the rendered DOM (Chrome, legacy).
selectors. Debugging information is available by sending in the appropriate
flags in the "debugOpts" property. Responds with an array of JSON objects.

Request

### Query Parameters
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `launch` (object | string) — Launch options for the browser, either as a JSON object or a JSON string.
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier

### Request Body (`application/json`)
- `addScriptTag` (object[])
- `addStyleTag` (object[])
- `authenticate` (object) — Credentials for HTTP authentication. Contains `username` and `password` properties.
  - `username` (string) **(required)**
  - `password` (string) **(required)**
- `bestAttempt` (boolean) — When bestAttempt is set to true, browserless attempt to proceed
- `cookies` (object[])
- `debugOpts` (object)
  - `console` (boolean)
  - `cookies` (boolean)
  - `html` (boolean)
  - `network` (boolean)
  - `screenshot` (boolean)
- `elements` (object[]) **(required)**
- `emulateMediaType` (string)
- `gotoOptions` (object) — The supported subset of puppeteer's goto options: `waitUntil` (mapped onto
  - `timeout` (number)
  - `waitUntil` (`domcontentloaded` | `load` | `networkidle0` | `networkidle2`[] | `domcontentloaded` | `load` | `networkidle0` | `networkidle2`)
  - `referer` (string)
- `html` (string)
- `rejectRequestPattern` (string[])
- `rejectResourceTypes` (`cspviolationreport` | `document` | `eventsource` | `fedcm` | `fetch` | `font` | `image` | `manifest` | `media` | `other` | `ping` | `prefetch` | `preflight` | `script` | `signedexchange` | `stylesheet` | `texttrack` | `websocket` | `xhr`[])
- `requestInterceptors` (object[])
- `setExtraHTTPHeaders` (object)
- `setJavaScriptEnabled` (boolean)
- `url` (string)
- `userAgent` (object)
  - `userAgent` (string)
  - `userAgentMetadata` (object) — Used to specify User Agent Client Hints to emulate. See https://wicg.github.io/ua-client-hints
  - `platform` (string)
- `viewport` (object) — The viewport dimensions and settings for the page.
  - `width` (number) **(required)** — The page width in CSS pixels.
  - `height` (number) **(required)** — The page height in CSS pixels.
  - `deviceScaleFactor` (number) — Specify device scale factor.
  - `isMobile` (boolean) — Whether the `meta viewport` tag is taken into account.
  - `isLandscape` (boolean) — Specifies if the viewport is in landscape mode.
  - `hasTouch` (boolean) — Specify if the viewport supports touch events.
- `waitForEvent` (object)
  - `event` (string) **(required)**
  - `timeout` (number)
- `waitForFunction` (object)
  - `fn` (string) **(required)** — The function, or statement, to be evaluated in browser context
  - `polling` (string,number) — An interval at which the pageFunction is executed, defaults to raf.
  - `timeout` (number) — Maximum time to wait for in milliseconds. Defaults to 30000 (30 seconds).
- `waitForSelector` (object)
  - `hidden` (boolean)
  - `selector` (string) **(required)**
  - `timeout` (number)
  - `visible` (boolean)
- `waitForTimeout` (number)

### Responses

- **200**:  — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /chrome/screenshot

URL: https://docs.browserless.io/open-api/chrome-screenshot

Returns a screenshot binary from a `url` or `html` payload (Chrome, legacy).
"url" or "html" payload in your request. Many options exist including
cookies, user-agents, setting timers and network mocks.

Request

### Query Parameters
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `launch` (object | string) — Launch options, which can be either an object
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.

### Request Body (`application/json`)
- `addScriptTag` (object[])
- `addStyleTag` (object[])
- `authenticate` (object | null) — Credentials for HTTP authentication. Contains `username` and `password` properties.
- `bestAttempt` (boolean) — When bestAttempt is set to true, browserless attempt to proceed
- `cookies` (object[])
- `emulateMediaType` (string)
- `gotoOptions` (object) — Options to configure the page navigation, such as `timeout` and `waitUntil`.
  - `referer` (string) — If provided, it will take preference over the referer header value set by
  - `referrerPolicy` (string) — If provided, it will take preference over the referer-policy header value
  - `timeout` (number) — Maximum wait time in milliseconds. Pass 0 to disable the timeout.
  - `waitUntil` (`domcontentloaded` | `load` | `networkidle0` | `networkidle2`[] | `domcontentloaded` | `load` | `networkidle0` | `networkidle2`) — When to consider waiting succeeds. Given an array of event strings, waiting
  - `signal` (object) — A signal object that allows you to cancel the call.
- `html` (string)
- `options` (object) — Screenshot options based on Puppeteer's ScreenshotOptions interface.
  - `optimizeForSpeed` (boolean)
  - `type` (`jpeg` | `png` | `webp`)
  - `quality` (number) — Quality of the image, between 0-100. Not applicable to `png` images.
  - `fromSurface` (boolean) — Capture the screenshot from the surface, rather than the view.
  - `fullPage` (boolean) — When `true`, takes a screenshot of the full page.
  - `omitBackground` (boolean) — Hides default white background and allows capturing screenshots with transparency.
  - `clip` (object) — Specifies the region of the page/element to clip.
  - `encoding` (`base64` | `binary`) — Encoding of the image.
  - `captureBeyondViewport` (boolean) — Capture the screenshot beyond the viewport.
- `rejectRequestPattern` (string[])
- `rejectResourceTypes` (`cspviolationreport` | `document` | `eventsource` | `fedcm` | `fetch` | `font` | `image` | `manifest` | `media` | `other` | `ping` | `prefetch` | `preflight` | `script` | `signedexchange` | `stylesheet` | `texttrack` | `websocket` | `xhr`[])
- `requestInterceptors` (object[])
- `scrollPage` (boolean)
- `selector` (string)
- `setExtraHTTPHeaders` (object)
- `setJavaScriptEnabled` (boolean)
- `url` (string)
- `userAgent` (object)
  - `userAgent` (string)
  - `userAgentMetadata` (object) — Used to specify User Agent Client Hints to emulate. See https://wicg.github.io/ua-client-hints
  - `platform` (string)
- `viewport` (object | null) — The viewport dimensions and settings for the page.
- `waitForEvent` (object)
  - `event` (string) **(required)**
  - `timeout` (number)
- `waitForFunction` (object)
  - `fn` (string) **(required)** — The function, or statement, to be evaluated in browser context
  - `polling` (string,number) — An interval at which the pageFunction is executed, defaults to raf.
  - `timeout` (number) — Maximum time to wait for in milliseconds. Defaults to 30000 (30 seconds).
- `waitForSelector` (object)
  - `hidden` (boolean)
  - `selector` (string) **(required)**
  - `timeout` (number)
  - `visible` (boolean)
- `waitForTimeout` (number)

### Responses

- **200**: Response can either be a text/plain base64 encoded body — Content: `image/png`, `image/jpeg`, `text/plain`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /chrome/stealth

URL: https://docs.browserless.io/open-api/chrome-stealth

Launch and connect to Stealthy Chrome with a library like puppeteer or others that work over chrome-devtools-protocol for scraping in a more stealth-like fashion.

Request

### Query Parameters
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `deviceScaleFactor` (number) — Device pixel ratio for the desktop screen above (allowed: 1 or 1.25).
- `deviceSlot` (number) — Pin this session to one of your identity's N stable device slots (0-based,
- `emulatedDevice` (string) — Device model for mobile personas (emulationOs=android), e.g. "pixel-8",
- `emulationOs` (`android` | `linux` | `macos` | `windows`) — Spoof OS for full per-platform stealth: windows | macos | linux | android.
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `integrations` (string)
- `launch` (object | string) — Launch options, which can be either an object
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.
- `record` (boolean)
- `replay` (boolean)
- `screen` (string) — Desktop screen resolution as WIDTHxHEIGHT (e.g. '1920x1080'). Sets the
- `solveCaptchas` (boolean)
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier

### Responses

- **101**: Indicates successful WebSocket upgrade.
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /chrome/unblock

URL: https://docs.browserless.io/open-api/chrome-unblock

Unblocks the provided URL from being blocked due to bot detection.
Returns a payload of Cookies, HTML, a base64 encoded screenshot,
and a "browserWSEndpoint" to allow connecting to the browser when
specified in the JSON Payload. Only supports CDP or Puppeteer
like libraries when connecting to the "browserWSEndpoint".

Request

### Query Parameters
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `emulationOs` (`android` | `linux` | `macos` | `windows`) — OS to emulate: `windows`/`macos`/`linux`/`android`. Omitted → native Linux
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `launch` (object | string) — Launch options for the browser, either as a JSON object or a JSON string.
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.
- `replay` (boolean) — Records the unblock session for replay (rrweb) and uploads on cleanup, reusing the
- `solveCaptchas` (boolean) — With `browserWSEndpoint`, persists captcha-solving on the kept-alive session so
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier

### Request Body (`application/json`)
- `bestAttempt` (boolean) — When true, browserless proceeds when "awaited" events fail or timeout
- `url` (string) **(required)** — The URL of the site you want to unblock.
- `browserWSEndpoint` (boolean) — Whether or not to keep the underlying browser alive and around for
- `cookies` (boolean) — Whether or not to to return cookies for the site, defaults to true.
- `content` (boolean) — Whether or not to to return content for the site, defaults to true.
- `screenshot` (boolean) — Whether or not to to return a full-page screenshot for the site, defaults to true.
- `ttl` (number) — How long to keep the browser alive for re-connection when browserWSEndpoint
- `gotoOptions` (object) — An optional goto parameter object for considering when the page is done loading.
  - `referer` (string) — If provided, it will take preference over the referer header value set by
  - `referrerPolicy` (string) — If provided, it will take preference over the referer-policy header value
  - `timeout` (number) — Maximum wait time in milliseconds. Pass 0 to disable the timeout.
  - `waitUntil` (`domcontentloaded` | `load` | `networkidle0` | `networkidle2`[] | `domcontentloaded` | `load` | `networkidle0` | `networkidle2`) — When to consider waiting succeeds. Given an array of event strings, waiting
  - `signal` (object) — A signal object that allows you to cancel the call.
- `waitForEvent` (object) — Options for waiting for a specific event to be fired on the page.
  - `event` (string) **(required)**
  - `timeout` (number)
- `waitForFunction` (object) — Options for waiting for a JavaScript function to execute.
  - `fn` (string) **(required)** — The function, or statement, to be evaluated in browser context
  - `polling` (string,number) — An interval at which the pageFunction is executed, defaults to raf.
  - `timeout` (number) — Maximum time to wait for in milliseconds. Defaults to 30000 (30 seconds).
- `waitForSelector` (object) — Options for waiting for a specific CSS selector to appear on the page.
  - `hidden` (boolean)
  - `selector` (string) **(required)**
  - `timeout` (number)
  - `visible` (boolean)
- `waitForTimeout` (number) — The amount of time in milliseconds to wait before proceeding.

### Responses

- **200**:  — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /chromium

URL: https://docs.browserless.io/open-api/chromium

Launch and connect to Chromium with a library like puppeteer or others that work over chrome-devtools-protocol.

**Note:** This endpoint is also available at: `/` for backwards compatibility.

Request

### Query Parameters
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `launch` (object | string) — Launch options, which can be either an object
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.

### Responses

- **101**: Indicates successful WebSocket upgrade.
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /chromium/agent

URL: https://docs.browserless.io/open-api/chromium-agent

WebSocket endpoint for agentic browser interaction. Connect to establish a
persistent browser session, then send JSON-RPC messages to control the browser.

Uses a simple JSON protocol instead of GraphQL:
Send: \{ "id": 1, "method": "goto", "params": \{ "url": "https://example.com" \} \}
Receive: \{ "id": 1, "result": \{ "status": 200, "url": "https://example.com" \} \}

All BQL mutations are available as methods (goto, click, type, screenshot, etc.).
Additionally supports a "snapshot" method that returns interactive page elements
with CSS selectors, plus tab-management methods (getTabs, switchTab, createTab,
closeTab) for multi-tab workflows.

Request

### Query Parameters
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `blockConsentModals` (boolean)
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `deviceScaleFactor` (number) — Device pixel ratio for the desktop screen above (allowed: 1 or 1.25).
- `deviceSlot` (number) — Pin this session to one of your identity's N stable device slots (0-based,
- `emulatedDevice` (string) — Device model for mobile personas (emulationOs=android), e.g. "pixel-8",
- `emulationOs` (`android` | `linux` | `macos` | `windows`) — Spoof OS for full per-platform stealth: windows | macos | linux | android.
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `humanlike` (boolean)
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `launch` (object | string) — Launch options, which can be either an object
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.
- `record` (boolean)
- `replay` (boolean)
- `screen` (string) — Desktop screen resolution as WIDTHxHEIGHT (e.g. '1920x1080'). Sets the
- `sessionId` (string) — Attach to a pre-created profile/creation session (autologin).
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier

### Responses

- **101**: Indicates successful WebSocket upgrade.
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /chromium/agent/capabilities

URL: https://docs.browserless.io/open-api/chromium-agent-capabilities

Returns the versioned capabilities declared by the Chromium agent route.
Supply the intended route query parameters so authentication and plan checks
can reject unsupported combinations before a browser is allocated.

Request

### Query Parameters
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `blockConsentModals` (boolean)
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `deviceScaleFactor` (number) — Device pixel ratio for the desktop screen above (allowed: 1 or 1.25).
- `deviceSlot` (number) — Pin this session to one of your identity's N stable device slots (0-based,
- `emulatedDevice` (string) — Device model for mobile personas (emulationOs=android), e.g. "pixel-8",
- `emulationOs` (`android` | `linux` | `macos` | `windows`) — Spoof OS for full per-platform stealth: windows | macos | linux | android.
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `humanlike` (boolean)
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `launch` (object | string) — Launch options, which can be either an object
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.
- `record` (boolean)
- `replay` (boolean)
- `screen` (string) — Desktop screen resolution as WIDTHxHEIGHT (e.g. '1920x1080'). Sets the
- `sessionId` (string) — Attach to a pre-created profile/creation session (autologin).
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier

### Responses

- **200**:  — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /chromium/bql?(/*)

URL: https://docs.browserless.io/open-api/chromium-bql-connect

WebSocket endpoint for BrowserQL. Connect to establish a persistent browser
session, then send multiple GraphQL operations as JSON messages. The browser
and page state persist across all messages for the connection duration.

Messages use the same JSON format as the HTTP POST endpoint:
Send: \{ "query": "mutation \{ goto(url: \"..\") \{ status \} \}", "variables": \{\} \}
Receive: \{ "data": \{ "goto": \{ "status": 200 \} \} \}

Request

### Query Parameters
- `acceptInsecureCerts` (boolean) — Whether to ignore TLS certificate errors (e.g. self-signed or untrusted
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `blockConsentModals` (boolean)
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `deviceScaleFactor` (number) — Device pixel ratio for the desktop screen above (allowed: 1 or 1.25).
- `deviceSlot` (number) — Pin this session to one of your identity's N stable device slots (0-based,
- `emulatedDevice` (string) — Device model for the mobile persona (emulationOs=android): a device slug such
- `emulationOs` (`android` | `linux` | `macos` | `windows`) — Spoof OS for the session: 'windows', 'macos', or 'linux'. When set, the
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `humanlike` (boolean)
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `launch` (object | string) — Launch options for the browser, either as a JSON object or a JSON string.
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.
- `replay` (boolean)
- `screen` (string) — Desktop screen resolution as WIDTHxHEIGHT (e.g. '1920x1080'). Sets the
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier

### Responses

- **101**: Indicates successful WebSocket upgrade.
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /chromium/bql?(/*)

URL: https://docs.browserless.io/open-api/chromium-bql-execute

Parses and executes BrowserQL requests, powered by the BrowserQL Editor or by other API integrations. See the BrowserQL Editor for more documentation on this API.

Request

### Query Parameters
- `acceptInsecureCerts` (boolean) — Whether to ignore TLS certificate errors (e.g. self-signed or untrusted
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `blockConsentModals` (boolean) — Whether to automatically block cookie consent modals and popups.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `deviceScaleFactor` (number) — Device pixel ratio for the desktop screen above (allowed: 1 or 1.25).
- `deviceSlot` (number) — Pin this session to one of your identity's N stable device slots (0-based,
- `emulatedDevice` (string) — Device model for the mobile persona (emulationOs=android): a device slug such
- `emulationOs` (`android` | `linux` | `macos` | `windows`) — Spoof OS for the session: 'windows', 'macos', or 'linux'. When set, the
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `humanlike` (boolean) — Whether to enable human-like behavior for interactions.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `launch` (object | string) — Launch options for the browser, either as a JSON object or a JSON string.
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.
- `replay` (boolean) — Whether to enable session recording for replay.
- `screen` (string) — Desktop screen resolution as WIDTHxHEIGHT (e.g. '1920x1080'). Sets the
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier

### Request Body (`application/json`)
- `query` (string) **(required)** — The BrowserQL query string to execute.
- `operationName` (string) — The name of the operation to execute if the query contains multiple operations.
- `variables` (object) — Variables to pass to the BrowserQL query.

### Responses

- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /chromium/cli

URL: https://docs.browserless.io/open-api/chromium-cli

Interactive hosted-agent chat. Browserless runs the LLM server-side (no
customer key) and drives a browser to complete the task. Send newline-JSON
control frames (\{"type":"start","prompt":"..."\}) and receive a stream of
data / tool_call / tool_result / usage / awaiting_input / end events.

Request

### Query Parameters
- `token` (string)
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.

### Responses

- **101**: Indicates successful WebSocket upgrade.
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /chromium/content

URL: https://docs.browserless.io/open-api/chromium-content

Given a `url` or `html` field, loads the page and returns the fully rendered HTML.

**Note:** Also available at `/content` as a convenience alias.

Request

### Query Parameters
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `launch` (object | string)
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.

### Request Body (`application/json`)
- `addScriptTag` (object[])
- `addStyleTag` (object[])
- `authenticate` (object | null) — Credentials for HTTP authentication. Contains `username` and `password` properties.
- `bestAttempt` (boolean) — When bestAttempt is set to true, browserless attempt to proceed
- `cookies` (object[])
- `emulateMediaType` (string)
- `gotoOptions` (object) — Options to configure the page navigation, such as `timeout` and `waitUntil`.
  - `referer` (string) — If provided, it will take preference over the referer header value set by
  - `referrerPolicy` (string) — If provided, it will take preference over the referer-policy header value
  - `timeout` (number) — Maximum wait time in milliseconds. Pass 0 to disable the timeout.
  - `waitUntil` (`domcontentloaded` | `load` | `networkidle0` | `networkidle2`[] | `domcontentloaded` | `load` | `networkidle0` | `networkidle2`) — When to consider waiting succeeds. Given an array of event strings, waiting
  - `signal` (object) — A signal object that allows you to cancel the call.
- `html` (string)
- `rejectRequestPattern` (string[])
- `rejectResourceTypes` (`cspviolationreport` | `document` | `eventsource` | `fedcm` | `fetch` | `font` | `image` | `manifest` | `media` | `other` | `ping` | `prefetch` | `preflight` | `script` | `signedexchange` | `stylesheet` | `texttrack` | `websocket` | `xhr`[])
- `requestInterceptors` (object[])
- `setExtraHTTPHeaders` (object)
- `setJavaScriptEnabled` (boolean) — Whether or not to allow JavaScript to run on the page.
- `url` (string)
- `userAgent` (object)
  - `userAgent` (string)
  - `userAgentMetadata` (object) — Used to specify User Agent Client Hints to emulate. See https://wicg.github.io/ua-client-hints
  - `platform` (string)
- `viewport` (object | null) — The viewport dimensions and settings for the page.
- `waitForEvent` (object)
  - `event` (string) **(required)**
  - `timeout` (number)
- `waitForFunction` (object)
  - `fn` (string) **(required)** — The function, or statement, to be evaluated in browser context
  - `polling` (string,number) — An interval at which the pageFunction is executed, defaults to raf.
  - `timeout` (number) — Maximum time to wait for in milliseconds. Defaults to 30000 (30 seconds).
- `waitForSelector` (object)
  - `hidden` (boolean)
  - `selector` (string) **(required)**
  - `timeout` (number)
  - `visible` (boolean)
- `waitForTimeout` (number)

### Responses

- **200**: An HTML payload of the website or HTML after JavaScript — Content: `text/html`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /chromium/download

URL: https://docs.browserless.io/open-api/chromium-download

Runs a Puppeteer or Playwright script and returns any files the browser downloaded during execution.
the execution of puppeteer code, which is ran inside context of the browser.
Browserless sets up a blank page, a fresh download directory, injects your puppeteer code, and then executes it.
You can load external libraries via the "import" syntax, and import ESM-style modules
that are written for execution inside of the browser. Once your script is finished, any
downloaded files from Chromium are returned back with the appropriate content-type header.

**Note:** Also available at `/download` as a convenience alias.

Request

### Query Parameters
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `launch` (object | string) — Launch options, which can be either an object
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.

### Request Body (`application/json`)
- `code` (string) **(required)**
- `context` (object)

### Request Body (`application/javascript`)
Type: string

### Responses

- **200**: Responses are determined by the returned value of the downloads — Content: `*/*`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /chromium/export

URL: https://docs.browserless.io/open-api/chromium-export

Exports a webpage to a PDF or image format. This API is useful for generating reports, screenshots, or PDFs of web pages.

**Note:** Also available at `/export` as a convenience alias.

Request

### Query Parameters
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `launch` (object | string) — Launch options for the browser, either as a JSON object or a JSON string.
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier

### Request Body (`application/json`)
- `bestAttempt` (boolean) — When bestAttempt is set to true, browserless will attempt to proceed
- `url` (string) **(required)** — The URL of the site you want to archive.
- `gotoOptions` (object) — An optional goto parameter object for considering when the page is done loading.
  - `referer` (string) — If provided, it will take preference over the referer header value set by
  - `referrerPolicy` (string) — If provided, it will take preference over the referer-policy header value
  - `timeout` (number) — Maximum wait time in milliseconds. Pass 0 to disable the timeout.
  - `waitUntil` (`domcontentloaded` | `load` | `networkidle0` | `networkidle2`[] | `domcontentloaded` | `load` | `networkidle0` | `networkidle2`) — When to consider waiting succeeds. Given an array of event strings, waiting
  - `signal` (object) — A signal object that allows you to cancel the call.
- `waitForEvent` (object) — Options for waiting for a specific event to be fired on the page.
  - `event` (string) **(required)**
  - `timeout` (number)
- `waitForFunction` (object) — Options for waiting for a JavaScript function to execute.
  - `fn` (string) **(required)** — The function, or statement, to be evaluated in browser context
  - `polling` (string,number) — An interval at which the pageFunction is executed, defaults to raf.
  - `timeout` (number) — Maximum time to wait for in milliseconds. Defaults to 30000 (30 seconds).
- `waitForSelector` (object) — Options for waiting for a specific CSS selector to appear on the page.
  - `hidden` (boolean)
  - `selector` (string) **(required)**
  - `timeout` (number)
  - `visible` (boolean)
- `waitForTimeout` (number) — The amount of time in milliseconds to wait before proceeding.
- `headers` (object) — An object containing additional HTTP headers to send with every request.
- `includeResources` (boolean) — Whether to include all linked resources (images, CSS, JS) in a zip file.

### Responses

- **200**:  — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /chromium/function

URL: https://docs.browserless.io/open-api/chromium-function

Executes a Puppeteer or Playwright script in the browser context and returns the result.
Browserless sets up a blank page, injects your puppeteer code, and runs it.
You can optionally load external libraries via the "import" module that are meant for browser usage.
Values returned from the function are checked and an appropriate content-type and response is sent back
to your HTTP call.

**Note:** Also available at `/function` as a convenience alias.

Request

### Query Parameters
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `launch` (object | string) — Launch options, which can be either an object
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.

### Request Body (`application/json`)
- `code` (string) **(required)**
- `context` (object)

### Request Body (`application/javascript`)
Type: string

### Responses

- **200**: Responses are determined by the returned value of the function — Content: `*/*`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /chromium/performance

URL: https://docs.browserless.io/open-api/chromium-performance

Run lighthouse performance audits with a supplied "url" in your JSON payload.

**Note:** Also available at `/performance` as a convenience alias.

Request

### Query Parameters
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `launch` (object | string) — Launch options, which can be either an object
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.

### Request Body (`application/json`)
- `budgets` (object[])
- `config` (object)

- `url` (string) **(required)**

### Responses

- **200**: The response of the lighthouse stats. Response objects are — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /chromium/playwright

URL: https://docs.browserless.io/open-api/chromium-playwright

Connect to Chromium with any playwright style library.

**Note:** Also available at `/playwright` as a convenience alias.

Request

### Query Parameters
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `launch` (object | string) — Launch options, which can be either an object
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.

### Responses

- **101**: Indicates successful WebSocket upgrade.
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /

URL: https://docs.browserless.io/open-api/chromium-root

Launch and connect to Chromium with a library like puppeteer or others that work over chrome-devtools-protocol.

**Note:** Also available at `/chromium` as a convenience alias.

Request

### Query Parameters
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `integrations` (string)
- `launch` (object | string) — Launch options, which can be either an object
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.
- `record` (boolean)
- `replay` (boolean)
- `solveCaptchas` (boolean)
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier

### Responses

- **101**: Indicates successful WebSocket upgrade.
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /chromium/scrape

URL: https://docs.browserless.io/open-api/chromium-scrape

Returns text, HTML, and metadata from a given list of CSS selectors against the rendered DOM.
Debugging information is available by sending in the appropriate flags in the "debugOpts"
property. Responds with an array of JSON objects.

**Note:** Also available at `/scrape` as a convenience alias.

Request

### Query Parameters
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `launch` (object | string)
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.

### Request Body (`application/json`)
- `addScriptTag` (object[])
- `addStyleTag` (object[])
- `authenticate` (object | null) — Credentials for HTTP authentication. Contains `username` and `password` properties.
- `bestAttempt` (boolean) — When bestAttempt is set to true, browserless attempt to proceed
- `cookies` (object[])
- `debugOpts` (object)
  - `console` (boolean)
  - `cookies` (boolean)
  - `html` (boolean)
  - `network` (boolean)
  - `screenshot` (boolean)
- `elements` (object[]) **(required)**
- `emulateMediaType` (string)
- `gotoOptions` (object) — Options to configure the page navigation, such as `timeout` and `waitUntil`.
  - `referer` (string) — If provided, it will take preference over the referer header value set by
  - `referrerPolicy` (string) — If provided, it will take preference over the referer-policy header value
  - `timeout` (number) — Maximum wait time in milliseconds. Pass 0 to disable the timeout.
  - `waitUntil` (`domcontentloaded` | `load` | `networkidle0` | `networkidle2`[] | `domcontentloaded` | `load` | `networkidle0` | `networkidle2`) — When to consider waiting succeeds. Given an array of event strings, waiting
  - `signal` (object) — A signal object that allows you to cancel the call.
- `html` (string)
- `rejectRequestPattern` (string[])
- `rejectResourceTypes` (`cspviolationreport` | `document` | `eventsource` | `fedcm` | `fetch` | `font` | `image` | `manifest` | `media` | `other` | `ping` | `prefetch` | `preflight` | `script` | `signedexchange` | `stylesheet` | `texttrack` | `websocket` | `xhr`[])
- `requestInterceptors` (object[])
- `setExtraHTTPHeaders` (object)
- `setJavaScriptEnabled` (boolean)
- `url` (string)
- `userAgent` (object)
  - `userAgent` (string)
  - `userAgentMetadata` (object) — Used to specify User Agent Client Hints to emulate. See https://wicg.github.io/ua-client-hints
  - `platform` (string)
- `viewport` (object | null) — The viewport dimensions and settings for the page.
- `waitForEvent` (object)
  - `event` (string) **(required)**
  - `timeout` (number)
- `waitForFunction` (object)
  - `fn` (string) **(required)** — The function, or statement, to be evaluated in browser context
  - `polling` (string,number) — An interval at which the pageFunction is executed, defaults to raf.
  - `timeout` (number) — Maximum time to wait for in milliseconds. Defaults to 30000 (30 seconds).
- `waitForSelector` (object)
  - `hidden` (boolean)
  - `selector` (string) **(required)**
  - `timeout` (number)
  - `visible` (boolean)
- `waitForTimeout` (number)

### Responses

- **200**: The JSON response body — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /chromium/stealth

URL: https://docs.browserless.io/open-api/chromium-stealth

Launch and connect to Stealthy Chromium with a library like puppeteer or others that work over chrome-devtools-protocol for scraping in a more stealth-like fashion.

Request

### Query Parameters
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `deviceScaleFactor` (number) — Device pixel ratio for the desktop screen above (allowed: 1 or 1.25).
- `deviceSlot` (number) — Pin this session to one of your identity's N stable device slots (0-based,
- `emulatedDevice` (string) — Device model for mobile personas (emulationOs=android), e.g. "pixel-8",
- `emulationOs` (`android` | `linux` | `macos` | `windows`) — Spoof OS for full per-platform stealth: windows | macos | linux | android.
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `integrations` (string)
- `launch` (object | string) — Launch options, which can be either an object
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.
- `record` (boolean)
- `replay` (boolean)
- `screen` (string) — Desktop screen resolution as WIDTHxHEIGHT (e.g. '1920x1080'). Sets the
- `solveCaptchas` (boolean)
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier

### Responses

- **101**: Indicates successful WebSocket upgrade.
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /chromium/stealth/reconnect/*

URL: https://docs.browserless.io/open-api/chromium-stealth-reconnect

Reconnect to an existing stealth browser session (e.g. from /unblock) with CDP proxy features like solveCaptchas and stealth-mode fingerprint masking.

Request

### Query Parameters
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `integrations` (string)
- `launch` (object | object | string) — Launch options, which can be either an object
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `replay` (boolean)
- `solveCaptchas` (boolean)
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.

### Responses

- **101**: Indicates successful WebSocket upgrade.
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /crawl/*

URL: https://docs.browserless.io/open-api/crawl-cancel

Cancel a running crawl job by its ID. Any pages already completed remain available for retrieval. Returns a 409 if the crawl is already in a terminal state.

Request

### Query Parameters
- `token` (string) — The API token for authenticating the request.

### Responses

- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /crawl

URL: https://docs.browserless.io/open-api/crawl-list

List all crawl jobs owned by the authenticated account (or, for keys not yet mapped to an account, the token). Returns an array of crawl summaries with status, progress counters, and timestamps. Supports cursor-based pagination via the `nextCursor` field.

Request

### Query Parameters
- `cursor` (string) — Cursor for fetching the next page of results.
- `limit` (number) — Maximum number of crawls to return per page (1–100, default 20).
- `status` (string) — Filter crawls by status: in-progress, completed, failed, or cancelled.
- `token` (string) — The API token for authenticating the request.

### Responses

- **200**:  — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /crawl

URL: https://docs.browserless.io/open-api/crawl-start

Start an asynchronous crawl job that spiders a website and scrapes every discovered page. Returns a crawl ID and status URL for polling results via GET /crawl/\{id\}. Supports depth control, path filtering, sitemap strategies, and webhook notifications.

Request

### Query Parameters
- `profile` (string) — Optional name of an authentication profile to hydrate into the browser
- `token` (string) — The API token for authenticating the request.

### Request Body (`application/json`)
- `url` (string) **(required)** — The URL to crawl. Must be a valid http or https URL.
- `limit` (number) — Maximum number of pages to crawl. Clamped to your plan's limit.
- `maxDepth` (number) — Maximum link-follow depth from the root URL.
- `maxRetries` (number) — Number of retry attempts per failed page.
- `allowExternalLinks` (boolean) — Whether to follow links to external domains.
- `allowSubdomains` (boolean) — Whether to follow links to subdomains of the root URL.
- `sitemap` (`auto` | `force` | `skip`) — Sitemap handling strategy.
- `includePaths` (string[]) — Regex patterns for URL paths to include.
- `excludePaths` (string[]) — Regex patterns for URL paths to exclude.
- `delay` (number) — Delay between requests in milliseconds.
- `scrapeOptions` (object) — Options controlling how each page is scraped.
  - `formats` (`html` | `markdown` | `rawText`[]) — Output formats for scraped content.
  - `onlyMainContent` (boolean) — Whether to extract only the main content of the page.
  - `includeTags` (string[]) — HTML tag selectors to include.
  - `excludeTags` (string[]) — HTML tag selectors to exclude.
  - `waitFor` (number) — Time in ms to wait after page load before scraping.
  - `headers` (object) — Custom HTTP headers to send with each request.
  - `timeout` (number) — Navigation timeout in milliseconds.
  - `proxy` (`datacenter` | `residential`) — Proxy network to route page fetches through: `"residential"` (default)
- `webhook` (object) — Webhook configuration for crawl event notifications.
  - `url` (string) **(required)** — The HTTPS URL to send webhook events to.
  - `events` (`completed` | `failed` | `page`[]) — Which events to send.

### Responses

- **200**:  — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /crawl/*

URL: https://docs.browserless.io/open-api/crawl-status

Get the status of a crawl job and its paginated page results. Returns progress counters, per-page metadata with content URLs, and a `next` URL for offset-based pagination via the `skip` parameter.

Request

### Query Parameters
- `skip` (number) — The number of pages to skip for pagination.
- `token` (string) — The API token for authenticating the request.

### Responses

- **200**:  — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /devtools/browser/*

URL: https://docs.browserless.io/open-api/devtools-browser

Connect to an already-running Chromium process with a library like
puppeteer, or others, that work over chrome-devtools-protocol. Chromium
must already be launched in order to not return a 404.

Request

### Query Parameters
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `launch` (object | string) — Launch options, which can be either an object
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.

### Responses

- **101**: Indicates successful WebSocket upgrade.
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /devtools/page/*

URL: https://docs.browserless.io/open-api/devtools-page

Connect to an existing page in Chromium with a library like
chrome-remote-interface or others that work the page websocketDebugger
URL. You can get this unique URL by calling the /json/list API
or by finding the page's unique ID from your library of choice.

Request

### Query Parameters
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `launch` (object | string) — Launch options, which can be either an object
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.

### Responses

- **101**: Indicates successful WebSocket upgrade.
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /edge

URL: https://docs.browserless.io/open-api/edge

Launch and connect to Chromium with a library like puppeteer or others that work over chrome-devtools-protocol.

Request

### Query Parameters
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `integrations` (string)
- `launch` (object | string) — Launch options, which can be either an object
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.
- `record` (boolean)
- `replay` (boolean)
- `solveCaptchas` (boolean)
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier

### Responses

- **101**: Indicates successful WebSocket upgrade.
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /edge/content

URL: https://docs.browserless.io/open-api/edge-content

Given a `url` or `html` field, loads the page in Edge and returns the fully rendered HTML.

Request

### Query Parameters
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `launch` (object | string)
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.

### Request Body (`application/json`)
- `addScriptTag` (object[])
- `addStyleTag` (object[])
- `authenticate` (object | null) — Credentials for HTTP authentication. Contains `username` and `password` properties.
- `bestAttempt` (boolean) — When bestAttempt is set to true, browserless attempt to proceed
- `cookies` (object[])
- `emulateMediaType` (string)
- `gotoOptions` (object) — Options to configure the page navigation, such as `timeout` and `waitUntil`.
  - `referer` (string) — If provided, it will take preference over the referer header value set by
  - `referrerPolicy` (string) — If provided, it will take preference over the referer-policy header value
  - `timeout` (number) — Maximum wait time in milliseconds. Pass 0 to disable the timeout.
  - `waitUntil` (`domcontentloaded` | `load` | `networkidle0` | `networkidle2`[] | `domcontentloaded` | `load` | `networkidle0` | `networkidle2`) — When to consider waiting succeeds. Given an array of event strings, waiting
  - `signal` (object) — A signal object that allows you to cancel the call.
- `html` (string)
- `rejectRequestPattern` (string[])
- `rejectResourceTypes` (`cspviolationreport` | `document` | `eventsource` | `fedcm` | `fetch` | `font` | `image` | `manifest` | `media` | `other` | `ping` | `prefetch` | `preflight` | `script` | `signedexchange` | `stylesheet` | `texttrack` | `websocket` | `xhr`[])
- `requestInterceptors` (object[])
- `setExtraHTTPHeaders` (object)
- `setJavaScriptEnabled` (boolean) — Whether or not to allow JavaScript to run on the page.
- `url` (string)
- `userAgent` (object)
  - `userAgent` (string)
  - `userAgentMetadata` (object) — Used to specify User Agent Client Hints to emulate. See https://wicg.github.io/ua-client-hints
  - `platform` (string)
- `viewport` (object | null) — The viewport dimensions and settings for the page.
- `waitForEvent` (object)
  - `event` (string) **(required)**
  - `timeout` (number)
- `waitForFunction` (object)
  - `fn` (string) **(required)** — The function, or statement, to be evaluated in browser context
  - `polling` (string,number) — An interval at which the pageFunction is executed, defaults to raf.
  - `timeout` (number) — Maximum time to wait for in milliseconds. Defaults to 30000 (30 seconds).
- `waitForSelector` (object)
  - `hidden` (boolean)
  - `selector` (string) **(required)**
  - `timeout` (number)
  - `visible` (boolean)
- `waitForTimeout` (number)

### Responses

- **200**: An HTML payload of the website or HTML after JavaScript — Content: `text/html`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /edge/download

URL: https://docs.browserless.io/open-api/edge-download

Runs a Puppeteer or Playwright script in Edge and returns any files the browser downloaded during execution.
the execution of puppeteer code, which is ran inside context of the browser.
Browserless sets up a blank page, a fresh download directory, injects your puppeteer code, and then executes it.
You can load external libraries via the "import" syntax, and import ESM-style modules
that are written for execution inside of the browser. Once your script is finished, any
downloaded files from Chromium are returned back with the appropriate content-type header.

Request

### Query Parameters
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `launch` (object | string) — Launch options, which can be either an object
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.

### Request Body (`application/json`)
- `code` (string) **(required)**
- `context` (object)

### Request Body (`application/javascript`)
Type: string

### Responses

- **200**: Responses are determined by the returned value of the downloads — Content: `*/*`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /edge/function

URL: https://docs.browserless.io/open-api/edge-function

Executes a Puppeteer or Playwright script in the Edge browser context and returns the result.
Browserless sets up a blank page, injects your puppeteer code, and runs it.
You can optionally load external libraries via the "import" module that are meant for browser usage.
Values returned from the function are checked and an appropriate content-type and response is sent back
to your HTTP call.

Request

### Query Parameters
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `launch` (object | string) — Launch options, which can be either an object
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.

### Request Body (`application/json`)
- `code` (string) **(required)**
- `context` (object)

### Request Body (`application/javascript`)
Type: string

### Responses

- **200**: Responses are determined by the returned value of the function — Content: `*/*`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /edge/pdf

URL: https://docs.browserless.io/open-api/edge-pdf

Returns a PDF binary from a `url` or `html` payload using Edge.
"url" or "html" payload in your request. Many options exist for
injecting cookies, request interceptors, user-agents and waiting for
selectors, timers and more.

Request

### Query Parameters
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `launch` (object | string) — Launch options, which can be either an object
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.

### Request Body (`application/json`)
- `addScriptTag` (object[])
- `addStyleTag` (object[])
- `authenticate` (object | null) — Credentials for HTTP authentication. Contains `username` and `password` properties.
- `bestAttempt` (boolean) — When bestAttempt is set to true, browserless attempt to proceed
- `cookies` (object[])
- `emulateMediaType` (string)
- `gotoOptions` (object) — Options to configure the page navigation, such as `timeout` and `waitUntil`.
  - `referer` (string) — If provided, it will take preference over the referer header value set by
  - `referrerPolicy` (string) — If provided, it will take preference over the referer-policy header value
  - `timeout` (number) — Maximum wait time in milliseconds. Pass 0 to disable the timeout.
  - `waitUntil` (`domcontentloaded` | `load` | `networkidle0` | `networkidle2`[] | `domcontentloaded` | `load` | `networkidle0` | `networkidle2`) — When to consider waiting succeeds. Given an array of event strings, waiting
  - `signal` (object) — A signal object that allows you to cancel the call.
- `html` (string)
- `options` (object) — PDF generation options based on Puppeteer's PDFOptions interface.
  - `scale` (number) — Scales the rendering of the web page. Amount must be between `0.1` and `2`.
  - `displayHeaderFooter` (boolean) — Whether to show the header and footer.
  - `headerTemplate` (string) — HTML template for the print header. Should be valid HTML with the following
  - `footerTemplate` (string) — HTML template for the print footer. Has the same constraints and support
  - `printBackground` (boolean) — Set to `true` to print background graphics.
  - `landscape` (boolean) — Whether to print in landscape orientation.
  - `pageRanges` (string) — Paper ranges to print, e.g. `1-5, 8, 11-13`.
  - `format` (`A0` | `A1` | `A2` | `A3` | `A4` | `A5` | `A6` | `LEDGER` | `LEGAL` | `LETTER` | `Ledger` | `Legal` | `Letter` | `TABLOID` | `Tabloid` | `a0` | `a1` | `a2` | `a3` | `a4` | `a5` | `a6` | `ledger` | `legal` | `letter` | `tabloid`) — All the valid paper format types when printing a PDF.
  - `width` (string,number) — Sets the width of paper. You can pass in a number or a string with a unit.
  - `height` (string,number) — Sets the height of paper. You can pass in a number or a string with a unit.
  - `preferCSSPageSize` (boolean) — Give any CSS `@page` size declared in the page priority over what is
  - `margin` (object) — Set the PDF margins.
  - `omitBackground` (boolean) — Hides default white background and allows generating pdfs with transparency.
  - `tagged` (boolean) — Generate tagged (accessible) PDF.
  - `outline` (boolean) — Generate document outline.
  - `timeout` (number) — Timeout in milliseconds. Pass `0` to disable timeout.
  - `waitForFonts` (boolean) — If true, waits for `document.fonts.ready` to resolve. This might require
  - `fullPage` (boolean)
- `rejectRequestPattern` (string[])
- `rejectResourceTypes` (`cspviolationreport` | `document` | `eventsource` | `fedcm` | `fetch` | `font` | `image` | `manifest` | `media` | `other` | `ping` | `prefetch` | `preflight` | `script` | `signedexchange` | `stylesheet` | `texttrack` | `websocket` | `xhr`[])
- `requestInterceptors` (object[])
- `setExtraHTTPHeaders` (object)
- `setJavaScriptEnabled` (boolean)
- `url` (string)
- `userAgent` (object)
  - `userAgent` (string)
  - `userAgentMetadata` (object) — Used to specify User Agent Client Hints to emulate. See https://wicg.github.io/ua-client-hints
  - `platform` (string)
- `viewport` (object | null) — The viewport dimensions and settings for the page.
- `waitForEvent` (object)
  - `event` (string) **(required)**
  - `timeout` (number)
- `waitForFunction` (object)
  - `fn` (string) **(required)** — The function, or statement, to be evaluated in browser context
  - `polling` (string,number) — An interval at which the pageFunction is executed, defaults to raf.
  - `timeout` (number) — Maximum time to wait for in milliseconds. Defaults to 30000 (30 seconds).
- `waitForSelector` (object)
  - `hidden` (boolean)
  - `selector` (string) **(required)**
  - `timeout` (number)
  - `visible` (boolean)
- `waitForTimeout` (number)

### Responses

- **200**: Responds with an application/pdf content-type and a binary PDF — Content: `application/pdf`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /edge/performance

URL: https://docs.browserless.io/open-api/edge-performance

Run lighthouse performance audits with a supplied "url" in your JSON payload.

Request

### Query Parameters
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `launch` (object | string) — Launch options, which can be either an object
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.

### Request Body (`application/json`)
- `budgets` (object[])
- `config` (object)

- `url` (string) **(required)**

### Responses

- **200**: The response of the lighthouse stats. Response objects are — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /edge/playwright

URL: https://docs.browserless.io/open-api/edge-playwright

Connect to Chromium with any playwright style library.

Request

### Query Parameters
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `launch` (object | string) — Launch options, which can be either an object
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.

### Responses

- **101**: Indicates successful WebSocket upgrade.
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /edge/scrape

URL: https://docs.browserless.io/open-api/edge-scrape

Returns text, HTML, and metadata from a given list of CSS selectors against the rendered DOM (Edge).
Debugging information is available by sending in the appropriate flags in the "debugOpts"
property. Responds with an array of JSON objects.

Request

### Query Parameters
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `launch` (object | string)
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.

### Request Body (`application/json`)
- `addScriptTag` (object[])
- `addStyleTag` (object[])
- `authenticate` (object | null) — Credentials for HTTP authentication. Contains `username` and `password` properties.
- `bestAttempt` (boolean) — When bestAttempt is set to true, browserless attempt to proceed
- `cookies` (object[])
- `debugOpts` (object)
  - `console` (boolean)
  - `cookies` (boolean)
  - `html` (boolean)
  - `network` (boolean)
  - `screenshot` (boolean)
- `elements` (object[]) **(required)**
- `emulateMediaType` (string)
- `gotoOptions` (object) — Options to configure the page navigation, such as `timeout` and `waitUntil`.
  - `referer` (string) — If provided, it will take preference over the referer header value set by
  - `referrerPolicy` (string) — If provided, it will take preference over the referer-policy header value
  - `timeout` (number) — Maximum wait time in milliseconds. Pass 0 to disable the timeout.
  - `waitUntil` (`domcontentloaded` | `load` | `networkidle0` | `networkidle2`[] | `domcontentloaded` | `load` | `networkidle0` | `networkidle2`) — When to consider waiting succeeds. Given an array of event strings, waiting
  - `signal` (object) — A signal object that allows you to cancel the call.
- `html` (string)
- `rejectRequestPattern` (string[])
- `rejectResourceTypes` (`cspviolationreport` | `document` | `eventsource` | `fedcm` | `fetch` | `font` | `image` | `manifest` | `media` | `other` | `ping` | `prefetch` | `preflight` | `script` | `signedexchange` | `stylesheet` | `texttrack` | `websocket` | `xhr`[])
- `requestInterceptors` (object[])
- `setExtraHTTPHeaders` (object)
- `setJavaScriptEnabled` (boolean)
- `url` (string)
- `userAgent` (object)
  - `userAgent` (string)
  - `userAgentMetadata` (object) — Used to specify User Agent Client Hints to emulate. See https://wicg.github.io/ua-client-hints
  - `platform` (string)
- `viewport` (object | null) — The viewport dimensions and settings for the page.
- `waitForEvent` (object)
  - `event` (string) **(required)**
  - `timeout` (number)
- `waitForFunction` (object)
  - `fn` (string) **(required)** — The function, or statement, to be evaluated in browser context
  - `polling` (string,number) — An interval at which the pageFunction is executed, defaults to raf.
  - `timeout` (number) — Maximum time to wait for in milliseconds. Defaults to 30000 (30 seconds).
- `waitForSelector` (object)
  - `hidden` (boolean)
  - `selector` (string) **(required)**
  - `timeout` (number)
  - `visible` (boolean)
- `waitForTimeout` (number)

### Responses

- **200**: The JSON response body — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /edge/screenshot

URL: https://docs.browserless.io/open-api/edge-screenshot

Returns a screenshot binary from a `url` or `html` payload using Edge.
"url" or "html" payload in your request. Many options exist including
cookies, user-agents, setting timers and network mocks.

Request

### Query Parameters
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `launch` (object | string) — Launch options, which can be either an object
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.

### Request Body (`application/json`)
- `addScriptTag` (object[])
- `addStyleTag` (object[])
- `authenticate` (object | null) — Credentials for HTTP authentication. Contains `username` and `password` properties.
- `bestAttempt` (boolean) — When bestAttempt is set to true, browserless attempt to proceed
- `cookies` (object[])
- `emulateMediaType` (string)
- `gotoOptions` (object) — Options to configure the page navigation, such as `timeout` and `waitUntil`.
  - `referer` (string) — If provided, it will take preference over the referer header value set by
  - `referrerPolicy` (string) — If provided, it will take preference over the referer-policy header value
  - `timeout` (number) — Maximum wait time in milliseconds. Pass 0 to disable the timeout.
  - `waitUntil` (`domcontentloaded` | `load` | `networkidle0` | `networkidle2`[] | `domcontentloaded` | `load` | `networkidle0` | `networkidle2`) — When to consider waiting succeeds. Given an array of event strings, waiting
  - `signal` (object) — A signal object that allows you to cancel the call.
- `html` (string)
- `options` (object) — Screenshot options based on Puppeteer's ScreenshotOptions interface.
  - `optimizeForSpeed` (boolean)
  - `type` (`jpeg` | `png` | `webp`)
  - `quality` (number) — Quality of the image, between 0-100. Not applicable to `png` images.
  - `fromSurface` (boolean) — Capture the screenshot from the surface, rather than the view.
  - `fullPage` (boolean) — When `true`, takes a screenshot of the full page.
  - `omitBackground` (boolean) — Hides default white background and allows capturing screenshots with transparency.
  - `clip` (object) — Specifies the region of the page/element to clip.
  - `encoding` (`base64` | `binary`) — Encoding of the image.
  - `captureBeyondViewport` (boolean) — Capture the screenshot beyond the viewport.
- `rejectRequestPattern` (string[])
- `rejectResourceTypes` (`cspviolationreport` | `document` | `eventsource` | `fedcm` | `fetch` | `font` | `image` | `manifest` | `media` | `other` | `ping` | `prefetch` | `preflight` | `script` | `signedexchange` | `stylesheet` | `texttrack` | `websocket` | `xhr`[])
- `requestInterceptors` (object[])
- `scrollPage` (boolean)
- `selector` (string)
- `setExtraHTTPHeaders` (object)
- `setJavaScriptEnabled` (boolean)
- `url` (string)
- `userAgent` (object)
  - `userAgent` (string)
  - `userAgentMetadata` (object) — Used to specify User Agent Client Hints to emulate. See https://wicg.github.io/ua-client-hints
  - `platform` (string)
- `viewport` (object | null) — The viewport dimensions and settings for the page.
- `waitForEvent` (object)
  - `event` (string) **(required)**
  - `timeout` (number)
- `waitForFunction` (object)
  - `fn` (string) **(required)** — The function, or statement, to be evaluated in browser context
  - `polling` (string,number) — An interval at which the pageFunction is executed, defaults to raf.
  - `timeout` (number) — Maximum time to wait for in milliseconds. Defaults to 30000 (30 seconds).
- `waitForSelector` (object)
  - `hidden` (boolean)
  - `selector` (string) **(required)**
  - `timeout` (number)
  - `visible` (boolean)
- `waitForTimeout` (number)

### Responses

- **200**: Response can either be a text/plain base64 encoded body — Content: `image/png`, `image/jpeg`, `text/plain`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /firefox/playwright

URL: https://docs.browserless.io/open-api/firefox-playwright

Connect to Firefox with any playwright-compliant library.

**Note:** Also available at `/playwright/firefox` as a convenience alias.

Request

### Query Parameters
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `launch` (object) — Launch options, which can be either an object
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.

### Responses

- **101**: Indicates successful WebSocket upgrade.
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /function/connect/*

URL: https://docs.browserless.io/open-api/function-connect

Internally used by the POST /function API to connect the underlying client-side code to. Not intended for direct use but documented for completeness and to distinguish between other reconnect style calls.

Request

### Query Parameters
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `launch` (object | string) — Launch options, which can be either an object
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.

### Responses

- **101**: Indicates successful WebSocket upgrade.
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /integrations/onepassword/*/audit

URL: https://docs.browserless.io/open-api/integrations-onepassword-audit

Lists recent credential activity for a single 1Password integration owned
by the token, newest first. One entry per Browserless.loadSecret or
saveSecret attempt: outcome, error code, the credential reference, and
non-secret target metadata. Secret values are never recorded or returned.
Paginated via "limit" (default 100, capped at 1000) and "offset" (default 0).
Returns 404 if no integration with that id exists for this token.

**Note:** This endpoint is also available at: `/management/integrations/onepassword/*/audit` for backwards compatibility.

Request

### Query Parameters
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `launch` (object | object | string) — Launch options, which can be either an object
- `limit` (number) — Maximum number of audit entries to return (1–1000). Defaults to 100.
- `offset` (number) — Number of audit entries to skip for pagination. Defaults to 0.
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier

### Responses

- **200**:  — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /integrations/onepassword/autologin/form

URL: https://docs.browserless.io/open-api/integrations-onepassword-autologin-form

Returns the selectable references for building an autologin profile: the
1Password integration's login origins and, per origin, the field labels it
fills (never values). Drives the create-profile form.

Request

### Query Parameters
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `integrationId` (string) — Which registered 1Password integration's login items to return.
- `launch` (object | object | string) — Launch options, which can be either an object
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `profileName` (string) — Recovery only: restrict choices to this tenant-owned saved artifact.
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier

### Responses

- **200**:  — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /integrations/script-generator

URL: https://docs.browserless.io/open-api/integrations-script-generator

Generates a Browserless automation script (BrowserQL / Playwright JS /
Playwright Python) from a natural-language task. A managed agent drives a
browser session; its ordered tool-call transcript is rendered into the
requested script(s). Streams Server-Sent Events: status, script, complete,
error.

Request

### Responses

- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /json/new

URL: https://docs.browserless.io/open-api/json-new

Returns a JSON payload that acts as a pass-through to the DevTools /json/new HTTP API in Chromium.
Browserless mocks this payload so that remote clients can connect to the underlying "webSocketDebuggerUrl"
which will cause Browserless to start the browser and proxy that request into a blank page.

Request

### Responses

- **200**:  — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /json/protocol

URL: https://docs.browserless.io/open-api/json-protocol

Returns Protocol JSON meta-data that Chrome and Chromium come with.

Request

### Responses

- **200**:  — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /json/version

URL: https://docs.browserless.io/open-api/json-version

Returns a JSON payload that acts as a pass-through to the DevTools /json/version protocol in Chrome and Chromium.

Request

### Responses

- **200**:  — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /kill/*

URL: https://docs.browserless.io/open-api/kill-0-9-a-z-a-z

Kill running sessions based on BrowserId or TrackingId.

Request

### Query Parameters
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `browserId` (string)
- `launch` (object | object | string) — Launch options, which can be either an object
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier

### Responses

- **200**:  — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /live/*

URL: https://docs.browserless.io/open-api/live

Websocket back-end that powers the live session experience.

**Note:** Also available at `/chromium/live/*` as a convenience alias.

Request

### Responses

- **101**: Indicates successful WebSocket upgrade.
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /map

URL: https://docs.browserless.io/open-api/map

Primarily discovers URLs from the site's sitemap, supplemented
with link extraction from the page via the smart-scrape strategy
pipeline (HTTP-first, browser fallback for JS-rendered pages).
Use the search parameter to order results by relevance.

Request

### Query Parameters
- `timeout` (number) — Request timeout in milliseconds
- `token` (string) — API authentication token
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.

### Request Body (`application/json`)
- `url` (string) **(required)** — The base URL to start mapping from (required)
- `search` (string) — Search query to order results by relevance
- `limit` (number) — Maximum number of links to return (default: 5000, max: 5000)
- `timeout` (number) — Request timeout in milliseconds
- `sitemap` (`include` | `only` | `skip`) — Controls sitemap behavior: "include" (default), "skip", "only"
- `includeSubdomains` (boolean) — Whether to include URLs from subdomains (default: true)
- `ignoreQueryParameters` (boolean) — Exclude URLs with query parameters (default: true)
- `location` (object) — Geo-targeting settings
  - `country` (string) — ISO 3166-1 alpha-2 country code for proxy routing (e.g., `"us"`, `"gb"`, `"de"`). Defaults to `"us"`.
  - `languages` (string[]) — Preferred language codes for the request (e.g., `["en", "fr"]`).
- `proxy` (`datacenter` | `residential`) — Proxy network to route through: `"residential"` (default) or `"datacenter"`.

### Responses

- **200**: An array of discovered URLs from the target page and its sitemap. — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /meta

URL: https://docs.browserless.io/open-api/meta

Returns a JSON payload of the current system versions, including the core API version.

Request

### Responses

- **200**:  — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /integrations/onepassword/profile

URL: https://docs.browserless.io/open-api/onepassword-autologin-start

Kicks off an autologin task: a managed agent creates a browser profile by
logging into the integration's domains, then saves it. Returns a taskId to
poll via GET /integrations/onepassword/profile/:taskId.

Request

### Request Body (`application/json`)
- `name` (string) **(required)** — Name to save the resulting profile under. Unique per token.
- `integrationId` (string) **(required)** — Registered 1Password integration id.
- `recover` (boolean) — Refresh an existing owned profile instead of creating a new one.
- `allowedDomains` (string[]) — Optional subset of the integration's allowed domains.
- `emailOtp` (object[])
- `proxy` (object) — Optional proxy for the browser session the agent logs in through. Omitted
  - `type` (`datacenter` | `residential`)
  - `sticky` (boolean)
  - `country` (string)
  - `city` (string)
  - `state` (string)
  - `preset` (string)
- `attemptId` (string) — Optional profile-setup funnel correlation UUID.

### Responses

- **200**:  — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /integrations/onepassword/profile/*

URL: https://docs.browserless.io/open-api/onepassword-autologin-status

Poll an autologin task by id. Returns its state, the current live rationale,
and (on success) the saved profile id plus a per-domain breakdown. Scoped to
the requesting account (or token, for keys not yet mapped to an account) —
returns 404 for unknown ids or another account's task.

Request

### Responses

- **200**: Public polling shape returned by GET /integrations/onepassword/profile/:taskId. — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /integrations/onepassword/*

URL: https://docs.browserless.io/open-api/onepassword-delete

Deletes a 1Password integration owned by the requesting token. The id is the
URL-encoded path segment after "/integrations/onepassword/". Returns 404 if
no integration with that id exists for this token. Deleting an integration
does not touch any authentication profile.

**Note:** This endpoint is also available at: `/management/integrations/onepassword/*` for backwards compatibility.

Request

### Responses

- **200**:  — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /integrations/onepassword/*

URL: https://docs.browserless.io/open-api/onepassword-get

Returns metadata for a specific 1Password integration owned by the token.
The id is the URL-encoded path segment after "/integrations/onepassword/".
Returns 404 if no integration with that id exists for this token. The
response never includes any token material.

**Note:** This endpoint is also available at: `/management/integrations/onepassword/*` for backwards compatibility.

Request

### Responses

- **200**:  — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /integrations/onepassword

URL: https://docs.browserless.io/open-api/onepassword-list

Lists the 1Password integrations owned by the requesting token. Paginated
via "limit" (default 100, capped at 1000) and "offset" (default 0). Records
never include any token material.

**Note:** This endpoint is also available at: `/management/integrations/onepassword` for backwards compatibility.

Request

### Query Parameters
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `launch` (object | object | string) — Launch options, which can be either an object
- `limit` (number) — Maximum number of integrations to return (1–1000). Defaults to 100.
- `offset` (number) — Number of integrations to skip for pagination. Defaults to 0.
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier

### Responses

- **200**:  — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /integrations/onepassword

URL: https://docs.browserless.io/open-api/onepassword-register

Registers a 1Password integration for the requesting token. The supplied
service-account token is encrypted at rest and is never returned by this or
any other endpoint. Returns the created integration without any token field.

**Note:** This endpoint is also available at: `/management/integrations/onepassword` for backwards compatibility.

Request

### Request Body (`application/json`)
- `label` (string) **(required)** — A user-visible label for the integration. Unique per token.
- `kind` (`connect` | `service_account`) — Integration kind. Defaults to "service_account".
- `serviceAccountToken` (string) **(required)** — The 1Password service-account token (or Connect access token when
- `allowedDomains` (string[]) — Origins this integration may fill credentials into. Empty means no fills pass.
- `connectUrl` (string) — Connect server URL. Required when kind is "connect", rejected otherwise.

### Responses

- **200**: Public-facing integration response returned by HTTP routes. — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# OpenAPI Reference Overview

URL: https://docs.browserless.io/open-api/overview

This is the API reference for **Browserless Enterprise and Cloud** deployments. You'll find endpoints for capturing screenshots and PDFs, scraping content, running custom browser functions, solving CAPTCHAs, bypassing bot detection with stealth browsing, managing authenticated browser profiles, and orchestrating crawls.

The API includes REST endpoints (JSON in, JSON or binary out), WebSocket connections for direct CDP/Playwright/Puppeteer access, and [CDP extensions](https://docs.browserless.io/api-reference/cdp-extensions) that expose Browserless-specific commands like `liveURL`, `solveCaptcha`, and `reconnect`. For the open-source container API, see the [GitHub repository](https://github.com/browserless/browserless).

## Endpoint Categories

## Just Getting Started?

Check out the [Quick Start guide](https://docs.browserless.io/baas/start) to launch your first session, or explore the [BrowserQL IDE](https://docs.browserless.io/browserql/start) for a GraphQL-based automation workflow.

## Base URL

All API requests go to your deployment's base URL. For Browserless Cloud:

```plaintext
https://production-sfo.browserless.io
```

Self-hosted Enterprise deployments use the host and port where the container is running (default `http://localhost:3000`).

## Authentication

Include your API token as a query parameter on every request:

```plaintext
https://production-sfo.browserless.io/chromium/content?token=YOUR_API_TOKEN
```

Tokens are managed from the [Account dashboard](https://www.browserless.io/account). Self-hosted deployments set the token via the `TOKEN` environment variable. See [connection details](https://docs.browserless.io/baas/connection) for all authentication options.

## Changelog

For release notes and version history, see the [Enterprise Changelog](https://docs.browserless.io/enterprise/changelog).

## Software keys

Self-hosted Enterprise deployments use time-limited software keys for offline licensing. No external connections or callbacks are required. Keys are cryptographically secure and can't be reverse engineered. When a key expires, the container exits with a semantic error code.

To use a software key, set the `KEY` environment variable when running the container:

```bash
docker run -e KEY=your-generated-key browserless/enterprise
```

---

# /pdf

URL: https://docs.browserless.io/open-api/pdf

Returns a PDF binary from a `url` or `html` payload.
"url" or "html" payload in your request. Many options exist for
injecting cookies, request interceptors, user-agents and waiting for
selectors, timers and more.

**Note:** This is the preferred endpoint. The `/chromium/pdf` route is equivalent.

Request

### Query Parameters
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `launch` (object | string) — Launch options for the browser, either as a JSON object or a JSON string.
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier

### Request Body (`application/json`)
- `blockConsentModals` (boolean) — Whether to automatically block cookie consent modals and popups.
- `options` (object) — PDF generation options based on Puppeteer's PDFOptions interface.
  - `scale` (number) — Scales the rendering of the web page. Amount must be between `0.1` and `2`.
  - `displayHeaderFooter` (boolean) — Whether to show the header and footer.
  - `headerTemplate` (string) — HTML template for the print header. Should be valid HTML with the following
  - `footerTemplate` (string) — HTML template for the print footer. Has the same constraints and support
  - `printBackground` (boolean) — Set to `true` to print background graphics.
  - `landscape` (boolean) — Whether to print in landscape orientation.
  - `pageRanges` (string) — Paper ranges to print, e.g. `1-5, 8, 11-13`.
  - `format` (`A0` | `A1` | `A2` | `A3` | `A4` | `A5` | `A6` | `LEDGER` | `LEGAL` | `LETTER` | `Ledger` | `Legal` | `Letter` | `TABLOID` | `Tabloid` | `a0` | `a1` | `a2` | `a3` | `a4` | `a5` | `a6` | `ledger` | `legal` | `letter` | `tabloid`) — All the valid paper format types when printing a PDF.
  - `width` (string,number) — Sets the width of paper. You can pass in a number or a string with a unit.
  - `height` (string,number) — Sets the height of paper. You can pass in a number or a string with a unit.
  - `preferCSSPageSize` (boolean) — Give any CSS `@page` size declared in the page priority over what is
  - `margin` (object) — Set the PDF margins.
  - `omitBackground` (boolean) — Hides default white background and allows generating pdfs with transparency.
  - `tagged` (boolean) — Generate tagged (accessible) PDF.
  - `outline` (boolean) — Generate document outline.
  - `timeout` (number) — Timeout in milliseconds. Pass `0` to disable timeout.
  - `waitForFonts` (boolean) — If true, waits for `document.fonts.ready` to resolve. This might require
  - `fullPage` (boolean)
- `addScriptTag` (object[]) — An array of script tags to add to the page before performing actions.
- `addStyleTag` (object[]) — An array of style tags to add to the page before performing actions.
- `authenticate` (object) — Credentials for HTTP authentication. Contains `username` and `password` properties.
  - `username` (string) **(required)**
  - `password` (string) **(required)**
- `bestAttempt` (boolean) — When bestAttempt is set to true, browserless will attempt to proceed
- `cookies` (object[]) — An array of cookies to set on the page before navigation.
- `emulateMediaType` (string) — Changes the CSS media type of the page. Accepts values like "screen" or "print".
- `gotoOptions` (object) — Options to configure the page navigation, such as `timeout` and `waitUntil`.
  - `referer` (string) — If provided, it will take preference over the referer header value set by
  - `referrerPolicy` (string) — If provided, it will take preference over the referer-policy header value
  - `timeout` (number) — Maximum wait time in milliseconds. Pass 0 to disable the timeout.
  - `waitUntil` (`domcontentloaded` | `load` | `networkidle0` | `networkidle2`[] | `domcontentloaded` | `load` | `networkidle0` | `networkidle2`) — When to consider waiting succeeds. Given an array of event strings, waiting
  - `signal` (object) — A signal object that allows you to cancel the call.
- `html` (string) — HTML content to set as the page content instead of navigating to a URL.
- `rejectRequestPattern` (string[]) — An array of patterns to match against request URLs for automatic rejection.
- `rejectResourceTypes` (`cspviolationreport` | `document` | `eventsource` | `fedcm` | `fetch` | `font` | `image` | `manifest` | `media` | `other` | `ping` | `prefetch` | `preflight` | `script` | `signedexchange` | `stylesheet` | `texttrack` | `websocket` | `xhr`[]) — An array of resource types to reject during page load.
- `requestInterceptors` (object[]) — An array of request interceptors that can modify or mock network requests.
- `setExtraHTTPHeaders` (object) — An object containing additional HTTP headers to send with every request.
- `setJavaScriptEnabled` (boolean) — Whether or not to allow JavaScript to run on the page.
- `url` (string) — The URL to navigate to before performing actions.
- `userAgent` (object) — The user agent string to use for the page.
  - `userAgent` (string)
  - `userAgentMetadata` (object) — Used to specify User Agent Client Hints to emulate. See https://wicg.github.io/ua-client-hints
  - `platform` (string)
- `viewport` (object) — The viewport dimensions and settings for the page.
  - `width` (number) **(required)** — The page width in CSS pixels.
  - `height` (number) **(required)** — The page height in CSS pixels.
  - `deviceScaleFactor` (number) — Specify device scale factor.
  - `isMobile` (boolean) — Whether the `meta viewport` tag is taken into account.
  - `isLandscape` (boolean) — Specifies if the viewport is in landscape mode.
  - `hasTouch` (boolean) — Specify if the viewport supports touch events.
- `waitForEvent` (object) — Options for waiting for a specific event to be fired on the page.
  - `event` (string) **(required)**
  - `timeout` (number)
- `waitForFunction` (object) — Options for waiting for a JavaScript function to execute.
  - `fn` (string) **(required)** — The function, or statement, to be evaluated in browser context
  - `polling` (string,number) — An interval at which the pageFunction is executed, defaults to raf.
  - `timeout` (number) — Maximum time to wait for in milliseconds. Defaults to 30000 (30 seconds).
- `waitForSelector` (object) — Options for waiting for a specific CSS selector to appear on the page.
  - `hidden` (boolean)
  - `selector` (string) **(required)**
  - `timeout` (number)
  - `visible` (boolean)
- `waitForTimeout` (number) — The amount of time in milliseconds to wait before proceeding.

### Responses

- **200**: Responds with an application/pdf content-type and a binary PDF — Content: `application/pdf`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /profile

URL: https://docs.browserless.io/open-api/profile

Removes selected sites and their cookies, localStorage and IndexedDB from a saved profile.

**Note:** This endpoint is also available at: `/management/profile` for backwards compatibility.

Request

### Request Body (`application/json`)
- `name` (string) **(required)**
- `domains` (string[]) **(required)** — Exact sites to remove, without cascading to subdomains.

### Responses

- **200**:  — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /profile

URL: https://docs.browserless.io/open-api/profile-create

Launches a temporary browser session for creating an authenticated profile.
Connect via the returned WebSocket URL, authenticate in the browser,
then call Browserless.saveProfile via CDP to capture and persist the auth state.
The profile is NOT saved automatically — you must explicitly call
Browserless.saveProfile before disconnecting. The session expires after 10 minutes
or your plan's session limit, whichever comes first. Free sessions are capped at
two minutes; higher-tier plans retain the 10-minute maximum.

**Note:** This endpoint is also available at: `/management/profile` for backwards compatibility.

Request

### Query Parameters
- `emulationOs` (`android` | `linux` | `macos` | `windows`) — OS persona to preserve while authoring the profile.
- `timeout` (number) — Maximum duration of the temporary profile-creation session, in

### Request Body (`application/json`)
- `name` (string) **(required)** — A user-visible name for the profile.
- `stealth` (boolean) — Whether or not to enable advanced stealth mode. Defaults to false.
- `browser` (`chrome` | `chromium` | `stealth`) — The type of browser to use. Defaults to 'stealth'.
- `args` (string[]) — An array of command-line arguments to pass to the browser.
- `proxy` (object) — Proxy parameters for the profile creation session.
  - `type` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
  - `sticky` (boolean) — Whether or not to use the same IP address for all proxied requests. Defaults to true.
  - `country` (string) — The country to proxy through. Defaults to 'us' or United States.
  - `city` (string) — The city to proxy through.
  - `state` (string) — The state to proxy through.
  - `preset` (string) — Preset code for website-specific proxy configurations (e.g., 'px_gov01' for government sites).
- `integrationId` (string) — Optional 1Password integration id. When set, the profile-authoring session
- `credentials` (object) — Optional alias → op:// reference map for the integration above.
- `allowedDomains` (string[]) — Optional per-session narrowing of the integration's allowed domains.

### Responses

- **200**:  — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /profile/*

URL: https://docs.browserless.io/open-api/profile-delete

Deletes an authentication profile and its captured state.
The profile name is the URL-encoded path segment after "/profile/".
Returns 404 if no profile with that name exists for this token.

**Note:** This endpoint is also available at: `/management/profile/*` for backwards compatibility.

Request

### Responses

- **200**:  — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /profile/*/download

URL: https://docs.browserless.io/open-api/profile-download

Downloads the captured authentication state for a specific profile.
The profile name is the URL-encoded path segment before "/download".
Returns 404 if no profile with that name exists for this token.

**Note:** This endpoint is also available at: `/profile/.*/download` for backwards compatibility.

Request

### Responses

- **200**:  — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /profile/*

URL: https://docs.browserless.io/open-api/profile-get

Returns metadata for a specific authentication profile.
The profile name is the URL-encoded path segment after "/profile/".
Returns 404 if no profile with that name exists for this token.

**Note:** This endpoint is also available at: `/management/profile/*` for backwards compatibility.

Request

### Responses

- **200**:  — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /profile/refresh

URL: https://docs.browserless.io/open-api/profile-refresh

Replaces the state of an existing authentication profile in place.
The "name" must already exist for the requesting token. Counts of any
dropped or truncated entries are surfaced under "diagnostics".

Request

### Request Body (`application/json`)
- `name` (string) **(required)** — Name of the existing profile to refresh. Must already exist for the
- `state` (any) **(required)** — Pre-captured authentication state. Same shape as `POST /profile/upload`.
- `captureSettings` (object) — Capture inputs this refresh ran with. Omit the key to leave the stored settings
  - `browser` (string) — Raw `--browser` value.
  - `profile` (string) — Raw `--profile` value.
  - `excludeDomains` (string[]) — Effective merged exclusion list, not just the `--exclude-domain` flags.
  - `onlyDomains` (string[]) — Normalized `--only-domain` hostnames, deduped and capped — not merged with local config.
  - `keepDomains` (string[]) — Normalized `--keep-domain` hostnames, same normalization, no merge.
  - `autoFit` (boolean) — Whether `--auto-fit` was used.
  - `passwordStore` (string) — Raw `--password-store` value. Linux only.
  - `cliVersion` (string) — CLI version that produced these settings.

### Responses

- **200**:  — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /profile/*

URL: https://docs.browserless.io/open-api/profile-rename

Renames an authentication profile. The new name must be unique per token.
The current name is the URL-encoded path segment after "/profile/".
Returns 400 if the new name is already taken, 404 if the source profile is not found.

**Note:** This endpoint is also available at: `/management/profile/*` for backwards compatibility.

Request

### Request Body (`application/json`)
- `name` (string) **(required)** — The new name for the profile.

### Responses

- **200**:  — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /profile/upload

URL: https://docs.browserless.io/open-api/profile-upload

Creates a new authentication profile from a pre-captured "state" payload
(cookies plus per-origin localStorage and IndexedDB). The "name" must be
unique per token. Counts of any dropped or truncated entries are
surfaced under "diagnostics".

Request

### Request Body (`application/json`)
- `name` (string) **(required)** — A user-visible name for the profile. Must be unique per token.
- `state` (any) **(required)** — Pre-captured authentication state. An object with two arrays —
- `captureSettings` (object) — Capture inputs the CLI ran with, stored so a later `browserless profile refresh`
  - `browser` (string) — Raw `--browser` value.
  - `profile` (string) — Raw `--profile` value.
  - `excludeDomains` (string[]) — Effective merged exclusion list, not just the `--exclude-domain` flags.
  - `onlyDomains` (string[]) — Normalized `--only-domain` hostnames, deduped and capped — not merged with local config.
  - `keepDomains` (string[]) — Normalized `--keep-domain` hostnames, same normalization, no merge.
  - `autoFit` (boolean) — Whether `--auto-fit` was used.
  - `passwordStore` (string) — Raw `--password-store` value. Linux only.
  - `cliVersion` (string) — CLI version that produced these settings.

### Responses

- **200**:  — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /profiles

URL: https://docs.browserless.io/open-api/profiles

Lists authentication profiles owned by the requesting token.
Paginated via "limit" (default 100, capped at 1000) and "offset" (default 0).
Returns an empty array if the token has no profiles.

**Note:** This endpoint is also available at: `/management/profiles` for backwards compatibility.

Request

### Query Parameters
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `launch` (object | object | string) — Launch options, which can be either an object
- `limit` (number) — Maximum number of profiles to return (1–1000). Defaults to 100.
- `offset` (number) — Number of profiles to skip for pagination. Defaults to 0.
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier

### Responses

- **200**:  — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /proxy/cities

URL: https://docs.browserless.io/open-api/proxy-cities

Returns a list of available cities for proxy connections.

This endpoint requires city proxying to be enabled on your plan.

Query Parameters:
- country (optional): Filter cities by two-letter country code (e.g., 'US', 'GB', 'DE')

The response includes a list of available cities grouped by country code.

Request

### Query Parameters
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `country` (string) — Optional two-letter country code to filter cities by country (e.g., 'US', 'GB', 'DE').
- `launch` (object | object | string) — Launch options, which can be either an object
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier

### Responses

- **200**:  — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /reconnect/*

URL: https://docs.browserless.io/open-api/reconnect

Reconnect to an existing Chromium or Chrome session with a library like puppeteer or others that work over chrome-devtools-protocol.

Request

### Query Parameters
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `integrations` (string)
- `launch` (object | object | string) — Launch options, which can be either an object
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `replay` (boolean)
- `solveCaptchas` (boolean)
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.

### Responses

- **101**: Indicates successful WebSocket upgrade.
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /scrape

URL: https://docs.browserless.io/open-api/scrape

A JSON-based API that returns text, html, and meta-data from a given list of
selectors. Debugging information is available by sending in the appropriate
flags in the "debugOpts" property. Responds with an array of JSON objects.

**Note:** This endpoint is also available at: `/chromium/scrape` for backwards compatibility.

Request

### Query Parameters
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `launch` (object | string) — Launch options for the browser, either as a JSON object or a JSON string.
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier

### Request Body (`application/json`)
- `addScriptTag` (object[])
- `addStyleTag` (object[])
- `authenticate` (object) — Credentials for HTTP authentication. Contains `username` and `password` properties.
  - `username` (string) **(required)**
  - `password` (string) **(required)**
- `bestAttempt` (boolean) — When bestAttempt is set to true, browserless attempt to proceed
- `cookies` (object[])
- `debugOpts` (object)
  - `console` (boolean)
  - `cookies` (boolean)
  - `html` (boolean)
  - `network` (boolean)
  - `screenshot` (boolean)
- `elements` (object[]) **(required)**
- `emulateMediaType` (string)
- `gotoOptions` (object) — The supported subset of puppeteer's goto options: `waitUntil` (mapped onto
  - `timeout` (number)
  - `waitUntil` (`domcontentloaded` | `load` | `networkidle0` | `networkidle2`[] | `domcontentloaded` | `load` | `networkidle0` | `networkidle2`)
  - `referer` (string)
- `html` (string)
- `rejectRequestPattern` (string[])
- `rejectResourceTypes` (`cspviolationreport` | `document` | `eventsource` | `fedcm` | `fetch` | `font` | `image` | `manifest` | `media` | `other` | `ping` | `prefetch` | `preflight` | `script` | `signedexchange` | `stylesheet` | `texttrack` | `websocket` | `xhr`[])
- `requestInterceptors` (object[])
- `setExtraHTTPHeaders` (object)
- `setJavaScriptEnabled` (boolean)
- `url` (string)
- `userAgent` (object)
  - `userAgent` (string)
  - `userAgentMetadata` (object) — Used to specify User Agent Client Hints to emulate. See https://wicg.github.io/ua-client-hints
  - `platform` (string)
- `viewport` (object) — The viewport dimensions and settings for the page.
  - `width` (number) **(required)** — The page width in CSS pixels.
  - `height` (number) **(required)** — The page height in CSS pixels.
  - `deviceScaleFactor` (number) — Specify device scale factor.
  - `isMobile` (boolean) — Whether the `meta viewport` tag is taken into account.
  - `isLandscape` (boolean) — Specifies if the viewport is in landscape mode.
  - `hasTouch` (boolean) — Specify if the viewport supports touch events.
- `waitForEvent` (object)
  - `event` (string) **(required)**
  - `timeout` (number)
- `waitForFunction` (object)
  - `fn` (string) **(required)** — The function, or statement, to be evaluated in browser context
  - `polling` (string,number) — An interval at which the pageFunction is executed, defaults to raf.
  - `timeout` (number) — Maximum time to wait for in milliseconds. Defaults to 30000 (30 seconds).
- `waitForSelector` (object)
  - `hidden` (boolean)
  - `selector` (string) **(required)**
  - `timeout` (number)
  - `visible` (boolean)
- `waitForTimeout` (number)

### Responses

- **200**:  — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /screenshot

URL: https://docs.browserless.io/open-api/screenshot

Returns a screenshot binary from a `url` or `html` payload.
"url" or "html" payload in your request. Many options exist including
cookies, user-agents, setting timers and network mocks.

**Note:** This is the preferred endpoint. The `/chromium/screenshot` route is equivalent.

Request

### Query Parameters
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `launch` (object | string) — Launch options for the browser, either as a JSON object or a JSON string.
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier

### Request Body (`application/json`)
- `blockConsentModals` (boolean) — Whether to automatically block cookie consent modals and popups.
- `options` (object) — Screenshot options based on Puppeteer's ScreenshotOptions interface.
  - `optimizeForSpeed` (boolean)
  - `type` (`jpeg` | `png` | `webp`)
  - `quality` (number) — Quality of the image, between 0-100. Not applicable to `png` images.
  - `fromSurface` (boolean) — Capture the screenshot from the surface, rather than the view.
  - `fullPage` (boolean) — When `true`, takes a screenshot of the full page.
  - `omitBackground` (boolean) — Hides default white background and allows capturing screenshots with transparency.
  - `clip` (object) — Specifies the region of the page/element to clip.
  - `encoding` (`base64` | `binary`) — Encoding of the image.
  - `captureBeyondViewport` (boolean) — Capture the screenshot beyond the viewport.
- `addScriptTag` (object[]) — An array of script tags to add to the page before performing actions.
- `addStyleTag` (object[]) — An array of style tags to add to the page before performing actions.
- `authenticate` (object) — Credentials for HTTP authentication. Contains `username` and `password` properties.
  - `username` (string) **(required)**
  - `password` (string) **(required)**
- `bestAttempt` (boolean) — When bestAttempt is set to true, browserless will attempt to proceed
- `cookies` (object[]) — An array of cookies to set on the page before navigation.
- `emulateMediaType` (string) — Changes the CSS media type of the page. Accepts values like "screen" or "print".
- `gotoOptions` (object) — Options to configure the page navigation, such as `timeout` and `waitUntil`.
  - `referer` (string) — If provided, it will take preference over the referer header value set by
  - `referrerPolicy` (string) — If provided, it will take preference over the referer-policy header value
  - `timeout` (number) — Maximum wait time in milliseconds. Pass 0 to disable the timeout.
  - `waitUntil` (`domcontentloaded` | `load` | `networkidle0` | `networkidle2`[] | `domcontentloaded` | `load` | `networkidle0` | `networkidle2`) — When to consider waiting succeeds. Given an array of event strings, waiting
  - `signal` (object) — A signal object that allows you to cancel the call.
- `html` (string) — HTML content to set as the page content instead of navigating to a URL.
- `rejectRequestPattern` (string[]) — An array of patterns to match against request URLs for automatic rejection.
- `rejectResourceTypes` (`cspviolationreport` | `document` | `eventsource` | `fedcm` | `fetch` | `font` | `image` | `manifest` | `media` | `other` | `ping` | `prefetch` | `preflight` | `script` | `signedexchange` | `stylesheet` | `texttrack` | `websocket` | `xhr`[]) — An array of resource types to reject during page load.
- `requestInterceptors` (object[]) — An array of request interceptors that can modify or mock network requests.
- `setExtraHTTPHeaders` (object) — An object containing additional HTTP headers to send with every request.
- `setJavaScriptEnabled` (boolean) — Whether or not to allow JavaScript to run on the page.
- `url` (string) — The URL to navigate to before performing actions.
- `userAgent` (object) — The user agent string to use for the page.
  - `userAgent` (string)
  - `userAgentMetadata` (object) — Used to specify User Agent Client Hints to emulate. See https://wicg.github.io/ua-client-hints
  - `platform` (string)
- `viewport` (object) — The viewport dimensions and settings for the page.
  - `width` (number) **(required)** — The page width in CSS pixels.
  - `height` (number) **(required)** — The page height in CSS pixels.
  - `deviceScaleFactor` (number) — Specify device scale factor.
  - `isMobile` (boolean) — Whether the `meta viewport` tag is taken into account.
  - `isLandscape` (boolean) — Specifies if the viewport is in landscape mode.
  - `hasTouch` (boolean) — Specify if the viewport supports touch events.
- `waitForEvent` (object) — Options for waiting for a specific event to be fired on the page.
  - `event` (string) **(required)**
  - `timeout` (number)
- `waitForFunction` (object) — Options for waiting for a JavaScript function to execute.
  - `fn` (string) **(required)** — The function, or statement, to be evaluated in browser context
  - `polling` (string,number) — An interval at which the pageFunction is executed, defaults to raf.
  - `timeout` (number) — Maximum time to wait for in milliseconds. Defaults to 30000 (30 seconds).
- `waitForSelector` (object) — Options for waiting for a specific CSS selector to appear on the page.
  - `hidden` (boolean)
  - `selector` (string) **(required)**
  - `timeout` (number)
  - `visible` (boolean)
- `waitForTimeout` (number) — The amount of time in milliseconds to wait before proceeding.
- `scrollPage` (boolean) — Whether to scroll through the entire page before capturing content.
- `selector` (string) — A CSS selector to target a specific element instead of the full page.

### Responses

- **200**: Response can either be a text/plain base64 encoded body — Content: `image/png`, `image/jpeg`, `text/plain`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /search

URL: https://docs.browserless.io/open-api/search

Search the web and optionally scrape result pages. Accepts a query, performs a web search via SearXNG, and optionally scrapes each result URL — returning structured, LLM-ready data (markdown, HTML, links, or screenshots).

Request

### Query Parameters
- `timeout` (number) — The timeout for the search operation in milliseconds.
- `token` (string) — The API token for authenticating the request.
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.

### Request Body (`application/json`)
- `query` (string) **(required)** — The search query string.
- `limit` (number) — Maximum number of results per source. Defaults to `10`. Capped by plan limits.
- `lang` (string) — Language code for results (e.g., `"en"`, `"es"`, `"de"`). Defaults to `"en"`.
- `location` (string) — City or region to narrow geo-targeting (e.g., `"San Francisco"`). Use with `country`.
- `country` (string) — ISO 3166-1 alpha-2 country code for geo-targeted results (e.g., `"us"`, `"gb"`, `"de"`).
- `proxy` (`datacenter` | `residential`) — Proxy network to route result scraping through: `"residential"` (default) or `"datacenter"`.
- `tbs` (`day` | `month` | `qdr:d` | `qdr:m` | `qdr:w` | `qdr:y` | `week` | `year`) — Time-based filter. Accepts `"day"`, `"week"`, `"month"`, `"year"`, or raw Google TBS syntax (`"qdr:d"`, `"qdr:w"`, `"qdr:m"`, `"qdr:y"`).
- `categories` (`github` | `pdf` | `research`[]) — Content category filters. Restricts results to `"github"` repos, `"research"` papers, or `"pdf"` documents.
- `sources` (`images` | `news` | `web`[]) — Sources to search. Defaults to `["web"]`. Also supports `"news"` and `"images"`.
- `timeout` (number) — Request timeout in milliseconds.
- `scrapeOptions` (object) — When provided, fetches and processes each result URL into structured content.
  - `formats` (`html` | `links` | `markdown` | `screenshot`[]) **(required)**
  - `stripNonContentTags` (boolean)
  - `onlyMainContent` (boolean)
  - `removeBase64Images` (boolean)
  - `includeTags` (string[])
  - `excludeTags` (string[])

### Responses

- **200**:  — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /session/bql/*

URL: https://docs.browserless.io/open-api/session-bql

&gt; Executes BrowserQL queries against an existing session.

BrowserQL is a GraphQL-based query language for browser automation that allows you to interact with web pages using a declarative syntax.
Queries execute within the context of your existing session, maintaining browser state and session data across multiple operations.

Request

### Query Parameters
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `launch` (object | string) — Launch options for the browser, either as a JSON object or a JSON string.
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `replay` (boolean)
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.

### Request Body (`application/json`)
- `query` (string) **(required)** — The GraphQL query string to execute against the browser session.
- `variables` (object) — Variables to pass to the GraphQL query. Useful for dynamic values.
- `operationName` (string) — The name of the operation to execute if the query contains multiple operations.

### Responses

- **200**:  — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /session/connect/*

URL: https://docs.browserless.io/open-api/session-connect

Connect to an existing session with a library like puppeteer or playwright that work over chrome-devtools-protocol. See documentation for more details on how to start the session.

The browser type (Chrome/Chromium, stealth/regular) is determined from the session metadata when it was created initially.

Request

### Query Parameters
- `launch` (object | string)
- `replay` (boolean)
- `timeout` (number)
- `token` (string)
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.

### Responses

- **101**: Indicates successful WebSocket upgrade.
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /session

URL: https://docs.browserless.io/open-api/session-create

Creates a new browser session with the specified parameters. The session can be used for persistent browser connections
with well-defined lifetimes and reconnection semantics.

Request

### Request Body (`application/json`)
- `ttl` (number) **(required)** — The time-to-live (TTL) for the session in milliseconds. Once reached, will be forcefully terminated
- `processKeepAlive` (number) — An optional time, in milliseconds, to keep the underlying browser process alive after a connection to the session closes.
- `stealth` (boolean) — Whether or not to enable advanced stealth mode. Defaults to false.
- `blockAds` (boolean) — Whether or not to enable ad-blocking. Defaults to false.
- `blockAdsInclude` (string) — Restrict blockAds to a specific set of uBlock ruleset ids (comma-separated
- `headless` (boolean) — Whether the browser should be launched in headless mode.
- `args` (string[]) — An array of command-line arguments to pass to the browser.
- `browser` (`chrome` | `chromium` | `stealth`) — The type of browser to use for the session.
- `url` (string) — The underlying page URL you're attempting to automate. Some pages may
- `proxy` (object) — Proxy Parameters for the session if desired.
  - `type` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
  - `sticky` (boolean) — Whether or not to use the same IP address for all proxied requests. Defaults to true
  - `country` (string) — The country to proxy through. Defaults to 'us' or United States
  - `city` (string) — The city to proxy through.
  - `state` (string) — The state to proxy through.
  - `preset` (string) — Preset code for website-specific proxy configurations (e.g., 'px_gov01' for government sites)
- `replay` (boolean) — Whether to enable session recording for replay.
- `extensions` (string[]) — An array of extension IDs to load into the browser session.
- `profile` (string) — Optional name of an authentication profile to use as initial browser state.
- `integrationId` (string) — Optional 1Password integration id. When set, Browserless.loadSecret can
- `credentials` (object) — Maps caller aliases to 1Password op:// references, e.g.
- `allowedDomains` (string[]) — Optionally narrows the integration's allowed-domain list for this session.
- `label` (string) — An optional, caller-supplied label for the session. Stored on the session

### Responses

- **200**:  — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /session/*

URL: https://docs.browserless.io/open-api/session-delete

Deletes a session and its associated data. This will immediately terminate any active connections to the session if "force" is applied.

Query Parameters:
- force (optional): If true, forces deletion even if the session has active connections

Request

### Query Parameters
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `force` (boolean) — Whether to force the deletion of the session even if it has active connections.
- `launch` (object | object | string) — Launch options, which can be either an object
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier

### Responses

- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /skills

URL: https://docs.browserless.io/open-api/skills

Returns the browser-automation skills known for a site: tuned, site-specific
recipes an agent should follow instead of planning its own steps.
"domain" is required; "task" narrows the result to a single skill slug.
Returns an empty array when no skill is known for the domain.

Request

### Query Parameters
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `domain` (string) — Hostname to look skills up for, e.g. "airbnb.com". A scheme, port or
- `launch` (object | object | string) — Launch options, which can be either an object
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `task` (string) — Optional skill slug, e.g. "search-listings". Omit to get every skill
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.

### Responses

- **200**:  — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /smart-scrape

URL: https://docs.browserless.io/open-api/smart-scrape

Intelligently scrapes a URL using cascading strategies (HTTP fetch, proxy, headless browser, etc).

Request

### Query Parameters
- `emulationOs` (`android` | `linux` | `macos` | `windows`) — OS to emulate: `windows`/`macos`/`linux`/`android`. Omitted → native Linux
- `profile` (string) — Optional name of an authentication profile to hydrate into the browser
- `timeout` (number) — The timeout for the scrape operation in milliseconds
- `token` (string) — The API token for authenticating the request. Can also be provided in the `Authorization` header as a Bearer token. If both are provided, the `Authorization` header takes precedence. If no token is provided, the route will attempt to authenticate the request using any legacy authentication methods (e.g. cookie-based sessions) before rejecting the request as unauthorized.
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.

### Request Body (`application/json`)
- `url` (string) **(required)** — The URL to scrape. Must be an http or https URL.
- `formats` (`html` | `links` | `markdown` | `pdf` | `rawText` | `screenshot`[]) — Output formats to include in the response. Accepts an array of format
- `actions` (any[]) — Browser actions to run in order before the final page is captured. The
- `proxy` (`datacenter` | `residential`) — The proxy network to route the scrape through. Defaults to `residential`.
- `onlyMainContent` (boolean) — For HTML webpages, extract the main article content, removing navigation,
- `includeTags` (string[]) — CSS selectors to keep in HTML webpage outputs. Malformed entries are
- `excludeTags` (string[]) — CSS selectors to remove from HTML webpage outputs. Malformed entries are
- `headers` (object) — Custom request headers sent to the target. host, authorization,
- `waitFor` (number) — Milliseconds to wait after page load. Clamped to [0, 30000]; positive
- `strategyCache` (boolean) — Whether to use and update the remembered winning strategy for this host.

### Responses

- **200**: An HTML payload of the scraped page content. — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /stealth

URL: https://docs.browserless.io/open-api/stealth

Launch and connect to Stealthy Chromium with a library like puppeteer or others that work over chrome-devtools-protocol for scraping in a more stealth-like fashion.

Request

### Query Parameters
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `deviceScaleFactor` (number) — Device pixel ratio for the desktop screen above (allowed: 1 or 1.25).
- `deviceSlot` (number) — Pin this session to one of your identity's N stable device slots (0-based,
- `emulatedDevice` (string) — Device model for mobile personas (emulationOs=android), e.g. "pixel-8",
- `emulationOs` (`android` | `linux` | `macos` | `windows`) — Spoof OS for full per-platform stealth: windows | macos | linux | android.
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `integrations` (string)
- `launch` (object | string) — Launch options, which can be either an object
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.
- `record` (boolean)
- `replay` (boolean)
- `screen` (string) — Desktop screen resolution as WIDTHxHEIGHT (e.g. '1920x1080'). Sets the
- `solveCaptchas` (boolean)
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier

### Responses

- **101**: Indicates successful WebSocket upgrade.
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /stealth/bql?(/*)

URL: https://docs.browserless.io/open-api/stealth-bql-connect

WebSocket endpoint for BrowserQL. Connect to establish a persistent browser
session, then send multiple GraphQL operations as JSON messages. The browser
and page state persist across all messages for the connection duration.

Messages use the same JSON format as the HTTP POST endpoint:
Send: \{ "query": "mutation \{ goto(url: \"..\") \{ status \} \}", "variables": \{\} \}
Receive: \{ "data": \{ "goto": \{ "status": 200 \} \} \}

Request

### Query Parameters
- `acceptInsecureCerts` (boolean) — Whether to ignore TLS certificate errors (e.g. self-signed or untrusted
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `blockConsentModals` (boolean)
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `deviceScaleFactor` (number) — Device pixel ratio for the desktop screen above (allowed: 1 or 1.25).
- `deviceSlot` (number) — Pin this session to one of your identity's N stable device slots (0-based,
- `emulatedDevice` (string) — Device model for the mobile persona (emulationOs=android): a device slug such
- `emulationOs` (`android` | `linux` | `macos` | `windows`) — Spoof OS for the session: 'windows', 'macos', or 'linux'. When set, the
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `humanlike` (boolean)
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `launch` (object | string) — Launch options for the browser, either as a JSON object or a JSON string.
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.
- `replay` (boolean)
- `screen` (string) — Desktop screen resolution as WIDTHxHEIGHT (e.g. '1920x1080'). Sets the
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier

### Responses

- **101**: Indicates successful WebSocket upgrade.
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /stealth/bql?(/*)

URL: https://docs.browserless.io/open-api/stealth-bql-execute

Parses and executes BrowserQL requests, powered by the BrowserQL Editor or by other API integrations. See the BrowserQL Editor for more documentation on this API.

Request

### Query Parameters
- `acceptInsecureCerts` (boolean) — Whether to ignore TLS certificate errors (e.g. self-signed or untrusted
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `blockConsentModals` (boolean) — Whether to automatically block cookie consent modals and popups.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `deviceScaleFactor` (number) — Device pixel ratio for the desktop screen above (allowed: 1 or 1.25).
- `deviceSlot` (number) — Pin this session to one of your identity's N stable device slots (0-based,
- `emulatedDevice` (string) — Device model for the mobile persona (emulationOs=android): a device slug such
- `emulationOs` (`android` | `linux` | `macos` | `windows`) — Spoof OS for the session: 'windows', 'macos', or 'linux'. When set, the
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `humanlike` (boolean) — Whether to enable human-like behavior for interactions.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `launch` (object | string) — Launch options for the browser, either as a JSON object or a JSON string.
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.
- `replay` (boolean) — Whether to enable session recording for replay.
- `screen` (string) — Desktop screen resolution as WIDTHxHEIGHT (e.g. '1920x1080'). Sets the
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier

### Request Body (`application/json`)
- `query` (string) **(required)** — The BrowserQL query string to execute.
- `operationName` (string) — The name of the operation to execute if the query contains multiple operations.
- `variables` (object) — Variables to pass to the BrowserQL query.

### Responses

- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /unblock

URL: https://docs.browserless.io/open-api/unblock

Unblocks the provided URL from being blocked due to bot detection.
Returns a payload of Cookies, HTML, a base64 encoded screenshot,
and a "browserWSEndpoint" to allow connecting to the browser when
specified in the JSON Payload. Only supports CDP or Puppeteer
like libraries when connecting to the "browserWSEndpoint".

**Note:** Also available at `/chromium/unblock` as a convenience alias.

Request

### Query Parameters
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `emulationOs` (`android` | `linux` | `macos` | `windows`) — OS to emulate: `windows`/`macos`/`linux`/`android`. Omitted → native Linux
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `launch` (object | string) — Launch options for the browser, either as a JSON object or a JSON string.
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.
- `replay` (boolean) — Records the unblock session for replay (rrweb) and uploads on cleanup, reusing the
- `solveCaptchas` (boolean) — With `browserWSEndpoint`, persists captcha-solving on the kept-alive session so
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier

### Request Body (`application/json`)
- `bestAttempt` (boolean) — When true, browserless proceeds when "awaited" events fail or timeout
- `url` (string) **(required)** — The URL of the site you want to unblock.
- `browserWSEndpoint` (boolean) — Whether or not to keep the underlying browser alive and around for
- `cookies` (boolean) — Whether or not to to return cookies for the site, defaults to true.
- `content` (boolean) — Whether or not to to return content for the site, defaults to true.
- `screenshot` (boolean) — Whether or not to to return a full-page screenshot for the site, defaults to true.
- `ttl` (number) — How long to keep the browser alive for re-connection when browserWSEndpoint
- `gotoOptions` (object) — An optional goto parameter object for considering when the page is done loading.
  - `referer` (string) — If provided, it will take preference over the referer header value set by
  - `referrerPolicy` (string) — If provided, it will take preference over the referer-policy header value
  - `timeout` (number) — Maximum wait time in milliseconds. Pass 0 to disable the timeout.
  - `waitUntil` (`domcontentloaded` | `load` | `networkidle0` | `networkidle2`[] | `domcontentloaded` | `load` | `networkidle0` | `networkidle2`) — When to consider waiting succeeds. Given an array of event strings, waiting
  - `signal` (object) — A signal object that allows you to cancel the call.
- `waitForEvent` (object) — Options for waiting for a specific event to be fired on the page.
  - `event` (string) **(required)**
  - `timeout` (number)
- `waitForFunction` (object) — Options for waiting for a JavaScript function to execute.
  - `fn` (string) **(required)** — The function, or statement, to be evaluated in browser context
  - `polling` (string,number) — An interval at which the pageFunction is executed, defaults to raf.
  - `timeout` (number) — Maximum time to wait for in milliseconds. Defaults to 30000 (30 seconds).
- `waitForSelector` (object) — Options for waiting for a specific CSS selector to appear on the page.
  - `hidden` (boolean)
  - `selector` (string) **(required)**
  - `timeout` (number)
  - `visible` (boolean)
- `waitForTimeout` (number) — The amount of time in milliseconds to wait before proceeding.

### Responses

- **200**:  — Content: `application/json`
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# /webkit/playwright

URL: https://docs.browserless.io/open-api/webkit-playwright

Connect to Webkit with any playwright-compliant library.

**Note:** Also available at `/playwright/webkit` as a convenience alias.

Request

### Query Parameters
- `blockAds` (boolean) — Whether or not to load ad-blocking extensions for the session.
- `blockAdsInclude` (string) — Ruleset IDs to load when `blockAds` is enabled, as CSV or JSON-array text.
- `launch` (object | string) — Launch options, which can be either an object
- `profile` (string) — Name of an authenticated profile to hydrate into the browser at launch.
- `timeout` (number) — Override the system-level timeout for this request.
- `token` (string) — The authorization token
- `trackingId` (string) — Custom session identifier
- `allowedDomains` (string[]) — Optional per-connection narrowing of the integration's allowed-domain list.
- `credentials` (StringRecord) — Optional alias → `op://…` reference map for the integration above. Extracted
- `externalProxyServer` (string) — External proxy server URL for user-provided proxies.
- `integrationId` (string) — Optional 1Password integration id for credential filling on a non-persistent
- `proxy` (`datacenter` | `residential`) — The type of proxy to use: 'residential' or 'datacenter'.
- `proxyCity` (string) — The city to use for the proxy.
- `proxyCountry` (string) — A two-letter country code for the proxy configuration.
- `proxyLocaleMatch` (`0` | `1` | `false` | `true`) — Sets the browser's language to match the proxy's geographic location.
- `proxyPreset` (string) — A preset code for website-specific proxy routing.
- `proxyState` (string) — The state or province to use for the proxy, whitespace must be replaced with underscores
- `proxySticky` (`0` | `1` | `false` | `true`) — Whether or not to use the same IP for all requests. Off by default.

### Responses

- **101**: Indicates successful WebSocket upgrade.
- **400**: The request contains errors or didn't properly encode content.
- **401**: The request is missing, or contains bad, authorization credentials.
- **403**: The request targets a destination that is not allowed.
- **404**: Resource couldn't be found.
- **408**: The request took has taken too long to process.
- **429**: Too many requests are currently being processed.
- **500**: An internal error occurred when handling the request.
- **503**: Service is unavailable.

---

# AI Assistant Privacy

URL: https://docs.browserless.io/overview/ai-assistant-privacy

# AI assistant privacy

The in-app AI assistant (Browserless AI) is powered by [Kapa.ai](https://www.kapa.ai) and grounded in the Browserless documentation. This page explains how your conversations are handled.

## Security and certification

- **SOC 2 Type II** — Kapa.ai holds a current SOC 2 Type II certification. Details are available at [Kapa security](https://docs.kapa.ai/security/certifications).
- **Infrastructure** — Kapa runs on ISO 27001-certified cloud infrastructure with encryption at rest and in transit.

## LLM providers

The assistant uses large-language-model providers (Cohere, OpenAI, Voyage, Anthropic) to generate answers. All providers operate under data-processing agreements that **prohibit training on your conversations**.

## Data retention

Your questions and the assistant's answers are retained to operate and improve the service (e.g., answer-quality analytics). If you need your data deleted, contact [security@browserless.io](mailto:security@browserless.io).

## What you should not share

The assistant cannot detect or redact sensitive information on your behalf. **Do not paste:**

- API tokens or passwords
- Session cookies or auth headers
- Customer PII (emails, names, payment info)

Use placeholders (e.g., `YOUR_API_TOKEN`) when asking questions that involve credentials.

## Governing policies

Conversations with the AI assistant are governed by the [Browserless Privacy Policy](https://www.browserless.io/privacy). For data-handling questions not covered here, reach out to [security@browserless.io](mailto:security@browserless.io).

---

# API Token Management

URL: https://docs.browserless.io/overview/api-keys

# API Token Management

Your API token authenticates all requests to Browserless services. Manage your token from the [Browserless dashboard](https://browserless.io/account/).

## Accessing your API token

1. Log in to your account at [browserless.io/account](https://browserless.io/account/)
2. Your API token is displayed on the account home page under the **API Key** section
3. Click the **Copy** button next to your token to copy it to your clipboard

## Managing multiple API tokens

Create additional API tokens so you can use a separate token per application, environment, or team member and revoke any one of them without affecting the others.

1. On your account home page, find the **API Key** section and click **Manage API keys**
2. Review the list of your existing tokens

Only account owners and admins can manage tokens, and each account can hold up to 20 tokens.

### Create a token

1. Click **Create API key**
2. Enter a name for the token. Choose any label that helps you recognize where it's used.
3. Confirm to generate the token

If you've reached the 20-token limit, revoke a token you no longer use before creating a new one.

> **Warning**
> Your new token is shown **only once**, at creation time. Copy it and store it somewhere secure right away. If you lose it, you can't view it again and must create a new one.

### Revoke a token

1. Find the token you want to remove in the list
2. Click **Revoke**

Revoking a token invalidates it immediately, and any application using that token stops working. For a planned rotation, update those applications to use a valid token before you revoke. If a token is compromised, revoke it right away, then update your applications.

You can't revoke your last remaining token. Create a replacement first, then revoke the old one. Your account always keeps one token designated as the primary (the one shown on your account home page); if you revoke the primary, another active token is automatically promoted to take its place.

## Using your API token

Add your API token to the URL query string as `?token=YOUR_TOKEN`. Invalid tokens result in HTTP 401 or 403 responses. Keep the URL secure and avoid exposing it in client-side code or logs.

**Example:**

```
https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE
```

## Rotating your API token

How you rotate depends on what your dashboard shows.

### Accounts with multiple tokens

If your account home page shows a **Manage API keys** link, rotate without downtime: create a new token, update your applications to use it, then revoke the old one. See [Managing multiple API tokens](#managing-multiple-api-tokens).

### Accounts with a single token

If your account home page shows a single API key with a **Change API Key** button, use that button to rotate:

1. Navigate to [browserless.io/account](https://browserless.io/account/)
2. Click the **Change API Key** button
3. Review the confirmation dialog carefully
4. Click **Change API Key** to confirm

> **Warning**
> Changing your API token requires a few minutes of downtime, and you will need to update all your applications to use the new token.

When you rotate this way:

- Your current key is immediately invalidated
- A new randomly-generated key is created
- The new key is displayed on your account page
- All active connections using the old key will stop working
- You must update the token in every application or service that connects to Browserless

Plan your rotation during a maintenance window to minimize disruption to your services.

## Best Practices

### Store tokens securely

- **Use environment variables** to store your API token rather than hardcoding it in source files
- **Never commit API tokens to version control** (add `.env` files to `.gitignore`)
- Use a secrets manager (such as AWS Secrets Manager, HashiCorp Vault, or 1Password) for production deployments

#### Example: using environment variables

```bash
# .env file (never commit this)
BROWSERLESS_TOKEN=YOUR_API_TOKEN_HERE
```

```javascript
// Access the token from your environment
const token = process.env.BROWSERLESS_TOKEN;
const url = `https://production-sfo.browserless.io/scrape?token=${token}`;
```

```python

token = os.environ["BROWSERLESS_TOKEN"]
url = f"https://production-sfo.browserless.io/scrape?token={token}"
```

### Rotation frequency

- Rotate your API token immediately if you suspect it has been compromised
- Rotate periodically (every 90 days) as a security best practice
- Rotate when team members with access leave your organization
- Rotate after revoking access from any third-party integration

### Limit exposure

- Create a separate API key per environment or application so you can revoke one without disrupting the others
- Monitor your [usage dashboard](https://browserless.io/account/) for unexpected activity
- Avoid sharing your key across multiple unrelated projects

## FAQ & Troubleshooting

<details>
<summary>How do I get started with Browserless?</summary>

Sign up for a free account at [browserless.io](https://browserless.io/signup/email?plan=free), get your API token, and follow the [Quickstart guide](/browserql/getting-started) for BQL or the [BaaS guide](/baas/start) for Puppeteer/Playwright.

</details>

<details>
<summary>What's the difference between BQL and BaaS?</summary>

BQL is a GraphQL API for declarative browser automation with built-in stealth. BaaS runs your existing Puppeteer or Playwright scripts on managed browsers. See [comparison](/overview/comparison) for details.

</details>

## Next steps

  <a href="/overview/dashboard" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Dashboard</h3>
      view, copy, and rotate your API token
    
  </a>
  <a href="/overview/comparison" className="next-step-card-link">
    
      <h3 className="next-step-card-title">API Comparison</h3>
      learn about authentication across Browserless APIs
    
  </a>
  <a href="/browserql/getting-started" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BQL Quickstart</h3>
      use your token to run your first BrowserQL query
    
  </a>

---

# API Comparison

URL: https://docs.browserless.io/overview/comparison

# API Comparison

Compare [BAP](/bap), [BrowserQL](/browserql/start), [BaaS v2](/baas/start), and [REST APIs](/rest-apis/intro) to choose the right tool for your use case. All APIs use the same cloud infrastructure and require an [API token](/overview/api-keys).

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Authentication

All Browserless APIs require an API token. Include your token as a query parameter:

```bash
?token=YOUR_API_TOKEN_HERE
```

Get your API token from the [Browserless dashboard](https://browserless.io/account/).

## Feature Comparison

Compare BAP, BrowserQL, BaaS v2, and REST APIs:

| Feature | BAP | BrowserQL | BaaS v2 | REST APIs |
|---------|-----|-----------|---------|-----------|
| **Connection** | WebSocket | HTTPS or WebSocket | WebSocket | HTTPS |
| **Usage** | Typed SDK methods (`page.goto()`) | GraphQL mutations | Puppeteer/Playwright | HTTP requests |
| **Stealth** | Built-in stealth, CAPTCHA solving, and human-like behavior (same engine as BrowserQL) | Built-in stealth, CAPTCHA solving, and human-like behavior | Browser-level stealth, proxies, and launch flags | Basic launch flags and request controls |
| **Flexibility** | High (typed, declarative) | High (declarative language) | Highest (full browser control) | Limited (predefined endpoints) |
| **State Management** | Yes (reconnect) | Yes (reconnect mutation) | Yes (persistent sessions) | No (stateless) |
| **Best For** | Bot detection bypass from TypeScript or Python | Bot detection bypass from any language | Custom automation scripts | Simple one-off tasks |

## When to Use Each API

### Use a BAP SDK when you need to:
- Bypass sophisticated bot detection systems from TypeScript or Python
- Solve CAPTCHAs automatically
- Mimic human-like browser behavior
- Get autocomplete and typed responses instead of hand-writing GraphQL

### Use BrowserQL directly when you need to:
- Automate from a language without a BAP SDK (not TypeScript or Python)
- Generate BQL documents from another tool
- Work in the hosted IDE

### Use BaaS v2 when you need to:
- Run existing Puppeteer or Playwright code
- Keep full browser control for complex workflows
- Use custom launch options, proxies, and persistent browser sessions
- Build multi-step automation sequences in the library you already use

### Use REST APIs when you need to:
- Take screenshots or generate PDFs
- Extract content from simple pages
- Integrate browser functionality via HTTP
- Perform one-off tasks without maintaining state

## FAQ & Troubleshooting

<details>
<summary>How do I get started with Browserless?</summary>

Sign up for a free account at [browserless.io](https://browserless.io/signup/email?plan=free), get your API token, and follow the [BAP overview](/bap) for a typed TypeScript or Python SDK, the [BQL quickstart](/browserql/getting-started) for raw GraphQL, or the [BaaS guide](/baas/start) for Puppeteer/Playwright.

</details>

<details>
<summary>What's the difference between BAP, BQL, and BaaS?</summary>

BAP is a typed SDK, in TypeScript or Python, that wraps BrowserQL mutations in a Puppeteer- or Playwright-shaped API. BQL is the underlying GraphQL API for declarative browser automation with built-in stealth, which BAP is built on. BaaS runs your existing Puppeteer or Playwright scripts on managed browsers, independent of BQL. See [comparison](/overview/comparison) for details.

</details>

## Next steps

  <a href="/bap" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BAP Overview</h3>
      pick the TypeScript or Python SDK
    
  </a>
  <a href="/browserql/getting-started" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BQL Quickstart</h3>
      run your first BrowserQL query
    
  </a>
  <a href="/baas/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BaaS Quickstart</h3>
      connect Puppeteer or Playwright to Browserless
    
  </a>

---

# Connection URLs and Endpoints

URL: https://docs.browserless.io/overview/connection-urls

# Connection URLs and Endpoints

Browserless uses HTTPS for [BrowserQL](/browserql/start) and [REST APIs](/rest-apis/intro), and WebSocket connections for [BaaS](/baas/start) sessions. Pick the host that matches your fleet, then choose the path that matches your browser or API.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Shared Fleet vs. Private Fleet URLs

Browserless operates two types of fleets. Use the URLs that match your account type.

### Shared fleet

The shared fleet uses regional URLs shared across all customers on that plan.

| Region | URL |
|---|---|
| US West | `https://production-sfo.browserless.io` |
| Europe UK | `https://production-lon.browserless.io` |
| Europe Amsterdam | `https://production-ams.browserless.io` |

For WebSocket connections, replace `https://` with `wss://`.

### Private fleet (Enterprise private deployments)

Enterprise customers with a dedicated fleet connect through `chrome.browserless.io`, which routes to your assigned region. Regional variants are available for lower latency.

| Region | URL |
|---|---|
| Default (auto-routed) | `https://chrome.browserless.io` |
| US West (SFO) | `https://chrome-us-west.browserless.io` |
| US East (NYC) | `https://chrome-us-east.browserless.io` |
| Europe UK (LON) | `https://chrome-eu-uk.browserless.io` |
| Europe Amsterdam (AMS) | `https://chrome-eu-ams.browserless.io` |

For WebSocket connections, replace `https://` with `wss://`.

> **Warning**
> Using a private fleet token against a shared fleet URL returns HTTP 401 Unauthorized. Always use your dedicated fleet URL with your private fleet token.

See [Load Balancing](/enterprise/private-deployment/load-balancing) for more on private fleet regional endpoints.

## BrowserQL Endpoints

BrowserQL uses HTTPS POST requests. Choose the endpoint that matches the browser you want.

| Purpose | Shared fleet URL | Notes |
|---|---|---|
| Chromium | `https://production-sfo.browserless.io/chromium/bql?token=YOUR_TOKEN` | Standard BrowserQL queries |
| Chrome | `https://production-sfo.browserless.io/chrome/bql?token=YOUR_TOKEN` | Chrome-based BrowserQL sessions |
| Stealth | `https://production-sfo.browserless.io/stealth/bql?token=YOUR_TOKEN` | Stealth-enabled BrowserQL sessions |

The same path patterns apply on dedicated fleets. Replace the shared-fleet host with `chrome.browserless.io` or your regional dedicated URL.

If you use the BQL IDE, Browserless handles the endpoint for you. If you reconnect to an existing BQL session, use the `browserQLEndpoint` returned by the `reconnect` mutation rather than building the reconnect URL by hand.

## BaaS v2 Endpoints

To connect Puppeteer or Playwright to Browserless, use a WebSocket URL. These endpoints launch a browser and hand control back to your library.

| Library | Example URL |
|---|---|
| Puppeteer / Chromium | `wss://production-sfo.browserless.io?token=YOUR_TOKEN` |
| Playwright Chromium over CDP | `wss://production-sfo.browserless.io?token=YOUR_TOKEN` |
| Playwright native Chromium | `wss://production-sfo.browserless.io/chromium/playwright?token=YOUR_TOKEN` |
| Playwright Chrome | `wss://production-sfo.browserless.io/chrome?token=YOUR_TOKEN` |
| Playwright Firefox | `wss://production-sfo.browserless.io/firefox/playwright?token=YOUR_TOKEN` |
| Playwright WebKit | `wss://production-sfo.browserless.io/webkit/playwright?token=YOUR_TOKEN` |

Use the nearest region to reduce latency. See [Load Balancers](/baas/infrastructure/load-balancers). All endpoints require the `?token=` query parameter.

## REST API Endpoints

Browserless provides REST API endpoints for common operations.

```bash
https://production-sfo.browserless.io/screenshot?token=YOUR_TOKEN
https://production-sfo.browserless.io/pdf?token=YOUR_TOKEN
https://production-sfo.browserless.io/content?token=YOUR_TOKEN
```

## Launch Parameters and Options

See [Launch Parameters](/overview/launch-parameters) for query parameters, the encoded JSON `launch` payload, and common options.

## FAQ & Troubleshooting

<details>
<summary>How do I get started with Browserless?</summary>

Sign up for a free account at [browserless.io](https://browserless.io/signup/email?plan=free), get your API token, and follow the [Quickstart guide](/browserql/getting-started) for BQL or the [BaaS guide](/baas/start) for Puppeteer/Playwright.

</details>

<details>
<summary>What's the difference between BQL and BaaS?</summary>

BQL is a GraphQL API for declarative browser automation with built-in stealth. BaaS runs your existing Puppeteer or Playwright scripts on managed browsers. See [comparison](/overview/comparison) for details.

</details>

## Next steps

  <a href="/browserql/getting-started" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BQL Quickstart</h3>
      run your first BrowserQL query
    
  </a>
  <a href="/baas/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BaaS Quickstart</h3>
      connect Puppeteer or Playwright to Browserless
    
  </a>

---

# Dashboard overview

URL: https://docs.browserless.io/overview/dashboard

# Dashboard overview

The Browserless dashboard at [browserless.io/account](https://browserless.io/account/) is where you manage API tokens, track unit usage, invite teammates, and test code against live browsers. This page walks through every page in the sidebar on Cloud Unit plans so you know where to find each tool.

- A Browserless account on a Cloud Unit plan. [Sign up for free](https://browserless.io/signup/email?plan=free) if you don't have one.

## Navigating the dashboard

The sidebar groups pages into three sections:

| Section | Pages |
| --- | --- |
| Account | Home, Dashboard, API Keys, Users, Email Alerts, Billing |
| Playgrounds | BrowserQL Editor, BaaS Debugger, REST API Playground |
| Tools | Logs, Code Generator, Session Replay, MCP, Profiles, Active Browsers, Extensions |

The bottom of the sidebar shows a **Units** meter with the units you've used against your plan's allowance, plus a **Manage Plan** button for changing plans. The button switches to **Upgrade Plan** when you're close to or out of units. Collapse the sidebar with the arrow at the top left to get more room for the playgrounds.

## Account

### Home

Home is the landing page after you log in. It's built to get you from signup to a first successful request quickly.

- **AI assistant prompt**: type what you want to build in the "What would you like to build or do?" field to ask the in-app assistant. See [AI assistant privacy](/overview/ai-assistant-privacy) for how conversations are handled.
- **API token card**: shows your primary API token (masked until you click the eye icon) with a copy button. Click **Manage API keys** to open the [API Keys](#api-keys) page.
- **Set up using an AI agent**: click **Copy prompt** to copy a setup prompt, including your live token, that you can paste into Claude, Codex, or Cursor. Click **Preview** to read the prompt first. The prompt contains your live token, so only paste it into a client you trust and keep it out of shared chats, logs, and repositories.
- **Set up yourself**: expand this box and pick whether you're starting or scaling a project, what you're using Browserless for, and which features you need. The dashboard then recommends REST APIs, BrowserQL, or BaaS, with links to the matching quickstart and a code snippet.

### Dashboard

The Dashboard page charts your account's activity so you can spot failures, rejections, and unit spend over time. Data refreshes every five minutes, or immediately when you click the refresh button.

**Usage chart.** The chart plots whichever metric you've selected below it. Pick a window from the dropdown: **Last Hour** (60 minutes), **Last Day** (24 hours), or **Last Week** (7 days). If your account has more than one API token, a token picker appears next to it so you can view usage for **All API keys** or a single token. Revoked tokens stay in the list so you can still audit their history.

**Status cards.** Click a card to chart that metric:

| Card | What it counts |
| --- | --- |
| Successful requests | Requests that completed normally |
| Timed out requests | Requests that hit their `timeout` before finishing |
| Errored requests | Requests that failed with an error |
| Rejected requests | Requests refused because you were at your concurrency limit, or because a free plan ran out of units |
| Proxy units | Units spent on built-in proxy bandwidth |
| Captcha units | Units spent on successful CAPTCHA solves |
| Time units | Units spent on browser session time |
| Peak concurrency | The most sessions you ran at the same time |

A spike in **Rejected requests** usually means you need to queue work on your side or move to a plan with higher concurrency. See [Unit consumption](/overview/unit-consumption) for how each unit type is metered.

**Cloud Subscription card.** Shows units used against your allowance for the current billing cycle, as a number and a percentage, plus your concurrency limit and the date the cycle resets. On paid plans it also shows your per-unit overage fee and your overage spend limit, with a **Manage spend limit** link to [Billing](#billing). Free plans have no overages, so requests are rejected once your units run out.

**Export data.** Click the download icon above the chart to open **Export Metrics**. Choose a **Time Range** (Last 7 Days, Last Month, or Last Year) and a **Format** (CSV or JSON), then click **Export** to download the file.

### API keys

API Keys lists every active token on your account with its name and creation date. From here you can:

- **Create** a named token per application, environment, or teammate. The full token is shown only once, so copy it right away.
- **Copy** or reveal a token.
- **Revoke** a token you no longer need. Revoking takes effect immediately.

Only owners and admins can create or revoke tokens, you can hold up to 20 tokens, and you can't revoke your last remaining one. See [API token management](/overview/api-keys) for rotation steps and best practices.

### Users

Users lets you share your account with teammates so they log in with their own email instead of sharing credentials. The table shows each user's **Email**, **Role**, and **Status** (**Invited** or **Active**).

- Click **Add User To Account**, enter their email, and pick a role:
  - **Admin**: can manage users, API tokens, and alert emails
  - **Viewer**: can see the dashboard but can't change account settings
- Use **Copy invite link** to resend an invitation manually, or the edit and delete actions to change a user's role or remove them.

### Email alerts

Email Alerts controls who gets account notifications, such as low unit balances and hitting queueing or concurrency limits. The account owner always receives alerts and can't be removed.

- Click **Add Email** to send alerts to another address. It doesn't need to belong to a dashboard user.
- Click **Remove Email** to stop alerts to an address. Removing a teammate here only stops their alerts; they keep dashboard and API access.

### Billing

Billing (at [Account Settings](https://www.browserless.io/account/settings)) holds your plan, payment, and account details:

- **Subscription Plan**: your account status, account type, and plan.
- **Overage spend limit** (paid plans): set a **Max overage per billing period ($)**. Once you reach it, new sessions pause while running ones finish, and the limit resets each cycle. Use this to cap surprise bills from a runaway script.
- **Usage notifications**: add percentages of your allowance (for example `75`, or `150` to be warned during overage) that trigger a notification email.
- **Billing Details**: billing cycle, credit, next invoice, usage this period, and your overage fee.
- **Payment Method**: update the card on file.
- **Invoices & Receipts**: date, amount, and a receipt link for every charge.
- **Account Management**: update your account email, or cancel and delete the account.

#### Closing your account

Click **Cancel Account** under **Account Management**. This cancels your subscription, and the account shuts down at the end of the current billing period. You can undo it any time before then with **Undo Cancellation**. Once cancelled, a **Delete Account** button appears, which permanently deletes the account. If your account is managed by a partner, contact them to cancel instead.

## Playgrounds

### BrowserQL editor

An in-browser IDE for writing and running [BrowserQL](/browserql/start) queries against real browsers.

- **Autocomplete**: GraphQL editor with inline suggestions for every mutation and argument
- **Live execution**: run a query and watch the browser as it executes
- **Save and share**: store queries and share them with your team

### BaaS debugger

A live code editor and runner for [Browsers as a Service (BaaS)](/baas/start) scripts with a real-time browser preview. Toggle between **Puppeteer** and [BAP](/bap) at the top of the editor. Use it to prototype a script before moving it into your own codebase or the `/function` API.

- **Code editor**: write scripts with syntax highlighting, with starter tabs for scraping, PDFs, and screenshots
- **Live preview**: watch the browser execute your code in real time
- **Export**: download your script as a complete Node.js project

> **Run without installing libraries**
> If you'd prefer not to install Puppeteer locally, send your debugger code directly to the [/function API](/rest-apis/function) as an HTTP request. This lets you run your code through clients such as n8n, Make, Zapier, or any conventional HTTP tool.

### REST API playground

An interactive tester for [REST API](/rest-apis/intro) endpoints, generated from the OpenAPI spec. See the [API Playground guide](/rest-apis/api-playground) for a full walkthrough.

- **Endpoint picker**: choose from endpoints such as smart-scrape, function, download, export, search, map, and crawl
- **Request configuration**: set the browser (Chromium, Chrome, or Edge), region, and API token
- **Live execution**: send the request and view the formatted response

## Tools

### Logs

A searchable feed of per-request logs for your account. See the [Request Logs guide](/overview/request-logs) for a full walkthrough.

- **Request lifecycle events**: completions, failures, navigations, and BrowserQL errors with levels and outcomes
- **Filters**: narrow by time range, request ID, target URL, endpoint, events, levels, or API token
- **Usage details**: per-request duration and unit consumption, broken down across time, proxy, captcha, and agent units

### Code generator

An AI code generator that writes browser automation scripts from a plain-language description. It's rolling out gradually, so you might not see it in your sidebar yet.

- **Natural language input**: describe what you want to automate
- **Format selection**: generate BrowserQL, JavaScript (Playwright), or Python (Playwright)
- **Live browser preview**: watch the AI drive a real browser while it builds the script

### Session replay

Recorded browser sessions you can replay and debug as interactive DOM replays.

- **Search and filter**: find sessions by ID or website, filtered by date range
- **Playback controls**: replay sessions on a timeline in a dedicated player
- **Download options**: export as a standalone HTML player or raw rrweb JSON

### MCP

Opens the [Browserless MCP Server](/mcp/browserless-mcp-server/setup) docs in a new tab, with setup steps for connecting Claude, Cursor, and other AI clients to Browserless.

### Profiles

Saved browser state (cookies and authentication data) you can attach to any automation run so it starts already logged in.

- **Interactive creation**: launch a live browser, log in to a site, and save the session as a profile
- **Profile management**: create, rename, and delete profiles across your account
- **Attach to runs**: use a saved profile to skip repeated login steps

### Active browsers

Sessions running on your account right now.

- **Session list**: browser type, start time, and timeout for each session
- **Terminate sessions**: stop a running session after a confirmation prompt, useful for killing a stuck script that's still consuming units
- **Status indicators**: session status and type badges at a glance

### Extensions

Chrome extensions you can load into your Browserless sessions.

- **Upload extensions**: upload a ZIP of a Chrome extension (Manifest v2 or v3) with a name and description
- **Extension list**: every uploaded extension with its name, description, status, and creation date
- **Launch integration**: enable an extension in a session by adding it to your launch options. See [Browser extensions](/baas/features/browser-extensions).

## FAQ & Troubleshooting

<details>
<summary>Why don't I see some pages in my sidebar?</summary>

**Code Generator** is rolling out gradually, so it isn't in every account's sidebar yet. Other pages, such as Persistent Sessions and Clusters, only exist on dedicated plans and aren't covered here.

</details>

<details>
<summary>Why can't I create API tokens, invite users, or add alert emails?</summary>

Your role is **Viewer**. Only the account owner and admins can make those changes. Ask your account owner to change your role on the [Users](#users) page.

</details>

<details>
<summary>My dashboard numbers don't match what I expected. Why?</summary>

Check the time window and API token picker above the chart. The chart defaults to **Last Hour** and can be scoped to a single token. For the full billing-cycle total, use the **Cloud Subscription** card instead.

</details>

<details>
<summary>Why are my requests being rejected?</summary>

You've hit your plan's concurrency limit, or your free plan has run out of units. Check **Peak concurrency** and the **Cloud Subscription** card on the Dashboard page, then close idle sessions from [Active Browsers](#active-browsers) or upgrade from the sidebar's **Upgrade Plan** button.

</details>

## Next steps

  <a href="/overview/api-keys" className="next-step-card-link">
    
      <h3 className="next-step-card-title">API tokens</h3>
      create, rotate, and revoke the tokens that authenticate your requests
    
  </a>
  <a href="/overview/unit-consumption" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Unit consumption</h3>
      understand what the dashboard's unit counts are measuring
    
  </a>
  <a href="/browserql/getting-started" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BQL quickstart</h3>
      run your first BrowserQL query
    
  </a>

---

# Bypass CAPTCHAs on a website

URL: https://docs.browserless.io/overview/getting-started/bypass-captchas

Two things get you past a CAPTCHA: not triggering one in the first place, and solving the ones that appear anyway. This page covers both, starting with a one-line change to your connection URL. It picks up from a working connection, so start with [Connect Puppeteer](/overview/getting-started/connect-puppeteer) or [Connect Playwright](/overview/getting-started/connect-playwright) if you don't have one yet. Writing new automation rather than adapting a script? Skip to [the BAP version](#the-shorter-version-with-bap), which does the same job in one method call.

## Steps

  Prevention beats solving. The `/stealth` route applies fingerprint mitigations and realistic browser entropy, so many sites never challenge the session at all. Solving a CAPTCHA costs units and takes seconds; never seeing one costs nothing.

```bash
wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE
```

    That's the whole change: add `/stealth` to the path you already connect to. There are also `/chromium/stealth` and `/chrome/stealth` variants covered in [stealth routes](/baas/bot-detection/stealth).

    :::note
    Stealth routes alter browser behavior, which occasionally surprises automation that depends on stock Chrome internals. Reach for them when you're being blocked, not as a default connection.
    :::

  
    Add `solveCaptchas=true` to the same URL. Browserless then watches the whole session and solves challenges as they appear, including ones that load after a later navigation or inside a popup. It handles the common types, reCAPTCHA and Cloudflare Turnstile among them.

```bash
wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE&solveCaptchas=true&timeout=300000
```

    Raise `timeout` while you're at it. Solving takes seconds to minutes, and the default session timeout can expire mid-solve.

    The solved token is injected into the page for you, so most scripts need nothing else. Wait on the `Browserless.captchaAutoSolved` CDP event only where the next action depends on the solve, such as clicking Submit:

    
      
**Puppeteer:**

```js
import puppeteer from "puppeteer-core";

const TOKEN = "YOUR_API_TOKEN_HERE";

const browser = await puppeteer.connect({
  browserWSEndpoint: `wss://production-sfo.browserless.io/stealth?token=${TOKEN}&solveCaptchas=true&timeout=300000`,
});

try {
  const page = await browser.newPage();
  const cdp = await page.createCDPSession();

  // Attach before navigating. The event can fire during goto().
  const captchaSolved = new Promise((resolve) => {
    cdp.on("Browserless.captchaAutoSolved", resolve);
  });

  await page.goto("https://www.google.com/recaptcha/api2/demo", {
    waitUntil: "networkidle2",
  });

  // Race a timeout, or a page with no CAPTCHA waits forever.
  const { solved, time } = await Promise.race([
    captchaSolved,
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error("CAPTCHA timeout")), 30000)
    ),
  ]);
  console.log({ solved, time });

  await page.click("#recaptcha-demo-submit");
} finally {
  await browser.close();
}
```
      
      
**Playwright:**

```js
import { chromium } from "playwright-core";

const TOKEN = "YOUR_API_TOKEN_HERE";

const browser = await chromium.connectOverCDP(
  `wss://production-sfo.browserless.io/stealth?token=${TOKEN}&solveCaptchas=true&timeout=300000`
);

try {
  // Reuse the default context and page, or Browserless CDP events won't reach you.
  const context = browser.contexts()[0];
  const page = context.pages()[0];
  const cdp = await context.newCDPSession(page);

  // Attach before navigating. The event can fire during goto().
  const captchaSolved = new Promise((resolve) => {
    cdp.on("Browserless.captchaAutoSolved", resolve);
  });

  await page.goto("https://www.google.com/recaptcha/api2/demo", {
    waitUntil: "networkidle",
  });

  // Race a timeout, or a page with no CAPTCHA waits forever.
  const { solved, time } = await Promise.race([
    captchaSolved,
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error("CAPTCHA timeout")), 30000)
    ),
  ]);
  console.log({ solved, time });

  await page.click("#recaptcha-demo-submit");
} finally {
  await browser.close();
}
```
      
    

    **Output**

```text
{ solved: true, time: 21482 }
```

    :::tip
    Both examples race the event against a `setTimeout` for a reason: on a page that turns out to have no CAPTCHA, nothing ever fires and a bare `await` hangs for the life of the session.
    :::
  

  Datacenter IPs are a strong bot signal on their own, which is why some sites, Cloudflare-fronted ones in particular, keep challenging a stealth session. Routing through residential IPs removes that signal:

```bash
wss://production-sfo.browserless.io/stealth?token=YOUR_API_TOKEN_HERE&solveCaptchas=true&proxy=residential&proxyCountry=us&timeout=300000
```

    Match `proxyCountry` to the audience the site expects. A US retailer served through a European exit node draws exactly the scrutiny you're trying to avoid. Residential traffic is billed by bandwidth, so turn it on for the sites that need it rather than globally. See [proxies](/baas/bot-detection/proxies) for the full parameter list.

## The shorter version with BAP

Everything above is CDP plumbing: attach a listener, race a timeout, read an event. BAP folds that into one method. `solve()` waits for a challenge, auto-detects its type, solves it, and returns what happened, and the session carries on from the page that was blocked.

  
**TypeScript:**

```typescript
import Browserless from "@browserless.io/bap-ts";

const TOKEN = "YOUR_API_TOKEN_HERE";

// The stealth endpoint, so fewer challenges appear in the first place.
const browser = Browserless.connect({
  browserWSEndpoint: "wss://production-sfo.browserless.io/stealth/bql",
  token: TOKEN,
});

try {
  const page = await browser.newPage();

  // Residential exit IP, matched to the audience the site expects.
  await page.proxy({ country: "US", sticky: true, type: ["document"] });

  await page.goto("https://www.google.com/recaptcha/api2/demo");

  // timeout bounds the wait for a CAPTCHA to appear, not the solve itself.
  const result = await page.solve({ timeout: 30_000 });

  if (result.solved) {
    await page.click("#recaptcha-demo-submit");
  } else if (result.found === false) {
    console.log("no captcha appeared");
  } else {
    console.error("solve failed:", result.error);
  }
} finally {
  await browser.close();
}
```
  
  
**Python:**

```python
import bap.sync_api as bap

TOKEN = "YOUR_API_TOKEN_HERE"

# The stealth endpoint, so fewer challenges appear in the first place.
with bap.Browserless.connect(
    browser_ws_endpoint="wss://production-sfo.browserless.io/stealth/bql",
    token=TOKEN,
) as browser:
    with browser.page() as page:
        # Residential exit IP, matched to the audience the site expects.
        page.proxy(country="US", sticky=True, type=["document"])

        page.goto("https://www.google.com/recaptcha/api2/demo")

        # timeout bounds the wait for a CAPTCHA to appear, not the solve itself.
        result = page.solve(timeout=30000)

        if result["solved"]:
            page.click("#recaptcha-demo-submit")
        elif result["found"] is False:
            print("no captcha appeared")
        else:
            print("solve failed:", result["error"])
```
  

Note that `solve()` doesn't throw when a challenge can't be cleared. It reports through the result, so branch on `found` and `solved` rather than wrapping it in a try/catch and assuming success. The [BAP CAPTCHA guide](/bap/bot-detection/captchas) covers image CAPTCHAs and the full result shape.

## What this costs

Each successful solve costs 10 units. Attempts that don't clear the challenge aren't charged. Residential proxy traffic is billed separately by bandwidth. That pricing is the reason the order above matters: stealth first, solving second, proxies only where they earn their place.

## When automatic solving isn't enough

Some flows need a person. Live URLs let you hand a running session to a human who finishes the challenge in a browser window while your script waits, then continues. See [hybrid automation](/baas/monitor-sessions/hybrid-automation).

For control over exactly when a solve happens, skip `solveCaptchas=true` and drive it yourself with the `Browserless.captchaFound` event and the `Browserless.solveCaptcha` command. The [CAPTCHA solving reference](/baas/bot-detection/captchas) documents both modes, the full event payloads, and the response fields.

## FAQ & Troubleshooting

<details>
<summary>The script hangs waiting for <code>Browserless.captchaAutoSolved</code>.</summary>

There was probably no CAPTCHA to solve, which means no event fires. Race the wait against a timeout, and only await the event where the next step genuinely depends on a solve.

</details>

<details>
<summary>I'm using Playwright and no CDP events arrive.</summary>

You're on a fresh context. `connectOverCDP` gives you a browser that already has a default context open, and Browserless events are bound to it. Use `browser.contexts()[0]` and `context.pages()[0]` rather than `browser.newPage()`.

</details>

<details>
<summary>The CAPTCHA solves, but the site still blocks me.</summary>

Solving clears one challenge, not the underlying detection. Add a residential proxy, and make sure the session isn't leaking other signals such as a mismatched timezone or locale. [Stealth routes](/baas/bot-detection/stealth) and [launch parameters](/overview/launch-parameters) cover the knobs worth setting.

</details>

<details>
<summary>Do the REST APIs solve CAPTCHAs too?</summary>

[`/smart-scrape`](/rest-apis/smart-scrape) solves challenges that block a page from loading, and [`/unblock`](/rest-apis/unblock) is built for bot-detection bypass. Neither fills in or submits forms, so a CAPTCHA sitting next to a Submit button needs a browser session or [BrowserQL](/browserql/bot-detection/solving-captchas).

</details>

## Next steps

  <a href="/baas/bot-detection/captchas" className="next-step-card-link">
    
      <h3 className="next-step-card-title">CAPTCHA solving reference</h3>
      Manual solving, event payloads, and response fields
    
  </a>
  <a href="/baas/bot-detection/stealth" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Stealth routes</h3>
      Every route, and what each one changes
    
  </a>
  <a href="/bap/bot-detection/captchas" className="next-step-card-link">
    
      <h3 className="next-step-card-title">CAPTCHAs with BAP</h3>
      solve(), image CAPTCHAs, and the result shape
    
  </a>

---

# Connect Playwright

URL: https://docs.browserless.io/overview/getting-started/connect-playwright

Run your Playwright scripts against managed browsers by replacing `chromium.launch()` with `chromium.connectOverCDP()`. Locators, assertions, and waits work exactly as they do locally.

This page is for code you already have. If you're writing automation from scratch, [the BAP SDK](#the-bap-alternative) is usually the better starting point.

## Steps

  [Sign up for a free account](https://www.browserless.io/signup/email?plan=free), then copy your API token from the [account dashboard](https://browserless.io/account/). Every connection carries it as a `?token=` query parameter.

  
    
      
**JavaScript:**

```bash
npm install playwright-core
```

        `playwright-core` skips the browser downloads that `playwright` performs on install. You don't need local binaries when the browser runs on Browserless.
      
      
**Python:**

```bash
pip install playwright
```

        Skip `playwright install`. That command downloads local browser binaries, which a remote connection never uses.
      
    
  

  
    
      
**JavaScript:**

```js
import { chromium } from "playwright-core";

const TOKEN = "YOUR_API_TOKEN_HERE";

// Before: a browser process on this machine.
// const browser = await chromium.launch();

// After: a managed browser reached over a CDP WebSocket.
const browser = await chromium.connectOverCDP(
  `wss://production-sfo.browserless.io?token=${TOKEN}`
);
```
      
      
**Python:**

```python
from playwright.sync_api import sync_playwright

TOKEN = "YOUR_API_TOKEN_HERE"

with sync_playwright() as p:
    # Before: a browser process on this machine.
    # browser = p.chromium.launch()

    # After: a managed browser reached over a CDP WebSocket.
    browser = p.chromium.connect_over_cdp(
        f"wss://production-sfo.browserless.io?token={TOKEN}"
    )
```
      
    

    Use `connectOverCDP`, not `connect`. The default Browserless endpoint speaks the Chrome DevTools Protocol, which is what `connectOverCDP` expects. `connect` speaks Playwright's own server protocol and needs a different path, covered in [Playwright native protocol](#playwright-native-protocol) below.
  

  
    
      
**JavaScript:**

```js
import { chromium } from "playwright-core";

const TOKEN = "YOUR_API_TOKEN_HERE";

const browser = await chromium.connectOverCDP(
  `wss://production-sfo.browserless.io?token=${TOKEN}`
);

try {
  const page = await browser.newPage();
  await page.goto("https://example.com");

  const title = await page.title();
  const heading = await page.textContent("h1");

  console.log({ title, heading });
} finally {
  // Release the session even if something above threw.
  await browser.close();
}
```

        **Output**

```text
{ title: 'Example Domain', heading: 'Example Domain' }
```
      
      
**Python:**

```python
from playwright.sync_api import sync_playwright

TOKEN = "YOUR_API_TOKEN_HERE"

with sync_playwright() as p:
    browser = p.chromium.connect_over_cdp(
        f"wss://production-sfo.browserless.io?token={TOKEN}"
    )

    try:
        page = browser.new_page()
        page.goto("https://example.com")

        print(page.title())
        print(page.text_content("h1"))
    finally:
        # Release the session even if something above raised.
        browser.close()
```

        **Output**

```text
Example Domain
Example Domain
```
      
    
  

Java, C#, and Go follow the same pattern. The [Browsers as a Service quickstart](/baas/quick-start) has a runnable example in each.

## Configuring the browser

Options you used to pass to `launch()` move onto the connection URL, since the browser starts before your code connects to it.

```js
const browser = await chromium.connectOverCDP(
  `wss://production-sfo.browserless.io?token=${TOKEN}&blockAds=true&timeout=60000`
);
```

See [launch parameters](/overview/launch-parameters) for the full list, including proxies, stealth, and Chrome flags.

## Playwright native protocol

`connectOverCDP` covers most scripts, but a few Playwright features need Playwright's own protocol rather than CDP: `page.route()` network interception, `APIRequestContext`, and any browser other than Chromium. For those, switch to `connect()` against a `/playwright` path:

```js

const browser = await firefox.connect(
  `wss://production-sfo.browserless.io/firefox/playwright?token=${TOKEN}`
);
```

`/chromium/playwright`, `/chrome/playwright`, `/firefox/playwright`, `/webkit/playwright`, and `/edge/playwright` are all available. The tradeoff is version coupling: native mode pins you to the Playwright version the endpoint runs, while CDP mode is far more tolerant of client version drift. See [connect vs connectOverCDP](/baas/advanced-configurations/playwright-customizations#connect-vs-connectovercdp) for the feature-by-feature breakdown.

## The BAP alternative

BAP is our own SDK. Its Python API borrows Playwright's method names, and its TypeScript API borrows Puppeteer's, so the script above ports with small changes:

  
**TypeScript:**

```typescript
import Browserless from "@browserless.io/bap-ts";

const TOKEN = "YOUR_API_TOKEN_HERE";

// connect() opens no socket. The WebSocket opens on newPage().
const browser = Browserless.connect({
  browserWSEndpoint: "wss://production-sfo.browserless.io/chromium/bql",
  token: TOKEN,
});

try {
  const page = await browser.newPage();
  await page.goto("https://example.com");

  const title = await page.title();
  const { text } = await page.text({ selector: "h1" });

  console.log({ title, heading: text });
} finally {
  await browser.close();
}
```

    **Output**

```text
{ title: 'Example Domain', heading: 'Example Domain' }
```
  
  
**Python:**

```python
import bap.sync_api as bap

TOKEN = "YOUR_API_TOKEN_HERE"

# The context manager closes the page and the browser when the block exits.
with bap.Browserless.connect(
    browser_ws_endpoint="wss://production-sfo.browserless.io/chromium/bql",
    token=TOKEN,
) as browser:
    with browser.page() as page:
        page.goto("https://example.com")

        print(page.title())
        print(page.text(selector="h1")["text"])
```

    **Output**

```text
Example Domain
Example Domain
```
  

It isn't a drop-in replacement. BAP runs over BrowserQL rather than a live CDP session, so frames, browser contexts, and direct input control have no equivalent, and `page.route()` interception isn't part of it either. What you get in exchange is `solve()`, `proxy()`, `markdown()`, and `liveURL()` as methods instead of plumbing.

Keep this page's `connectOverCDP()` approach when your script needs those APIs, or when you're running Playwright Test suites. See [Migrate to BAP](/bap/migrate) for the full comparison.

## FAQ & Troubleshooting

<details>
<summary>Why does <code>connect()</code> fail against the default endpoint?</summary>

`connect()` expects Playwright's server protocol, and the default endpoint serves CDP. Either use `connectOverCDP()` on the default endpoint, or keep `connect()` and point it at a `/playwright` path such as `/chromium/playwright`.

</details>

<details>
<summary>Why is <code>page.route()</code> not intercepting anything?</summary>

Route interception is a Playwright-protocol feature and isn't available over CDP. Connect through `/chromium/playwright` with `connect()` to use it, or block resources with the [`rejectResourceTypes`](/rest-apis/request-configuration#rejecting-undesired-requests) request option instead.

</details>

<details>
<summary>Can I run Playwright test runner suites against Browserless?</summary>

Yes. Create the connection in a worker-scoped fixture so the tests in each worker share one remote browser. Global setup runs in its own process and can't hand a live browser to the workers, so it won't do. Each parallel worker opens its own session and counts against your plan's concurrency limit. Note that `launch`-only options in `playwright.config.js` have no effect on a remote browser, since the options travel on the connection URL instead.

</details>

<details>
<summary>Do I need <code>playwright install</code>?</summary>

No. That command downloads local browser binaries. A remote connection never touches them, so skipping it keeps installs and CI images small.

</details>

## Next steps

  <a href="/baas/quick-start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Browsers as a Service</h3>
      Runnable examples in Java, C#, and Go
    
  </a>
  <a href="/baas/advanced-configurations/playwright-customizations" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Playwright customizations</h3>
      connect vs connectOverCDP, and per-browser paths
    
  </a>
  <a href="/overview/getting-started/bypass-captchas" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Bypass CAPTCHAs</h3>
      Stealth routes and automatic solving on the same connection
    
  </a>

---

# Connect Puppeteer

URL: https://docs.browserless.io/overview/getting-started/connect-puppeteer

Run your Puppeteer scripts against managed browsers by replacing `puppeteer.launch()` with `puppeteer.connect()`. Everything after the connection line, including selectors, waits, and evaluation, stays exactly as you wrote it.

This page is for code you already have. If you're writing automation from scratch, [the BAP SDK](#the-bap-alternative) is usually the better starting point.

## Steps

[Sign up for a free account](https://www.browserless.io/signup/email?plan=free), then copy your API token from the [account dashboard](https://browserless.io/account/). Every connection carries it as a `?token=` query parameter.

```bash
  npm install puppeteer-core
```

    Use `puppeteer-core` rather than `puppeteer`. The full package downloads a Chromium binary at install time, and you won't be running it: the browser lives on Browserless. The two packages expose the same API, so imports and method calls don't change.

```js
  import puppeteer from "puppeteer-core";

  const TOKEN = "YOUR_API_TOKEN_HERE";

  // Before: a browser process on this machine.
  // const browser = await puppeteer.launch();

  // After: a managed browser reached over a CDP WebSocket.
  const browser = await puppeteer.connect({
    browserWSEndpoint: `wss://production-sfo.browserless.io?token=${TOKEN}`,
  });
```

    The token goes in the query string. `wss://` is required, since the connection is a WebSocket rather than an HTTP request.

```js
  import puppeteer from "puppeteer-core";

  const TOKEN = "YOUR_API_TOKEN_HERE";

  const browser = await puppeteer.connect({
    browserWSEndpoint: `wss://production-sfo.browserless.io?token=${TOKEN}`,
  });

  try {
    const page = await browser.newPage();
    await page.goto("https://example.com", { waitUntil: "networkidle2" });

    const title = await page.title();
    const heading = await page.$eval("h1", (el) => el.textContent);

    console.log({ title, heading });
  } finally {
    // Release the session even if something above threw.
    await browser.close();
  }
```

    **Output**

```text
{ title: 'Example Domain', heading: 'Example Domain' }
```

## Configuring the browser

Options you used to pass to `launch()` now travel on the connection URL, because the browser starts before your code ever talks to it.

```js
const browser = await puppeteer.connect({
  browserWSEndpoint: `wss://production-sfo.browserless.io?token=${TOKEN}&blockAds=true&timeout=60000`,
});
```

Array-valued options such as Chrome `args` go in the JSON `launch` parameter instead, since their brackets and commas need encoding. The [launch parameters reference](/overview/launch-parameters) has the full list and both syntaxes.

## What changes and what doesn't

Page-level code is unchanged: `page.goto`, `page.$eval`, `page.waitForSelector`, `page.pdf`, and the rest behave as they do locally. Three things are worth knowing:

- **`browser.close()` ends a remote session**, not a local process. Skip it and the session stays alive until it times out, and you're billed for that time. Put it in a `finally` block.
- **Local file paths don't exist on the browser's machine.** Downloads and uploads go through the [file transfer APIs](/baas/features/file-transfers) rather than your own disk.
- **Latency is between the browser and the target site**, not between you and the browser. Pick the region closest to the sites you're scraping. See [connection URLs](/overview/connection-urls).

## The BAP alternative

BAP is our own SDK, and its TypeScript API borrows Puppeteer's method names, so the script above is close to what you'd write with it:

```typescript

const TOKEN = "YOUR_API_TOKEN_HERE";

// connect() opens no socket. The WebSocket opens on newPage().
const browser = Browserless.connect({
  browserWSEndpoint: "wss://production-sfo.browserless.io/chromium/bql",
  token: TOKEN,
});

try {
  const page = await browser.newPage();
  await page.goto("https://example.com");

  const title = await page.title();
  const { text } = await page.text({ selector: "h1" });

  console.log({ title, heading: text });
} finally {
  await browser.close();
}
```

**Output**

```text
{ title: 'Example Domain', heading: 'Example Domain' }
```

It isn't a drop-in replacement. BAP runs over BrowserQL rather than a live CDP session, so `page.mouse`, `page.keyboard`, frames, and browser contexts have no equivalent, and shared names like `$eval` behave differently: it returns the matched element's text rather than running your callback. What you get in exchange is `solve()`, `proxy()`, `markdown()`, and `liveURL()` as methods instead of plumbing.

Keep this page's `connect()` approach when your script needs direct browser control. See [Migrate to BAP](/bap/migrate) for the full method-by-method comparison.

## FAQ & Troubleshooting

<details>
<summary>Why does <code>puppeteer.connect()</code> hang or time out?</summary>

Usually the URL. It must start with `wss://` (not `https://`) and carry `?token=`. A missing or invalid token closes the socket during the handshake, which surfaces as a hang rather than a clean `401`.

</details>

<details>
<summary>Can I still use <code>puppeteer</code> instead of <code>puppeteer-core</code>?</summary>

Yes, `connect()` works from either package. You'll just be downloading and storing a Chromium build you never launch, which slows installs and bloats CI images.

</details>

<details>
<summary>My script works locally but the page renders differently on Browserless.</summary>

The remote browser has its own viewport, user agent, timezone, and locale. Set them explicitly with [launch parameters](/overview/launch-parameters) so both environments match, rather than relying on local defaults.

</details>

<details>
<summary>Do concurrent scripts need separate connections?</summary>

Yes. Each `puppeteer.connect()` call takes its own session and counts against your plan's concurrency limit. Reuse one `browser` object across pages within a script, and open a new connection per parallel job.

</details>

## Next steps

  <a href="/baas/quick-start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Browsers as a Service</h3>
      The full connection guide, with Java, C#, and Go examples
    
  </a>
  <a href="/overview/launch-parameters" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch parameters</h3>
      Proxies, ad blocking, timeouts, and Chrome flags
    
  </a>
  <a href="/overview/getting-started/bypass-captchas" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Bypass CAPTCHAs</h3>
      Stealth routes and automatic solving on the same connection
    
  </a>

---

# Create a browser agent

URL: https://docs.browserless.io/overview/getting-started/create-a-browser-agent

Set up an agent that drives a real browser. There are two ways in, and which one you want depends on where the agent lives.

Use **MCP** when the agent is an assistant someone talks to, such as Claude, Cursor, or VS Code. The client keeps its own loop, and Browserless supplies the browser tools.

Use the **Agent Run API** when your own code needs a task done. You send the task in plain English, Browserless runs both the browser and the model, and you poll for the result.

## Set up MCP in your AI client

  [Sign up for a free account](https://www.browserless.io/signup/email?plan=free), then copy your API token from the [account dashboard](https://browserless.io/account/).

    Clients that support OAuth can skip this and sign in to Browserless when they first connect.

  
    The hosted server is `https://mcp.browserless.io/mcp`. There's nothing to install.

    
      
**Claude Code:**

```bash
claude mcp add --transport http browserless https://mcp.browserless.io/mcp \
  --header "Authorization: Bearer YOUR_API_TOKEN_HERE"
```
      
      
**Claude Desktop:**

        Add to `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "browserless": {
      "type": "http",
      "url": "https://mcp.browserless.io/mcp",
      "headers": {
        "Authorization": "Bearer YOUR_API_TOKEN_HERE"
      }
    }
  }
}
```
      
      
**Cursor:**

        Add to your Cursor MCP settings:

```json
{
  "mcpServers": {
    "browserless": {
      "type": "http",
      "url": "https://mcp.browserless.io/mcp",
      "headers": {
        "Authorization": "Bearer YOUR_API_TOKEN_HERE"
      }
    }
  }
}
```
      
      
**Claude.ai:**

        The custom connector form takes a URL only, so the token goes in the query string:

```bash
https://mcp.browserless.io/mcp?token=YOUR_API_TOKEN_HERE
```

        Add it under **Customize > Connectors > Add custom connector**.
      
    

    Other clients, including VS Code and Windsurf, are in [MCP server setup](/mcp/browserless-mcp-server/setup).
  

  Restart the client so it picks up the server, then ask for something that needs a browser:

```text
Open news.ycombinator.com and give me the titles of the top three stories.
```

    The client picks the tool itself. For multi-step work such as signing in or paginating, it reaches for [`browserless_agent`](/mcp/browser-agent). In MCP server v1.33.0+, it must set `keepSessionAlive: true` on the first call and echo the returned `sessionId` on later calls so cookies and history survive between turns. See [session lifetime](/mcp/browser-agent#session-lifetime).

## Set up the Agent Run API

> **BETA**
> The Agent Run API is currently in beta. Parameters and response shapes may change in future releases.

> The Agent Run API is only available for Cloud plans. [Contact us for more information.](https://www.browserless.io/contact/)

  Copy your API token from the [account dashboard](https://browserless.io/account/). You don't need a model key: Browserless runs the browser and the model.

  
    `POST /agent/run` takes the task in plain English and returns a run ID immediately. The agent works in the background.

    
      
**cURL:**

```bash
curl --request POST \
  --url 'https://production-sfo.browserless.io/agent/run?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{
  "query": "Go to https://example.com and tell me the exact text of the top-level heading."
}'
```
      
      
**JavaScript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";

const response = await fetch(
  `https://production-sfo.browserless.io/agent/run?token=${TOKEN}`,
  {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      query:
        "Go to https://example.com and tell me the exact text of the top-level heading.",
    }),
  }
);

const { id } = await response.json();
console.log(id);
```
      
      
**Python:**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"

response = requests.post(
    f"https://production-sfo.browserless.io/agent/run?token={TOKEN}",
    json={
        "query": "Go to https://example.com and tell me the exact text of the top-level heading."
    },
)

print(response.json()["id"])
```
      
    

    **Response**

```json
{
  "id": "run_abc123def456",
  "status": "pending"
}
```

    The `query` must be at least 10 characters.
  

  
    Request the run until `status` reaches a terminal value: `succeeded`, `failed`, `timed_out`, or `stopped`. Each poll also returns the steps taken so far, so you can show progress instead of a spinner.

    
      
**cURL:**

```bash
curl --request GET \
  --url 'https://production-sfo.browserless.io/agent/run/run_abc123def456?token=YOUR_API_TOKEN_HERE'
```
      
      
**JavaScript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const runId = "run_abc123def456";
const TERMINAL = ["succeeded", "failed", "timed_out", "stopped"];

while (true) {
  const response = await fetch(
    `https://production-sfo.browserless.io/agent/run/${runId}?token=${TOKEN}`
  );
  const result = await response.json();
  console.log(result.status, result.steps?.at(-1) ?? "");

  if (TERMINAL.includes(result.status)) {
    console.log(result.data);
    break;
  }

  // A request right after submitting returns pending, so poll on an interval.
  await new Promise((resolve) => setTimeout(resolve, 3000));
}
```
      
      
**Python:**

```python
import time
import requests

TOKEN = "YOUR_API_TOKEN_HERE"
run_id = "run_abc123def456"
url = f"https://production-sfo.browserless.io/agent/run/{run_id}?token={TOKEN}"
TERMINAL = {"succeeded", "failed", "timed_out", "stopped"}

while True:
    result = requests.get(url).json()
    print(result["status"], (result.get("steps") or [""])[-1])

    if result["status"] in TERMINAL:
        print(result["data"])
        break

    # A request right after submitting returns pending, so poll on an interval.
    time.sleep(3)
```
      
    

    **Response**

```json
{
  "id": "run_abc123def456",
  "status": "succeeded",
  "data": {
    "answer": "The exact text of the top-level heading is: Example Domain"
  },
  "error": null,
  "steps": [
    "Opening the example homepage",
    "Reading the main heading",
    "Closing the completed browser session"
  ]
}
```

    `data` is `{ "answer": "..." }` unless you pass a [`responseSchema`](/rest-apis/agent-run#structured-output), which shapes it to match. Results are kept for 7 days.
  

## FAQ & Troubleshooting

<details>
<summary>My MCP client doesn't list the Browserless tools.</summary>

Restart the client after editing its config. If they still don't appear, check that the config uses `"type": "http"` and that the token is on the `Authorization` header as `Bearer YOUR_API_TOKEN_HERE`. The query-string form is only for Claude.ai's connector field, which accepts a URL and nothing else.

</details>

<details>
<summary>A run came back <code>timed_out</code>.</summary>

The task was too broad, or the site was too slow. Narrow it, give the agent a starting URL, and say what a finished answer looks like. "Find the pricing" invites wandering. "Open this URL and return the monthly price of each plan" doesn't.

</details>

<details>
<summary>Can I constrain where the agent goes, or what it returns?</summary>

Yes. `POST /agent/run` accepts a starting URL, a navigation allowlist, an authentication profile, and a `responseSchema` for the shape of the answer. See the [Agent Run API reference](/rest-apis/agent-run).

</details>

<details>
<summary>Do I need my own model API key?</summary>

Not for either path here. The Agent Run API manages the model for you, and with MCP the client you're already using brings its own.

</details>

## Next steps

  <a href="/rest-apis/agent-run" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Agent Run API</h3>
      Output schemas, allowlists, webhooks, and polling
    
  </a>
  <a href="/mcp/browser-agent" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Browser Agent tool</h3>
      The stateful snapshot protocol behind MCP browsing
    
  </a>
  <a href="/mcp/overview" className="next-step-card-link">
    
      <h3 className="next-step-card-title">MCP overview</h3>
      Both Browserless MCP servers, and what each one is for
    
  </a>

---

# Create a browser session

URL: https://docs.browserless.io/overview/getting-started/create-a-browser-session

A session is a browser that stays open between commands, so cookies, logins, and navigation history survive from one step to the next. This page opens one with [BAP](/bap/getting-started), our SDK, and covers the whole lifecycle: open it, work in it, keep it alive past your own connection, and close it.

One-shot jobs don't need any of this. If you just want the contents of a URL, [scrape a website URL](/overview/getting-started/scrape-a-website) is a single call.

## Steps

  [Sign up for a free account](https://www.browserless.io/signup/email?plan=free), then copy your API token from the [account dashboard](https://browserless.io/account/).

  
    
      
**TypeScript:**

```bash
npm install @browserless.io/bap-ts
```
      
      
**Python:**

```bash
python -m pip install bap-py
```

        Async applications import `bap` instead of `bap.sync_api`, with the same class and method names under `async with` and `await`.
      
    
  

  
    `connect()` opens no socket on its own. The session starts when you ask for a page, and it runs until you close the browser or it times out.

    
      
**TypeScript:**

```typescript
import Browserless from "@browserless.io/bap-ts";

const TOKEN = "YOUR_API_TOKEN_HERE";

const browser = Browserless.connect({
  browserWSEndpoint: "wss://production-sfo.browserless.io/chromium/bql",
  token: TOKEN,
});

try {
  // The WebSocket opens here, not on connect().
  const page = await browser.newPage();
  await page.goto("https://example.com");

  console.log(await page.title());
} finally {
  // Always close. An abandoned session bills until it times out.
  await browser.close();
}
```
      
      
**Python:**

```python
import bap.sync_api as bap

TOKEN = "YOUR_API_TOKEN_HERE"

with bap.Browserless.connect(
    browser_ws_endpoint="wss://production-sfo.browserless.io/chromium/bql",
    token=TOKEN,
) as browser:
    # The WebSocket opens when the page block is entered.
    with browser.page() as page:
        page.goto("https://example.com")
        print(page.title())
```

        The `with` blocks close the page and the browser when they exit, including on an exception.
      
    

    **Output**

```text
Example Domain
```

    The endpoint must end in `/bql`. The same host also serves CDP paths such as `/chromium`, and those don't speak BrowserQL. Swap the path for `/stealth/bql` when a site is challenging you.
  

## Run several steps against the same page

This is what the session buys you. Each call lands on the page the last one left behind, so a login holds for everything after it.

  
**TypeScript:**

```typescript
await page.goto("https://example.com/login");

await page.type("#email", "user@example.com");
await page.type("#password", "hunter2");
await page.click("button[type=submit]");

// Same browser, same cookies: the session is authenticated from here on.
await page.goto("https://example.com/account");
const { text } = await page.text({ selector: "h1" });
console.log(text);
```
  
  
**Python:**

```python
page.goto("https://example.com/login")

page.type("#email", "user@example.com")
page.type("#password", "hunter2")
page.click("button[type=submit]")

# Same browser, same cookies: the session is authenticated from here on.
page.goto("https://example.com/account")
print(page.text(selector="h1")["text"])
```
  

**Output**

```text
Your account
```

Save what the session earned with `cookies()` and replay it into a later session to skip the login entirely. See [cookies and page setup](/bap/session-state).

## Keep the session alive past your connection

By default the browser shuts down when your page's WebSocket closes. `reconnect()` changes that: the browser waits for a new connection for as long as you specify, and you get back the endpoints to return to it.

Call it while the page is still open, before the `finally` block from the steps above runs. Once `browser.close()` has returned there's no live connection left to extend. Closing the original connection *after* a successful `reconnect()` is fine and doesn't cancel the waiting window.

  
**TypeScript:**

```typescript
// After this, the browser survives 60 seconds waiting for a reconnect.
const session = await page.reconnect({ timeout: 60_000 });

// Returned as https://; BAP talks BrowserQL over WebSocket.
const endpoint = session.browserQLEndpoint!.replace(/^https/, "wss");

const resumed = Browserless.connect({
  browserWSEndpoint: endpoint,
  token: TOKEN,
});

try {
  const samePage = await resumed.newPage();
  // Same browser, same state: still on the account page.
  console.log(await samePage.url());
} finally {
  await resumed.close();
}
```
  
  
**Python:**

```python
# After the block exits, the browser survives 60 seconds waiting for us.
session = page.reconnect(timeout=60000)

# Returned as https://; BAP talks BrowserQL over WebSocket.
endpoint = session["browserQLEndpoint"].replace("https", "wss", 1)

with bap.Browserless.connect(
    browser_ws_endpoint=endpoint,
    token=TOKEN,
) as resumed:
    with resumed.page() as same_page:
        # Same browser, same state: still on the account page.
        print(same_page.url())
```
  

**Output**

```text
https://example.com/account
```

The reconnected page is still on the account page from the previous section, which is the point: the browser kept its state while nothing was connected to it. That's how a session survives a client restart or moves between processes. Alongside `browserQLEndpoint`, `reconnect()` returns `browserWSEndpoint` for rejoining with Puppeteer or Playwright over CDP, and `devtoolsFrontendUrl` for opening the live browser in DevTools. See [reconnecting to sessions](/bap/session-management/reconnects).

## Watch the session while it runs

`liveURL()` returns a shareable link that streams the browser. Pass `interactable: true` and the viewer can click and type, which is how you hand a one-time passcode or a stubborn login to a person mid-run.

  
**TypeScript:**

```typescript
const { liveURL } = await page.liveURL({ interactable: true });
console.log(`Hand off to a human: ${liveURL}`);

// Block until the person's part is done, then carry on.
await page.waitForSelector("#dashboard", { timeout: 120_000 });
```
  
  
**Python:**

```python
live = page.live_url(interactable=True)
print(f"Hand off to a human: {live['liveURL']}")

# Block until the person's part is done, then carry on.
page.wait_for_selector("#dashboard", timeout=120000)
```
  

**Output**

```text
Hand off to a human: https://production-sfo.browserless.io/live/?i=a1b2c3d4...
```

Treat that URL as a bearer secret. With `interactable: true`, anyone holding it controls the browser and everything logged into it, so send it over a trusted channel and keep it out of logs and tickets.

A session waiting on a human is still a session that bills, so keep the wait bounded and close in a `finally`. See [live URLs and recording](/bap/session-management/live-url).

## FAQ & Troubleshooting

<details>
<summary>The connection fails immediately with no clear error.</summary>

Check the path. A BAP endpoint ends in `/bql`, as in `wss://production-sfo.browserless.io/chromium/bql`. The CDP paths on the same host, such as `/chromium`, don't speak BrowserQL, and a missing token fails at connection time rather than on the first method call.

</details>

<details>
<summary>My session ended while I was still using it.</summary>

Every session has a timeout, and solving a CAPTCHA or waiting on a human eats into it. Raise it with `preferences()` for the session, or pass `timeout` on the individual call that needs longer.

</details>

<details>
<summary>Do I need one session per page?</summary>

No, but know what you're paying for. Each `newPage()` opens its own WebSocket and holds it until you close the browser, and each concurrent session counts against your plan limit. Reuse one page for a sequential flow, and open more only for work that genuinely runs in parallel.

</details>

<details>
<summary>Can I hand the session to Puppeteer or Playwright?</summary>

Yes. `reconnect()` returns `browserWSEndpoint` alongside the BrowserQL one, and that's a CDP endpoint you can pass straight to `puppeteer.connect()` or `chromium.connectOverCDP()`. Append your token when you connect, since the returned URLs don't include it.

</details>

## Next steps

  <a href="/bap/session-state" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Cookies and page setup</h3>
      Save a login and replay it into later sessions
    
  </a>
  <a href="/bap/session-management/reconnects" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Reconnecting to sessions</h3>
      Every endpoint reconnect returns, and what each one is for
    
  </a>
  <a href="/bap" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BAP introduction</h3>
      What the SDK wraps, and how it compares to raw BrowserQL
    
  </a>

---

# Scrape a website URL

URL: https://docs.browserless.io/overview/getting-started/scrape-a-website

Pull data off a live page with a single REST call, then run the same job over a WebSocket connection. After this page you'll have a working API token and know which of the two connection styles fits the job in front of you.

## Steps

  [Sign up for a free account](https://www.browserless.io/signup/email?plan=free), then copy your API token from the [account dashboard](https://browserless.io/account/).

    Every request carries the token as a `?token=` query parameter. There are no other auth headers to set.

  
    Three ways to run the same job. They all reach the same managed browsers, so pick the one that fits how you already work. The tradeoffs are below.

    
      
**BAP SDK:**

        BAP is our own SDK, and the shortest path if you're starting fresh. It runs over BrowserQL rather than CDP, and extraction happens inside the browser, so you get back the content you asked for instead of a page to parse.

        
          
**TypeScript:**

```bash
npm install @browserless.io/bap-ts
```

```typescript
import Browserless from "@browserless.io/bap-ts";

const TOKEN = "YOUR_API_TOKEN_HERE";

const browser = Browserless.connect({
  browserWSEndpoint: "wss://production-sfo.browserless.io/chromium/bql",
  token: TOKEN,
});

try {
  const page = await browser.newPage();
  await page.goto("https://example.com");

  const { text } = await page.text({ selector: "h1" });
  console.log(text);
} finally {
  await browser.close();
}
```
          
          
**Python:**

```bash
python -m pip install bap-py
```

```python
import bap.sync_api as bap

TOKEN = "YOUR_API_TOKEN_HERE"

# The context manager closes the page and the browser when the block exits.
with bap.Browserless.connect(
    browser_ws_endpoint="wss://production-sfo.browserless.io/chromium/bql",
    token=TOKEN,
) as browser:
    with browser.page() as page:
        page.goto("https://example.com")
        print(page.text(selector="h1")["text"])
```
          
        

        **Output**

```text
Example Domain
```

        Feeding a page to an LLM? Swap `text()` for `markdown()` and the conversion happens in the browser, so no markup crosses the wire. See [extracting content](/bap/extracting-content).
      
      
**REST:**

        `POST /scrape` takes a URL and a list of CSS selectors, runs them against the fully rendered DOM, and returns what matched. Browserless launches and disposes of the browser for you, so there's nothing to install and nothing to clean up.

        
          
**cURL:**

```bash
curl --request POST \
  --url 'https://production-sfo.browserless.io/scrape?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{
  "url": "https://example.com",
  "elements": [{ "selector": "h1" }]
}'
```
          
          
**JavaScript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";

const response = await fetch(
  `https://production-sfo.browserless.io/scrape?token=${TOKEN}`,
  {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      url: "https://example.com",
      elements: [{ selector: "h1" }],
    }),
  }
);

console.log(await response.json());
```
          
          
**Python:**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"

response = requests.post(
    f"https://production-sfo.browserless.io/scrape?token={TOKEN}",
    json={
        "url": "https://example.com",
        "elements": [{"selector": "h1"}],
    },
)

print(response.json())
```
          
        

        **Response**

```json
{
  "data": [
    {
      "results": [
        {
          "html": "Example Domain",
          "text": "Example Domain"
        }
      ],
      "selector": "h1"
    }
  ]
}
```

        Each result also carries the element's attributes and its position on the page. The full shape is on the [/scrape reference](/rest-apis/scrape).
      
      
**Puppeteer or Playwright:**

        The same job through Puppeteer or Playwright. Instead of describing the work in JSON, you drive the browser yourself over a Chrome DevTools Protocol (CDP) WebSocket, and the page stays open between commands.

```bash
npm install puppeteer-core   # or: npm install playwright-core
```

        
          
**Puppeteer:**

```js
import puppeteer from "puppeteer-core";

const TOKEN = "YOUR_API_TOKEN_HERE";

// connect() replaces launch(). The browser runs on Browserless, not your machine.
const browser = await puppeteer.connect({
  browserWSEndpoint: `wss://production-sfo.browserless.io?token=${TOKEN}`,
});

try {
  const page = await browser.newPage();
  await page.goto("https://example.com");
  console.log(await page.$eval("h1", (el) => el.textContent));
} finally {
  // Always close, or the session runs until it times out and keeps burning units.
  await browser.close();
}
```
          
          
**Playwright:**

```js
import { chromium } from "playwright-core";

const TOKEN = "YOUR_API_TOKEN_HERE";

// connectOverCDP(), not connect(). Browserless speaks CDP, not the Playwright server protocol.
const browser = await chromium.connectOverCDP(
  `wss://production-sfo.browserless.io?token=${TOKEN}`
);

try {
  const page = await browser.newPage();
  await page.goto("https://example.com");
  console.log(await page.textContent("h1"));
} finally {
  // Always close, or the session runs until it times out and keeps burning units.
  await browser.close();
}
```
          
        

        **Output**

```text
Example Domain
```
      
    
  

## Which path should you use?

All three hit the same managed browsers. They differ in who holds the browser open and how much you have to install.

Use **BAP** when you're writing new automation. It's our SDK over BrowserQL, and it ships stealth, CAPTCHA solving, and content extraction as methods rather than as things you assemble yourself. `markdown()` replaces the fetch-then-parse step that both other paths leave to you.

Use **REST** for stateless, one-shot work: fetch a page, grab some fields, render a PDF, take a screenshot. One request in, one JSON response out, nothing to manage.

Use **Puppeteer or Playwright** when you already have scripts running locally. Swapping `launch()` for `connect()` is the smallest possible change, and the rest of your code is untouched.

See the [API comparison](/overview/comparison) for the full breakdown, including where raw BrowserQL fits.

> **Tip**
> The examples use the US West region (`production-sfo`). Browserless also runs in Europe. Pick the region closest to the sites you're targeting, since that's where the latency comes from. See [connection URLs](/overview/connection-urls) for the full list.

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>401 Unauthorized</code> or <code>403 Forbidden</code> response?</summary>

The token is missing, mistyped, or sent in the wrong place. It goes in the URL as `?token=YOUR_API_TOKEN_HERE`, not in a header and not in the JSON body. Confirm the value against your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>The <code>results</code> array came back empty.</summary>

Either the selector didn't match, or the site served different content to the browser. Check the selector in your own browser's devtools first. If it matches there but not through the API, the site is likely blocking automation, so try the [/unblock API](/rest-apis/unblock) or a [stealth route](/overview/getting-started/bypass-captchas).

</details>

<details>
<summary>Do I need <code>puppeteer</code> or <code>puppeteer-core</code>?</summary>

`puppeteer-core`. The full `puppeteer` package downloads a Chromium binary on install, which you don't need when the browser runs on Browserless. The API of the two is identical.

</details>

<details>
<summary>My script finished but the session kept running.</summary>

You didn't close the browser. Wrap the work in `try`/`finally` and call `browser.close()` in the `finally` block, as in the example above. An abandoned session stays open until it hits the timeout, and you're charged for that time.

</details>

## Next steps

  <a href="/bap/getting-started" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BAP quickstart</h3>
      The SDK path, in TypeScript or Python
    
  </a>
  <a href="/rest-apis/scrape" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape API reference</h3>
      Every parameter the /scrape endpoint accepts
    
  </a>
  <a href="/overview/comparison" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Choosing an API</h3>
      Pick the right API for the job in front of you
    
  </a>

---

# Browserless Overview

URL: https://docs.browserless.io/overview/intro

# Introduction

## What is Browserless?

Browserless provides managed headless browsers for automation. Connect Puppeteer or Playwright over WebSocket, or use REST and GraphQL APIs for scraping, screenshots, and PDFs. Run it in our cloud or self-host with Docker. We handle the browser infrastructure so you don't have to.

> **Before you start**
> [Sign up for a free account](https://www.browserless.io/signup/email?plan=free) and copy your API token from the [Browserless dashboard](https://browserless.io/account/). You'll pass it as the `?token=` query parameter on every request.

## Common use cases

Short, task-shaped guides. Each one ends with a working request.

- [Scrape a website URL](/overview/getting-started/scrape-a-website) - get a token and pull data off a page, over REST or a WebSocket
- [Create a browser session](/overview/getting-started/create-a-browser-session) - keep one browser open across a multi-step flow
- [Connect Puppeteer](/overview/getting-started/connect-puppeteer) - point an existing script at a managed browser
- [Connect Playwright](/overview/getting-started/connect-playwright) - the same swap, in JavaScript or Python
- [Create a browser agent](/overview/getting-started/create-a-browser-agent) - wire an LLM to a real browser
- [Bypass CAPTCHAs on a website](/overview/getting-started/bypass-captchas) - stealth routes, automatic solving, and proxies

## Get started

Not sure which to pick? See the [API Comparison](/overview/comparison).

---

# Launch Parameters

URL: https://docs.browserless.io/overview/launch-parameters

# Launch Parameters

Configure how browsers launch and behave with query parameters. This section covers the shared options used across [BrowserQL](/browserql/start), [BaaS v2](/baas/start), and [REST APIs](/rest-apis/intro).

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Passing Launch Options

Two ways to specify launch options:

**1. Individual query parameters** — add options directly to the URL. Best for simple boolean or string options.

```
wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&headless=false&proxy=residential
```

**2. Combined `launch` parameter (encoded JSON)** — for complex configurations, use a single query param `launch` whose value is a URL-encoded JSON string. It can include browser-level settings and Chrome flags in a structured way.

```js
const launchArgs = {
  headless: false,
  args: ['--window-size=1920,1080'],
};

const launch = encodeURIComponent(JSON.stringify(launchArgs));

const browser = await puppeteer.connect({
  browserWSEndpoint: `wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&launch=${launch}`,
});
```

Browserless merges both methods if used together, with individual parameters taking precedence. Use query params for simple toggles and the `launch` parameter for grouped browser settings.

## Common Launch Options

Below is a list of common launch options you can use in query strings. Unless stated otherwise,
these can be used in BaaS v2 (library connections) and REST API calls alike. BrowserQL
internally uses some of these, but BQL users typically set these via the IDE session settings
rather than manually in a URL.

## BaaS v2 Advanced Options

For BaaS v2, encode the `launch` JSON before adding it to the URL. Learn more in our [Launch parameters guide](/baas/launch-options):

  
**Puppeteer:**

```js
const launchArgs = {
  headless: false,
  stealth: true,
  args: ['--window-size=1920,1080', '--force-color-profile=srgb']
};

const launch = encodeURIComponent(JSON.stringify(launchArgs));

const browser = await puppeteer.connect({
  browserWSEndpoint: `wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&launch=${launch}`,
});
```
  
  
**Playwright:**

```js
const launchArgs = {
  headless: false,
  stealth: true,
  args: ['--window-size=1920,1080', '--force-color-profile=srgb']
};

const launch = encodeURIComponent(JSON.stringify(launchArgs));

const browser = await playwright.chromium.connectOverCDP(
  `wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE&launch=${launch}`
);
```
  

## REST API Options

When using REST APIs, keep launch settings in the URL query string. Use the JSON body for the endpoint's own payload:

```js
// Query string launch parameters
fetch("https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE&blockAds=true&stealth=true")

// Endpoint-specific body payload
fetch("https://production-sfo.browserless.io/pdf?token=YOUR_API_TOKEN_HERE", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    url: "https://example.com",
    options: { format: "A4" },
  })
})
```

For a comprehensive list of available REST API endpoints and their parameters, please refer to our [API Reference](/open-api/overview).

For a comprehensive list of Chrome flags and Browserless-specific options, please refer to our [Launch parameters](/baas/launch-options) documentation.

## FAQ & Troubleshooting

<details>
<summary>How do I get started with Browserless?</summary>

Sign up for a free account at [browserless.io](https://browserless.io/signup/email?plan=free), get your API token, and follow the [Quickstart guide](/browserql/getting-started) for BQL or the [BaaS guide](/baas/start) for Puppeteer/Playwright.

</details>

<details>
<summary>What's the difference between BQL and BaaS?</summary>

BQL is a GraphQL API for declarative browser automation with built-in stealth. BaaS runs your existing Puppeteer or Playwright scripts on managed browsers. See [comparison](/overview/comparison) for details.

</details>

## Next steps

  <a href="/browserql/getting-started" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BQL Quickstart</h3>
      run your first BrowserQL query
    
  </a>
  <a href="/baas/start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">BaaS Quickstart</h3>
      connect Puppeteer or Playwright to Browserless
    
  </a>

---

# Request Logs

URL: https://docs.browserless.io/overview/request-logs

# Request Logs

The Logs page gives you a searchable, per-request activity feed for your account: every API call's lifecycle events, errors, durations, and unit usage. Use it to debug failed requests, trace a specific request ID, or audit what an API key has been doing.

Open it from the dashboard sidebar under **Tools → Logs**, or go directly to [browserless.io/account/logs](https://browserless.io/account/logs). The sidebar entry appears on Cloud Unit and Dedicated plans.

## Availability and lookback

Request logs are available on Cloud Unit and Dedicated plans. How far back you can search depends on your plan:

| Plan | Maximum lookback |
| --- | --- |
| Free | 8 hours |
| Prototyping | 24 hours |
| Starter | 3 days |
| Scale | 7 days |
| Dedicated | 7 days |

Custom and legacy cloud plans can search back up to 7 days.

## What gets logged

Each request produces lifecycle events as it runs. You can filter by these event names:

- `request.completed` / `request.failed`: the outcome of an API request
- `navigation.started` / `navigation.completed`: page navigations performed during the request
- `bql.parse.failed`, `bql.validation.failed`, `bql.operation.failed`, `bql.execution.failed`, `bql.transport.failed`: BrowserQL-specific failures, from a query that failed to parse or validate down to an individual mutation that errored

Every event carries a level (`INFO`, `WARN`, or `ERROR`) and, where applicable, an outcome.

## Reading the log table

| Column | What it shows |
| --- | --- |
| Timestamp | When the event occurred, shown in your local time |
| Event | The event name, such as `request.failed` |
| Level | `INFO`, `WARN`, or `ERROR` |
| Outcome | The result recorded for the event |
| Details | The error message, reason, or category, plus the HTTP status when one was recorded |
| Endpoint / region | Which API endpoint handled the request and the region it ran in |
| Target URL | The page the request was operating on |
| Duration | How long the request took |
| Units | Total units the request consumed; hover to see the breakdown across time, proxy, captcha, and agent units |
| Request ID | The unique ID for the request, with a copy button |

Columns are resizable: drag the divider next to a column header, or focus it and use the arrow keys.

## Filtering logs

Set any combination of filters and click **Apply**:

- **Time range**: last hour, 8 hours, 24 hours, 3 days, or 7 days, capped at your plan's maximum lookback
- **Request ID**: find every event from a single request
- **Target URL**: match requests by the page they operated on
- **Endpoint**: match a specific API endpoint
- **Outcome**: match the recorded outcome
- **Events**: one or more event names from the list above
- **Levels**: one or more of `INFO`, `WARN`, `ERROR`
- **API key**: scope results to one of your API keys, including revoked keys

Results are paginated 50 entries at a time; use **Previous** and **Next** to move through pages.

## Tracing a failed request

Error responses from Browserless APIs include the request's ID as a suffix on the error message:

```
Timed out waiting for selector "h1" (requestId: f3a9c2d81b7e4a06)
```

Paste that ID into the **Request ID** filter to see everything the request did before it failed: navigations, the failing operation, its duration, and the units it consumed. This is also the ID to include when contacting support, so they can find the matching server-side logs.

## Next steps

  <a href="/overview/unit-consumption" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Unit Consumption</h3>
      understand how requests consume units
    
  </a>
  <a href="/overview/api-keys" className="next-step-card-link">
    
      <h3 className="next-step-card-title">API Token Management</h3>
      create and rotate the keys logs are scoped to
    
  </a>

---

# Unit Consumption

URL: https://docs.browserless.io/overview/unit-consumption

# Unit consumption

Every Browserless Cloud plan includes a monthly unit allowance. Units are the billing currency for all activity on the platform, and browser sessions, proxy traffic, and CAPTCHA solving each draw from the same balance.

## What consumes units

### Browser time

Browser time is billed in **30-second increments**. Every 30 seconds a browser session is open consumes **1 unit**. Partial increments are rounded up, so a session open for 31 seconds consumes **2 units**. This applies to all connection methods: BrowserQL, BaaS (Puppeteer/Playwright), and REST APIs.

A session that runs for 45 seconds consumes 2 units, regardless of whether the browser is actively navigating or sitting idle. Close sessions as soon as you're done to avoid unnecessary consumption.

> **Tip**
> Set a custom `timeout` on your connections to prevent runaway sessions from draining your balance. The default timeout is 30 seconds.

### Proxy traffic

Built-in residential and datacenter proxies consume units based on bandwidth:

| Proxy type | Cost |
|------------|------|
| Residential | **6 units per MB** |
| Datacenter | **2 units per MB** |

Proxy units are charged on top of browser time. A 30-second session using a residential proxy that transfers 2 MB of data costs 1 (browser time) + 12 (proxy) = 13 units.

Third-party proxies connected through `--proxy-server` launch args don't consume proxy units. Only Browserless's built-in proxies are metered.

For more details, see the [Proxies](/browserql/bot-detection/proxies) documentation.

### CAPTCHA solving

Each successful CAPTCHA solve costs **10 units**. Attempts that don't clear the challenge aren't charged. This covers the cost of the third-party solving service.

Using [stealth mode](/browserql/bot-detection/overview) and residential proxies reduces how often CAPTCHAs appear, which in turn reduces unit spend on solving.

### Summary

| Activity | Unit cost |
|----------|-----------|
| Browser session time | 1 unit/30 seconds |
| Residential proxy traffic | 6 units/MB |
| Datacenter proxy traffic | 2 units/MB |
| Successful CAPTCHA solve | 10 units/solve |

## MCP session cost

When an AI agent uses the [Browserless MCP Server](/mcp/browserless-mcp-server/setup), browser sessions consume units. Agent calls can reuse a kept-alive session rather than opening a new one each time. The longer a session stays open, the more browser-time units it uses. Proxy traffic and CAPTCHA solving add units on top of browser time.

The MCP Server exposes two types of tools:

**Stateful tool** — [`browserless_agent`](/mcp/browser-agent#session-lifetime) is one-shot by default in MCP server v1.33.0+: it closes after a successful command batch and download drain, freeing the concurrency slot. For multiple turns, set `keepSessionAlive: true` on the first call, then echo `sessionId` on later calls. Close the kept session when finished rather than waiting for the 15-minute idle backstop.

**Stateless tools** — each call opens a browser, performs one action, and closes. See the full list of available tools in the [MCP Server documentation](/mcp/browserless-mcp-server/setup).

## Check my consumption

### Account dashboard

The **Cloud Subscription** card on your [account dashboard](https://www.browserless.io/account/dashboard) shows your current billing cycle usage at a glance: units consumed, total allowance, concurrency limit, and cycle reset date.

The dashboard also provides interactive charts with hour, day, and week aggregation so you can spot usage trends over time.

The dashboard also includes a **download icon** (<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{verticalAlign: 'middle'}}><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>) that opens a popup where you can select a timeframe and export your session and unit-consumption data as a CSV or JSON file.

### Usage API

You can fetch your current consumption programmatically:

**cURL:**

```bash
curl --location "https://api.browserless.io/v1/account/usage?token=YOUR_API_TOKEN"
```

**JavaScript:**

```javascript
const TOKEN = "YOUR_API_TOKEN_HERE";

const response = await fetch(
  `https://api.browserless.io/v1/account/usage?token=${TOKEN}`
);
const usage = await response.json();
console.log(usage);
```

**Python:**

```python

TOKEN = "YOUR_API_TOKEN_HERE"

response = requests.get(
    f"https://api.browserless.io/v1/account/usage?token={TOKEN}"
)
print(response.json())
```

This is useful for building your own monitoring dashboards or setting up alerts when consumption approaches your plan limit.

## FAQ & Troubleshooting

<details>
<summary>Are units consumed on failed requests (HTTP 5xx)?</summary>

Units meter browser time actually used, so it depends on when the failure happens. A request rejected before a browser starts (for example a `429` from a full queue or a validation error) consumes nothing. A request that fails mid-run still consumes the browser time used up to the failure, plus any proxy bandwidth already transferred.

</details>

<details>
<summary>Do individual endpoints like <code>/pdf</code> have their own price?</summary>

No. REST endpoints, BaaS connections, and BrowserQL all meter the same way: browser session time in 30-second increments, plus proxy bandwidth and CAPTCHA solves if you use them. A `/pdf` call that finishes in under 30 seconds costs 1 unit.

</details>

<details>
<summary>Do multiple tabs multiply unit consumption?</summary>

No. Units meter how long the browser session is open, not how many tabs or pages it has. Opening several tabs inside one session costs the same as one tab for the same duration, though more tabs usually means the session stays open longer.

</details>

<details>
<summary>Are there hourly or daily unit limits?</summary>

No. Units are a monthly allowance. The per-plan limits that apply moment to moment are concurrency (how many browsers can run at once) and session time; see your [plan details](https://www.browserless.io/pricing) for those caps.

</details>

## Next steps

  <a href="/overview/dashboard" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Dashboard</h3>
      check your current unit consumption and billing cycle
    
  </a>
  <a href="/overview/api-keys" className="next-step-card-link">
    
      <h3 className="next-step-card-title">API Tokens</h3>
      manage the token used to query the Usage API
    
  </a>
  <a href="/browserql/bot-detection/proxies" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Proxies</h3>
      configure residential and datacenter proxies, a major driver of unit cost
    
  </a>

---

# Agent Run API

URL: https://docs.browserless.io/rest-apis/agent-run

> **BETA**
> The Agent Run API is currently in beta. Parameters and response shapes may change in future releases.

> The Agent Run API is only available for Cloud plans. [Contact us for more information here.](https://www.browserless.io/contact/)

Run a managed AI agent that drives a real browser to complete a task described in plain English. The agent plans and executes its own browser steps (navigate, click, type, read) and returns a structured, LLM-ready result. You can optionally constrain the run with a JSON output schema, a starting URL, a navigation allowlist, an authentication profile, or webhook notifications.

The browser and the model are fully managed. You do not supply an AI model key or run a browser yourself.

**Endpoints**

- **Start a run:** `POST /agent/run`
- **Get run status and result:** GET /agent/run/&#123;id&#125;
- **List all runs:** `GET /agent/run`
- **Cancel or delete a run:** DELETE /agent/run/&#123;id&#125;
- Auth: `token` query parameter (`?token=`)
- Content-Type: `application/json`
- Response: `application/json`

- A Browserless API token from your [account
  dashboard](https://browserless.io/account/)

## Quickstart

Submit a task inside the query. The response returns a run ID immediately while the agent runs asynchronously in the background.

  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/agent/run?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{
  "query": "Go to https://example.com and tell me the exact text of the top-level heading."
}'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/agent/run?token=${TOKEN}`;
const headers = {
  "Content-Type": "application/json",
};

const data = {
  query:
    "Go to https://example.com and tell me the exact text of the top-level heading.",
};

const startRun = async () => {
  const response = await fetch(url, {
    method: "POST",
    headers: headers,
    body: JSON.stringify(data),
  });

  const result = await response.json();
  console.log(result);
};

startRun();
```

  
  
**Python:**

```python

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/agent/run?token={TOKEN}"
headers = {
    "Content-Type": "application/json"
}

data = {
    "query": "Go to https://example.com and tell me the exact text of the top-level heading."
}

response = requests.post(url, headers=headers, json=data)
result = response.json()

print(result)
```

  

**Response** — `200 OK`

```js
{
  "id": "run_abc123def456",
  "status": "pending"
}
```

Use the `id` to poll for status and the result via `GET /agent/run/&#123;id&#125;`. The `query` must be at least 10 characters.

## Polling for results

Poll GET /agent/run/&#123;id&#125; on an interval until `status` reaches a terminal value (`succeeded`, `failed`, `timed_out`, or `stopped`). A single request right after submitting usually returns `pending`. Each poll returns the steps the agent has taken so far (short, human-readable rationales), so you can watch its progress.

  
**cURL:**

```sh
# Repeat this request on an interval until "status" is terminal
# (succeeded, failed, timed_out, or stopped).
curl --request GET \
  --url 'https://production-sfo.browserless.io/agent/run/run_abc123def456?token=YOUR_API_TOKEN_HERE'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const runId = "run_abc123def456";
const url = `https://production-sfo.browserless.io/agent/run/${runId}?token=${TOKEN}`;
const TERMINAL = ["succeeded", "failed", "timed_out", "stopped"];

const poll = async () => {
  while (true) {
    const response = await fetch(url);
    const result = await response.json();
    console.log(result.status, result.steps?.at(-1) ?? "");
    if (TERMINAL.includes(result.status)) return result;
    await new Promise((resolve) => setTimeout(resolve, 3000));
  }
};

const result = await poll();
console.log(result.data);
```

  
  
**Python:**

```python

TOKEN = "YOUR_API_TOKEN_HERE"
run_id = "run_abc123def456"
url = f"https://production-sfo.browserless.io/agent/run/{run_id}?token={TOKEN}"
TERMINAL = {"succeeded", "failed", "timed_out", "stopped"}

while True:
    result = requests.get(url).json()
    print(result["status"], (result.get("steps") or [""])[-1])
    if result["status"] in TERMINAL:
        break
    time.sleep(3)

print(result["data"])
```

  

**Response**

```js
{
  "id": "run_abc123def456",
  "status": "succeeded",
  "data": {
    "answer": "The exact text of the top-level heading is: Example Domain"
  },
  "error": null,
  "steps": [
    "Opening the example homepage",
    "Reading the main heading",
    "Closing the completed browser session"
  ],
  "turns": [
    {
      "turnId": "turn_0a1b2c3d4e5f",
      "query": "Go to https://example.com and tell me the exact text of the top-level heading.",
      "status": "succeeded",
      "data": {
        "answer": "The exact text of the top-level heading is: Example Domain"
      },
      "steps": ["Opening the example homepage", "Reading the main heading"],
      "units": 4
    }
  ],
  "created_at": "2025-06-30T10:00:00.000Z",
  "completed_at": "2025-06-30T10:00:18.000Z",
  "expires_at": "2025-07-07T10:00:18.000Z"
}
```

By default `data` is `{ "answer": "<the agent's answer>" }`. When you provide a [`responseSchema`](#structured-output), `data` is shaped to match it instead. Results are retained for **7 days** after completion (see `expires_at`).

## Human handoff

Set `onHumanNeeded: "pause"` when starting a run to let a person handle a login wall or step-up MFA in the same browser session. The default, `"fail"`, ends the run instead of pausing for help.

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/agent/run?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{
    "query": "Sign in to my account and summarize my recent orders.",
    "startUrl": "https://example.com/login",
    "onHumanNeeded": "pause",
    "timeout": 300000
  }'
```

Keep polling the run. `needs_human` is a paused, non-terminal status. Its response includes a `humanRequest` object:

```json
{
  "reason": "step_up_mfa",
  "instructions": "Enter the verification code, then choose Done.",
  "liveURL": "https://production-sfo.browserless.io/live/index.html?i=...",
  "expiresAt": "2026-10-01T12:05:00.000Z"
}
```

Open `humanRequest.liveURL` to interact with the page. The handoff viewer hides its browser navigation controls and resizes the remote viewport to fit the viewer. Its **Instructions** panel contains **Done** and **Couldn't finish**. If there are no custom instructions, the panel shows “Finish interacting with the page, then choose Done.” If the panel is collapsed, use **Show instructions** to reopen it.

![LiveURL Instructions panel requesting an MFA code, with Couldn't finish and Done buttons](/img/agent-human-handoff-instructions.jpg)

- Choose **Done** after completing the requested step. The agent inspects the current page and continues the original task in the same session. This does not mark the run as succeeded; keep polling for its final result.
- Choose **Couldn't finish** if you cannot complete the step. The run fails rather than resuming automation.
- Use `DELETE /agent/run/{id}` to cancel while paused. The run becomes `stopped`.

The original run and browser-session deadlines keep counting down during the handoff. The live URL expires no later than either deadline. Opening the viewer or choosing **Done** does not extend the timeout or reserve time for the resumed agent. There is no `humanTimeoutMs` option or minimum pause/resume window; choose a sufficient `timeout` when starting the run.

### Handoff notifications and outcomes

A webhook can notify your application when the run pauses. With `onHumanNeeded: "pause"`, omitted or empty `webhook.events` includes `needs_human` alongside the terminal events. If you supply a nonempty event list, include `needs_human` explicitly to receive handoff notifications.

A handoff notification has `type: "needs_human"`, `success: null`, the run `id`, and the `reason`, optional `instructions`, `liveURL`, and `expiresAt` fields at the top level. It is not a completion notification: continue polling or wait for a terminal webhook.

After the handoff ends, subsequent poll responses retain only `humanRequest.reason` and `humanRequest.outcome`; the URL, instructions, and link expiry are removed. Outcomes are `resumed`, `userFailed`, `expired`, `stopped`, or `closed`. Use the run's `status` to determine whether it is still active or has finished.

## Structured output

Pass a JSON Schema in `responseSchema` to ask the agent to return its answer in a specific shape. This is best-effort: the agent is instructed to conform to the schema, but conformance is not strictly enforced, so validate the result on your side.

  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/agent/run?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{
  "query": "Go to https://news.ycombinator.com and list the titles of the top 3 stories.",
  "responseSchema": {
    "type": "object",
    "properties": {
      "stories": { "type": "array", "items": { "type": "string" } }
    },
    "required": ["stories"]
  }
}'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/agent/run?token=${TOKEN}`;
const headers = {
  "Content-Type": "application/json",
};

const data = {
  query:
    "Go to https://news.ycombinator.com and list the titles of the top 3 stories.",
  responseSchema: {
    type: "object",
    properties: {
      stories: { type: "array", items: { type: "string" } },
    },
    required: ["stories"],
  },
};

const startRun = async () => {
  const response = await fetch(url, {
    method: "POST",
    headers: headers,
    body: JSON.stringify(data),
  });

  const result = await response.json();
  console.log(result);
};

startRun();
```

  
  
**Python:**

```python

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/agent/run?token={TOKEN}"
headers = {
    "Content-Type": "application/json"
}

data = {
    "query": "Go to https://news.ycombinator.com and list the titles of the top 3 stories.",
    "responseSchema": {
        "type": "object",
        "properties": {
            "stories": {"type": "array", "items": {"type": "string"}}
        },
        "required": ["stories"]
    }
}

response = requests.post(url, headers=headers, json=data)
result = response.json()

print(result)
```

  

## Page captures

Add `formats` to capture the agent's current tab when the run ends. Choose any combination of `html`, `markdown`, `links`, and `screenshot`. Omit it or pass an empty array for no captures; duplicate names are ignored. Captures supplement the agent's `data` and `steps`, rather than replacing them.

Replace `TOKEN` with your API token:

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/agent/run?token=TOKEN' \
  --header 'Content-Type: application/json' \
  --data '{
    "query": "Open https://example.com and tell me the main heading",
    "formats": ["html", "markdown", "links", "screenshot"]
  }'
```

Poll the returned run ID as usual. Its completed result includes these top-level fields, also included in webhook notifications:

```json
{
  "id": "run_abc123def456",
  "status": "succeeded",
  "data": { "answer": "Example Domain" },
  "error": null,
  "steps": ["Opening example.com", "Reading the heading"],
  "html": "<html><body><h1>Example Domain</h1></body></html>",
  "markdown": "Example Domain\n==============",
  "links": [],
  "screenshot": "iVBORw0KGgo…"
}
```

The screenshot is a **visible-viewport PNG encoded as base64**, not a URL or a full-page image. The example abbreviates the base64. Each capture is capped at **5 MiB, or 5,242,880 bytes**; the screenshot cap applies to its base64 string, and the links cap to the serialized JSON array. Oversized captures are omitted, never truncated.

Capture is attempted on any terminal outcome, including failures and timeouts, when the worker-owned browser is still available. Cancellation can become visible before capture finishes. Capture failures do not change the run status or the agent's answer. Captures have a total 15-second budget and are not repeated on polling.

All four fields are always present and nullable. Unrequested formats are `null`. When a requested capture is missing, `formatErrors` maps its name to one of these reasons; the key is omitted when no requested capture is missing. Captures are not repeated inside `turns`.

| Reason | Meaning |
| --- | --- |
| `session_unavailable` | The run has no live worker-owned agent connection to capture from. |
| `blocked_saved_credentials` | Saved credentials were resolved in the browser, so all requested page captures are blocked. |
| `too_large` | This capture exceeded the per-format size cap. |
| `capture_failed` | Capture failed or exceeded the total time budget. |
| `not_captured` | A requested capture has not been stored, for example while the run is still active. |

## Guiding the agent

Steer where the agent starts and how far it can wander. `startUrl` sets the first navigation target, `allowedDomains` restricts navigation to an allowlist, and `maxSteps` bounds how many browser actions the agent may take (default `30`, hard cap `60`).

  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/agent/run?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{
  "query": "Find the current price of the Pro plan.",
  "startUrl": "https://www.browserless.io/pricing",
  "allowedDomains": ["browserless.io"],
  "maxSteps": 15
}'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/agent/run?token=${TOKEN}`;
const headers = {
  "Content-Type": "application/json",
};

const data = {
  query: "Find the current price of the Pro plan.",
  startUrl: "https://www.browserless.io/pricing",
  allowedDomains: ["browserless.io"],
  maxSteps: 15,
};

const startRun = async () => {
  const response = await fetch(url, {
    method: "POST",
    headers: headers,
    body: JSON.stringify(data),
  });

  const result = await response.json();
  console.log(result);
};

startRun();
```

  
  
**Python:**

```python

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/agent/run?token={TOKEN}"
headers = {
    "Content-Type": "application/json"
}

data = {
    "query": "Find the current price of the Pro plan.",
    "startUrl": "https://www.browserless.io/pricing",
    "allowedDomains": ["browserless.io"],
    "maxSteps": 15
}

response = requests.post(url, headers=headers, json=data)
result = response.json()

print(result)
```

  

## Browsing hints

`proxy`, `proxyCountry`, and `stealth` configure the browser session the agent drives. The run applies them when it provisions the session (proxy network, exit country, stealth mode) and repeats them in the agent's instructions. They're fixed for the life of the run; the agent can't change them mid-task. `solveCaptchas` works differently: it tells the agent to use its CAPTCHA-solving tool when it hits one, it is not a session setting.

  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/agent/run?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{
  "query": "Check whether this product is in stock and report the price.",
  "startUrl": "https://www.example-shop.com/product/123",
  "proxy": "residential",
  "proxyCountry": "US",
  "stealth": true,
  "solveCaptchas": true
}'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/agent/run?token=${TOKEN}`;
const headers = {
  "Content-Type": "application/json",
};

const data = {
  query: "Check whether this product is in stock and report the price.",
  startUrl: "https://www.example-shop.com/product/123",
  proxy: "residential",
  proxyCountry: "US",
  stealth: true,
  solveCaptchas: true,
};

const startRun = async () => {
  const response = await fetch(url, {
    method: "POST",
    headers: headers,
    body: JSON.stringify(data),
  });

  const result = await response.json();
  console.log(result);
};

startRun();
```

  
  
**Python:**

```python

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/agent/run?token={TOKEN}"
headers = {
    "Content-Type": "application/json"
}

data = {
    "query": "Check whether this product is in stock and report the price.",
    "startUrl": "https://www.example-shop.com/product/123",
    "proxy": "residential",
    "proxyCountry": "US",
    "stealth": True,
    "solveCaptchas": True
}

response = requests.post(url, headers=headers, json=data)
result = response.json()

print(result)
```

  

## Using a profile

Run against authenticated pages by passing a saved [profile](/baas/features/authenticated-profiles) name in the `profile` field. The agent's browser loads the profile's cookies, `localStorage`, and `IndexedDB` before it starts, so it browses as the logged-in user.

  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/agent/run?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{
  "query": "Open my dashboard and tell me how many open tickets I have.",
  "startUrl": "https://app.example.com/dashboard",
  "profile": "acme-prod"
}'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/agent/run?token=${TOKEN}`;
const headers = {
  "Content-Type": "application/json",
};

const data = {
  query: "Open my dashboard and tell me how many open tickets I have.",
  startUrl: "https://app.example.com/dashboard",
  profile: "acme-prod",
};

const startRun = async () => {
  const response = await fetch(url, {
    method: "POST",
    headers: headers,
    body: JSON.stringify(data),
  });

  const result = await response.json();
  console.log(result);
};

startRun();
```

  
  
**Python:**

```python

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/agent/run?token={TOKEN}"
headers = {
    "Content-Type": "application/json"
}

data = {
    "query": "Open my dashboard and tell me how many open tickets I have.",
    "startUrl": "https://app.example.com/dashboard",
    "profile": "acme-prod"
}

response = requests.post(url, headers=headers, json=data)
result = response.json()

print(result)
```

  

> **Tip**
> Create and manage profiles via the [Authenticated Profiles](/baas/features/authenticated-profiles) workflow. The profile name is scoped to your API token.

## Recording the run

Set `replay: true` to record the run as a [session replay](/baas/monitor-sessions/session-replay). The recording shows up in your account dashboard's [Session Replay](https://browserless.io/account/session-replay) list once the run's browser session closes. Recording is off by default.

Unlike the browsing hints, `replay` never reaches the agent's instructions. The run applies it only when it provisions the browser session. We keep it out of the prompt because whether a run gets recorded is your decision, not the model's.

  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/agent/run?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{
  "query": "Go to https://example.com and tell me the exact text of the top-level heading.",
  "replay": true
}'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/agent/run?token=${TOKEN}`;
const headers = {
  "Content-Type": "application/json",
};

const data = {
  query:
    "Go to https://example.com and tell me the exact text of the top-level heading.",
  replay: true,
};

const startRun = async () => {
  const response = await fetch(url, {
    method: "POST",
    headers: headers,
    body: JSON.stringify(data),
  });

  const result = await response.json();
  console.log(result);
};

startRun();
```

  
  
**Python:**

```python

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/agent/run?token={TOKEN}"
headers = {
    "Content-Type": "application/json"
}

data = {
    "query": "Go to https://example.com and tell me the exact text of the top-level heading.",
    "replay": True
}

response = requests.post(url, headers=headers, json=data)
result = response.json()

print(result)
```

  

**Response** — `200 OK`

```js
{
  "id": "run_abc123def456",
  "status": "pending"
}
```

Session replay records DOM events, not video. It's a different feature from [Screen Recording](/baas/monitor-sessions/screen-recording), which `/agent/run` doesn't support: a client connected to the browser starts and stops screen recording, and this route has no client connection.

> **Caution**
> A run can finish without a recording even when you set `replay: true`. The run normally provisions its own browser session and applies `replay` to it. If that provisioning fails, the run falls back to a session it doesn't control, the flag can't reach that session, and the run completes unrecorded. Neither the run's result nor its webhook payload tells you when this happens.

## Webhook notifications

Receive a callback when a run finishes instead of polling. Two terminal events can fire: `succeeded` (the run finished with a result) and `failed` (the run failed, timed out, or was stopped). Subscribing to both covers every terminal outcome. The non-terminal `needs_human` event reports a pause for human input; see [Handoff notifications and outcomes](#handoff-notifications-and-outcomes). Provide an HTTPS URL and, optionally, the subset of events you want.

  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/agent/run?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{
  "query": "Summarize the latest blog post on browserless.io.",
  "webhook": {
    "url": "https://your-server.com/webhook",
    "events": ["succeeded", "failed"]
  }
}'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/agent/run?token=${TOKEN}`;
const headers = {
  "Content-Type": "application/json",
};

const data = {
  query: "Summarize the latest blog post on browserless.io.",
  webhook: {
    url: "https://your-server.com/webhook",
    events: ["succeeded", "failed"],
  },
};

const startRun = async () => {
  const response = await fetch(url, {
    method: "POST",
    headers: headers,
    body: JSON.stringify(data),
  });

  const result = await response.json();
  console.log(result);
};

startRun();
```

  
  
**Python:**

```python

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/agent/run?token={TOKEN}"
headers = {
    "Content-Type": "application/json"
}

data = {
    "query": "Summarize the latest blog post on browserless.io.",
    "webhook": {
        "url": "https://your-server.com/webhook",
        "events": ["succeeded", "failed"]
    }
}

response = requests.post(url, headers=headers, json=data)
result = response.json()

print(result)
```

  

When the run finishes, Browserless `POST`s the following payload to your webhook URL. For these terminal notifications, `type` is `succeeded` or `failed` (a timed-out or stopped run is delivered as `failed`), and `success` mirrors it as a boolean. A handoff notification instead has `type: "needs_human"` and `success: null`, with the [handoff fields described above](#handoff-notifications-and-outcomes):

```js
{
  "success": true,
  "type": "succeeded",
  "id": "run_abc123def456",
  "data": { "answer": "..." },
  "error": null,
  "html": null,
  "markdown": null,
  "links": null,
  "screenshot": null
}
```

If you requested [page captures](#page-captures), the webhook includes their values in `html`, `markdown`, `links`, and `screenshot`, plus `formatErrors` when a requested capture is missing. Without `formats`, all four values are `null` and `formatErrors` is absent.

The webhook URL must be `https://` and must not resolve to a private or internal network address. If the operator has configured a signing key, deliveries include an `X-Browserless-Signature: sha256=<hmac>` header you can use to verify authenticity.

## Cancelling a run

Send a `DELETE` with the run ID. A pending, running, or `needs_human` run is cancelled (its in-flight work is aborted and the run moves to `stopped`); a run that has already finished is deleted.

**Cancel an active run** (200):

```js
{
  "status": "stopped"
}
```

**Delete a finished run** (200):

```js
{
  "status": "deleted"
}
```

If the run reaches a terminal state at the same moment you cancel, the API returns a `409 Conflict`:

```js
{
  "id": "run_abc123def456",
  "status": "succeeded",
  "message": "Run is already succeeded"
}
```

  
**cURL:**

```sh
curl --request DELETE \
  --url 'https://production-sfo.browserless.io/agent/run/run_abc123def456?token=YOUR_API_TOKEN_HERE'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const runId = "run_abc123def456";
const url = `https://production-sfo.browserless.io/agent/run/${runId}?token=${TOKEN}`;

const cancelRun = async () => {
  const response = await fetch(url, {
    method: "DELETE",
  });

  const result = await response.json();
  console.log(result);
};

cancelRun();
```

  
  
**Python:**

```python

TOKEN = "YOUR_API_TOKEN_HERE"
run_id = "run_abc123def456"
url = f"https://production-sfo.browserless.io/agent/run/{run_id}?token={TOKEN}"

response = requests.delete(url)
result = response.json()

print(result)
```

  

## Listing all runs

List your account's agent runs, newest first. Results are paginated — pass the `nextCursor` from a response to fetch the next page.

**Query parameters**

| Parameter | Type     | Default | Description                                                                                                                      |
| --------- | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `token`   | `string` | —       | Your API token (required).                                                                                                       |
| `limit`   | `number` | `20`    | Results per page (1–100).                                                                                                        |
| `cursor`  | `string` | —       | Opaque pagination cursor from `nextCursor` in a previous response. A malformed cursor is ignored and the first page is returned. |
| `status`  | `string` | —       | Filter by status: `"pending"`, `"running"`, `"succeeded"`, `"failed"`, `"timed_out"`, `"stopped"`, or `"capped"`.                |

  
**cURL:**

```sh
curl --request GET \
  --url 'https://production-sfo.browserless.io/agent/run?token=YOUR_API_TOKEN_HERE&limit=20'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/agent/run?token=${TOKEN}&limit=20`;

const listRuns = async () => {
  const response = await fetch(url);
  const result = await response.json();
  console.log(result);
};

listRuns();
```

  
  
**Python:**

```python

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/agent/run?token={TOKEN}&limit=20"

response = requests.get(url)
result = response.json()

print(result)
```

  

**Response**

```js
{
  "runs": [
    {
      "id": "run_abc123def456",
      "status": "succeeded",
      "created_at": "2025-06-30T10:00:00.000Z",
      "completed_at": "2025-06-30T10:00:18.000Z"
    },
    {
      "id": "run_def456abc789",
      "status": "running",
      "created_at": "2025-06-30T10:05:00.000Z",
      "completed_at": null
    }
  ],
  "nextCursor": "eyJpZCI6InJ1bl9hYmMxMjNkZWY0NTYifQ"
}
```

## Request body

### POST /agent/run

| Field            | Type       | Required | Default  | Description                                                                                                       |
| ---------------- | ---------- | -------- | -------- | ----------------------------------------------------------------------------------------------------------------- |
| `query`          | `string`   | Yes      | —        | The natural-language task for the agent. Trimmed; must be at least 10 characters.                                 |
| `timeout`        | `number`   | No       | `900000` | Per-turn timeout in milliseconds, clamped to `[10000, 900000]` (15-minute hard cap).                              |
| `onHumanNeeded` | `string` | No | `"fail"` | `"pause"` enables [human handoff](#human-handoff) for login walls and step-up MFA without extending the timeout; `"fail"` ends the run instead. |
| `maxSteps`       | `number`   | No       | `30`     | Soft cap on browser actions the agent may take, clamped to `[1, 60]`.                                             |
| `responseSchema` | `object`   | No       | —        | JSON Schema for the agent's final answer. Best-effort — validate the result on your side.                         |
| `formats`        | `string[]` | No       | —        | Page captures: `html`, `markdown`, `links`, `screenshot`. Omit or pass `[]` for none. See [Page captures](#page-captures). |
| `startUrl`       | `string`   | No       | —        | First navigation target. Must be an `http(s)` URL and must not point to a private/internal network address.       |
| `allowedDomains` | `string[]` | No       | —        | Navigation allowlist passed to the agent.                                                                         |
| `proxy`          | `string`   | No       | —        | Route the agent's browser session through a `"residential"` or `"datacenter"` proxy. Applied at session creation. |
| `proxyCountry`   | `string`   | No       | —        | Two-letter ISO country code for the proxy exit (e.g. `"US"`). Used with `proxy`.                                  |
| `stealth`        | `boolean`  | No       | —        | Run the agent's browser session in stealth / anti-bot-detection mode.                                             |
| `solveCaptchas`  | `boolean`  | No       | —        | Instruct the agent to solve CAPTCHAs it encounters (a prompt instruction, not a session setting).                 |
| `profile`        | `string`   | No       | —        | Name of a saved [authentication profile](/baas/features/authenticated-profiles) to load before browsing.          |
| `replay`         | `boolean`  | No       | `false`  | Record the run as a session replay in your account dashboard. See [Recording the run](#recording-the-run).        |
| `webhook`        | `object`   | No       | —        | Webhook configuration for terminal states and human handoffs. See below.                                                |

Unknown query parameters and unknown body fields are rejected with a `400`. `proxy`, `proxyCountry`, and `stealth` configure the agent's browser session; `solveCaptchas`, `startUrl`, `allowedDomains`, and `maxSteps` steer the agent through its instructions (see [Browsing hints](#browsing-hints)). `replay` configures the session only and is never passed to the agent.

### webhook

| Field    | Type       | Required | Default                   | Description                                                                                                                                                                    |
| -------- | ---------- | -------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `url`    | `string`   | Yes      | —                         | HTTPS URL to receive the notification. Cannot point to a private/internal network address.                                                                                     |
| `events` | `string[]` | No       | `["succeeded", "failed"]` (also `"needs_human"` when pausing is enabled) | Which events to deliver. `succeeded` fires when the run completes; `failed` fires when the run fails, times out, or is stopped. `needs_human` fires when the run pauses for human input. Must be a subset of `["succeeded", "failed", "needs_human"]`. |

## Response fields

### POST /agent/run (start)

| Field    | Type     | Description                                                                    |
| -------- | -------- | ------------------------------------------------------------------------------ |
| `id`     | `string` | The unique run ID (prefixed `run_`). Use it to poll for status and the result. |
| `status` | `string` | Initial run status — `"pending"`.                                              |

### GET /agent/run/&#123;id&#125; (status and result)

| Field          | Type             | Description                                                                                                                                                             |
| -------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`           | `string`         | The run ID.                                                                                                                                                             |
| `status`       | `string`         | Run status. See [Run statuses](#run-statuses).                                                                                                                          |
| `data`         | `object \| null` | The agent's final result from the latest turn: `{ "answer": string }` by default, or your `responseSchema` shape when provided. `null` until the run produces a result. |
| `error`        | `string \| null` | Error message if the run failed. `null` on success.                                                                                                                     |
| `humanRequest` | `object` | Present when a handoff has been requested. While paused: `reason` (`"login_wall"` or `"step_up_mfa"`), optional `instructions`, `liveURL`, and ISO 8601 `expiresAt`. Afterward: `reason` and `outcome` only. See [Human handoff](#human-handoff). |
| `steps`        | `string[]`       | Short human-readable rationales streamed as the agent works.                                                                                                            |
| `html`         | `string \| null` | Requested HTML from the agent's final current tab. |
| `markdown`     | `string \| null` | Requested Markdown converted from that HTML. |
| `links`        | `string[] \| null` | Requested links extracted from that HTML. |
| `screenshot`   | `string \| null` | Requested viewport PNG encoded as base64. |
| `formatErrors` | `object` | Present only for missing requested captures; maps format names to [reason codes](#page-captures). |
| `turns`        | `AgentTurn[]`    | The turn transcript. In v1 a run has a single turn. See below.                                                                                                          |
| `created_at`   | `string`         | ISO 8601 timestamp when the run was created.                                                                                                                            |
| `completed_at` | `string \| null` | ISO 8601 timestamp when the run finished. `null` while active.                                                                                                          |
| `expires_at`   | `string \| null` | ISO 8601 timestamp when the result expires (7 days after completion). `null` while active.                                                                              |

### AgentTurn

| Field    | Type             | Description                                                                                                          |
| -------- | ---------------- | -------------------------------------------------------------------------------------------------------------------- |
| `turnId` | `string`         | The turn ID (prefixed `turn_`).                                                                                      |
| `query`  | `string`         | The task submitted for this turn.                                                                                    |
| `status` | `string`         | Turn status: `"running"`, `"succeeded"`, `"failed"`, or `"timed_out"` (`"capped"` is reserved for a future release). |
| `data`   | `object \| null` | The turn's result (same shape as the top-level `data`).                                                              |
| `steps`  | `string[]`       | Step rationales for this turn.                                                                                       |
| `units`  | `number`         | Billing units charged for this turn, derived from the model tokens it consumed.                                      |

### GET /agent/run (list all)

| Field        | Type                 | Description                                                               |
| ------------ | -------------------- | ------------------------------------------------------------------------- |
| `runs`       | `AgentRunListItem[]` | Array of runs, newest first.                                              |
| `nextCursor` | `string \| null`     | Cursor for fetching the next page. `null` when there are no more results. |

### AgentRunListItem

| Field          | Type             | Description                                                       |
| -------------- | ---------------- | ----------------------------------------------------------------- |
| `id`           | `string`         | The run ID.                                                       |
| `status`       | `string`         | Run status.                                                       |
| `created_at`   | `string`         | ISO 8601 timestamp when the run was created.                      |
| `completed_at` | `string \| null` | ISO 8601 timestamp when the run finished. `null` if still active. |

## Run statuses

| Status      | Description                                                                                                                             |
| ----------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `pending`   | Accepted and queued; the agent has not started yet.                                                                                     |
| `running`   | The agent is actively driving the browser.                                                                                              |
| `needs_human` | Paused for human interaction through `humanRequest.liveURL`; the original deadline still applies. |
| `succeeded` | The run completed and produced a result in `data`.                                                                                      |
| `failed`    | The run ended with an error (see `error`).                                                                                              |
| `timed_out` | The run exceeded its `timeout`.                                                                                                         |
| `stopped`   | The run was cancelled via `DELETE`.                                                                                                     |
| `capped`    | Reserved for a future release (strict cost cap). Not emitted in v1, though it is accepted as a `status` filter value when listing runs. |

## Error responses

| Status                    | Description                                                                                                                                                                                                                        |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400 Bad Request`         | Invalid body or query parameters, a `query` shorter than 10 characters, an unknown query/body field, or a task rejected by the relevance/abuse gate (not a browser task, or a prohibited use). Rejected tasks do not create a run. |
| `401 Unauthorized`        | Missing or invalid API token.                                                                                                                                                                                                      |
| `403 Forbidden`           | The API key is not account-scoped. Agent runs are account-owned and require an account-scoped key.                                                                                                                                 |
| `404 Not Found`           | The run ID does not exist or belongs to another token.                                                                                                                                                                             |
| `409 Conflict`            | (`DELETE`) the run reached a terminal state concurrently with your cancel; or the reserved `POST /agent/run/{id}/continue` endpoint, which is not enabled in v1.                                                                   |
| `429 Too Many Requests`   | Concurrent agent-run limit reached for your plan.                                                                                                                                                                                  |
| `503 Service Unavailable` | The agent-run service is suspended, unavailable, or temporarily degraded.                                                                                                                                                          |

## FAQ & Troubleshooting

<details>
<summary>How is this different from the MCP <code>browserless_agent</code> tool?</summary>

They split the reasoning differently. With `/agent/run`, **Browserless's** managed agent does the thinking: you POST a task in natural language, it browses autonomously, and you poll for the structured result. With the [MCP Browser Agent](/mcp/browser-agent), **your** AI model does the thinking: it reads page snapshots, plans, and drives the browser step by step through MCP tool calls. Use `/agent/run` from any HTTP client with no model of your own in the loop; use the MCP tool when an MCP-connected assistant is doing the reasoning.

</details>

<details>
<summary>Why am I getting a <code>401 Unauthorized</code> error?</summary>

Your API token is missing or invalid. Include it as a `?token=` query parameter. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>Why am I getting a <code>403 Forbidden</code> error?</summary>

Agent runs are account-owned and require an account-scoped API key. If your token is not mapped to an account, the API rejects the request with `403`. Contact support if you believe your key should be account-scoped.

</details>

<details>
<summary>My run finished with status <code>succeeded</code> but <code>data.answer</code> is empty</summary>

The agent completed but did not produce answer text — often because the task was ambiguous or the target page could not be reached. Refine the `query`, set a `startUrl`, or inspect `steps` to see what the agent did.

</details>

<details>
<summary>My task was rejected with a <code>400</code> before a run was created</summary>

Every task passes a relevance and abuse gate first. Tasks that don't require a browser (e.g. "write me a poem") and tasks that request a prohibited use are rejected with a `400` and no run is created. Rephrase the task as a concrete browsing objective.

</details>

<details>
<summary>I set <code>replay: true</code> but no recording shows up</summary>

Wait until the run reaches a terminal status, then check [Session Replay](https://browserless.io/account/session-replay) again: the recording uploads only after the run's browser session closes. If it's still missing, the run probably fell back to a session that `replay` can't reach (see [Recording the run](#recording-the-run)). The run's result is unaffected. Send `replay` as a JSON boolean: the API rejects a string such as `"true"` with a `400`.

</details>

## Next steps

  <a href="/rest-apis/crawl" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Crawl API</h3>
      
        asynchronously crawl and scrape a whole site
      
    
  </a>
  <a href="/rest-apis/intro" className="next-step-card-link">
    
      <h3 className="next-step-card-title">REST API overview</h3>
      
        introduction to all available endpoints
      
    
  </a>

---

# API Playground

URL: https://docs.browserless.io/rest-apis/api-playground

The [REST API Playground](https://browserless.io/account/rest-api-playground/) is a dashboard tool for testing Browserless REST API endpoints directly from your browser. It provides a visual interface for building requests, configuring parameters, and viewing responses without writing any code.

![REST API Playground](/img/rest-api-playground.png)

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Features

The API Playground dynamically generates forms based on the OpenAPI specification, making it easy to explore and test the REST endpoints that Browserless exposes. You can select an endpoint, configure the request parameters, execute the request, and view the response all in one place.

The playground supports all REST API endpoints including `/content`, `/download`, `/export`, `/function`, `/pdf`, `/performance`, `/scrape`, `/screenshot`, and `/unblock`. Each endpoint has its own set of configurable parameters that the playground automatically presents based on the API specification.

## Using the Playground

Access the playground at [browserless.io/account/rest-api-playground](https://browserless.io/account/rest-api-playground/). You'll need to be logged into your Browserless account to use it. This is separate from the Live Debugger used for `/function` troubleshooting.

The main interface consists of a URL input field where you enter the target website, an endpoint selector dropdown, and a Run button to execute requests. The response panel on the right displays the API response once you run a request.

For endpoints that accept a URL (like `/screenshot` or `/content`), simply enter the target website URL and click Run. The playground automatically constructs the full API request using your account's API token and the selected endpoint settings.

## Configuration Options

Click the settings icon to access additional configuration options. You can select the browser type (Chromium, Chrome, or Edge), choose your preferred region (San Francisco, London, or Amsterdam), and configure your API token.

The request body button opens a modal where you can configure advanced request parameters like path parameters, headers, and the full JSON request body. This is useful for endpoints that require complex configurations.

## Copying Requests

The playground generates both cURL commands and request summaries that you can copy to your clipboard. This makes it easy to reproduce requests in your terminal or integrate them into your applications. The cURL tab shows the exact command you can run, while the Request tab displays a formatted summary of the request configuration.

## FAQ & Troubleshooting

<details>
<summary>What is the Browserless API Playground?</summary>

The API Playground is an interactive tool in the Browserless dashboard where you can build REST API requests, configure parameters, and see live responses without writing code.

</details>

<details>
<summary>Can I test all REST API endpoints in the Playground?</summary>

Yes. The Playground supports all Browserless REST endpoints including screenshot, PDF, content, scrape, unblock, and more. You can configure request bodies and see formatted responses in real time.

</details>

<details>
<summary>Does the Playground generate code snippets?</summary>

The Playground shows the raw HTTP request you can copy for use in cURL, Python, Node.js, or any HTTP client. This makes it easy to prototype and then integrate into your application.

</details>

## Next steps

  <a href="/rest-apis/request-configuration" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Request Configuration</h3>
      Learn about all available request options
    
  </a>

  <a href="/rest-apis/screenshot-api" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Screenshot API</h3>
      Capture screenshots of web pages
    
  </a>

  <a href="/rest-apis/scrape" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Scrape API</h3>
      Extract structured data from websites
    
  </a>

---

# Content API

URL: https://docs.browserless.io/rest-apis/content

Fetch fully rendered HTML from a real browser, including JavaScript-rendered content. Send a `url` to navigate, or `html` to render inline content.

**Endpoint**
- Method: `POST`
- Path: `/content`
- Auth: `token` query parameter (`?token=`)
- Content-Type: `application/json`
- Response: `text/html`

See the [OpenAPI reference](/open-api/chrome-content) for complete details.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Quickstart

  
**cURL:**

```sh
curl -X POST \
  "https://production-sfo.browserless.io/content?token=YOUR_API_TOKEN_HERE" \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/json' \
  -d '{
  "url": "https://example.com/"
}'
```
  
  
**JavaScript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/content?token=${TOKEN}`;
const headers = {
    "Cache-Control": "no-cache",
    "Content-Type": "application/json"
};
const data = {
    url: "https://example.com/"
};

const fetchContent = async () => {
    const response = await fetch(url, {
        method: 'POST',
        headers: headers,
        body: JSON.stringify(data)
    });

    const content = await response.text();
    console.log(content);
};

fetchContent();
```
  
  
**Python:**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/content?token={TOKEN}"
headers = {
    "Cache-Control": "no-cache",
    "Content-Type": "application/json"
}
data = {
    "url": "https://example.com/"
}

response = requests.post(url, headers=headers, json=data)
print(response.text)
```
  

**Response**

The API returns the fully rendered HTML content as `text/html`:

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <title>Example Domain</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style>
    body{
      background:#eee;
      width:60vw;
      margin:15vh auto;
      font-family:system-ui,sans-serif
    }
    h1{font-size:1.5em}
    div{opacity:0.8}
    a:link,a:visited{color:#348}
  </style>
</head>
<body>
  
    <h1>Example Domain</h1>
    This domain is for use in documentation examples without needing permission. Avoid use in operations.
    <a href="https://iana.org/domains/example">Learn more</a>
  
</body>
</html>
```

## Bot detection troubleshooting

If content is empty or incomplete, the site may be blocking automation. Signs include:

- Empty HTML response or minimal content
- Partial page content missing key elements
- Different content compared to a regular browser
- Blocked requests or access denied messages

The [/unblock API](/rest-apis/unblock) is specifically designed to bypass bot detection mechanisms like Datadome and passive CAPTCHAs. It can return HTML content directly when `content: true` is set and works best with [residential proxies](/rest-apis/proxies).

## Configuration options

The `/content` API supports shared [request configuration options](/rest-apis/request-configuration) that apply across REST endpoints:

- [**Waiting for things**](/rest-apis/request-configuration#waiting-for-things): Wait for [events](/rest-apis/request-configuration#waitforevent), [functions](/rest-apis/request-configuration#waitforfunction), [selectors](/rest-apis/request-configuration#waitforselector), or [timeouts](/rest-apis/request-configuration#waitfortimeout) before returning content
- [**Navigation options**](/rest-apis/request-configuration#navigation-options): Customize navigation behavior with `gotoOptions`
- [**Rejecting undesired requests**](/rest-apis/request-configuration#rejecting-undesired-requests): Block resources with `rejectResourceTypes` and `rejectRequestPattern`
- [**Continue on error**](/rest-apis/request-configuration#continue-on-error): Use `bestAttempt` to continue when async events fail or time out

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>401 Unauthorized</code> error?</summary>

Your API token is missing or invalid. Include it as a `?token=` query parameter or in the `Authorization` header. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My request returns an empty response</summary>

The page may not have finished loading. Increase `waitForTimeout` or use `waitForSelector` to wait for specific content before extracting data.

</details>

## Next steps

  <a href="/rest-apis/intro" className="next-step-card-link">
    
      <h3 className="next-step-card-title">REST API overview</h3>
      introduction to all available endpoints
    
  </a>
  <a href="/rest-apis/api-playground" className="next-step-card-link">
    
      <h3 className="next-step-card-title">API playground</h3>
      test endpoints interactively
    
  </a>

---

# Crawl API

URL: https://docs.browserless.io/rest-apis/crawl

> **BETA**
> The Crawl API is currently in beta. Parameters and response shapes may change in future releases.

> The Crawl API is only available for Cloud plans. [Contact us for more information here.](https://www.browserless.io/contact/)

Asynchronously crawl a website and scrape every discovered page. Submit a starting URL and receive a crawl ID you can poll for status and results. Configure crawl depth, link-following rules, path filters, scrape output formats, and optional webhook notifications. Each scraped page is returned as structured, LLM-ready data.

**Endpoints**
- **Start a crawl:** `POST /crawl`
- **Get crawl status and results:** GET /crawl/&#123;id&#125;
- **List all crawl jobs:** `GET /crawl`
- **Cancel a crawl:** DELETE /crawl/&#123;id&#125;
- Auth: `token` query parameter (`?token=`)
- Content-Type: `application/json`
- Response: `application/json`

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Quickstart

  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/crawl?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{
  "url": "https://www.browserless.io"
}'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/crawl?token=${TOKEN}`;
const headers = {
  "Content-Type": "application/json"
};

const data = {
  url: "https://www.browserless.io"
};

const startCrawl = async () => {
  const response = await fetch(url, {
    method: 'POST',
    headers: headers,
    body: JSON.stringify(data)
  });

  const result = await response.json();
  console.log(result);
};

startCrawl();
```

  
  
**Python:**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/crawl?token={TOKEN}"
headers = {
    "Content-Type": "application/json"
}

data = {
    "url": "https://www.browserless.io"
}

response = requests.post(url, headers=headers, json=data)
result = response.json()

print(result)
```

  

**Response**

```js
{
  "success": true,
  "id": "crawl_abc123def456",
  "url": "https://production-sfo.browserless.io/crawl/crawl_abc123def456"
}
```

The `url` field is a status-check URL. Use it to poll for results via `GET /crawl/&#123;id&#125;`.

## Polling for results

Once you have a crawl ID, poll GET /crawl/&#123;id&#125; to check progress and retrieve scraped pages. Results are paginated. Use the `next` URL to fetch additional pages.

**Query parameters**

| Parameter | Type | Default | Description |
|---|---|---|---|
| `token` | `string` | — | Your API token (required). |
| `skip` | `number` | `0` | Number of pages to skip for pagination (non-negative integer). |

  
**cURL:**

```sh
curl --request GET \
  --url 'https://production-sfo.browserless.io/crawl/crawl_abc123def456?token=YOUR_API_TOKEN_HERE'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const crawlId = "crawl_abc123def456";
const url = `https://production-sfo.browserless.io/crawl/${crawlId}?token=${TOKEN}`;

const getCrawlStatus = async () => {
  const response = await fetch(url);
  const result = await response.json();
  console.log(result);
};

getCrawlStatus();
```

  
  
**Python:**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"
crawl_id = "crawl_abc123def456"
url = f"https://production-sfo.browserless.io/crawl/{crawl_id}?token={TOKEN}"

response = requests.get(url)
result = response.json()

print(result)
```

  

**Response**

```js
{
  "status": "completed",
  "total": 15,
  "completed": 15,
  "failed": 0,
  "expiresAt": "2025-07-01T12:00:00.000Z",
  "next": "https://production-sfo.browserless.io/crawl/crawl_abc123def456?skip=10",
  "data": [
    {
      "status": "completed",
      "contentUrl": "https://crawl-artifacts.s3.us-east-1.amazonaws.com/crawls/crawl_abc123def456/page_0a1b2c3d4e5f.json?X-Amz-Algorithm=AWS4-HMAC-SHA256&...",
      "metadata": {
        "title": "Browserless - Headless Browser Automation",
        "description": "Headless browser automation, without the hosting headaches.",
        "language": "en",
        "scrapedAt": "2025-06-30T10:00:00.000Z",
        "sourceURL": "https://www.browserless.io",
        "statusCode": 200,
        "error": null
      }
    }
    // ...more pages
  ]
}
```

## Depth and link control

Control how deep the crawler follows links and whether it stays within the original domain.

  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/crawl?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{
  "url": "https://www.browserless.io",
  "maxDepth": 3,
  "limit": 50,
  "allowSubdomains": true,
  "allowExternalLinks": false
}'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/crawl?token=${TOKEN}`;
const headers = {
  "Content-Type": "application/json"
};

const data = {
  url: "https://www.browserless.io",
  maxDepth: 3,
  limit: 50,
  allowSubdomains: true,
  allowExternalLinks: false
};

const startCrawl = async () => {
  const response = await fetch(url, {
    method: 'POST',
    headers: headers,
    body: JSON.stringify(data)
  });

  const result = await response.json();
  console.log(result);
};

startCrawl();
```

  
  
**Python:**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/crawl?token={TOKEN}"
headers = {
    "Content-Type": "application/json"
}

data = {
    "url": "https://www.browserless.io",
    "maxDepth": 3,
    "limit": 50,
    "allowSubdomains": True,
    "allowExternalLinks": False
}

response = requests.post(url, headers=headers, json=data)
result = response.json()

print(result)
```

  

## Sitemap control

The `sitemap` parameter controls how the crawler uses XML sitemaps for URL discovery:

| Mode | Description |
|---|---|
| `"auto"` | **(default)** Attempts to use the sitemap if available, falls back to link extraction. |
| `"force"` | Only uses the sitemap for URL discovery. Fails if no sitemap is found. |
| `"skip"` | Ignores the sitemap entirely. Only discovers URLs by following on-page links. |

  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/crawl?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{
  "url": "https://www.browserless.io",
  "sitemap": "force"
}'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/crawl?token=${TOKEN}`;
const headers = {
  "Content-Type": "application/json"
};

const data = {
  url: "https://www.browserless.io",
  sitemap: "force"
};

const startCrawl = async () => {
  const response = await fetch(url, {
    method: 'POST',
    headers: headers,
    body: JSON.stringify(data)
  });

  const result = await response.json();
  console.log(result);
};

startCrawl();
```

  
  
**Python:**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/crawl?token={TOKEN}"
headers = {
    "Content-Type": "application/json"
}

data = {
    "url": "https://www.browserless.io",
    "sitemap": "force"
}

response = requests.post(url, headers=headers, json=data)
result = response.json()

print(result)
```

  

## Path filtering

Use `includePaths` and `excludePaths` to control which URL paths the crawler visits. Both accept arrays of regex patterns matched against the URL path.

  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/crawl?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{
  "url": "https://www.browserless.io",
  "includePaths": ["^/blog"],
  "excludePaths": ["^/blog/draft"]
}'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/crawl?token=${TOKEN}`;
const headers = {
  "Content-Type": "application/json"
};

const data = {
  url: "https://www.browserless.io",
  includePaths: ["^/blog"],
  excludePaths: ["^/blog/draft"]
};

const startCrawl = async () => {
  const response = await fetch(url, {
    method: 'POST',
    headers: headers,
    body: JSON.stringify(data)
  });

  const result = await response.json();
  console.log(result);
};

startCrawl();
```

  
  
**Python:**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/crawl?token={TOKEN}"
headers = {
    "Content-Type": "application/json"
}

data = {
    "url": "https://www.browserless.io",
    "includePaths": ["^/blog"],
    "excludePaths": ["^/blog/draft"]
}

response = requests.post(url, headers=headers, json=data)
result = response.json()

print(result)
```

  

## Scrape options

Control how each crawled page is scraped with the `scrapeOptions` object. Choose output formats, filter content, and set per-page timeouts.

  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/crawl?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{
  "url": "https://www.browserless.io",
  "limit": 10,
  "scrapeOptions": {
    "formats": ["markdown", "html"],
    "onlyMainContent": true,
    "excludeTags": ["nav", "footer"]
  }
}'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/crawl?token=${TOKEN}`;
const headers = {
  "Content-Type": "application/json"
};

const data = {
  url: "https://www.browserless.io",
  limit: 10,
  scrapeOptions: {
    formats: ["markdown", "html"],
    onlyMainContent: true,
    excludeTags: ["nav", "footer"]
  }
};

const startCrawl = async () => {
  const response = await fetch(url, {
    method: 'POST',
    headers: headers,
    body: JSON.stringify(data)
  });

  const result = await response.json();
  console.log(result);
};

startCrawl();
```

  
  
**Python:**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/crawl?token={TOKEN}"
headers = {
    "Content-Type": "application/json"
}

data = {
    "url": "https://www.browserless.io",
    "limit": 10,
    "scrapeOptions": {
        "formats": ["markdown", "html"],
        "onlyMainContent": True,
        "excludeTags": ["nav", "footer"]
    }
}

response = requests.post(url, headers=headers, json=data)
result = response.json()

print(result)
```

  

## Webhook notifications

Receive real-time notifications as pages are scraped or when the crawl completes or fails. Provide an HTTPS URL and choose which events to subscribe to.

  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/crawl?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{
  "url": "https://www.browserless.io",
  "webhook": {
    "url": "https://your-server.com/webhook",
    "events": ["page", "completed", "failed"]
  }
}'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/crawl?token=${TOKEN}`;
const headers = {
  "Content-Type": "application/json"
};

const data = {
  url: "https://www.browserless.io",
  webhook: {
    url: "https://your-server.com/webhook",
    events: ["page", "completed", "failed"]
  }
};

const startCrawl = async () => {
  const response = await fetch(url, {
    method: 'POST',
    headers: headers,
    body: JSON.stringify(data)
  });

  const result = await response.json();
  console.log(result);
};

startCrawl();
```

  
  
**Python:**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/crawl?token={TOKEN}"
headers = {
    "Content-Type": "application/json"
}

data = {
    "url": "https://www.browserless.io",
    "webhook": {
        "url": "https://your-server.com/webhook",
        "events": ["page", "completed", "failed"]
    }
}

response = requests.post(url, headers=headers, json=data)
result = response.json()

print(result)
```

  

## Using a profile

Crawl authenticated pages by passing a saved [profile](/baas/features/authenticated-profiles) via the `?profile=` query parameter. The browser loads the profile's cookies, `localStorage`, and `IndexedDB` before navigating, so every crawled page is accessed as the logged-in user.

  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/crawl?token=YOUR_API_TOKEN_HERE&profile=acme-prod' \
  --header 'Content-Type: application/json' \
  --data '{
  "url": "https://app.example.com/docs",
  "limit": 20,
  "maxDepth": 2
}'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/crawl?token=${TOKEN}&profile=acme-prod`;
const headers = {
  "Content-Type": "application/json"
};

const data = {
  url: "https://app.example.com/docs",
  limit: 20,
  maxDepth: 2
};

const startCrawl = async () => {
  const response = await fetch(url, {
    method: 'POST',
    headers: headers,
    body: JSON.stringify(data)
  });

  const result = await response.json();
  console.log(result);
};

startCrawl();
```

  
  
**Python:**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/crawl?token={TOKEN}&profile=acme-prod"
headers = {
    "Content-Type": "application/json"
}

data = {
    "url": "https://app.example.com/docs",
    "limit": 20,
    "maxDepth": 2
}

response = requests.post(url, headers=headers, json=data)
result = response.json()

print(result)
```

  

> **Tip**
> Create and manage profiles via the [Authenticated Profiles](/baas/features/authenticated-profiles) workflow. The profile name is scoped to your API token. Other tokens cannot access your profiles.

## Cancelling a crawl

Cancel a running crawl by sending a `DELETE` request with the crawl ID. Pages already scraped remain available.

If the crawl is already in a terminal state (`completed`, `failed`, or `cancelled`), the API returns a `409 Conflict`:

```js
{
  "id": "crawl_abc123def456",
  "status": "completed",
  "message": "Crawl is already completed"
}
```

**Success response** (200):

```js
{
  "status": "cancelled"
}
```

  
**cURL:**

```sh
curl --request DELETE \
  --url 'https://production-sfo.browserless.io/crawl/crawl_abc123def456?token=YOUR_API_TOKEN_HERE'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
  const crawlId = "crawl_abc123def456";
  const url = `https://production-sfo.browserless.io/crawl/${crawlId}?token=${TOKEN}`;

  const cancelCrawl = async () => {
  const response = await fetch(url, {
    method: 'DELETE'
  });

  const result = await response.json();
  console.log(result);
};

cancelCrawl();
```

  
  
**Python:**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"
  crawl_id = "crawl_abc123def456"
  url = f"https://production-sfo.browserless.io/crawl/{crawl_id}?token={TOKEN}"

  response = requests.delete(url)
result = response.json()

print(result)
```

  

## Listing all crawls

List all crawl jobs for your account. Results are paginated. Use `nextCursor` to fetch the next page.

**Query parameters**

| Parameter | Type | Default | Description |
|---|---|---|---|
| `token` | `string` | — | Your API token (required). |
| `limit` | `number` | `20` | Results per page (1–100). |
| `cursor` | `string` | — | Opaque pagination cursor from `nextCursor` in a previous response. |
| `status` | `string` | — | Filter by status: `"in-progress"`, `"completed"`, `"failed"`, or `"cancelled"`. |

  
**cURL:**

```sh
curl --request GET \
  --url 'https://production-sfo.browserless.io/crawl?token=YOUR_API_TOKEN_HERE'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/crawl?token=${TOKEN}`;

const listCrawls = async () => {
  const response = await fetch(url);
  const result = await response.json();
  console.log(result);
};

listCrawls();
```

  
  
**Python:**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/crawl?token={TOKEN}"

response = requests.get(url)
result = response.json()

print(result)
```

  

**Response**

```js
{
  "crawls": [
    {
      "id": "crawl_abc123def456",
      "url": "https://www.browserless.io",
      "status": "completed",
      "total": 15,
      "completed": 15,
      "createdAt": "2025-06-30T09:00:00.000Z",
      "completedAt": "2025-06-30T09:05:00.000Z"
    },
    {
      "id": "crawl_def456abc789",
      "url": "https://docs.browserless.io",
      "status": "in-progress",
      "total": 50,
      "completed": 23,
      "createdAt": "2025-06-30T10:00:00.000Z",
      "completedAt": null
    }
  ],
  "nextCursor": "eyJza2lwIjoxMH0"
}
```

## Request body

### POST /crawl

| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| `url` | `string` | Yes | — | The URL to crawl. Must be `http://` or `https://`. |
| `limit` | `number` | No | `100` | Maximum number of pages to crawl (min `1`, clamped to your plan's limit). |
| `maxDepth` | `number` | No | `5` | Maximum link-follow depth from the root URL (0–20). |
| `maxRetries` | `number` | No | `1` | Number of retry attempts per failed page (0–5). |
| `allowExternalLinks` | `boolean` | No | `false` | Whether to follow links to external domains. |
| `allowSubdomains` | `boolean` | No | `false` | Whether to follow links to subdomains of the root URL. |
| `sitemap` | `string` | No | `"auto"` | Sitemap handling strategy: `"auto"`, `"force"`, or `"skip"`. |
| `includePaths` | `string[]` | No | `[]` | Regex patterns for URL paths to include. |
| `excludePaths` | `string[]` | No | `[]` | Regex patterns for URL paths to exclude. |
| `delay` | `number` | No | `200` | Delay between requests in milliseconds (0–10,000). |
| `scrapeOptions` | `object` | No | — | Options controlling how each page is scraped. See below. |
| `webhook` | `object` | No | — | Webhook configuration for crawl event notifications. See below. |

### scrapeOptions

| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| `formats` | `string[]` | No | `["markdown"]` | Output formats: `"markdown"`, `"html"`, `"rawText"` |
| `onlyMainContent` | `boolean` | No | `true` | Whether to extract only the main content of the page. |
| `includeTags` | `string[]` | No | `[]` | HTML tag selectors to include. |
| `excludeTags` | `string[]` | No | `[]` | HTML tag selectors to exclude. |
| `waitFor` | `number` | No | `0` | Time in milliseconds to wait after page load before scraping (0–30,000). |
| `headers` | `object` | No | — | Custom HTTP headers to send with each request. The following headers are blocked and will return a 400 error: `host`, `authorization`, `proxy-authorization`, `cookie`, `set-cookie`, `x-forwarded-for`, `x-real-ip`, `forwarded`. |
| `timeout` | `number` | No | `30000` | Navigation timeout in milliseconds (1,000–180,000). Defaults to your server's configured timeout (30,000ms for cloud). |
| `proxy` | `string` | No | `"residential"` | Proxy network to route page fetches through: `"residential"` (6 units/MB) or `"datacenter"` (2 units/MB). |

### webhook

| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| `url` | `string` | Yes | — | The HTTPS URL to send webhook events to. |
| `events` | `string[]` | No | `["completed"]` | Which events to send: `"page"`, `"completed"`, `"failed"`. Defaults to `["completed"]` when omitted. |

## Response fields

### POST /crawl (start)

| Field | Type | Description |
|---|---|---|
| `success` | `boolean` | Whether the crawl was started successfully. |
| `id` | `string` | The unique crawl job ID. Use this to poll for status and results. |
| `url` | `string` | Status-check URL for polling results via GET /crawl/&#123;id&#125;. |

### GET /crawl/&#123;id&#125; (status and results)

| Field | Type | Description |
|---|---|---|
| `status` | `string` | Crawl status: `"in-progress"`, `"completed"`, `"failed"`, or `"cancelled"`. |
| `total` | `number` | Total number of pages discovered. |
| `completed` | `number` | Number of pages successfully scraped. |
| `failed` | `number` | Number of pages that failed to scrape. |
| `expiresAt` | `string \| null` | ISO 8601 timestamp when the crawl results expire (24 hours after completion). `null` while the crawl is still in progress. |
| `next` | `string \| null` | URL to fetch the next page of results (uses `skip` offset parameter). `null` when all results have been returned. |
| `data` | `CrawlPageResponse[]` | Array of scraped page results. See below. |

### CrawlPageResponse

| Field | Type | Description |
|---|---|---|
| `status` | `string` | Page status: `"queued"`, `"in-progress"`, `"completed"`, `"failed"`, or `"cancelled"`. |
| `contentUrl` | `string \| null` | Pre-signed S3 URL to fetch the full scraped content for this page. Expires after **1 hour**. `null` if the page has not yet completed. |
| `metadata.title` | `string \| null` | Page title. `null` if not extracted. |
| `metadata.description` | `string \| null` | Page meta description. `null` if not extracted. |
| `metadata.language` | `string \| null` | Detected page language. `null` if not detected. |
| `metadata.scrapedAt` | `string \| null` | ISO 8601 timestamp when the page was scraped. `null` if not yet scraped. |
| `metadata.sourceURL` | `string` | The original URL that was scraped. |
| `metadata.statusCode` | `number \| null` | HTTP status code of the page response. `null` if not yet scraped. |
| `metadata.error` | `string \| null` | Error message if the page failed to scrape. `null` on success. |

### GET /crawl (list all)

| Field | Type | Description |
|---|---|---|
| `crawls` | `CrawlListItem[]` | Array of crawl jobs. |
| `nextCursor` | `string \| null` | Cursor for fetching the next page of results. `null` when there are no more results. |

### CrawlListItem

| Field | Type | Description |
|---|---|---|
| `id` | `string` | The crawl job ID. |
| `url` | `string` | The root URL being crawled. |
| `status` | `string` | Crawl status: `"in-progress"`, `"completed"`, `"failed"`, or `"cancelled"`. |
| `total` | `number` | Total pages discovered. |
| `completed` | `number` | Pages successfully scraped. |
| `createdAt` | `string` | ISO 8601 timestamp when the crawl was created. |
| `completedAt` | `string \| null` | ISO 8601 timestamp when the crawl finished. `null` if still running. |

## Error responses

All crawl endpoints may return the following error responses:

| Status | Description |
|---|---|
| `400 Bad Request` | Invalid parameters, unrecognized query params, invalid regex in `includePaths`/`excludePaths`, or blocked headers in `scrapeOptions.headers`. |
| `401 Unauthorized` | Missing or invalid API token. |
| `404 Not Found` | Crawl ID does not exist or belongs to another token. |
| `409 Conflict` | (DELETE only) The crawl is already in a terminal state (`completed`, `failed`, or `cancelled`). |
| `429 Too Many Requests` | Concurrent crawl limit reached for your plan. |
| `503 Service Unavailable` | Crawl service is not running or temporarily degraded. |

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>401 Unauthorized</code> error?</summary>

Your API token is missing or invalid. Include it as a `?token=` query parameter or in the `Authorization` header. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My request returns an empty response</summary>

The page may not have finished loading. Increase `waitForTimeout` or use `waitForSelector` to wait for specific content before extracting data.

</details>

## Next steps

  <a href="/rest-apis/intro" className="next-step-card-link">
    
      <h3 className="next-step-card-title">REST API overview</h3>
      introduction to all available endpoints
    
  </a>
  <a href="/rest-apis/api-playground" className="next-step-card-link">
    
      <h3 className="next-step-card-title">API playground</h3>
      test endpoints interactively
    
  </a>

---

# Download API

URL: https://docs.browserless.io/rest-apis/download

Run custom Puppeteer code and return files that Chrome downloads during execution. The response includes appropriate `Content-Type` and `Content-Disposition` headers. You can load external libraries via `import` and use ESM modules that run inside the browser context.

**Endpoint**
- Method: `POST`
- Path: `/download`
- Auth: `token` query parameter (`?token=`)
- Content-Type: `application/javascript` or `application/json`
- Response: `*/*` (based on downloaded file)

See the [OpenAPI reference](/open-api/chromium-download) for complete details.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Quickstart

The `/download` API accepts requests in two content types: `application/javascript` (raw code) and `application/json` (code + context data).

### Sending code as Javascript

This example navigates to a page with CSV sample files, clicks the first `.csv` download link, and waits for the file to download.

**JS Code**

```js

  await page.goto("https://filesamples.com/formats/csv", {
    waitUntil: "networkidle2",
  });
  await page.click('a[href*=".csv"]');
  await new Promise((r) => setTimeout(r, 5000));
};
```

  
**cURL:**

```sh
curl -s -X POST "https://production-sfo.browserless.io/download?token=YOUR_API_TOKEN_HERE" -H "Content-Type: application/javascript" -d 'export default async({page})=>{await page.goto("https://filesamples.com/formats/csv",{waitUntil:"networkidle2"});await page.click("a[href*=\".csv\"]");await new Promise(r=>setTimeout(r,5000));}' -o sample.csv
```
  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/download?token=${TOKEN}`;

const code = `export default async ({ page }) => {
  await page.goto("https://filesamples.com/formats/csv", {
    waitUntil: "networkidle2",
  });
  await page.click('a[href*=".csv"]');
  await new Promise((r) => setTimeout(r, 5000));
};`;

const response = await fetch(url, {
  method: "POST",
  headers: { "Content-Type": "application/javascript" },
  body: code,
});

const blob = await response.blob();
console.log("Downloaded", blob.size, "bytes");
```
  
  
**Python:**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/download?token={TOKEN}"

code = """export default async ({ page }) => {
  await page.goto("https://filesamples.com/formats/csv", {
    waitUntil: "networkidle2",
  });
  await page.click('a[href*=".csv"]');
  await new Promise((r) => setTimeout(r, 5000));
};"""

response = requests.post(
    url,
    headers={"Content-Type": "application/javascript"},
    data=code,
)

with open("sample.csv", "wb") as f:
    f.write(response.content)
print("Saved sample.csv")
```
  

### Sending code as JSON

You can send a JSON payload with the following fields:

- `code`: String, required — your JavaScript code as a string
- `context`: Object, optional — variables to pass to your function

This example uses `context` to pass the target URL, navigates to the page, and clicks the first `.csv` download link.

**JS Code**

```js

  await page.goto(context.url, { waitUntil: "networkidle2" });
  await page.click('a[href*=".csv"]');
  await new Promise((r) => setTimeout(r, 5000));
};
```

  
**cURL:**

```sh
curl -s -X POST "https://production-sfo.browserless.io/download?token=YOUR_API_TOKEN_HERE" -H "Content-Type: application/json" -d '{"code":"export default async({page,context})=>{await page.goto(context.url,{waitUntil:\"networkidle2\"});await page.click(\"a[href*=\\\".csv\\\"]\");await new Promise(r=>setTimeout(r,5000));}","context":{"url":"https://filesamples.com/formats/csv"}}' -o sample.csv
```
  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/download?token=${TOKEN}`;

const payload = {
  code: `export default async ({ page, context }) => {
    await page.goto(context.url, { waitUntil: "networkidle2" });
    await page.click('a[href*=".csv"]');
    await new Promise((r) => setTimeout(r, 5000));
  };`,
  context: { url: "https://filesamples.com/formats/csv" },
};

const response = await fetch(url, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(payload),
});

const blob = await response.blob();
console.log("Downloaded", blob.size, "bytes");
```
  
  
**Python:**

```python
import requests
import json

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/download?token={TOKEN}"

payload = {
    "code": """export default async ({ page, context }) => {
      await page.goto(context.url, { waitUntil: "networkidle2" });
      await page.click('a[href*=".csv"]');
      await new Promise((r) => setTimeout(r, 5000));
    };""",
    "context": {"url": "https://filesamples.com/formats/csv"},
}

response = requests.post(
    url,
    headers={"Content-Type": "application/json"},
    data=json.dumps(payload),
)

with open("sample.csv", "wb") as f:
    f.write(response.content)
print("Saved sample.csv")
```
  

## Importing libraries

Since the `/download` API uses ESM modules, you can use `import` syntax over HTTP to load modules. For instance, let's try loading the [Faker](https://fakerjs.dev/) module.

**JS Code**

```js

  const rndName = faker.person.fullName();
  const rndEmail = faker.internet.email();

  await page.evaluate(
    (name, email) => {
      const jsonStr = JSON.stringify({ name, email });
      const jsonContent = `data:application/json,${jsonStr}`;
      const encodedUri = encodeURI(jsonContent);
      const link = document.createElement("a");

      link.setAttribute("href", encodedUri);
      link.setAttribute("download", "data.json");
      document.body.appendChild(link);
      return link.click();
    },
    rndName,
    rndEmail
  );
}
```

**cURL**

```sh
curl -X POST "https://production-sfo.browserless.io/download?token=YOUR_API_TOKEN_HERE" -H 'Content-Type: application/javascript' -d 'import{faker}from"https://esm.sh/@faker-js/faker";export default async function({page}){const n=faker.person.fullName();const e=faker.internet.email();await page.evaluate((name,email)=>{const j=JSON.stringify({name,email});const c=`data:application/json,${j}`;const u=encodeURI(c);const l=document.createElement("a");l.setAttribute("href",u);l.setAttribute("download","data.json");document.body.appendChild(l);return l.click();},n,e);}'
```

**Response**

```json
{
  "name": "Jasmine Littel",
  "email": "Giovanna26@hotmail.com"
}
```

## Generate and download a file

If there isn't a file on the site that you need to download, you can create the content you want to download and then trigger a download by creating a link element on the page.

This example navigates to Hacker News, scrapes the top 10 story titles and URLs, creates a JSON blob, and triggers a download.

**JS Code**

```js

  await page.goto("https://news.ycombinator.com/", { waitUntil: "networkidle2" });
  const stories = await page.$$eval(".athing", (rows) =>
    rows.slice(0, 10).map((r) => ({
      title: r.querySelector(".titleline > a")?.textContent || "",
      url: r.querySelector(".titleline > a")?.href || "",
    }))
  );
  await page.evaluate((d) => {
    const a = document.createElement("a");
    a.href = URL.createObjectURL(
      new Blob([JSON.stringify(d, null, 2)], { type: "application/json" })
    );
    a.download = "hn.json";
    document.body.appendChild(a);
    a.click();
  }, stories);
  await new Promise((r) => setTimeout(r, 2000));
};
```

**cURL**

```sh
curl -s -X POST "https://production-sfo.browserless.io/download?token=YOUR_API_TOKEN_HERE" -H "Content-Type: application/javascript" -d 'export default async({page})=>{await page.goto("https://news.ycombinator.com/",{waitUntil:"networkidle2"});const s=await page.$$eval(".athing",r=>r.slice(0,10).map(r=>({title:r.querySelector(".titleline > a")?.textContent||"",url:r.querySelector(".titleline > a")?.href||""})));await page.evaluate(d=>{const a=document.createElement("a");a.href=URL.createObjectURL(new Blob([JSON.stringify(d,null,2)],{type:"application/json"}));a.download="hn.json";document.body.appendChild(a);a.click()},s);await new Promise(r=>setTimeout(r,2000));}' -o hn.json
```

**Response**

```json
[
  {
    "title": "Show HN: A open-source tool for building AI agents",
    "url": "https://example.com/article1"
  },
  {
    "title": "Why Rust is the future of systems programming",
    "url": "https://example.com/article2"
  }
]
```

You can also send the code as a JSON payload. This example scrapes book data from [Books to Scrape](https://books.toscrape.com/), builds a CSV string, and uses `context` to pass the page number.

**JS Code**

```js

  await page.goto(
    "https://books.toscrape.com/catalogue/page-" + context.pageNumber + ".html",
    { waitUntil: "networkidle2" }
  );
  const books = await page.$$eval(".product_pod", (nodes) =>
    nodes.map((e) => ({
      title: e.querySelector("h3 a").getAttribute("title"),
      price: e.querySelector(".price_color").textContent,
    }))
  );
  const csv = ["Title,Price", ...books.map((x) => x.title + "," + x.price)].join(
    "\n"
  );
  await page.evaluate((d) => {
    const a = document.createElement("a");
    a.href = URL.createObjectURL(new Blob([d], { type: "text/csv" }));
    a.download = "books.csv";
    document.body.appendChild(a);
    a.click();
  }, csv);
  await new Promise((r) => setTimeout(r, 2000));
};
```

**cURL**

```sh
curl -s -X POST "https://production-sfo.browserless.io/download?token=YOUR_API_TOKEN_HERE" -H "Content-Type: application/json" -d '{"code":"export default async({page,context})=>{await page.goto(\"https://books.toscrape.com/catalogue/page-\"+context.pageNumber+\".html\",{waitUntil:\"networkidle2\"});const b=await page.$$eval(\".product_pod\",n=>n.map(e=>({title:e.querySelector(\"h3 a\").getAttribute(\"title\"),price:e.querySelector(\".price_color\").textContent})));const csv=[\"Title,Price\",...b.map(x=>x.title+\",\"+x.price)].join(\"\\n\");await page.evaluate(d=>{const a=document.createElement(\"a\");a.href=URL.createObjectURL(new Blob([d],{type:\"text/csv\"}));a.download=\"books.csv\";document.body.appendChild(a);a.click()},csv);await new Promise(r=>setTimeout(r,2000));}","context":{"pageNumber":1}}' -o books.csv
```

**Response**

```text
Title,Price
A Light in the Attic,£51.77
Tipping the Velvet,£53.74
Soumission,£50.10
```

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>401 Unauthorized</code> error?</summary>

Your API token is missing or invalid. Include it as a `?token=` query parameter or in the `Authorization` header. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My request returns an empty response</summary>

The page may not have finished loading. Increase `waitForTimeout` or use `waitForSelector` to wait for specific content before extracting data.

</details>

## Next steps

  <a href="/rest-apis/intro" className="next-step-card-link">
    
      <h3 className="next-step-card-title">REST API overview</h3>
      introduction to all available endpoints
    
  </a>
  <a href="/rest-apis/api-playground" className="next-step-card-link">
    
      <h3 className="next-step-card-title">API playground</h3>
      test endpoints interactively
    
  </a>

---

# Export API

URL: https://docs.browserless.io/rest-apis/export

Fetch a URL and stream the result in its native content type (HTML, PDF, image, etc.). While its capabilities may overlap with the [/download](/rest-apis/download) API, the `/export` API is best suited for downloading files from a URL when you don't know the content type ahead of time. It automatically detects the file type and returns the appropriate response. It can also bundle all resources from a page (HTML, CSS, JavaScript, images, etc.) into a single ZIP file for convenient offline use.

**Endpoint**
- Method: `POST`
- Path: `/export`
- Auth: `token` query parameter (`?token=`)
- Content-Type: `application/json`
- Response: `*/*` (based on upstream content)

See the [OpenAPI reference](/open-api/chrome-export) for complete details.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Quickstart

  
**cURL:**

```bash
curl -X POST "https://production-sfo.browserless.io/export?token=YOUR_API_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf","gotoOptions":{"waitUntil":["networkidle0"]}}' \
  -o output.pdf
```
  
  
**Javascript:**

```js
import { writeFile } from 'fs/promises';

const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/export?token=${TOKEN}`;
const headers = {
  'Content-Type': 'application/json'
};

const data = {
  url: "https://example.com/"
};

const exportPage = async () => {
  const response = await fetch(url, {
    method: 'POST',
    headers: headers,
    body: JSON.stringify(data)
  });

  const contentType = response.headers.get('content-type');

  if (contentType && contentType.includes('text/html')) {
    const content = await response.text();
    await writeFile('page.html', content);
    console.log('Content saved as page.html');
  } else {
    const arrayBuffer = await response.arrayBuffer();
    const buffer = Buffer.from(arrayBuffer);
    await writeFile('page.html', buffer);
    console.log('Content saved as page.html');
  }
};

exportPage();
```
  
  
**Python:**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/export?token={TOKEN}"
headers = {
    'Content-Type': 'application/json'
}

data = {
    "url": "https://example.com/"
}

response = requests.post(url, headers=headers, json=data)

with open("page.html", "wb") as file:
    file.write(response.content)

print("Content saved as page.html")
```
  

**Response**

```http
HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Disposition: attachment; filename="output.pdf"

<binary>
```

For PDFs, images, or other binary content, the API returns the appropriate content type (e.g., `application/pdf`, `image/jpeg`) with a `Content-Disposition: attachment` header. The API returns the content in its native format. For HTML pages, you'll receive the HTML content with `Content-Type: text/html`.

## Handling Different Content Types

The `/export` API automatically detects and returns content in its native format. Here's how to handle different response types in your code:

**HTML Content**

```js
const response = await fetch(url, options);
if (response.headers.get('content-type')?.includes('text/html')) {
  const htmlContent = await response.text();
  // Process HTML content
}
```

**PDF Content**

```js
const response = await fetch(url, options);
if (response.headers.get('content-type')?.includes('application/pdf')) {
  const arrayBuffer = await response.arrayBuffer();
  const pdfBuffer = Buffer.from(arrayBuffer);
  // Save or process PDF buffer
}
```

**Binary Content (Images, etc.)**

```js
const response = await fetch(url, options);
const contentType = response.headers.get('content-type');
if (contentType?.includes('image/') || !contentType?.includes('text/')) {
  const arrayBuffer = await response.arrayBuffer();
  const binaryBuffer = Buffer.from(arrayBuffer);
  // Save or process binary buffer
}
```

## Export with All Resources (Zip File)

When `includeResources` is set to `true`, the API returns a zip file containing the HTML and all linked resources (images, CSS, JavaScript):

```bash
curl -s -X POST "https://production-sfo.browserless.io/export?token=YOUR_API_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://quotes.toscrape.com/","includeResources":true}' \
  -o rest-api-export-quotes.zip
```

**Response**

The API returns a zip file with `Content-Type: application/zip` and `Content-Disposition: attachment` header:

```http
HTTP/1.1 200 OK
Content-Type: application/zip
Content-Disposition: attachment; filename="rest-api-export-quotes.zip"

[Binary zip file content containing HTML and all resources]
```

## Configuration options

The `/export` API supports shared [request configuration options](/rest-apis/request-configuration) that apply across REST endpoints:

- [**Waiting for things**](/rest-apis/request-configuration#waiting-for-things): Wait for [events](/rest-apis/request-configuration#waitforevent), [functions](/rest-apis/request-configuration#waitforfunction), [selectors](/rest-apis/request-configuration#waitforselector), or [timeouts](/rest-apis/request-configuration#waitfortimeout) before exporting
- [**Navigation options**](/rest-apis/request-configuration#navigation-options): Customize navigation behavior with `gotoOptions`
- [**Continue on error**](/rest-apis/request-configuration#continue-on-error): Use `bestAttempt` to continue when async events fail or time out

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>401 Unauthorized</code> error?</summary>

Your API token is missing or invalid. Include it as a `?token=` query parameter or in the `Authorization` header. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My request returns an empty response</summary>

The page may not have finished loading. Increase `waitForTimeout` or use `waitForSelector` to wait for specific content before extracting data.

</details>

## Next steps

  <a href="/rest-apis/intro" className="next-step-card-link">
    
      <h3 className="next-step-card-title">REST API overview</h3>
      introduction to all available endpoints
    
  </a>
  <a href="/rest-apis/api-playground" className="next-step-card-link">
    
      <h3 className="next-step-card-title">API playground</h3>
      test endpoints interactively
    
  </a>

---

# Function API

URL: https://docs.browserless.io/rest-apis/function

Execute custom Puppeteer code via HTTP requests without installing libraries. Your function receives a `page` object (Puppeteer page) and optional `context` data from the JSON body. Return an object with `data` (Buffer, JSON, or plain text) and `type` (Content-Type string) to control the response.

**Endpoint**
- Method: `POST`
- Path: `/function`
- Auth: `token` query parameter (`?token=`)
- Content-Type: `application/javascript` or `application/json`
- Response: `*/*` (based on function return `type`)

See the [OpenAPI reference](/open-api/chrome-function) for complete details.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Quickstart

The `/function` API accepts requests in two content types: `application/javascript` (raw code) and `application/json` (code + context data).

### Sending code as Javascript

**JS Code**

```js

  await page.goto("https://books.toscrape.com/", { waitUntil: "networkidle2" });
  const books = await page.$$eval(".product_pod", (nodes) =>
    nodes.slice(0, 5).map((el) => ({
      title: el.querySelector("h3 a").getAttribute("title"),
      price: el.querySelector(".price_color").textContent,
    }))
  );
  return {
    data: { books },
    type: "application/json",
  };
};
```

  
**cURL:**

```sh
curl -s -X POST "https://production-sfo.browserless.io/function?token=YOUR_API_TOKEN_HERE" -H "Content-Type: application/javascript" -d 'export default async({page})=>{await page.goto("https://books.toscrape.com/",{waitUntil:"networkidle2"});const b=await page.$$eval(".product_pod",n=>n.slice(0,5).map(e=>({title:e.querySelector("h3 a").getAttribute("title"),price:e.querySelector(".price_color").textContent})));return{data:{books:b},type:"application/json"};}'
```
  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/function?token=${TOKEN}`;

const code = `export default async ({ page }) => {
  await page.goto("https://books.toscrape.com/", { waitUntil: "networkidle2" });
  const books = await page.$$eval(".product_pod", (nodes) =>
    nodes.slice(0, 5).map((el) => ({
      title: el.querySelector("h3 a").getAttribute("title"),
      price: el.querySelector(".price_color").textContent,
    }))
  );
  return {
    data: { books },
    type: "application/json",
  };
};`;

const response = await fetch(url, {
  method: "POST",
  headers: { "Content-Type": "application/javascript" },
  body: code,
});

const result = await response.json();
console.log(result);
```
  
  
**Python:**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/function?token={TOKEN}"

code = """export default async ({ page }) => {
  await page.goto("https://books.toscrape.com/", { waitUntil: "networkidle2" });
  const books = await page.$$eval(".product_pod", (nodes) =>
    nodes.slice(0, 5).map((el) => ({
      title: el.querySelector("h3 a").getAttribute("title"),
      price: el.querySelector(".price_color").textContent,
    }))
  );
  return {
    data: { books },
    type: "application/json",
  };
};"""

response = requests.post(
    url,
    headers={"Content-Type": "application/javascript"},
    data=code,
)

result = response.json()
print(result)
```
  

**Response**

```json
{
  "data": {
    "books": [
      { "title": "A Light in the Attic", "price": "£51.77" },
      ...
    ]
  },
  "type": "application/json"
}
```

### Sending code as JSON

You can send a JSON payload with the following fields:

- `code`: String, required — custom function code.
- `context`: Object, optional — value used to pass context values and arguments to the `code`

**JS Code**

```js

  await page.goto(
    `https://quotes.toscrape.com/page/${context.pageNumber}/`,
    { waitUntil: "networkidle2" }
  );
  const quotes = (
    await page.$$eval(".quote", (nodes) =>
      nodes.map((el) => ({
        text: el.querySelector(".text").textContent,
        author: el.querySelector(".author").textContent,
      }))
    )
  ).slice(0, context.maxQuotes);
  return {
    data: { page: context.pageNumber, quotes },
    type: "application/json",
  };
};
```

  
**cURL:**

```sh
curl -s -X POST "https://production-sfo.browserless.io/function?token=YOUR_API_TOKEN_HERE" -H "Content-Type: application/json" -d '{"code":"export default async({page,context})=>{await page.goto(\"https://quotes.toscrape.com/page/\"+context.pageNumber+\"/\",{waitUntil:\"networkidle2\"});const q=(await page.$$eval(\".quote\",n=>n.map(e=>({text:e.querySelector(\".text\").textContent,author:e.querySelector(\".author\").textContent})))).slice(0,context.maxQuotes);return{data:{page:context.pageNumber,quotes:q},type:\"application/json\"};}","context":{"pageNumber":2,"maxQuotes":3}}'
```
  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/function?token=${TOKEN}`;

const code = `export default async ({ page, context }) => {
  await page.goto(
    \`https://quotes.toscrape.com/page/\${context.pageNumber}/\`,
    { waitUntil: "networkidle2" }
  );
  const quotes = (
    await page.$$eval(".quote", (nodes) =>
      nodes.map((el) => ({
        text: el.querySelector(".text").textContent,
        author: el.querySelector(".author").textContent,
      }))
    )
  ).slice(0, context.maxQuotes);
  return {
    data: { page: context.pageNumber, quotes },
    type: "application/json",
  };
};`;

const context = { pageNumber: 2, maxQuotes: 3 };

const response = await fetch(url, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ code, context }),
});

const result = await response.json();
console.log(result);
```
  
  
**Python:**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/function?token={TOKEN}"

code = """export default async ({ page, context }) => {
  await page.goto(
    `https://quotes.toscrape.com/page/${context.pageNumber}/`,
    { waitUntil: "networkidle2" }
  );
  const quotes = (
    await page.$$eval(".quote", (nodes) =>
      nodes.map((el) => ({
        text: el.querySelector(".text").textContent,
        author: el.querySelector(".author").textContent,
      }))
    )
  ).slice(0, context.maxQuotes);
  return {
    data: { page: context.pageNumber, quotes },
    type: "application/json",
  };
};"""

context = {"pageNumber": 2, "maxQuotes": 3}

response = requests.post(
    url,
    headers={"Content-Type": "application/json"},
    json={"code": code, "context": context},
)

result = response.json()
print(result)
```
  

**Response**

```json
{
  "data": {
    "page": 2,
    "quotes": [
      {
        "text": "\u201cThere are only two ways to live your life. One is as though nothing is a miracle. The other is as though everything is a miracle.\u201d",
        "author": "Albert Einstein"
      },
      ...
    ]
  },
  "type": "application/json"
}
```

## Debug your code

Before sending your code through the `/function` API, you can test and troubleshoot it using our **Live Debugger**. It lets you write and execute your Puppeteer code in a browser environment, making it easy to iterate and debug before making API calls.

You can access the Live Debugger directly from your [account page](https://browserless.io/account/debugger/). Once your code is working as expected, simply copy and paste it as the request body and send it with the `application/javascript` content-type header to the `/function` endpoint.

For more details on how to use the BaaS Debugger and its features, see our [Dashboard documentation](/overview/dashboard#baas-debugger).

## Importing libraries

Since the `/function` API uses ESM modules, you can use `import` syntax over HTTP to load modules. For instance, let's try loading the [Faker](https://fakerjs.dev/) module.

**JS Code**

```js

  const Internet = faker.internet;
  const rndData = [...Array(5)].map(() => ({
    domain: Internet.domainName(),
    ip: Internet.ip(),
    mac: Internet.mac(),
    protocol: Internet.protocol(),
  }));

  return {
    data: { domains: rndData },
    type: "application/json",
  };
}
```

**cURL**

```sh
curl -X POST "https://production-sfo.browserless.io/function?token=YOUR_API_TOKEN_HERE" -H 'Content-Type: application/javascript' -d 'import{faker}from"https://esm.sh/@faker-js/faker";export default async function(){const I=faker.internet;const d=[...Array(5)].map(()=>({domain:I.domainName(),ip:I.ip(),mac:I.mac(),protocol:I.protocol()}));return{data:{domains:d},type:"application/json"};};'
```

## Returning files

The `/function` API can return other file types beyond JSON, such as PDF files. In this example, we'll generate a PDF of [Hacker News](https://news.ycombinator.com/) using the built-in `page.pdf()` method.

**JS Code**

```js

  await page.goto("https://news.ycombinator.com/", { waitUntil: "networkidle2" });
  return await page.pdf({ printBackground: true });
};
```

**cURL**

```sh
curl -s -X POST "https://production-sfo.browserless.io/function?token=YOUR_API_TOKEN_HERE" -H "Content-Type: application/javascript" -d 'export default async({page})=>{await page.goto("https://news.ycombinator.com/",{waitUntil:"networkidle2"});return await page.pdf({printBackground:true});}' -o hn.pdf
```

## Exception handling

When working with the `/function` API, you can catch exceptions and handle errors gracefully. This is particularly useful for debugging and providing meaningful error responses when automation fails.

The following example shows how to implement error handling, allowing you to:

- Capture the error message when an automation step fails
- Take a screenshot when an error occurs for visual debugging
- Handle screenshot capture errors gracefully
- Return both successful data and error information in a structured format

**JS Code**

```js

  let error;
  let title;
  try {
    await page.goto("https://books.toscrape.com/", { waitUntil: "networkidle2" });
    title = await page.title();
    await page.click(".nonexistent");
  } catch (err) {
    error = { message: err.message };
    try {
      error.screenshot = await page.screenshot({ encoding: "base64" });
    } catch (_) {}
  }
  return {
    data: { title, error },
    type: "application/json",
  };
};
```

**cURL**

```sh
curl -s -X POST "https://production-sfo.browserless.io/function?token=YOUR_API_TOKEN_HERE" -H "Content-Type: application/javascript" -d 'export default async({page})=>{let e,t;try{await page.goto("https://books.toscrape.com/",{waitUntil:"networkidle2"});t=await page.title();await page.click(".nonexistent");}catch(r){e={message:r.message};try{e.screenshot=await page.screenshot({encoding:"base64"});}catch(x){};}return{data:{title:t,error:e},type:"application/json"};};'
```

> **Session Persistence**
> The `/function` API, like all REST APIs, automatically closes the browser session after execution completes. If you need to persist sessions or keep browsers alive between requests, consider using [BaaS with session management](/baas/session-management/standard-sessions) or [BrowserQL with the reconnect mutation](https://docs.browserless.io/browserql/session-management/reconnect-to-Browserless).

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>401 Unauthorized</code> error?</summary>

Your API token is missing or invalid. Include it as a `?token=` query parameter or in the `Authorization` header. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My request returns an empty response</summary>

The page may not have finished loading. Increase `waitForTimeout` or use `waitForSelector` to wait for specific content before extracting data.

</details>

## Next steps

  <a href="/rest-apis/intro" className="next-step-card-link">
    
      <h3 className="next-step-card-title">REST API overview</h3>
      introduction to all available endpoints
    
  </a>
  <a href="/rest-apis/api-playground" className="next-step-card-link">
    
      <h3 className="next-step-card-title">API playground</h3>
      test endpoints interactively
    
  </a>

---

# REST APIs

URL: https://docs.browserless.io/rest-apis/intro

Browserless REST APIs provide HTTP endpoints for common browser tasks like [screenshots](/rest-apis/screenshot-api), [PDFs](/rest-apis/pdf-api), [content scraping](/rest-apis/scrape), [file downloads](/rest-apis/download), [function execution](/rest-apis/function), and [website unblocking](/rest-apis/unblock). Use them when you want a single HTTP request to do one browser task without managing browser infrastructure.

For a working example, see the [Quickstart](/rest-apis/quick-start).

## Which API Should I Use?

| API | Best For | Input | Output |
|-----|----------|-------|--------|
| [/smart-scrape](/rest-apis/smart-scrape) | Scraping a page with automatic fallbacks for blocked or JavaScript-heavy sites | URL + optional output formats | `application/json` |
| [/content](/rest-apis/content) | Getting fully rendered HTML, including JS-rendered content | URL or raw HTML | `text/html` |
| [/scrape](/rest-apis/scrape) | Extracting structured data with CSS selectors | URL + `elements` array of CSS selectors | `application/json` |
| [/screenshot](/rest-apis/screenshot-api) | Capturing full-page or viewport screenshots | URL or raw HTML + Puppeteer `options` | `image/png`, `image/jpeg`, or `image/webp` |
| [/pdf](/rest-apis/pdf-api) | Generating PDF documents from pages | URL or raw HTML + Puppeteer `options` | `application/pdf` |
| [/search](/rest-apis/search) | Searching the web and optionally scraping result pages | Query + optional sources and scrape options | `application/json` |
| [/map](/rest-apis/map) | Discovering URLs on a site or sitemap | Base URL + optional search and sitemap settings | `application/json` |
| [/function](/rest-apis/function) | Running custom Puppeteer code server-side | JavaScript code (+ optional `context` object) | Any (based on function return `type`) |
| [/download](/rest-apis/download) | Retrieving files that Chrome downloads during execution | JavaScript code (+ optional `context` object) | Any (matches downloaded file) |
| [/export](/rest-apis/export) | Fetching a URL and streaming it in its native content type | URL | Any (native content type, or zip with `includeResources`) |
| [/unblock](/rest-apis/unblock) | Bypassing CAPTCHAs and bot detection | URL + data flags (`content`, `cookies`, `screenshot`, `browserWSEndpoint`) | `application/json` (HTML, cookies, screenshot, WebSocket endpoint) |
| [/performance](/rest-apis/performance) | Running Lighthouse audits for SEO, accessibility, and speed | URL + optional Lighthouse `config` | `application/json` (Lighthouse metrics) |
| [/crawl](/rest-apis/crawl) | Asynchronously crawling an entire site and scraping every page | URL + depth, path filters, scrape options | `application/json` (structured page data) |

## REST API Limitations

REST APIs are stateless, single-action endpoints. Each request launches a browser, performs one task, and closes the session. Keep these constraints in mind:

- **No multi-step workflows** — you cannot click a button, fill a form, and then scrape the result in a single request. Each call is independent (except through `/function`, which still runs in a single session).
- **No session persistence** — cookies and state are discarded after every response. Use [BaaS sessions](/baas/session-management/standard-sessions) or [BrowserQL persisted state](/browserql/session-management/persisting-state) if you need to keep a browser alive between requests.
- **Limited bot-detection bypass** — `/unblock` handles basic protections, but sites with advanced fingerprinting or interactive CAPTCHAs may still block requests. Use [BrowserQL](/browserql/start) for advanced stealth and CAPTCHA solving.
- **No real-time interaction** — you cannot observe the page, react to changes, or branch logic mid-execution. To prototype and debug Puppeteer code with a live browser view, use the [BaaS Debugger](/overview/dashboard#baas-debugger) in your Browserless dashboard, then deploy the working code via the `/function` API. The [REST API Playground](/rest-apis/api-playground) is for building and testing REST requests, not for debugging `/function` code live.

## Authentication

All REST API requests require an API token. Include your token as a query parameter:

```bash
https://production-sfo.browserless.io/scrape?token=YOUR_API_TOKEN_HERE
```

Get your API token from the [Browserless dashboard](https://browserless.io/account/).

> **Bot Detection**
> For sites with sophisticated bot detection, use [BrowserQL](/browserql/start) which provides advanced stealth capabilities and CAPTCHA solving.

## FAQ & Troubleshooting

<details>
<summary>What can I do with Browserless REST APIs?</summary>

Browserless REST APIs let you take screenshots, generate PDFs, extract rendered HTML, scrape structured data, download files, run Lighthouse audits, and unblock protected sites. Each is a simple HTTP POST request.

</details>

<details>
<summary>Do I need Puppeteer or Playwright to use the REST APIs?</summary>

No. REST APIs are standalone HTTP endpoints. Send a POST request with your parameters and receive the result directly. No browser libraries, WebSocket connections, or SDKs required.

</details>

<details>
<summary>Which REST API should I use for web scraping?</summary>

Use the /content endpoint for full rendered HTML, /scrape for structured JSON extraction via CSS selectors, or /smart-scrape for an intelligent cascading approach that tries HTTP first and falls back to a full browser.

</details>

<details>
<summary>Can REST APIs handle bot-protected sites?</summary>

Yes. Use the /unblock endpoint or add stealth and proxy parameters to any REST API call. Browserless applies fingerprint randomization and CAPTCHA solving server-side.

</details>

## Next steps

  <a href="/rest-apis/quick-start" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Quickstart</h3>
      Run your first REST API request in cURL, Node.js, or Python
    
  </a>

  <a href="/rest-apis/launch-parameters" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch Parameters</h3>
      Configure stealth mode, proxies, and browser options
    
  </a>

  <a href="/rest-apis/request-configuration" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Request Configuration</h3>
      Configure navigation, resource blocking, and error handling
    
  </a>

  <a href="/rest-apis/timeouts" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Timeout Configuration</h3>
      Control request timeouts and navigation waits
    
  </a>

---

# Launch Parameters and Options

URL: https://docs.browserless.io/rest-apis/launch-parameters

# Launch Parameters

Launch parameters let you configure the browser environment for every REST API call (`/pdf`, `/screenshot`, `/content`, `/scrape`, etc.).
For BrowserQL and BaaS v2 launch configuration, see [BrowserQL Launch Parameters](/browserql/launch-parameters) and [BaaS Launch Parameters](/baas/launch-options).

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## How to Pass Launch Parameters

### As Query String Parameters

Append individual parameters directly to the request URL. This is the simplest approach for toggling a few options:

```bash
https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE&headless=false&stealth=true
```

### As a JSON Launch Payload

For complex configurations, especially when passing Chrome flags, use a single `&launch=` parameter containing a JSON object. Here's the raw JSON for readability:

```json
{
  "headless": false,
  "stealth": true,
  "args": ["--window-size=1920,1080"]
}
```

#### Why you must encode the JSON

JSON contains characters like `{`, `"`, and `:` that have special meaning in URLs. If you pass raw JSON directly in a query string, the server won't parse it correctly. You need to either **URL-encode** or **base64-encode** the JSON before sending it.

URL-encoded, the above JSON becomes:

```bash
%7B%22headless%22%3Afalse%2C%22stealth%22%3Atrue%2C%22args%22%3A%5B%22--window-size%3D1920%2C1080%22%5D%7D
```

And the full request URL looks like:

```bash
https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE&launch=%7B%22headless%22%3Afalse%2C%22stealth%22%3Atrue%2C%22args%22%3A%5B%22--window-size%3D1920%2C1080%22%5D%7D
```

#### How to encode in code

Most languages handle encoding for you. You just need to pass the JSON string through the right function:

  
**cURL:**

```sh
# Use --data-urlencode to let cURL handle encoding
LAUNCH='{"headless":false,"stealth":true,"args":["--window-size=1920,1080"]}'

curl -X POST \
  "https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE" \
  --data-urlencode "launch=$LAUNCH" \
  -H 'Content-Type: application/json' \
  -d '{"url": "https://example.com"}' \
  -o screenshot.png
```

  
  
**JavaScript:**

```js
// URLSearchParams automatically URL-encodes values
const launchOptions = {
  headless: false,
  stealth: true,
  args: ["--window-size=1920,1080"],
};

const params = new URLSearchParams({
  token: "YOUR_API_TOKEN_HERE",
  launch: JSON.stringify(launchOptions),
});

const url = `https://production-sfo.browserless.io/screenshot?${params}`;
```

  
  
**Python:**

```python
import json
from urllib.parse import urlencode

# urlencode handles URL-encoding the JSON string
launch_options = {
    "headless": False,
    "stealth": True,
    "args": ["--window-size=1920,1080"],
}

params = urlencode({
    "token": "YOUR_API_TOKEN_HERE",
    "launch": json.dumps(launch_options),
})

url = f"https://production-sfo.browserless.io/screenshot?{params}"
```

  

> **Tip**
> You can also **base64-encode** the JSON payload and pass it as `&launch=<base64string>`. This is an alternative to URL-encoding that some developers find easier to work with.

### Combining Both Methods

When you use both query parameters and a `launch` payload in the same request, Browserless merges them. **Individual query parameters take precedence**.

For example, consider this request (shown unencoded for readability):

```bash
/screenshot?token=YOUR_API_TOKEN_HERE&headless=true&launch={"headless":false,"stealth":true}
```

The effective configuration would be:

```json
{
  "headless": true,
  "stealth": true
}
```

The `headless=true` query parameter overrides the `"headless": false` inside the `launch` JSON.

## Parameter Reference

Not all parameters are supported by every REST API endpoint. Check the [REST API reference](/open-api/chrome-content) to verify which parameters are available for the specific endpoint you're using. The table below lists all available launch parameters:

### Restricting ad-block rulesets (`blockAdsInclude`)

When `blockAds=true`, Chrome compiles all of uBlock Origin Lite's filter rulesets
on every browser launch. `blockAdsInclude` loads only the rulesets you specify,
lowering that startup cost under load. It is opt-in — omit it and `blockAds=true`
loads the full extension, unchanged.

```text
?blockAds=true&blockAdsInclude=ublock-filters,easylist,easyprivacy,pgl,ublock-badware,urlhaus-full
```

The 6 ids above are the recommended, language-neutral core set (ads + trackers +
malware) and lose no meaningful coverage for typical traffic. Add specific locale
lists only if you scrape sites in that language. See the [BrowserQL launch
parameters](/browserql/launch-parameters#restricting-ad-block-rulesets-blockadsinclude)
page for the full ruleset breakdown.

## Common Recipes

### Scraping a Bot-Protected Site

Combine stealth mode with a proxy and ad blocking to maximize your chances of bypassing bot detection. Use `proxy=residential` for maximum stealth or `proxy=datacenter` for lower cost:

```sh
curl -X POST \
  "https://production-sfo.browserless.io/scrape?token=YOUR_API_TOKEN_HERE&stealth=true&proxy=residential&proxyCountry=us&blockAds=true" \
  -H 'Content-Type: application/json' \
  -d '{
    "url": "https://example.com",
    "elements": [{ "selector": "h1" }]
  }'
```

### Generating a Clean PDF

Strip ads and cookie banners so your PDFs render cleanly:

```sh
curl -X POST \
  "https://production-sfo.browserless.io/pdf?token=YOUR_API_TOKEN_HERE&blockAds=true&blockConsentModals=true" \
  -H 'Content-Type: application/json' \
  -d '{
    "url": "https://example.com",
    "options": {
      "format": "A4",
      "printBackground": true
    }
  }' \
  -o clean-output.pdf
```

### Using Your Own Proxy

Route traffic through your own proxy server by passing credentials in the URL:

```sh
curl -X POST \
  "https://production-sfo.browserless.io/content?token=YOUR_API_TOKEN_HERE&externalProxyServer=http://user:pass@proxy.example.com:8080" \
  -H 'Content-Type: application/json' \
  -d '{
    "url": "https://example.com"
  }'
```

## Full Example

A realistic scenario: taking a screenshot of a bot-protected site using stealth mode, a US residential proxy, and ad blocking.

  
**cURL:**

```sh
curl -X POST \
  "https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE&stealth=true&proxy=residential&proxyCountry=us&blockAds=true&blockConsentModals=true" \
  -H 'Content-Type: application/json' \
  -d '{
    "url": "https://example.com",
    "options": {
      "type": "png",
      "fullPage": true
    }
  }' \
  -o screenshot.png
```

  
  
**JavaScript:**

```js
import fs from "fs/promises";

const TOKEN = "YOUR_API_TOKEN_HERE";
const params = new URLSearchParams({
  token: TOKEN,
  stealth: "true",
  proxy: "residential",
  proxyCountry: "us",
  blockAds: "true",
  blockConsentModals: "true",
});

const takeScreenshot = async () => {
  const response = await fetch(
    `https://production-sfo.browserless.io/screenshot?${params}`,
    {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        url: "https://example.com",
        options: {
          type: "png",
          fullPage: true,
        },
      }),
    }
  );

  if (!response.ok) {
    const text = await response.text();
    throw new Error(`HTTP ${response.status}: ${text}`);
  }

  const buffer = Buffer.from(await response.arrayBuffer());
  await fs.writeFile("screenshot.png", buffer);
  console.log("Screenshot saved as screenshot.png");
};

takeScreenshot().catch(console.error);
```

  
  
**Python:**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"
params = {
    "token": TOKEN,
    "stealth": "true",
    "proxy": "residential",
    "proxyCountry": "us",
    "blockAds": "true",
    "blockConsentModals": "true",
}

payload = {
    "url": "https://example.com",
    "options": {
        "type": "png",
        "fullPage": True,
    },
}

response = requests.post(
    "https://production-sfo.browserless.io/screenshot",
    params=params,
    json=payload,
)
response.raise_for_status()

with open("screenshot.png", "wb") as f:
    f.write(response.content)

print("Screenshot saved as screenshot.png")
```

  

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>401 Unauthorized</code> error?</summary>

Your API token is missing or invalid. Include it as a `?token=` query parameter or in the `Authorization` header. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My request returns an empty response</summary>

The page may not have finished loading. Increase `waitForTimeout` or use `waitForSelector` to wait for specific content before extracting data.

</details>

## Next steps

  <a href="/rest-apis/intro" className="next-step-card-link">
    
      <h3 className="next-step-card-title">REST API overview</h3>
      introduction to all available endpoints
    
  </a>
  <a href="/rest-apis/api-playground" className="next-step-card-link">
    
      <h3 className="next-step-card-title">API playground</h3>
      test endpoints interactively
    
  </a>

---

# Map API

URL: https://docs.browserless.io/rest-apis/map

> The Map API is only available for Cloud plans. [Contact us for more information here.](https://www.browserless.io/contact/)

Discover all URLs on a website. Send a base URL and receive a deduplicated list of pages with optional titles and descriptions. Use the `search` parameter to rank results by relevance, and `sitemap` to control whether discovery uses the site's XML sitemap, on-page links, or both. To search the open web rather than a single site, see the [Search API](/rest-apis/search). For extracting page content from discovered URLs, use the [Smart Scrape API](/rest-apis/smart-scrape).

**Endpoint**
- Method: `POST`
- Path: `/map`
- Auth: `token` query parameter (`?token=`)
- Content-Type: `application/json`
- Response: `application/json`

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Quickstart

  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/map?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{
  "url": "https://www.browserless.io"
}'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/map?token=${TOKEN}`;
const headers = {
  "Content-Type": "application/json"
};

const data = {
  url: "https://www.browserless.io"
};

const mapSite = async () => {
  const response = await fetch(url, {
    method: 'POST',
    headers: headers,
    body: JSON.stringify(data)
  });

  const result = await response.json();
  console.log(result);
};

mapSite();
```

  
  
**Python:**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/map?token={TOKEN}"
headers = {
    "Content-Type": "application/json"
}

data = {
    "url": "https://www.browserless.io"
}

response = requests.post(url, headers=headers, json=data)
result = response.json()

print(result)
```

  

**Response**

```js
{
  "success": true,
  "links": [
    {
      "url": "https://www.browserless.io",
      "title": "Browserless - Headless Browser Automation",
      "description": "Headless browser automation, without the hosting headaches."
    },
    {
      "url": "https://www.browserless.io/pricing",
      "title": "Pricing",
      "description": "Simple, transparent pricing for headless browser automation."
    },
    {
      "url": "https://www.browserless.io/blog",
      "title": "Blog",
      "description": "Insights, tips, and updates on headless browser automation."
    }
    // ...more URLs up to the specified limit
  ]
}
```

## Search relevance

Use the `search` parameter to order results by relevance to a query. This is useful when you only need URLs related to a specific topic from a large site.

  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/map?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{
  "url": "https://www.browserless.io",
  "search": "pricing",
  "limit": 10
}'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/map?token=${TOKEN}`;
const headers = {
  "Content-Type": "application/json"
};

const data = {
  url: "https://www.browserless.io",
  search: "pricing",
  limit: 10
};

const mapSite = async () => {
  const response = await fetch(url, {
    method: 'POST',
    headers: headers,
    body: JSON.stringify(data)
  });

  const result = await response.json();
  console.log(result);
};

mapSite();
```

  
  
**Python:**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/map?token={TOKEN}"
headers = {
    "Content-Type": "application/json"
}

data = {
    "url": "https://www.browserless.io",
    "search": "pricing",
    "limit": 10
}

response = requests.post(url, headers=headers, json=data)
result = response.json()

print(result)
```

  

**Response**

```js
{
  "success": true,
  "links": [
    {
      "url": "https://www.browserless.io/pricing",
      "title": "Pricing",
      "description": "Simple, transparent pricing for headless browser automation."
    },
    {
      "url": "https://www.browserless.io/blog/pricing-update"
    }
    // URLs ordered by relevance to "pricing"
  ]
}
```

## Sitemap control

The `sitemap` parameter controls how the API uses sitemaps for URL discovery:

| Mode | Description |
|---|---|
| `"include"` | **(default)** Uses both sitemap and on-page links for discovery. |
| `"skip"` | Ignores the sitemap. Only discovers URLs found on the page itself. |
| `"only"` | Returns only URLs listed in the sitemap. |

  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/map?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{
  "url": "https://www.browserless.io",
  "sitemap": "only"
}'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/map?token=${TOKEN}`;
const headers = {
  "Content-Type": "application/json"
};

const data = {
  url: "https://www.browserless.io",
  sitemap: "only"
};

const mapSite = async () => {
  const response = await fetch(url, {
    method: 'POST',
    headers: headers,
    body: JSON.stringify(data)
  });

  const result = await response.json();
  console.log(result);
};

mapSite();
```

  
  
**Python:**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/map?token={TOKEN}"
headers = {
    "Content-Type": "application/json"
}

data = {
    "url": "https://www.browserless.io",
    "sitemap": "only"
}

response = requests.post(url, headers=headers, json=data)
result = response.json()

print(result)
```

  

## Geo-targeting

Use the `location` parameter to route requests through a proxy in a specific country. This is useful for discovering region-specific URLs or bypassing geographic restrictions.

  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/map?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{
  "url": "https://example.com",
  "location": {
    "country": "gb"
  }
}'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/map?token=${TOKEN}`;
const headers = {
  "Content-Type": "application/json"
};

const data = {
  url: "https://example.com",
  location: {
    country: "gb"
  }
};

const mapSite = async () => {
  const response = await fetch(url, {
    method: 'POST',
    headers: headers,
    body: JSON.stringify(data)
  });

  const result = await response.json();
  console.log(result);
};

mapSite();
```

  
  
**Python:**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/map?token={TOKEN}"
headers = {
    "Content-Type": "application/json"
}

data = {
    "url": "https://example.com",
    "location": {
        "country": "gb"
    }
}

response = requests.post(url, headers=headers, json=data)
result = response.json()

print(result)
```

  

## Filtering options

Control which URLs are included in results:

  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/map?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{
  "url": "https://www.browserless.io",
  "limit": 100,
  "includeSubdomains": false,
  "ignoreQueryParameters": true
}'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/map?token=${TOKEN}`;
const headers = {
  "Content-Type": "application/json"
};

const data = {
  url: "https://www.browserless.io",
  limit: 100,
  includeSubdomains: false,
  ignoreQueryParameters: true
};

const mapSite = async () => {
  const response = await fetch(url, {
    method: 'POST',
    headers: headers,
    body: JSON.stringify(data)
  });

  const result = await response.json();
  console.log(result);
};

mapSite();
```

  
  
**Python:**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/map?token={TOKEN}"
headers = {
    "Content-Type": "application/json"
}

data = {
    "url": "https://www.browserless.io",
    "limit": 100,
    "includeSubdomains": False,
    "ignoreQueryParameters": True
}

response = requests.post(url, headers=headers, json=data)
result = response.json()

print(result)
```

  

## Request body

| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| `url` | `string` | Yes | — | The base URL to discover links from. Must be `http://` or `https://`. |
| `search` | `string` | No | — | Search query to order results by relevance. |
| `limit` | `number` | No | `5000` | Maximum number of URLs to return. Must be between 1 and 5000. |
| `timeout` | `number` | No | Server default | Request timeout in milliseconds. Capped at the server-configured maximum. |
| `sitemap` | `string` | No | `"include"` | Sitemap behavior: `"include"`, `"skip"`, or `"only"`. |
| `includeSubdomains` | `boolean` | No | `true` | Whether to include URLs from subdomains of the base URL. |
| `ignoreQueryParameters` | `boolean` | No | `true` | Exclude query parameters from discovered URLs, reducing duplicates. |
| `location` | `object` | No | — | Geo-targeting settings. See below. |
| `location.country` | `string` | No | `"us"` | Country code for proxy routing (e.g., `"us"`, `"gb"`, `"de"`). |
| `location.languages` | `string[]` | No | — | Preferred languages for the request. |
| `proxy` | `string` | No | `"residential"` | Proxy network to route through: `"residential"` (6 units/MB) or `"datacenter"` (2 units/MB). |

## Response fields

| Field | Type | Description |
|---|---|---|
| `success` | `boolean` | Whether the mapping operation succeeded. |
| `links` | `MapLink[]` | Array of discovered URLs with optional metadata. |
| `links[].url` | `string` | The discovered URL. |
| `links[].title` | `string \| undefined` | Page title, when available. |
| `links[].description` | `string \| undefined` | Page description, when available. |

## Configuration options

The `/map` API supports a `timeout` query parameter to control the maximum time allowed for the mapping operation:

```http
POST /map?token=YOUR_API_TOKEN_HERE&timeout=30000
```

The timeout value is in milliseconds. If not specified, the server default timeout is used.

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>401 Unauthorized</code> error?</summary>

Your API token is missing or invalid. Include it as a `?token=` query parameter or in the `Authorization` header. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My request returns an empty response</summary>

The page may not have finished loading. Increase `waitForTimeout` or use `waitForSelector` to wait for specific content before extracting data.

</details>

## Next steps

  <a href="/rest-apis/intro" className="next-step-card-link">
    
      <h3 className="next-step-card-title">REST API overview</h3>
      introduction to all available endpoints
    
  </a>
  <a href="/rest-apis/api-playground" className="next-step-card-link">
    
      <h3 className="next-step-card-title">API playground</h3>
      test endpoints interactively
    
  </a>

---

# PDF API

URL: https://docs.browserless.io/rest-apis/pdf-api

Navigate to a URL or render raw HTML and return a PDF file. Send either a `url` or an `html` field in the JSON body, but not both.

**Endpoint**
- Method: `POST`
- Path: `/pdf`
- Auth: `token` query parameter (`?token=`)
- Content-Type: `application/json`
- Response: `application/pdf`

See the [OpenAPI reference](/open-api/chrome-pdf) for complete details.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Quickstart

  
**cURL:**

```sh
curl -X POST \
  "https://production-sfo.browserless.io/pdf?token=YOUR_API_TOKEN_HERE" \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/json' \
  -d '{
  "url": "https://example.com/",
  "options": {
    "displayHeaderFooter": true,
    "printBackground": false,
    "format": "A0"
  }
}' \
  -o result.pdf
```

  
  
**Javascript:**

```js
import fs from 'fs/promises';

const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/pdf?token=${TOKEN}`;
const headers = {
  "Cache-Control": "no-cache",
  "Content-Type": "application/json"
};

const data = {
  url: "https://example.com/",
  options: {
    displayHeaderFooter: true,
    printBackground: false,
    format: "A0"
  }
};

const generatePDF = async () => {
  const response = await fetch(url, {
    method: 'POST',
    headers: headers,
    body: JSON.stringify(data)
  });

  const pdfBuffer = await response.arrayBuffer();
  await fs.writeFile("output.pdf", Buffer.from(pdfBuffer));
  console.log("PDF saved as output.pdf");
};

generatePDF();
```

  
  
**Python:**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/pdf?token={TOKEN}"
headers = {
    "Cache-Control": "no-cache",
    "Content-Type": "application/json"
}

data = {
    "url": "https://example.com/",
    "options": {
        "displayHeaderFooter": True,
        "printBackground": False,
        "format": "A0"
    }
}

response = requests.post(url, headers=headers, json=data)

with open("output.pdf", "wb") as file:
    file.write(response.content)

print("PDF saved as output.pdf")
```

  

**Response**

```http
HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Disposition: attachment; filename="output.pdf"

<binary>
```

## Examples

### Setting HTML content

Use the `html` field to render inline HTML instead of navigating to a URL.

> **Danger**
> When you send `html`, do not include `url` in the same request.

```sh
curl -X POST \
  "https://production-sfo.browserless.io/pdf?token=YOUR_API_TOKEN_HERE" \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/json' \
  -d '{
  "html": "<h1>Hello World!</h1>",
  "options": {
    "displayHeaderFooter": true,
    "printBackground": false,
    "format": "A0"
  }
}' \
  -o result.pdf
```

### Adding custom styles and scripts

Use `addScriptTag` and `addStyleTag` to inject scripts and styles before the PDF is generated.

```sh
curl -X POST \
  "https://production-sfo.browserless.io/pdf?token=YOUR_API_TOKEN_HERE" \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/json' \
  -d '{
  "url": "https://example.com/",
  "addScriptTag": [
    { "url": "https://code.jquery.com/jquery-3.7.1.min.js" },
    { "content": "document.querySelector(`h1`).innerText = `Hello World!`" }
  ],
  "addStyleTag": [
    {
      "content": "body { height: 100vh; background: linear-gradient(45deg, #da5a44, #a32784); }"
    },
    {
      "url": "https://interactive-examples.mdn.mozilla.net/live-examples/css-examples/text-decoration/text-decoration-color.css"
    }
  ]
}' \
  -o result.pdf
```

### Long PDFs

If a PDF is too large and fails to render in a single request, split it into smaller chunks with the `pageRanges` option and merge them afterward. Render each range in its own request:

```sh
# First chunk: pages 1-20
curl -X POST --fail-with-body \
  "https://production-sfo.browserless.io/pdf?token=YOUR_API_TOKEN_HERE" \
  -H 'Content-Type: application/json' \
  -d '{
  "url": "https://scraping-sandbox.netlify.app/long-pdf",
  "options": { "pageRanges": "1-20" }
}' \
  -o chunk-1.pdf

# Second chunk: pages 21-40
curl -X POST --fail-with-body \
  "https://production-sfo.browserless.io/pdf?token=YOUR_API_TOKEN_HERE" \
  -H 'Content-Type: application/json' \
  -d '{
  "url": "https://scraping-sandbox.netlify.app/long-pdf",
  "options": { "pageRanges": "21-40" }
}' \
  -o chunk-2.pdf
```

`--fail-with-body` makes curl exit non-zero on a non-2xx response, so a failed request stops your script before the merge step instead of sailing through (plain curl exits 0 even on a 401/500, writing the error text into the `.pdf` and only surfacing later as an opaque parse error during the merge). The server's message is still written to the `-o` file rather than printed, so rely on the exit code — don't treat a `.pdf` file's existence as success. If you'd rather inspect the status yourself, capture it with `-w '%{http_code}'` and check for `200` before merging.

> **Caution**
> `pageRanges` is honored exactly, so **size your ranges to the document's real page count and make them cover the whole document**. The `"1-20"` and `"21-40"` values above are illustrative placeholders — adjust them to your document. For example, a 58-page document should use `"1-29"` and `"30-58"`, not `"1-20"` and `"21-40"`. Ranges that don't cover every page silently drop the uncovered pages from the merged file, and a range that starts past the last page returns an HTTP 500 with a plain-text error body instead of a PDF.

Then merge the chunks into a single file with a library such as [`pdf-lib`](https://pdf-lib.js.org/). Install it first:

```sh
npm install pdf-lib
```

```js

const merged = await PDFDocument.create();

for (const file of ["chunk-1.pdf", "chunk-2.pdf"]) {
  const bytes = await fs.readFile(file);
  const doc = await PDFDocument.load(bytes);
  const pages = await merged.copyPages(doc, doc.getPageIndices());
  pages.forEach((page) => merged.addPage(page));
}

await fs.writeFile("output.pdf", await merged.save());
```

This script uses ESM `import` syntax and top-level `await`, so save it as `merge.mjs` (or add `"type": "module"` to your `package.json`) and run it with `node merge.mjs` on Node.js 16 or newer. Running it as a plain `.js` file throws `SyntaxError: Cannot use import statement outside a module`.

> **Note**
> Each range is a separate request that reloads the source, so time-dependent, personalized, or lazy-loaded content can differ between chunks. Point every request at a deterministic source and repeat the same authentication, cookies, and wait/navigation options for each range so the merged output stays consistent.

## Fullpage PDF

The `/pdf` REST API doesn't support creating a single long-page PDF file that captures an entire webpage on one page. However, you can create custom full-page PDFs using our [/function API](/rest-apis/function#returning-files).

The `/function` API gives you full control over the PDF generation process, allowing you to calculate the page height dynamically and format your PDF accordingly. This is particularly useful when you need to capture an entire webpage as a single continuous page rather than breaking it into multiple pages.

For a complete example of generating full-page PDFs, including code snippets in multiple languages, see the [Returning files section in the /function API documentation](/rest-apis/function#returning-files).

## Tagged (accessible) PDFs

PDFs from `/pdf` are generated by Chrome's print engine from the rendered HTML, so they contain real, selectable text rather than a screenshot. For documents that screen readers and automated parsers (such as ATS resume systems) need to read reliably, also set `options.tagged: true` to embed the document's structure tags:

```json
{
  "url": "https://example.com/",
  "options": {
    "tagged": true
  }
}
```

Tagging carries structural information (reading order, headings, and similar) from the source HTML into the PDF, which can improve how assistive technology and parsers handle it. The quality of the result depends on the accessibility of the input markup, and Chrome's tagged output isn't a certified PDF/UA document; run the result through a validator if you need formal compliance.

## PDF Metadata

Browserless's `/pdf` API uses Puppeteer under the hood to generate PDFs. By default, Puppeteer doesn't provide a built-in API to set PDF metadata (like `Title`, `Author`, `Subject`, `Keywords`, etc.) when generating a PDF with `page.pdf()`.

Puppeteer relies on Chrome's `printToPDF` DevTools protocol, which only exposes a limited set of options (like page size, margins, header/footer, etc.), but not document metadata.

The workaround is to generate the PDF with Browserless first, then adjust the metadata with a library such as `pdf-lib`.

## Configuration options

The `/pdf` API supports shared [request configuration options](/rest-apis/request-configuration) that apply across REST endpoints:

- [**Waiting for things**](/rest-apis/request-configuration#waiting-for-things): Wait for [events](/rest-apis/request-configuration#waitforevent), [functions](/rest-apis/request-configuration#waitforfunction), [selectors](/rest-apis/request-configuration#waitforselector), or [timeouts](/rest-apis/request-configuration#waitfortimeout) before generating the PDF
- [**Navigation options**](/rest-apis/request-configuration#navigation-options): Customize navigation behavior with `gotoOptions`
- [**Rejecting undesired requests**](/rest-apis/request-configuration#rejecting-undesired-requests): Block resources with `rejectResourceTypes` and `rejectRequestPattern`
- [**Continue on error**](/rest-apis/request-configuration#continue-on-error): Use `bestAttempt` to continue when async events fail or time out

## FAQ & Troubleshooting

<details>
<summary>Is the output a screenshot PDF or real text?</summary>

Real text. `/pdf` uses Chrome's print engine, so text stays selectable and machine-readable. To also embed structural tags that can help screen readers and automated parsers, enable `options.tagged: true`. See [Tagged (accessible) PDFs](#tagged-accessible-pdfs).

</details>

<details>
<summary>Do non-Latin scripts render correctly?</summary>

Yes, as long as the page's fonts load. The rendering browsers include fonts for common scripts, and pages that ship their own web fonts render with those. Right-to-left text (Hebrew, Arabic) follows the page's HTML direction settings, so set `dir="rtl"` in your markup where appropriate.

</details>

<details>
<summary>How do I generate a PDF with the Browserless REST API?</summary>

Send a POST request to the /pdf endpoint with a URL or raw HTML. The API returns a PDF rendered by Chrome's print engine with configurable page size, margins, headers, and footers.

</details>

<details>
<summary>Can I set custom page sizes for generated PDFs?</summary>

Yes. Use the format parameter (A4, Letter, etc.) or set explicit width and height values. You can also control margins, orientation, and whether to include backgrounds.

</details>

<details>
<summary>Does the PDF API support header and footer templates?</summary>

Yes. Use headerTemplate and footerTemplate with HTML strings. Special CSS classes like pageNumber, totalPages, date, title, and url inject dynamic values into each page.

</details>

## Next steps

  <a href="/rest-apis/intro" className="next-step-card-link">
    
      <h3 className="next-step-card-title">REST API overview</h3>
      introduction to all available endpoints
    
  </a>
  <a href="/rest-apis/api-playground" className="next-step-card-link">
    
      <h3 className="next-step-card-title">API playground</h3>
      test endpoints interactively
    
  </a>

---

# Performance API

URL: https://docs.browserless.io/rest-apis/performance

Run Lighthouse audits and return metrics for accessibility, best practices, performance, PWA, and SEO. Includes latency, time-to-interaction, design contrast, and other recommendations.

**Endpoint**
- Method: `POST`
- Path: `/performance`
- Auth: `token` query parameter (`?token=`)
- Content-Type: `application/json`
- Response: `application/json`

See the [OpenAPI reference](/open-api/chrome-performance) for complete details.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Quickstart

  
**cURL:**

```sh
curl -X POST \
  "https://production-sfo.browserless.io/performance?token=YOUR_API_TOKEN_HERE" \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/json' \
  -d '{
  "url": "https://example.com/"
}'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/performance?token=${TOKEN}`;
const headers = {
  "Cache-Control": "no-cache",
  "Content-Type": "application/json"
};

const data = {
  url: "https://example.com/"
};

const fetchPerformanceMetrics = async () => {
  const response = await fetch(url, {
    method: 'POST',
    headers: headers,
    body: JSON.stringify(data)
  });

  const result = await response.json();
  console.log(result);
};

fetchPerformanceMetrics();
```

  
  
**Python:**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/performance?token={TOKEN}"
headers = {
    "Cache-Control": "no-cache",
    "Content-Type": "application/json"
}

data = {
    "url": "https://example.com/"
}

response = requests.post(url, headers=headers, json=data)
result = response.json()

print(result)
```

  

**Response**

```text
  // ...
  "audits": {
    "is-on-https": {
      "title": "Uses HTTPS",
      "score": 1
      // ...
    },
    "viewport": {
      "title": "Has a `<meta name=\"viewport\">` tag with `width` or `initial-scale`",
      "score": 1
      // ...
    },
    "first-contentful-paint": {
      "score": 1,
      "displayValue": "0.8 s"
      // ...
    }
    // ...
  }
```

Each test has a score that indicates how well it performed, where 1 is the maximum and 0 is the lowest.

> **Warning**
> Due to the number of checks gathered, requests can take anywhere from several seconds to minutes depending on the site and worker size.

## Examples

By default, the `/performance` API will gather **all** the metrics. This will result in a **really** big JSON response (350kb to 800kb on average) and it **will** take some seconds (to minutes) to complete.

### Gathering Metrics for a Category

You can get metrics from specific categories, using the `config` property. [Just like Lighthouse](https://github.com/GoogleChrome/lighthouse/blob/master/readme.md#cli-options), available categories are `accessibility`, `best practices`, `performance`, `pwa`, and `seo`

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/performance?token=YOUR_API_TOKEN_HERE' \
  --header 'Cache-Control: no-cache' \
  --header 'Content-Type: application/json' \
  --data '{
  "url": "https://www.browserless.io",
  "config": {
    "extends": "lighthouse:default",
    "settings": {
      "onlyCategories": ["accessibility"]
    }
  }
}'
```

This will gather the predefined metrics defined by Lighthouse for that category:

```json
{
  "data": {
    "lighthouseVersion": "13.0.3",
    "requestedUrl": "https://www.browserless.io/",
    // ...
    "audits": {
      "aria-command-name": {
        "title": "`button`, `link`, and `menuitem` elements have accessible names",
        "score": 1
        // ...
      },
      "aria-hidden-body": {
        "title": "`[aria-hidden=\"true\"]` is not present on the document `<body>`",
        "score": 1
        // ...
      },
      "aria-hidden-focus": {
        "title": "`[aria-hidden=\"true\"]` elements do not contain focusable descendents",
        "score": 1
        // ...
      },
      // ...
    }
  }
}
```

### Gathering metrics from a single audit

You can get any specific metrics [valid in Lighthouse](https://github.com/GoogleChrome/lighthouse/tree/master/core/audits)

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/performance?token=YOUR_API_TOKEN_HERE' \
  --header 'Cache-Control: no-cache' \
  --header 'Content-Type: application/json' \
  --data '{
  "url": "https://www.browserless.io",
  "config": {
    "extends": "lighthouse:default",
    "settings": {
      "onlyAudits": ["unminified-css"]
    }
  }
}'
```

**Response example**

```json
{
  "data": {
    "lighthouseVersion": "13.0.3",
    "requestedUrl": "https://www.browserless.io/",
    // ...
    "audits": {
      "unminified-css": {
        "title": "Minify CSS",
        "score": 1
        // ...
      },
      // ...
    }
  }
}
```

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>401 Unauthorized</code> error?</summary>

Your API token is missing or invalid. Include it as a `?token=` query parameter or in the `Authorization` header. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My request returns an empty response</summary>

The page may not have finished loading. Increase `waitForTimeout` or use `waitForSelector` to wait for specific content before extracting data.

</details>

## Next steps

  <a href="/rest-apis/intro" className="next-step-card-link">
    
      <h3 className="next-step-card-title">REST API overview</h3>
      introduction to all available endpoints
    
  </a>
  <a href="/rest-apis/api-playground" className="next-step-card-link">
    
      <h3 className="next-step-card-title">API playground</h3>
      test endpoints interactively
    
  </a>

---

# Proxies

URL: https://docs.browserless.io/rest-apis/proxies

Browserless offers built-in residential and datacenter proxies. All REST APIs function the same for proxying: simply add the proxy parameters to your request query strings and you're all done. No need for credentials or otherwise.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Browserless Proxies

```bash
https://production-sfo.browserless.io/content?token=YOUR_TOKEN&proxy=residential&proxyCountry=us
```

| Network | Description | Cost |
|---|---|---|
| `residential` (default) | Routes through real residential IP addresses. Harder to detect. | **6 units/MB** |
| `datacenter` | Routes through datacenter IPs. Cheaper but more easily detected. | **2 units/MB** |

The datacenter pool covers fewer countries than residential. If a country has no datacenter coverage, requests won't exit from the requested location, so use `residential` when you need full geographic coverage.

**Available proxy parameters:**
- `proxy=residential` - Enables residential proxy routing (6 units/MB)
- `proxy=datacenter` - Enables datacenter proxy routing (2 units/MB)
- `proxyCountry=us` - Routes through <a href="/data/proxy-countries.json" download="proxy-countries.json">a specific country</a> (ISO 3166 country codes)
- `proxyCity=chicago` - Routes through a specific city within the selected country
- `proxySticky=true` - Keeps one stable exit IP for the whole session. On by default for stealth routes, BrowserQL, `/unblock`, and `/scrape` — a mid-session IP change can re-trigger anti-bot systems (Akamai, DataDome) that bind their sensor/cookie to the egress IP. Plain REST endpoints (`/content`, `/pdf`, `/screenshot`, `/export`, `/download`, `/function`) rotate the IP unless you set this. Pass `proxySticky=false` to opt out of the sticky default.
- `proxyPreset=px_gov01` - Website-specific proxy preset (residential only); use `px_gov01` for government websites or `px_ipv6` for Google domains such as Maps and YouTube

> **Sticky sessions**
> A sticky session keeps **one stable exit IP for the whole session** instead of rotating the IP on each request. This is now the **default** for stealth routes (`/stealth`, `/chromium/stealth`, `/chrome/stealth`, or any request with `?stealth=true`), `/scrape`, `/unblock`, and all BrowserQL sessions — because anti-bot systems like Akamai and DataDome bind their sensor and cookies to the egress IP, so a mid-session IP change can re-trigger the block.
> 
> Plain non-stealth REST endpoints (`/content`, `/pdf`, `/screenshot`, `/export`, `/download`, `/function`) still rotate the IP by default; add `proxySticky=true` to pin them.
> 
> `proxySticky` is a real boolean: pass `proxySticky=false` (or `proxySticky=0`) to opt out and force IP rotation where sticky is the default.

> **City-Level Proxying**
> City-level proxying requires a **Scale plan** (500k+ units). Plans under 500k units will receive a `401` error.
> 
> To get a list of available cities, use the following endpoints:
> 
> - **All supported cities**: `https://production-sfo.browserless.io/proxy/cities?token=YOUR_TOKEN`
> - **Cities for a specific country**: `https://production-sfo.browserless.io/proxy/cities?country=US&token=YOUR_TOKEN`

## External Proxies

```bash
https://production-sfo.browserless.io/screenshot?token=YOUR_TOKEN&externalProxyServer=http://user:pass@proxy.example.com:8080
```

The `externalProxyServer` query parameter routes traffic through Browserless's internal proxy tunnel infrastructure, chaining to your proxy as the upstream. This gives Browserless visibility into the connection (bytes tracking, logging) and supports HTTPS certificate bypass automatically.

Pass your proxy URL as a query parameter in the format `http(s)://[username:password@]host:port`.

> **Important Notes**
> - On cloud plans, third-party proxies require a **paid plan**. Free plans will receive a `401` error.
> - You cannot combine `externalProxyServer` with built-in proxies (`proxy=residential` or `proxy=datacenter`). Using both returns a `400` error.
> - Third-party proxy usage is not billed for proxy units — only Browserless's built-in proxies consume proxy units.

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>401 Unauthorized</code> error?</summary>

Your API token is missing or invalid. Include it as a `?token=` query parameter or in the `Authorization` header. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My request returns an empty response</summary>

The page may not have finished loading. Increase `waitForTimeout` or use `waitForSelector` to wait for specific content before extracting data.

</details>

## Next steps

  <a href="/rest-apis/intro" className="next-step-card-link">
    
      <h3 className="next-step-card-title">REST API overview</h3>
      introduction to all available endpoints
    
  </a>
  <a href="/rest-apis/api-playground" className="next-step-card-link">
    
      <h3 className="next-step-card-title">API playground</h3>
      test endpoints interactively
    
  </a>

---

# Quickstart

URL: https://docs.browserless.io/rest-apis/quick-start

Scrape a website with a single HTTP request using the Browserless REST APIs.

  [Sign up for a Browserless account](https://www.browserless.io/signup/email?plan=free) (free plan available).

  Get your **API token** from the [account dashboard](https://browserless.io/account/).

  
    
      
**cURL:**

```bash
curl -X POST "https://production-sfo.browserless.io/scrape?token=YOUR_API_TOKEN_HERE" \
     -H "Content-Type: application/json" \
     -d '{"url":"https://example.com","elements":[{"selector":"h1"}]}'
```
      
      
**Node.js:**

```javascript
const response = await fetch(
  'https://production-sfo.browserless.io/scrape?token=YOUR_API_TOKEN_HERE',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      url: 'https://example.com',
      elements: [{ selector: 'h1' }]
    })
  }
);
const data = await response.json();
console.log(data);
```
      
      
**Python:**

```python
import requests

response = requests.post(
    'https://production-sfo.browserless.io/scrape?token=YOUR_API_TOKEN_HERE',
    json={
        'url': 'https://example.com',
        'elements': [{'selector': 'h1'}]
    }
)
data = response.json()
print(data)
```
      
    

    **Response**

    <pre>{`{
  "data": [
    {
      "results": [
        {
          "html": "Example Domain",
          "text": "Example Domain"
        }
      ],
      "selector": "h1"
    }
  ]
}`}</pre>
  

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>403 Forbidden</code> response?</summary>

Your API token is missing or malformed. Pass it as a `?token=` query parameter, not in the request body. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>Do I need Puppeteer or Playwright to use the REST APIs?</summary>

No. REST APIs are standalone HTTP endpoints. Send a POST request with your parameters and receive the result directly. No browser libraries, WebSocket connections, or SDKs required.

</details>

## Next steps

  <a href="/rest-apis/intro#which-api-should-i-use" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Which API should I use?</h3>
      Compare every REST API endpoint by input, output, and use case
    
  </a>

  <a href="/rest-apis/launch-parameters" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Launch Parameters</h3>
      Configure stealth mode, proxies, and browser options
    
  </a>

  <a href="/rest-apis/request-configuration" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Request Configuration</h3>
      Configure navigation, resource blocking, and error handling
    
  </a>

---

# Request Configuration

URL: https://docs.browserless.io/rest-apis/request-configuration

This page documents configuration options shared across all REST API endpoints. The examples below use `/content` for demonstration, but every option works the same way on any endpoint. For an exhaustive list of all parameters and response schemas, see the [OpenAPI specification](/open-api/overview).

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Waiting for Things

Browserless offers 4 different ways to wait for preconditions to be met on the page before returning the response. These are `timeouts`, `selectors`, `functions` and `events`.

### waitForTimeout

Waits for a specified number of milliseconds before continuing. Useful when you need to wait for animations, transitions, or other time-based operations to complete.

#### Parameters

- `waitForTimeout`: Number, required: The number of milliseconds to wait.

**Example**

```sh
curl -X POST \
  "https://production-sfo.browserless.io/content?token=YOUR_API_TOKEN_HERE" \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/json' \
  -d '{
  "url": "https://example.com/",
  "waitForTimeout": 10000
}'
```

### waitForSelector

Waits for a selector to appear on the page. If the selector already exists when the request arrives, Browserless returns immediately. If the selector doesn't appear within the timeout, the API returns a non-200 response with an error message.

#### Parameters

- `selector`: String, required: A valid CSS selector.
- `hidden`: Boolean, optional: Wait for the selected element to not be found in the DOM or to be hidden, i.e. have `display: none` or `visibility: hidden` CSS properties.
- `timeout`: Number, optional: Maximum number of milliseconds to wait for the selector before failing.
- `visible`: Boolean, optional: Wait for the selected element to be present in DOM and to be visible, i.e. to not have `display: none` or `visibility: hidden` CSS properties.

**Example**

```sh
curl -X POST \
  "https://production-sfo.browserless.io/content?token=YOUR_API_TOKEN_HERE" \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/json' \
  -d '{
  "url": "https://example.com/",
  "waitForSelector": {
    "selector": "h1",
    "timeout": 5000
  }
}'
```

### waitForFunction

Waits for the provided function to return before continuing. The function can be any valid JavaScript function including `async` functions.

#### Parameters

- `fn`: String, required: A stringified JavaScript function to evaluate. Must return a truthy value to signal completion.
- `timeout`: Number, optional: Maximum number of milliseconds to wait for the function to return before failing.

**Example**

**JS function:**

```js
async () => {
  const res = await fetch("https://jsonplaceholder.typicode.com/todos/1");
  const json = await res.json();
  document.querySelector("h1").innerText = json.title;
  return true;
};
```

```sh
curl -X POST \
  "https://production-sfo.browserless.io/content?token=YOUR_API_TOKEN_HERE" \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/json' \
  -d '{
  "url": "https://example.com/",
  "waitForFunction": {
    "fn": "async()=>{const res=await fetch(\"https://jsonplaceholder.typicode.com/todos/1\");const json=await res.json();document.querySelector(\"h1\").innerText=json.title;return true}",
    "timeout": 5000
  }
}'
```

### waitForEvent

Waits for a custom event dispatched by the page. Use this for application-specific events your code fires after page load.

Browserless registers the event listener on both `window` and `document`, so you can dispatch from either:

```js
// Both of these work
window.dispatchEvent(new Event('customReady'));
document.dispatchEvent(new Event('customReady'));
```

#### Parameters

- `event`: String, required: The name of the custom event to wait for.
- `timeout`: Number, optional: Maximum number of milliseconds to wait for the event before failing. Default: 30,000ms.

**Example**

This example waits for a custom event that the page will dispatch:

```sh
curl -X POST \
  "https://production-sfo.browserless.io/content?token=YOUR_API_TOKEN_HERE" \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/json' \
  -d '{
  "html": "<html><body><h1>Test</h1><script>setTimeout(() => document.dispatchEvent(new Event(\"customReady\")), 1000);</script></body></html>",
  "waitForEvent": {
    "event": "customReady",
    "timeout": 5000
  }
}'
```

If the event never fires, the behavior depends on `bestAttempt`:
- `bestAttempt: false` (default): returns **HTTP 408**
- `bestAttempt: true`: proceeds with whatever page state exists

> **Warning**
> `waitForEvent` only works with custom events, not lifecycle events like `load` or `DOMContentLoaded`. Use [`gotoOptions.waitUntil`](#navigation-options) for lifecycle events.

## Navigation Options

Pass `gotoOptions` to customize navigation behavior, such as specifying when to consider the page fully loaded. The options mirror [Puppeteer's `GoToOptions` interface](https://pptr.dev/api/puppeteer.gotooptions).

**Example**

```sh
curl -X POST \
  "https://production-sfo.browserless.io/content?token=YOUR_API_TOKEN_HERE" \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/json' \
  -d '{
  "url": "https://example.com/",
  "gotoOptions": {
    "waitUntil": "networkidle2",
    "timeout": 10000
  }
}'
```

**`waitUntil` options**

- `load`: Fires when the entire page has loaded, including all dependent resources such as stylesheets, scripts, and images.
- `domcontentloaded`: Fires when the initial HTML document has been fully parsed, without waiting for stylesheets, images, or subframes to finish loading.
- `networkidle0`: Fires when there are zero network connections for at least 500ms. Best for pages that fully finish loading all resources.
- `networkidle2`: Fires when there are no more than 2 network connections for at least 500ms. Best for pages that keep long-polling or background connections open.

## Rejecting Undesired Requests

Use `rejectResourceTypes` and `rejectRequestPattern` to block specific content, resources, and requests. For the full schema of available resource types, see the [REST API Swagger Schema](/open-api/overview).

**Example**

```sh
curl -X POST \
  "https://production-sfo.browserless.io/content?token=YOUR_API_TOKEN_HERE" \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/json' \
  -d '{
  "url": "https://browserless.io/",
  "rejectResourceTypes":["image"],
  "rejectRequestPattern": ["/^.*\\.(css)"]
}'
```

## Continue on Error

Use `bestAttempt: true` to tell Browserless to proceed when async events fail or time out. This applies to operations like `goto` and `waitForSelector`. The request continues even if a timeout fires, returning whatever page state exists at that point.

**Example**

```sh
curl -X POST \
  "https://production-sfo.browserless.io/content?token=YOUR_API_TOKEN_HERE" \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/json' \
  -d '{
  "url": "https://example.com/",
  "bestAttempt": true,
  "waitForSelector": { "selector": "table", "timeout": 500 }
}'
```

## Response codes from sites

When you make a REST API call to Browserless, you may receive an HTTP 200 status code indicating that the connection between Browserless and your client was successful. However, the underlying site that Browserless visits may return a different status response code.

Browserless propagates the actual site's response code through response headers. If a site returns an error like 403, find the status code in the response headers under `X-Response-Code`.

**Response headers provided:**
- `X-Response-Code`: The HTTP status code from the target site
- `X-Response-Status`: The status text from the target site
- `X-Response-URL`: The final URL after any redirects
- `X-Response-IP`: The IP address of the target site
- `X-Response-Port`: The port used to connect to the target site

A successful Browserless API call (HTTP 200) can still mean the target site returned an error (e.g., 403, 404, 500). Check the `X-Response-Code` header to determine the actual site response.

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>401 Unauthorized</code> error?</summary>

Your API token is missing or invalid. Include it as a `?token=` query parameter or in the `Authorization` header. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My request returns an empty response</summary>

The page may not have finished loading. Increase `waitForTimeout` or use `waitForSelector` to wait for specific content before extracting data.

</details>

## Next steps

  <a href="/rest-apis/intro" className="next-step-card-link">
    
      <h3 className="next-step-card-title">REST API overview</h3>
      introduction to all available endpoints
    
  </a>
  <a href="/rest-apis/api-playground" className="next-step-card-link">
    
      <h3 className="next-step-card-title">API playground</h3>
      test endpoints interactively
    
  </a>

---

# Scrape API

URL: https://docs.browserless.io/rest-apis/scrape

Extract structured JSON from a fully rendered page using CSS selectors. Each request must include a `url` and an `elements` array that lists the selectors you want to capture.

**Endpoint**
- Method: `POST`
- Path: `/scrape`
- Auth: `token` query parameter (`?token=`)
- Content-Type: `application/json`
- Response: `application/json`

See the [OpenAPI reference](/open-api/chrome-scrape) for complete details.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Quickstart

  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/scrape?token=YOUR_API_TOKEN_HERE' \
  --header 'content-type: application/json' \
  --data '{
  "url": "https://browserless.io/",
  "elements": [
    {
      "selector": "h1"
    }
  ]
}'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/scrape?token=${TOKEN}`;
const headers = {
  "Cache-Control": "no-cache",
  "Content-Type": "application/json"
};

const data = {
  url: "https://browserless.io/",
  elements: [
    { selector: "h1" }
  ]
};

const scrapeContent = async () => {
  const response = await fetch(url, {
    method: 'POST',
    headers: headers,
    body: JSON.stringify(data)
  });

  const result = await response.json();
  console.log(result);
};

scrapeContent();
```

  
  
**Python:**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/scrape?token={TOKEN}"
headers = {
    "Cache-Control": "no-cache",
    "Content-Type": "application/json"
}

data = {
    "url": "https://browserless.io/",
    "elements": [
        { "selector": "h1" }
    ]
}

response = requests.post(url, headers=headers, json=data)
result = response.json()

print(result)
```

  

**Response**

```json
{
  "data": [
    {
      "results": [
        {
          "attributes": [
            { "name": "class", "value": "..." }
          ],
          "height": 120,
          "html": "Headless browser automation, without the hosting headaches",
          "left": 32,
          "text": "Headless browser automation, without the hosting headaches",
          "top": 196,
          "width": 736
        }
      ],
      "selector": "h1"
    }
  ]
}
```

## How scraping works

The API uses `document.querySelectorAll` under the hood. Browserless loads the page, runs client-side JavaScript, and then waits (up to 30 seconds by default) for your selectors before scraping. Use more specific selectors to narrow down results.

## Bot detection troubleshooting

If scraped results are empty or missing expected elements, the site may be blocking automation. Signs include:

- Empty or missing `results` arrays in the response
- Partial page content missing key elements
- Different content compared to a regular browser
- Blocked requests or access denied messages

The [/unblock API](/rest-apis/unblock) is specifically designed to bypass bot detection mechanisms like Datadome and passive CAPTCHAs.

## Configuration options

The `/scrape` API supports shared [request configuration options](/rest-apis/request-configuration) that apply across REST endpoints. In addition to `elements` and selectors, you can:

- [**Waiting for things**](/rest-apis/request-configuration#waiting-for-things): Wait for [events](/rest-apis/request-configuration#waitforevent), [functions](/rest-apis/request-configuration#waitforfunction), [selectors](/rest-apis/request-configuration#waitforselector), or [timeouts](/rest-apis/request-configuration#waitfortimeout) before scraping
- [**Navigation options**](/rest-apis/request-configuration#navigation-options): Customize navigation behavior with `gotoOptions`
- [**Rejecting undesired requests**](/rest-apis/request-configuration#rejecting-undesired-requests): Block resources with `rejectResourceTypes` and `rejectRequestPattern`
- [**Continue on error**](/rest-apis/request-configuration#continue-on-error): Use `bestAttempt` to continue when async events fail or time out

## FAQ & Troubleshooting

<details>
<summary>How does the Browserless /scrape API work?</summary>

Send a POST request with a URL and an elements array defining CSS selectors and what to extract (text, HTML, attributes). The API returns structured JSON matching your selectors.

</details>

<details>
<summary>Can I scrape multiple elements from a single page?</summary>

Yes. The elements array accepts multiple selector definitions. Each entry can target a different element and extract different properties, all returned in a single API response.

</details>

<details>
<summary>Does the /scrape API handle JavaScript-rendered content?</summary>

Yes. The API renders the page in a real headless browser, waits for JavaScript to execute, and then applies your selectors. You can also add custom wait conditions to handle lazy-loaded content.

</details>

## Next steps

  <a href="/rest-apis/intro" className="next-step-card-link">
    
      <h3 className="next-step-card-title">REST API overview</h3>
      introduction to all available endpoints
    
  </a>
  <a href="/rest-apis/api-playground" className="next-step-card-link">
    
      <h3 className="next-step-card-title">API playground</h3>
      test endpoints interactively
    
  </a>

---

# Screenshot API

URL: https://docs.browserless.io/rest-apis/screenshot-api

Capture PNG, JPEG, or WebP screenshots of a page. Pass Puppeteer-style settings through the `options` object.

**Endpoint**
- Method: `POST`
- Path: `/screenshot`
- Auth: `token` query parameter (`?token=`)
- Content-Type: `application/json`
- Response: `image/png` (or `image/jpeg`, `image/webp` based on `options.type`)

See the [OpenAPI reference](/open-api/chrome-screenshot) for complete details.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Quickstart

  
**cURL:**

```sh
curl -X POST \
  "https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE" \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/json' \
  -d '{
  "url": "https://example.com/",
  "options": {
    "fullPage": true,
    "type": "png"
  }
}' \
  --output "screenshot.png"
```

  
  
**Javascript:**

```js
import fs from 'fs/promises';

const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/screenshot?token=${TOKEN}`;
const headers = {
  "Cache-Control": "no-cache",
  "Content-Type": "application/json"
};

const data = {
  url: "https://example.com/",
  options: {
    fullPage: true,
    type: "png"
  }
};

const takeScreenshot = async () => {
  const response = await fetch(url, {
    method: 'POST',
    headers: headers,
    body: JSON.stringify(data)
  });

  const imageBuffer = await response.arrayBuffer();
  await fs.writeFile("screenshot.png", Buffer.from(imageBuffer));
  console.log("Screenshot saved as screenshot.png");
};

takeScreenshot();
```

  
  
**Python:**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/screenshot?token={TOKEN}"
headers = {
    "Cache-Control": "no-cache",
    "Content-Type": "application/json"
}

data = {
    "url": "https://example.com/",
    "options": {
        "fullPage": True,
        "type": "png"
    }
}

response = requests.post(url, headers=headers, json=data)

with open("screenshot.png", "wb") as file:
    file.write(response.content)

print("Screenshot saved as screenshot.png")
```

  

**Response**

```http
HTTP/1.1 200 OK
Content-Type: image/png

<binary>
```

## Examples

### Setting HTML content

Use the `html` field to render inline HTML instead of navigating to a URL.

> **Danger**
> When you send `html`, do not include `url` in the same request.

  
**cURL:**

```sh
curl -X POST \
  "https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE" \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/json' \
  -d '{
  "html": "<html><body><h1>Hello World!</h1></body></html>",
  "options": {
    "fullPage": true,
    "type": "png"
  }
}' \
  --output "screenshot.png"
```

  
  
**Javascript:**

```js
import fs from 'fs/promises';

const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/screenshot?token=${TOKEN}`;
const headers = {
  "Cache-Control": "no-cache",
  "Content-Type": "application/json"
};

const data = {
  html: "<html><body><h1>Hello World!</h1></body></html>",
  options: {
    fullPage: true,
    type: "png"
  }
};

const takeScreenshot = async () => {
  const response = await fetch(url, {
    method: 'POST',
    headers: headers,
    body: JSON.stringify(data)
  });

  const imageBuffer = await response.arrayBuffer();
  await fs.writeFile("screenshot.png", Buffer.from(imageBuffer));
  console.log("Screenshot saved as screenshot.png");
};

takeScreenshot();
```

  
  
**Python:**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/screenshot?token={TOKEN}"
headers = {
    "Cache-Control": "no-cache",
    "Content-Type": "application/json"
}

data = {
    "html": "<html><body><h1>Hello World!</h1></body></html>",
    "options": {
        "fullPage": True,
        "type": "png"
    }
}

response = requests.post(url, headers=headers, json=data)

with open("screenshot.png", "wb") as file:
    file.write(response.content)

print("Screenshot saved as screenshot.png")
```

  

### Adding custom styles and scripts

Use `addScriptTag` and `addStyleTag` to inject scripts and styles before the screenshot is taken.

```sh
curl -X POST \
  "https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE" \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/json' \
  -d '{
  "url": "https://example.com/",
  "addScriptTag": [
    { "url": "https://code.jquery.com/jquery-3.7.1.min.js" },
    { "content": "document.querySelector(`h1`).innerText = `Hello Word!`" }
  ],
  "addStyleTag": [
    {
      "content": "body { height: 100vh; background: linear-gradient(45deg, #da5a44, #a32784); }"
    },
    {
      "url": "https://interactive-examples.mdn.mozilla.net/live-examples/css-examples/text-decoration/text-decoration-color.css"
    }
  ]
}' \
  --output "screenshot.png"
```

## Bot detection troubleshooting

If screenshots return blank images, CAPTCHA pages, or content that differs from what a real browser would show, the site is likely blocking automation. Signs include:

- Blank or white screenshots
- CAPTCHA challenge pages captured instead of actual content
- Access denied or 403 error pages in the screenshot
- Missing or broken page elements compared to a regular browser

The [/unblock API](/rest-apis/unblock) is specifically designed to bypass bot detection mechanisms like Datadome and passive CAPTCHAs. It can return a screenshot directly when `screenshot: true` is set, delivering a base64-encoded PNG of the page after anti-bot measures have been bypassed. For best results, combine it with [residential proxies](/rest-apis/proxies).

## Configuration options

The `/screenshot` API supports shared [request configuration options](/rest-apis/request-configuration) that apply across REST endpoints:

- [**Waiting for things**](/rest-apis/request-configuration#waiting-for-things): Wait for [events](/rest-apis/request-configuration#waitforevent), [functions](/rest-apis/request-configuration#waitforfunction), [selectors](/rest-apis/request-configuration#waitforselector), or [timeouts](/rest-apis/request-configuration#waitfortimeout) before capturing the screenshot
- [**Navigation options**](/rest-apis/request-configuration#navigation-options): Customize navigation behavior with `gotoOptions`
- [**Rejecting undesired requests**](/rest-apis/request-configuration#rejecting-undesired-requests): Block resources with `rejectResourceTypes` and `rejectRequestPattern`
- [**Continue on error**](/rest-apis/request-configuration#continue-on-error): Use `bestAttempt` to continue when async events fail or time out

## FAQ & Troubleshooting

<details>
<summary>How do I take a screenshot with the Browserless REST API?</summary>

Send a POST request to the /screenshot endpoint with a URL and optional Puppeteer-style screenshot options. The API returns a PNG or JPEG image of the rendered page.

</details>

<details>
<summary>Can I capture a full-page screenshot via the API?</summary>

Yes. Set the fullPage option to true in your request body. The API scrolls and captures the entire page content, not just the visible viewport.

</details>

<details>
<summary>What screenshot options are available?</summary>

You can set the format (PNG/JPEG), quality, full-page mode, clip region, viewport size, device scale factor, and element-specific selectors. All standard Puppeteer screenshot options are supported.

</details>

<details>
<summary>How do I capture just one element?</summary>

Pass a `selector` at the top level of the request body (alongside `url`, not inside `options`). The API waits for the element and crops the capture to its bounding box, so you don't need manual `clip` math. For a fixed region instead, use `options.clip` with `x`, `y`, `width`, and `height`.

</details>

<details>
<summary>Images or content are missing from the capture</summary>

Lazy-loaded content only renders when it scrolls into view. Set `scrollPage: true` at the top level of the request body to scroll through the page before capturing, which triggers lazy loading. Combine it with `options.fullPage: true` for complete long-page captures.

</details>

## Next steps

  <a href="/rest-apis/intro" className="next-step-card-link">
    
      <h3 className="next-step-card-title">REST API overview</h3>
      introduction to all available endpoints
    
  </a>
  <a href="/rest-apis/api-playground" className="next-step-card-link">
    
      <h3 className="next-step-card-title">API playground</h3>
      test endpoints interactively
    
  </a>

---

# Search API

URL: https://docs.browserless.io/rest-apis/search

> **🚧 BETA 🚧**
> The Search API is currently in beta. Parameters and response shapes may change in future releases. 🚧

> The Web Search API is only available for Cloud plans. [Contact us for more information here.](https://www.browserless.io/contact/)

Search the web and optionally scrape each result page, returning structured, LLM-ready data. Combine multiple sources (web, news, images), geo-targeting, time filters, and category filters in a single request. When scraping is enabled, each result URL is fetched and processed into your preferred format: clean markdown, raw HTML, extracted links, or a screenshot. For page-level content extraction without search, see the [Smart Scrape API](/rest-apis/smart-scrape). To discover all URLs on a single site, use the [Map API](/rest-apis/map).

**Endpoint**
- Method: `POST`
- Path: `/search`
- Auth: `token` query parameter (`?token=`)
- Content-Type: `application/json`
- Response: `application/json`

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Quickstart

  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/search?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{
  "query": "browserless.io"
}'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/search?token=${TOKEN}`;
const headers = {
  "Content-Type": "application/json"
};

const data = {
  query: "browserless.io"
};

const search = async () => {
  const response = await fetch(url, {
    method: 'POST',
    headers: headers,
    body: JSON.stringify(data)
  });

  const result = await response.json();
  console.log(result);
};

search();
```

  
  
**Python:**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/search?token={TOKEN}"
headers = {
    "Content-Type": "application/json"
}

data = {
    "query": "browserless.io"
}

response = requests.post(url, headers=headers, json=data)
result = response.json()

print(result)
```

  

**Response**

```js
{
  "success": true,
  "data": {
    "web": [
      {
        "title": "Browserless - Headless Browser Automation",
        "url": "https://www.browserless.io/",
        "description": "Headless browser automation, without the hosting headaches.",
        "position": 1
      },
      {
        "title": "Browserless Documentation",
        "url": "https://docs.browserless.io/",
        "description": "Official documentation for the Browserless platform.",
        "position": 2
      }
      // ...more results up to the specified limit
    ]
  },
  "totalResults": 10
}
```

## Plan limits

The maximum number of results per search request (`limit`) varies by plan. The same cap applies whether you are performing a plain search, a search with scraping, or an image search.

| Plan | Max results per search |
|---|---|
| Free | 3 |
| Prototyping | 5 |
| Starter | 10 |
| Scale and above | 20 |

If you request a `limit` higher than your plan allows, the API returns a `400` error. When `limit` is omitted, it defaults to `10` (or your plan's maximum, whichever is lower).

For self-hosted Enterprise deployments, these defaults are all `10` and can be configured via the `MAX_SEARCH_RESULTS`, `MAX_SCRAPE_RESULTS`, and `MAX_SCRAPE_IMAGE_RESULTS` environment variables.

## Multiple sources

Search across web, news, and images simultaneously. Each source returns its own array in the `data` object.

  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/search?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{
  "query": "latest AI news",
  "sources": ["web", "news", "images"],
  "limit": 1
}'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/search?token=${TOKEN}`;
const headers = {
  "Content-Type": "application/json"
};

const data = {
  query: "latest AI news",
  sources: ["web", "news", "images"],
  limit: 1
};

const search = async () => {
  const response = await fetch(url, {
    method: 'POST',
    headers: headers,
    body: JSON.stringify(data)
  });

  const result = await response.json();
  console.log(result);
};

search();
```

  
  
**Python:**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/search?token={TOKEN}"
headers = {
    "Content-Type": "application/json"
}

data = {
    "query": "latest AI news",
    "sources": ["web", "news", "images"],
    "limit": 1
}

response = requests.post(url, headers=headers, json=data)
result = response.json()

print(result)
```

  

**Response**

```js
{
  "success": true,
  "data": {
    "web": [
      {
        "title": "AI News - Latest Developments",
        "url": "https://example.com/ai-news",
        "description": "Breaking AI and machine learning news.",
        "position": 1
      }
    ],
    "news": [
      {
        "title": "OpenAI Announces New Model",
        "url": "https://example.com/openai-announcement",
        "description": "OpenAI has released a new language model...",
        "date": "2025-01-15",
        "position": 1
      }
    ],
    "images": [
      {
        "title": "AI Generated Art",
        "imageUrl": "https://example.com/image.jpg",
        "imageWidth": 1200,
        "imageHeight": 800,
        "url": "https://example.com/gallery",
        "position": 1
      }
    ]
  },
  "totalResults": 3
}
```

## Search with scraping

When `scrapeOptions` is provided, Browserless fetches each result URL and returns the content in your requested format. This is useful for building RAG pipelines, knowledge bases, and other LLM-powered workflows.

  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/search?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{
  "query": "web scraping best practices",
  "limit": 3,
  "scrapeOptions": {
    "formats": ["markdown"],
    "onlyMainContent": true
  }
}'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/search?token=${TOKEN}`;
const headers = {
  "Content-Type": "application/json"
};

const data = {
  query: "web scraping best practices",
  limit: 3,
  scrapeOptions: {
    formats: ["markdown"],
    onlyMainContent: true
  }
};

const search = async () => {
  const response = await fetch(url, {
    method: 'POST',
    headers: headers,
    body: JSON.stringify(data)
  });

  const result = await response.json();
  console.log(result);
};

search();
```

  
  
**Python:**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/search?token={TOKEN}"
headers = {
    "Content-Type": "application/json"
}

data = {
    "query": "web scraping best practices",
    "limit": 3,
    "scrapeOptions": {
        "formats": ["markdown"],
        "onlyMainContent": True
    }
}

response = requests.post(url, headers=headers, json=data)
result = response.json()

print(result)
```

  

**Response**

```js
{
  "success": true,
  "data": {
    "web": [
      {
        "title": "Web Scraping Best Practices Guide",
        "url": "https://example.com/scraping-guide",
        "description": "A comprehensive guide to ethical web scraping.",
        "position": 1,
        "markdown": "# Web Scraping Best Practices\n\nWeb scraping is a powerful technique for extracting data...",
        "metadata": {
          "statusCode": 200,
          "strategy": "http-fetch"
        }
      },
      // ...more results up to the specified limit
    ]
  },
  "totalResults": 3
}
```

## Language targeting

You can use the `lang` parameter to search results in a specific language. This uses standard language codes (e.g. `"en"`, `"es"`, `"de"`, `"fr"`, `"ja"`). Defaults to `"en"` when not specified.

  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/search?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{
  "query": "tutorial de Python",
  "lang": "es"
}'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/search?token=${TOKEN}`;
const headers = {
  "Content-Type": "application/json"
};

const data = {
  query: "tutorial de Python",
  lang: "es"
};

const search = async () => {
  const response = await fetch(url, {
    method: 'POST',
    headers: headers,
    body: JSON.stringify(data)
  });

  const result = await response.json();
  console.log(result);
};

search();
```

  
  
**Python:**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/search?token={TOKEN}"
headers = {
    "Content-Type": "application/json"
}

data = {
    "query": "tutorial de Python",
    "lang": "es"
}

response = requests.post(url, headers=headers, json=data)
result = response.json()

print(result)
```

  

**Response**

```js
{
  "success": true,
  "data": {
    "web": [
     {
        "title": "El tutorial de Python — documentación de Python - 3.14.3",
        "url": "https://docs.python.org/es/3/tutorial/",
        "description": "El intérprete de Python es fácilmente extensible con funciones y tipos de datos implementados en C o C++...",
        "position": 1
      }
      // ...more results up to the specified limit
    ]
  },
  "totalResults": 10
}
```

## Category filters

Focus your search on specific content types. Categories modify the search query to target GitHub repositories, research papers, or PDF documents.

  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/search?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{
  "query": "puppeteer automation",
  "categories": ["github"]
}'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/search?token=${TOKEN}`;
const headers = {
  "Content-Type": "application/json"
};

const data = {
  query: "puppeteer automation",
  categories: ["github"]
};

const search = async () => {
  const response = await fetch(url, {
    method: 'POST',
    headers: headers,
    body: JSON.stringify(data)
  });

  const result = await response.json();
  console.log(result);
};

search();
```

  
  
**Python:**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/search?token={TOKEN}"
headers = {
    "Content-Type": "application/json"
}

data = {
    "query": "puppeteer automation",
    "categories": ["github"]
}

response = requests.post(url, headers=headers, json=data)
result = response.json()

print(result)
```

  

**Response**
```js
{
  "success": true,
  "data": {
    "web": [
      {
        "title": "GitHub - puppeteer/puppeteer: JavaScript API for Chrome and Firefox · GitHub",
        "url": "https://github.com/puppeteer/puppeteer",
        "description": "import puppeteer from 'puppeteer'; // Or import puppeteer from 'puppeteer-core'; // Launch the browser and open a new blank page. const browser = await puppeteer.launch(); const page = await browser.newPage(); // Navigate the page to a URL. await page.goto('https://developer.chrome.com/'); // Set screen size. await page.setViewport({width: 1080, height: 1024}); // Open the search menu using the keyboard. await page.keyboard.press('/'); // Type into search box using accessible input name. await page.locator('::-p-aria(Search)').fill('automate beyond recorder'); // Wait and click on first result.",
        "position": 1
      },
      {
        "title": "GitHub - addyosmani/puppeteer-webperf: Automating Web Performance testing with Puppeteer 🎪",
        "url": "https://github.com/addyosmani/puppeteer-webperf",
        "description": "🕹 Puppeteer is a Node library which provides a high-level API to control headless Chrome or Chromium over the DevTools Protocol. This repository has recipes for automating Web Performance measurement with Puppeteer.",
        "position": 2
      },
      // ...more results up to the specified limit
    ]
  },
  "totalResults": 10
}
```

Available categories:

| Category     | Description                                           |
|-------------|-------------------------------------------------------|
| `"github"`   | Restricts results to GitHub repositories              |
| `"research"` | Targets academic sources (arxiv.org, scholar.google.com, etc.) |
| `"pdf"`      | Filters for PDF documents only                        |

## Time-based filtering

Restrict results to a specific time range. Useful for finding recent content or filtering out outdated results.

  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/search?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{
  "query": "browserless.io blog",
  "tbs": "month"
}'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/search?token=${TOKEN}`;
const headers = {
  "Content-Type": "application/json"
};

const data = {
  query: "browserless.io blog",
  tbs: "month"
};

const search = async () => {
  const response = await fetch(url, {
    method: 'POST',
    headers: headers,
    body: JSON.stringify(data)
  });

  const result = await response.json();
  console.log(result);
};

search();
```

  
  
**Python:**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/search?token={TOKEN}"
headers = {
    "Content-Type": "application/json"
}

data = {
    "query": "browserless.io blog",
    "tbs": "month"
}

response = requests.post(url, headers=headers, json=data)
result = response.json()

print(result)
```

  

**Response**

```js
{
  "success": true,
  "data": {
    "web": [
      {
        "title": "Browserless blog",
        "url": "https://www.browserless.io/blog",
        "description": "4 days ago — Discover insights, tips, and updates on our headless browser platform, empowering seamless automation at scale. Dive into our blog now!",
        "position": 1
      },
      {
        "title": "GraphQL vs. REST for Web Scraping APIs: A Practical Guide",
        "url": "https://www.browserless.io/blog/graphql-vs-rest",
        "description": "4 days ago — This guide is a practical GraphQL vs REST decision framework for scraping and automation. We'll cover how data shape, rate limits, latency, pagination, ...",
        "position": 2
      },
      // ...more results up to the specified limit
    ]
  },
  "totalResults": 10
}
```

## Request body

| Parameter | Type | Default | Description |
|---|---|---|---|
| `query` | `string` | _(required)_ | Search query string |
| `limit` | `number` | `10` | Max results per source (capped by [plan limits](#plan-limits)) |
| `lang` | `string` | `"en"` | Language code (e.g. `"en"`, `"de"`, `"fr"`) |
| `country` | `string` | — | Two-letter country code (e.g. `"us"`, `"gb"`) |
| `location` | `string` | — | Location for local results (e.g. `"San Francisco"`) |
| `tbs` | `string` | — | Time filter: `"day"`, `"week"`, `"month"`, `"year"` (or `"qdr:d"`, `"qdr:w"`, `"qdr:m"`, `"qdr:y"`) |
| `categories` | `string[]` | — | Category filters: `"github"`, `"research"`, `"pdf"` |
| `sources` | `string[]` | `["web"]` | Source types: `"web"`, `"news"`, `"images"` |
| `timeout` | `number` | server default | Request timeout in milliseconds |
| `proxy` | `string` | `"residential"` | Proxy network to route result scraping through: `"residential"` (6 units/MB) or `"datacenter"` (2 units/MB) |
| `scrapeOptions` | `object` | — | When provided, scrapes each result URL (see below) |

### scrapeOptions

| Parameter | Type | Default | Description |
|---|---|---|---|
| `formats` | `string[]` | _(required)_ | Output formats: `"markdown"`, `"html"`, `"links"`, `"screenshot"` |
| `onlyMainContent` | `boolean` | `false` | Extract article body using Readability |
| `stripNonContentTags` | `boolean` | `false` | Remove `<script>`, `<style>`, `<noscript>` elements |
| `removeBase64Images` | `boolean` | `false` | Strip inline base64 data URIs from images |
| `includeTags` | `string[]` | — | CSS selectors to include |
| `excludeTags` | `string[]` | — | CSS selectors to exclude |

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>401 Unauthorized</code> error?</summary>

Your API token is missing or invalid. Include it as a `?token=` query parameter or in the `Authorization` header. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My request returns an empty response</summary>

The page may not have finished loading. Increase `waitForTimeout` or use `waitForSelector` to wait for specific content before extracting data.

</details>

## Next steps

  <a href="/rest-apis/intro" className="next-step-card-link">
    
      <h3 className="next-step-card-title">REST API overview</h3>
      introduction to all available endpoints
    
  </a>
  <a href="/rest-apis/api-playground" className="next-step-card-link">
    
      <h3 className="next-step-card-title">API playground</h3>
      test endpoints interactively
    
  </a>

---

# Smart Scrape API

URL: https://docs.browserless.io/rest-apis/smart-scrape

Intelligently scrape any URL using cascading strategies that automatically escalate from fast HTTP fetching to headless browsers and captcha solving as needed. Specify output formats to receive HTML, markdown, screenshots, PDFs, or extracted links, all in a single request.

**Endpoint**
- Method: `POST`
- Path: `/smart-scrape`
- Auth: `token` query parameter (`?token=`)
- Content-Type: `application/json`
- Response: `application/json`

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Quickstart

  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/smart-scrape?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{
  "url": "https://news.ycombinator.com/",
  "formats": ["html", "markdown", "links"]
}'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/smart-scrape?token=${TOKEN}`;
const headers = {
  "Content-Type": "application/json"
};

const data = {
  url: "https://news.ycombinator.com/",
  formats: ["html", "markdown", "links"]
};

const smartScrape = async () => {
  const response = await fetch(url, {
    method: 'POST',
    headers: headers,
    body: JSON.stringify(data)
  });

  const result = await response.json();
  console.log(result);
};

smartScrape();
```

  
  
**Python:**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/smart-scrape?token={TOKEN}"
headers = {
    "Content-Type": "application/json"
}

data = {
    "url": "https://news.ycombinator.com/",
    "formats": ["html", "markdown", "links"]
}

response = requests.post(url, headers=headers, json=data)
result = response.json()

print(result)
```

  

**Response**

```json
{
  "ok": true,
  "statusCode": 200,
  "content": "<html lang=\"en\" op=\"news\"><head><meta name=\"referrer\" content=\"origin\">...</html>",
  "contentType": "text/html; charset=utf-8",
  "headers": {
    "content-type": "text/html; charset=utf-8",
    "cache-control": "private; max-age=0"
  },
  "strategy": "http-fetch",
  "attempted": ["http-fetch"],
  "message": null,
  "actions": null,
  "screenshot": null,
  "pdf": null,
  "markdown": "# Hacker News\n\n[new](newest) | [past](front) | [comments](newcomments) | [ask](ask) | [show](show) | [jobs](jobs) | [submit](submit)\n\n1. [Motorola GrapheneOS devices will be bootloader unlockable/relockable](https://grapheneos.social/...)...",
  "rawText": null,
  "links": [
    "https://news.ycombinator.com/news",
    "https://news.ycombinator.com/newest",
    "https://news.ycombinator.com/front"
  ],
  "metadata": {
    "title": "Hacker News",
    "description": null,
    "language": "en",
    "sourceURL": "https://news.ycombinator.com/",
    "statusCode": 200
  }
}
```

## How it works

The Smart Scrape API uses a cascading strategy pipeline to fetch content in the most efficient way possible. It starts with the fastest, cheapest approach and automatically escalates to heavier strategies only when needed:

1. **Fast HTTP fetch**: Makes a lightweight HTTP request that mimics a real browser's network fingerprint. This handles the majority of static and server-rendered sites in under 2 seconds.

2. **Proxied HTTP fetch**: If the initial request is blocked (e.g., by IP detection), the same request is retried through the selected proxy network (residential by default, or datacenter if `proxy: "datacenter"` is set in the request body).

3. **Headless browser**: If the page requires JavaScript rendering (single-page apps, client-rendered content), a full stealth browser is launched to render the page.

4. **Browser + captcha solving**: If a captcha or bot detection challenge is encountered, the browser automatically detects and solves it (supports reCAPTCHA, Cloudflare Turnstile, and others).

The pipeline stops as soon as a strategy succeeds. The `strategy` field in the response tells you which approach was used, and the `attempted` array shows the full sequence of strategies tried.

> **Captcha handling scope**
> Smart Scrape only solves captchas that **gate access to the page itself** — for example, a Cloudflare Turnstile interstitial or a reCAPTCHA that blocks the page from loading. In these cases the browser solves the challenge automatically so the underlying content can be returned.
> 
> Captchas that are **embedded in a form on the page** (e.g., a reCAPTCHA next to a "Submit" button on a contact or signup form) are not solved. Actions can interact with ordinary form controls, but they do not solve embedded form captchas. If you need to submit a form behind a captcha, use [BrowserQL](/browserql/start) with the [`solve` mutation](/browserql/bot-detection/solving-captchas) instead.

## Request body

| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| `url` | `string` | Yes | - | The URL to scrape. Must be `http://` or `https://`. |
| `formats` | `string[]` | No | `["html"]` | Output formats to include. Options: `"html"`, `"markdown"`, `"rawText"`, `"screenshot"`, `"pdf"`, `"links"`. |
| `proxy` | `string` | No | `"residential"` | Proxy network to route the scrape through: `"residential"` (6 units/MB) or `"datacenter"` (2 units/MB). |
| `onlyMainContent` | `boolean` | No | `false` | Removes `nav`, `footer`, `aside`, `[role="navigation"]`, `script`, `style`, and `noscript` elements. Browserless defaults to `false`; Firecrawl's equivalent defaults to `true`. |
| `includeTags` | `string[]` | No | `[]` | Keeps HTML webpage content matching up to 100 CSS selectors. Malformed entries are ignored; if no selector matches, unfiltered content is returned. It can't be combined with `excludeTags` or `onlyMainContent`. |
| `excludeTags` | `string[]` | No | `[]` | Removes HTML webpage content matching up to 100 CSS selectors. Malformed selectors are ignored. It can't be combined with `includeTags`. |
| `headers` | `object` | No | `{}` | Adds request headers sent to the target. Unsafe transport, proxy, authentication, and cookie headers are removed. |
| `waitFor` | `number` | No | `0` | Waits after page load before reading content. Values are clamped to 0–30,000 milliseconds, and any positive value forces a browser strategy. |
| `actions` | `object[]` | No | - | Sequential browser actions to run before the final page is captured. A non-empty array forces a browser strategy. |

## Actions

Use `actions` to interact with a rendered page before Smart Scrape returns its final content. Actions run in submission order after page-level anti-bot checks have completed. The first failed action aborts the sequence, returns `ok: false`, and discards partial action results.

| Type | Fields | Behavior |
|---|---|---|
| `wait` | Exactly one of `milliseconds` or `selector`; optional `timeout` | Wait for a duration or for a visible selector. |
| `click` | `selector`; optional `timeout` | Click the first matching element. |
| `write` | `selector`, `text`; optional `timeout` | Type into an element. A trailing `\n` sends Enter. |
| `scroll` | Optional `selector`; optional `timeout` | Scroll the matching element into view, or scroll through the page when omitted. |
| `scrape` | None | Save the current page URL and HTML in `actions.scrapes`. |
| `executeJavascript` | `script`; optional `timeout` | Evaluate JavaScript in the page and save its scalar return value. |

A request may contain at most 20 actions. Action timeouts default to 10 seconds and cannot exceed 30 seconds. Every action and the final network settle also share the request's overall `timeout` budget. `actions: []` is a no-op: it does not force a browser strategy, and the response contains `actions: null`.

Combined action results are limited to 25 MiB of serialized UTF-8 JSON. Exceeding this limit aborts the sequence with `ok: false` and `actions: null`; results are not truncated, and later actions do not run. This bounds accumulated results, not the browser's memory while producing an individual result.

This example dismisses a cookie banner, waits for the page to update, scrolls to a result, and captures the intermediate page:

```json
{
  "url": "https://example.com/products",
  "formats": ["html", "links", "screenshot"],
  "actions": [
    { "type": "click", "selector": "button[data-accept-cookies]" },
    { "type": "wait", "selector": "main[data-ready]" },
    { "type": "scroll", "selector": "#featured-product" },
    { "type": "scrape" }
  ]
}
```

The `scrape` action records the URL and HTML at that exact point. After the complete sequence, Smart Scrape waits for the page to settle and then re-reads the final status, headers, content type, HTML, and URL. Relative links and top-level screenshot/PDF formats therefore describe the final page after navigation or mutation.

The final URL is used internally to resolve relative links; it is not a separate response field. `metadata.sourceURL` remains the requested URL, while each `actions.scrapes[].url` identifies the page at that particular `scrape` action.

`executeJavascript` runs in the page context with the same network restrictions as BrowserQL. It cannot access Node.js globals or browser-level credential configuration; ordinary page APIs remain available. Scripts are limited to 10,000 characters.

> **Firecrawl action differences**
> Smart Scrape does not support a `press` action; append `\n` to `write.text` to send Enter. It also does not support mid-sequence `screenshot` or `pdf` actions. Add those values to top-level `formats` to capture the final page.

```json
{
  "actions": {
    "scrapes": [
      {
        "url": "https://example.com/products",
        "html": "<html>...</html>"
      }
    ],
    "javascriptReturns": [
      { "type": "string", "value": "ready" },
      { "type": "null", "value": null }
    ]
  }
}
```

## Output formats

The `formats` array controls what data is returned. The `content` field contains raw HTML for webpages, parsed JSON for API endpoints, or extracted plain text for PDF targets. Additional formats populate their respective response fields.

### Markdown

Converts the page content to clean markdown, stripping scripts, styles, and non-visible elements.

**JSON body:**

```json
{
  "url": "https://news.ycombinator.com/",
  "formats": ["markdown"]
}
```

**cURL:**

```sh
curl -s -X POST "https://production-sfo.browserless.io/smart-scrape?token=YOUR_API_TOKEN_HERE" -H "Content-Type: application/json" -d '{"url":"https://news.ycombinator.com/","formats":["markdown"]}'
```

**Response:**

```json
{
  "ok": true,
  "statusCode": 200,
  "content": "<!DOCTYPE html><html>...</html>",
  "markdown": "# Hacker News\n\n[new](newest) | [past](front) | [comments](newcomments)...",
  "screenshot": null,
  "pdf": null,
  "links": null,
  "strategy": "http-fetch",
  "attempted": ["http-fetch"],
  "message": null
}
```

### Raw text

Returns DOM text with `script`, `style`, and `noscript` elements removed and whitespace collapsed. PDF targets return extracted PDF text. Request it with `"formats": ["rawText"]`; the result is returned in the `rawText` response field.

### Screenshot

Returns a full-page screenshot as a base64-encoded PNG string. Including `"screenshot"` in formats forces a headless browser to be used.

**JSON body:**

```json
{
  "url": "https://news.ycombinator.com/",
  "formats": ["screenshot"]
}
```

**cURL:**

```sh
curl -s -X POST "https://production-sfo.browserless.io/smart-scrape?token=YOUR_API_TOKEN_HERE" -H "Content-Type: application/json" -d '{"url":"https://news.ycombinator.com/","formats":["screenshot"]}'
```

**Response:**

```json
{
  "ok": true,
  "statusCode": 200,
  "content": "<!DOCTYPE html><html>...</html>",
  "screenshot": "iVBORw0KGgoAAAANSUhEUgAA...",
  "pdf": null,
  "markdown": null,
  "links": null,
  "strategy": "browser",
  "attempted": ["browser"],
  "message": null
}
```

### PDF

Returns the page as a base64-encoded PDF string. Like `"screenshot"`, including `"pdf"` forces a headless browser.

**JSON body:**

```json
{
  "url": "https://news.ycombinator.com/",
  "formats": ["pdf"]
}
```

**cURL:**

```sh
curl -s -X POST "https://production-sfo.browserless.io/smart-scrape?token=YOUR_API_TOKEN_HERE" -H "Content-Type: application/json" -d '{"url":"https://news.ycombinator.com/","formats":["pdf"]}'
```

**Response:**

```json
{
  "ok": true,
  "statusCode": 200,
  "content": "<!DOCTYPE html><html>...</html>",
  "pdf": "JVBERi0xLjQKMSAwIG9iago8PA...",
  "screenshot": null,
  "markdown": null,
  "links": null,
  "strategy": "browser",
  "attempted": ["browser"],
  "message": null
}
```

### Links

Extracts all links (`<a href>`) from the page, resolves relative URLs to absolute, and filters to `http://` and `https://` links only.

**JSON body:**

```json
{
  "url": "https://news.ycombinator.com/",
  "formats": ["links"]
}
```

**cURL:**

```sh
curl -s -X POST "https://production-sfo.browserless.io/smart-scrape?token=YOUR_API_TOKEN_HERE" -H "Content-Type: application/json" -d '{"url":"https://news.ycombinator.com/","formats":["links"]}'
```

**Response:**

```json
{
  "ok": true,
  "statusCode": 200,
  "content": "<!DOCTYPE html><html>...</html>",
  "links": [
    "https://news.ycombinator.com/news",
    "https://news.ycombinator.com/newest",
    "https://news.ycombinator.com/front",
    "https://grapheneos.social/@GrapheneOS/116160393783585567"
  ],
  "screenshot": null,
  "pdf": null,
  "markdown": null,
  "strategy": "http-fetch",
  "attempted": ["http-fetch"],
  "message": null
}
```

## Content shaping

For HTML webpages, use `onlyMainContent` to remove `nav`, `footer`, `aside`, `[role="navigation"]`, `script`, `style`, and `noscript` elements. Use `includeTags` when you want only specific CSS selector matches, or `excludeTags` when you want the full page minus selected elements. Each selector list accepts up to 100 entries. Malformed selector entries are ignored and do not return HTTP 400. Valid `includeTags` matches are still kept when another entry is malformed; Smart Scrape returns unfiltered content only when no include selector matches. Content shaping affects DOM-derived webpage outputs; parsed JSON `content` and parsed PDF output are unchanged.

```json
{
  "url": "https://example.com/",
  "formats": ["html", "markdown", "rawText", "links"],
  "onlyMainContent": true,
  "excludeTags": ["nav", ".ad"]
}
```

The following combinations return HTTP 400:

- `includeTags` with `excludeTags`: `"includeTags" and "excludeTags" are mutually exclusive`
- `includeTags` with `onlyMainContent: true`: `"includeTags" and "onlyMainContent" are mutually exclusive`

`waitFor` is measured in milliseconds after page load and is clamped to the `[0, 30000]` range. Any positive value forces a browser strategy because an HTTP fetch can't honor a post-load delay.

The `headers` object sends custom request headers to the target. Browserless first drops `host`, `authorization`, `proxy-authorization`, `cookie`, `set-cookie`, `x-forwarded-for`, `x-real-ip`, and `forwarded`. Among the remaining headers, invalid names, non-string values, and values containing CR, LF, or NUL characters return HTTP 400. Custom header names and values can total up to 65,536 bytes.

> **Note**
> Content shaping affects `content`, `markdown`, `rawText`, and `links`. It doesn't affect `screenshot` or `pdf`; those formats always capture the full rendered page.

## PDF targets

A PDF target is a URL that points directly to an existing PDF. This is different from requesting the `"pdf"` output format above, which renders a webpage as a new PDF.

Smart Scrape recognizes a PDF when the response uses `Content-Type: application/pdf`. For responses with a missing content type or `application/octet-stream`, the final URL path must end in `.pdf`. The body must also begin with the `%PDF-` signature. Recognized PDFs handled by the `http-fetch` and `http-proxy` strategies have their plain text extracted into `content`. When `"markdown"` is requested, `markdown` contains the extracted text with pages separated by blank lines. Because Smart Scrape does not extract links from inside PDFs, `links` is `null` for PDF targets.

Password-protected PDFs, corrupt PDFs, PDFs larger than 25 MiB, PDFs over 10,000 pages, and PDFs whose extracted text exceeds 25 MiB return `ok: false` with an explanatory `message`. Requests that force a browser strategy remain unchanged and do not use PDF-target parsing. This includes requests for `"screenshot"` or `"pdf"` output and requests using a `profile`. A PDF target combined with a non-empty `actions` array returns a controlled failure because browser actions require an HTML page; it never returns Chromium's PDF-viewer HTML as scraped content.

## Response fields

The response examples above are abbreviated. Every response includes all fields listed below.

| Field | Type | Description |
|---|---|---|
| `ok` | `boolean` | Whether the scrape succeeded. |
| `statusCode` | `number \| null` | The HTTP status code from the target site, or `null` on network errors. |
| `content` | `string \| object \| null` | Page content as an HTML string, extracted PDF text, or a parsed JSON object if the target returns `application/json`. `null` on failure. |
| `contentType` | `string \| null` | The content type of the scraped page. |
| `headers` | `object` | HTTP response headers from the target site. |
| `strategy` | `string` | The strategy that produced the result (or was being attempted on failure). |
| `attempted` | `string[]` | All strategies attempted, in order. |
| `message` | `string \| null` | Error message on failure, `null` on success. |
| `actions` | `object \| null` | Ordered `scrapes` and `javascriptReturns` collected by successful actions. `null` when actions are omitted, empty, or the scrape fails. |
| `screenshot` | `string \| null` | Base64-encoded PNG screenshot, when `"screenshot"` is in `formats`. |
| `pdf` | `string \| null` | Base64-encoded PDF, when `"pdf"` is in `formats`. |
| `markdown` | `string \| null` | Markdown conversion of a webpage, or page-separated extracted text for a PDF target, when `"markdown"` is in `formats`. |
| `links` | `string[] \| null` | Extracted links, when `"links"` is in `formats`. Always `null` for PDF targets. |
| `rawText` | `string \| null` | DOM text with scripts/styles removed and whitespace collapsed, or extracted PDF text, when `"rawText"` is in `formats`. |
| `metadata` | `object \| null` | Page metadata on success, or `null` on failure. Contains `title`, `description`, `language`, `sourceURL` (the requested URL, not the final URL after redirects), and `statusCode`. |

## JSON auto-parsing

When the target URL returns JSON content (e.g., an API endpoint with `Content-Type: application/json`), the `content` field will contain the parsed JSON object rather than a raw string:

```json
{
  "ok": true,
  "statusCode": 200,
  "content": {
    "userId": 1,
    "id": 1,
    "title": "Example post title",
    "body": "Example post body..."
  },
  "contentType": "application/json; charset=utf-8",
  "strategy": "http-fetch",
  "attempted": ["http-fetch"],
  "message": null
}
```

## Error handling

On failure, the response still returns HTTP 200 with `ok: false` and a `message` describing the error:

```json
{
  "ok": false,
  "statusCode": null,
  "content": null,
  "contentType": null,
  "headers": {},
  "strategy": "browser-captcha",
  "attempted": ["http-fetch", "http-proxy", "browser", "browser-captcha"],
  "message": "Captcha was detected but could not be solved",
  "actions": null,
  "screenshot": null,
  "pdf": null,
  "markdown": null,
  "links": null,
  "rawText": null,
  "metadata": null
}
```

## Using a profile

Scrape authenticated pages by passing a saved [profile](/baas/features/authenticated-profiles) via the `?profile=` query parameter. The browser loads the profile's cookies, `localStorage`, and `IndexedDB` before navigating, so the page is accessed as the logged-in user.

  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/smart-scrape?token=YOUR_API_TOKEN_HERE&profile=acme-prod' \
  --header 'Content-Type: application/json' \
  --data '{
  "url": "https://app.example.com/dashboard",
  "formats": ["html", "markdown"]
}'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/smart-scrape?token=${TOKEN}&profile=acme-prod`;
const headers = {
  "Content-Type": "application/json"
};

const data = {
  url: "https://app.example.com/dashboard",
  formats: ["html", "markdown"]
};

const smartScrape = async () => {
  const response = await fetch(url, {
    method: 'POST',
    headers: headers,
    body: JSON.stringify(data)
  });

  const result = await response.json();
  console.log(result);
};

smartScrape();
```

  
  
**Python:**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/smart-scrape?token={TOKEN}&profile=acme-prod"
headers = {
    "Content-Type": "application/json"
}

data = {
    "url": "https://app.example.com/dashboard",
    "formats": ["html", "markdown"]
}

response = requests.post(url, headers=headers, json=data)
result = response.json()

print(result)
```

  

> **Tip**
> Create and manage profiles via the [Authenticated Profiles](/baas/features/authenticated-profiles) workflow. The profile name is scoped to your API token — other tokens cannot access your profiles.

## Configuration options

The `/smart-scrape` API supports a `timeout` query parameter to control the maximum time allowed for the scrape operation:

```http
POST /smart-scrape?token=YOUR_API_TOKEN_HERE&timeout=30000
```

The timeout value is in milliseconds and applies to each strategy attempt. If not specified, the server default timeout is used.

## FAQ & Troubleshooting

<details>
<summary>What is the Browserless Smart Scrape API?</summary>

Smart Scrape uses a cascading strategy: it tries a fast HTTP fetch first and only launches a full browser if the initial request fails or returns incomplete content. This reduces cost and latency on pages that don't need a full browser.

</details>

<details>
<summary>When should I use Smart Scrape vs the /content API?</summary>

Use Smart Scrape when you want automatic fallback from HTTP to browser rendering. Use /content when you know the page requires JavaScript rendering and want to skip the initial HTTP attempt.

</details>

<details>
<summary>Does Smart Scrape handle bot-protected pages?</summary>

Yes. When the HTTP fetch is blocked, Smart Scrape falls back to a full browser with stealth capabilities. You can also enable proxies for additional protection.

</details>

## Next steps

  <a href="/rest-apis/intro" className="next-step-card-link">
    
      <h3 className="next-step-card-title">REST API overview</h3>
      introduction to all available endpoints
    
  </a>
  <a href="/rest-apis/api-playground" className="next-step-card-link">
    
      <h3 className="next-step-card-title">API playground</h3>
      test endpoints interactively
    
  </a>

---

# Timeout Configuration

URL: https://docs.browserless.io/rest-apis/timeouts

This document explains how to configure timeouts for Browserless REST APIs. Proper timeout configuration is essential for reliable browser automation, especially when dealing with slow-loading pages or complex interactions.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Overview

Browserless REST APIs support timeout configuration through two methods:

1. **Query Parameter Timeout**: Global timeout for the entire request
2. **JSON Body Timeout Parameters**: Granular timeout control for specific operations

All timeout values are specified in milliseconds.

## Query Parameter Timeout

You can set a global timeout for any REST API request using the `timeout` query parameter. This controls the maximum duration for the entire operation.

For detailed information about query parameter timeouts, see the [Launch Parameters documentation](/rest-apis/launch-parameters#parameter-reference).

## JSON Body Timeout Parameters

REST APIs support several timeout parameters in the JSON request body for fine-grained control over different operations. For full details on what each option does and its available parameters, see the [Request Configuration](/rest-apis/request-configuration#waiting-for-things) page.

### Navigation Timeouts

Controls how long to wait for page navigation to complete. This is part of the `gotoOptions` object which mirrors [Puppeteer's GoToOptions interface](https://pptr.dev/api/puppeteer.gotooptions).

```sh
curl -X POST \
  "https://production-sfo.browserless.io/content?token=YOUR_API_TOKEN_HERE" \
  -H 'Content-Type: application/json' \
  -d '{
  "url": "https://example.com/",
  "gotoOptions": {
    "timeout": 30000,
    "waitUntil": "networkidle2"
  }
}'
```

### Selector Timeouts

Controls how long to wait for a specific element to appear on the page. If the element doesn't appear within the timeout period, the operation will fail (unless `bestAttempt` is enabled). See [waitForSelector](/rest-apis/request-configuration#waitforselector) for full parameter details.

```sh
curl -X POST \
  "https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE" \
  -H 'Content-Type: application/json' \
  -d '{
  "url": "https://example.com/",
  "waitForSelector": {
    "selector": "#main-content",
    "timeout": 10000,
    "visible": true
  }
}' \
  --output "screenshot.png"
```

### Function Timeouts

Controls how long to wait for a custom JavaScript function to return a truthy value. Useful for waiting on complex conditions or asynchronous operations. See [waitForFunction](/rest-apis/request-configuration#waitforfunction) for full parameter details.

```sh
curl -X POST \
  "https://production-sfo.browserless.io/content?token=YOUR_API_TOKEN_HERE" \
  -H 'Content-Type: application/json' \
  -d '{
  "url": "https://example.com/",
  "waitForFunction": {
    "fn": "() => document.title === \"Example Domain\"",
    "timeout": 15000
  }
}'
```

### Event Timeouts

Controls how long to wait for a specific custom event to fire before proceeding with the operation. See [waitForEvent](/rest-apis/request-configuration#waitforevent) for full parameter details.

> **Warning**
> `waitForEvent` only works with custom events, not lifecycle events like `load` or `DOMContentLoaded`. Use [`gotoOptions.waitUntil`](#navigation-timeouts) for lifecycle events.

```sh
curl -X POST \
  "https://production-sfo.browserless.io/content?token=YOUR_API_TOKEN_HERE" \
  -H 'Content-Type: application/json' \
  -d '{
  "html": "<html><body><h1>Test</h1><script>setTimeout(() => document.dispatchEvent(new Event(\"customReady\")), 1000);</script></body></html>",
  "waitForEvent": {
    "event": "customReady",
    "timeout": 5000
  }
}'
```

### Fixed Delay Timeout

Waits for a specified number of milliseconds before proceeding. Useful when you need to wait for animations, transitions, or other time-based operations to complete. See [waitForTimeout](/rest-apis/request-configuration#waitfortimeout) for full parameter details.

```sh
curl -X POST \
  "https://production-sfo.browserless.io/scrape?token=YOUR_API_TOKEN_HERE" \
  -H 'Content-Type: application/json' \
  -d '{
  "url": "https://example.com/",
  "elements": [
    { "selector": "h1" }
  ],
  "waitForTimeout": 3000
}'
```

## Combining Multiple Timeout Parameters

You can combine multiple timeout parameters in a single request for comprehensive control over the browser automation process:

```sh
curl -X POST \
  "https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE&timeout=60000" \
  -H 'Content-Type: application/json' \
  -d '{
  "url": "https://example.com/",
  "gotoOptions": {
    "timeout": 30000,
    "waitUntil": "networkidle2"
  },
  "waitForSelector": {
    "selector": "h1",
    "timeout": 10000,
    "visible": true
  },
  "waitForTimeout": 2000
}' \
  --output "screenshot.png"
```

## Best Practices

1. **Set Realistic Timeouts**: Don't set timeouts too low for slow-loading content, but avoid unnecessarily high values that could delay error detection.

2. **Use Appropriate `waitUntil` Options**: Choose the right navigation completion criteria for your use case. See [Navigation Options](/rest-apis/request-configuration#navigation-options) for the full list of available values.

3. **Combine Timeouts Strategically**: Use navigation timeouts for page loading, selector timeouts for dynamic content, and fixed delays sparingly.

4. **Monitor Total Request Time**: Remember that the query parameter timeout applies to the entire request, including all wait operations.

5. **Handle Timeout Errors**: Always implement proper error handling for timeout scenarios in your application code.

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>401 Unauthorized</code> error?</summary>

Your API token is missing or invalid. Include it as a `?token=` query parameter or in the `Authorization` header. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My request returns an empty response</summary>

The page may not have finished loading. Increase `waitForTimeout` or use `waitForSelector` to wait for specific content before extracting data.

</details>

## Next steps

  <a href="/rest-apis/intro" className="next-step-card-link">
    
      <h3 className="next-step-card-title">REST API overview</h3>
      introduction to all available endpoints
    
  </a>
  <a href="/rest-apis/api-playground" className="next-step-card-link">
    
      <h3 className="next-step-card-title">API playground</h3>
      test endpoints interactively
    
  </a>

---

# Unblock API

URL: https://docs.browserless.io/rest-apis/unblock

Bypass CAPTCHAs and bot detection, then return content, cookies, screenshots, or a `browserWSEndpoint` for custom automation. Works best with our [Browserless Residential Proxy Service](/rest-apis/proxies).

**Endpoint**
- Method: `POST`
- Path: `/unblock`
- Auth: `token` query parameter (`?token=`)
- Content-Type: `application/json`
- Response: `application/json`

See the [OpenAPI reference](/open-api/unblock) for complete details.

- A Browserless API token from your [account dashboard](https://browserless.io/account/)

## Quickstart

  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/unblock?token=YOUR_API_TOKEN_HERE&proxy=residential' \
  --header 'Content-Type: application/json' \
  --data '{
  "url": "https://turo.com/",
  "content": true,
  "cookies": false,
  "screenshot": false,
  "browserWSEndpoint": false
}'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/unblock?token=${TOKEN}&proxy=residential`;
const headers = {
  "Content-Type": "application/json"
};

const data = {
  url: "https://turo.com/",
  content: true,
  cookies: false,
  screenshot: false,
  browserWSEndpoint: false
};

const fetchContent = async () => {
  const response = await fetch(url, {
    method: 'POST',
    headers: headers,
    body: JSON.stringify(data)
  });

  const content = await response.json();
  console.log(content);
};

fetchContent();
```

  
  
**Python:**

```python
import requests

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/unblock?token={TOKEN}&proxy=residential"
headers = {
    "Content-Type": "application/json"
}

data = {
    "url": "https://turo.com/",
    "content": True,
    "cookies": False,
    "screenshot": False,
    "browserWSEndpoint": False
}

response = requests.post(url, headers=headers, json=data)

print(response.json())
```

  

**Response**

```json
{
  "content": "<!DOCTYPE html><html>...</html>",
  "cookies": [],
  "screenshot": null,
  "browserWSEndpoint": null
}
```

## Data Extraction Modes

The Unblock API is optimized to return only the data you request. Set fields to `true` for the data you need, and `false` for everything else to reduce execution time and resource usage.

### Return Content

Returns the full HTML of the page after the site has been unblocked and anti-bot measures have been bypassed. Use this to scrape page content with libraries like [Scrapy](/baas/libraries/scrapy) or [Beautiful Soup](/baas/libraries/beautifulsoup).

**JSON body:**

```json
{
  "url": "https://turo.com/",
  "content": true,
  "cookies": false,
  "screenshot": false,
  "browserWSEndpoint": false
}
```

**cURL:**

```sh
curl -s -X POST "https://production-sfo.browserless.io/unblock?token=YOUR_API_TOKEN_HERE&proxy=residential" -H "Content-Type: application/json" -d '{"url":"https://news.ycombinator.com/","content":true,"cookies":false,"screenshot":false,"browserWSEndpoint":false}'
```

**Response:**

```json
{
  "content": "<!DOCTYPE html><html>...</html>",
  "cookies": [],
  "screenshot": null,
  "browserWSEndpoint": null
}
```

### Return Cookies

Returns an array of cookie objects set by the site after unblocking. Each cookie includes properties such as `name`, `value`, `domain`, `path`, `secure`, `httpOnly`, and expiration details. Useful for maintaining authenticated sessions or passing cookies into subsequent requests.

**JSON body:**

```json
{
  "url": "https://turo.com/",
  "content": false,
  "cookies": true,
  "screenshot": false,
  "browserWSEndpoint": false
}
```

**cURL:**

```sh
curl -s -X POST "https://production-sfo.browserless.io/unblock?token=YOUR_API_TOKEN_HERE&proxy=residential" -H "Content-Type: application/json" -d '{"url":"https://news.ycombinator.com/","content":false,"cookies":true,"screenshot":false,"browserWSEndpoint":false}'
```

**Response:**

```json
{
  "content": null,
  "cookies": [
    {
      "name": "session_id",
      "value": "XYZ123",
      "domain": "turo.com",
      "path": "/",
      "secure": true,
      "httpOnly": true
    }
  ],
  "screenshot": null,
  "browserWSEndpoint": null
}
```

### Return Screenshot

Returns a base64-encoded, full-page JPEG screenshot (quality 50) of the page after anti-bot measures have been bypassed. You can decode the base64 string to save it as a `.jpg` image.

**JSON body:**

```json
{
  "url": "https://turo.com/",
  "content": false,
  "cookies": false,
  "screenshot": true,
  "browserWSEndpoint": false
}
```

**cURL:**

```sh
curl -s -X POST "https://production-sfo.browserless.io/unblock?token=YOUR_API_TOKEN_HERE&proxy=residential" -H "Content-Type: application/json" -d '{"url":"https://news.ycombinator.com/","content":false,"cookies":false,"screenshot":true,"browserWSEndpoint":false}'
```

**Response:**

```json
{
  "content": null,
  "cookies": [],
  "screenshot": "/9j/4AAQSkZJRgABAQAAAQAB...",
  "browserWSEndpoint": null
}
```

### Return WebSocket Endpoint

The `/unblock` API can bypass bot detection, then return a WebSocket endpoint and cookies for you to connect with your own Puppeteer or Playwright code. Set `browserWSEndpoint: true`, `cookies: true`, and specify a `ttl` (time-to-live in milliseconds, up to a maximum of 60000) to keep the browser session alive for your automation.

**JSON body:**

```json
{
  "url": "https://turo.com/",
  "content": false,
  "cookies": true,
  "screenshot": false,
  "browserWSEndpoint": true,
  "ttl": 30000
}
```

**cURL:**

```sh
curl -s -X POST "https://production-sfo.browserless.io/unblock?token=YOUR_API_TOKEN_HERE&proxy=residential" -H "Content-Type: application/json" -d '{"url":"https://news.ycombinator.com/","content":false,"cookies":true,"screenshot":false,"browserWSEndpoint":true,"ttl":30000}'
```

**Response:**

```json
{
  "browserWSEndpoint": "wss://production-sfo.browserless.io/p/53616c74...646b292c/devtools/browser/102ea3e9-74d7-42c9-a856-1bf254649b9a",
  "cookies": [
    {
      "name": "session_id",
      "value": "XYZ123",
      "domain": "turo.com",
      "path": "/",
      "secure": true,
      "httpOnly": true
    }
  ],
  "content": null,
  "screenshot": null
}
```

#### Connecting to the Browser

After receiving the response with the `browserWSEndpoint` and `cookies`, you can use Puppeteer, Playwright, or another CDP library to connect to the browser instance and inject the cookies to continue your automation:

  
**Puppeteer:**

```js
import puppeteer from "puppeteer-core";

const TOKEN = "YOUR_API_TOKEN_HERE";

const unblock = async (url) => {
  const response = await fetch(
    `https://production-sfo.browserless.io/unblock?token=${TOKEN}&proxy=residential`,
    {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        url: url,
        browserWSEndpoint: true,
        cookies: true,
        ttl: 30000
      })
    }
  );
  return await response.json();
};

// Get the WebSocket endpoint after bot detection bypass
const { browserWSEndpoint, cookies } = await unblock("https://turo.com/");

// Connect to the browser
const browser = await puppeteer.connect({
  browserWSEndpoint: `${browserWSEndpoint}?token=${TOKEN}`
});
const page = (await browser.pages())[0];

// Inject cookies if needed
// await page.setCookie(...cookies);

await page.screenshot({ path: "screenshot.png" });
await browser.close();
```
  
  
**Playwright:**

    
      
**Javascript:**

```js
import playwright from "playwright-core";

const TOKEN = "YOUR_API_TOKEN_HERE";

const unblock = async (url) => {
  const response = await fetch(
    `https://production-sfo.browserless.io/unblock?token=${TOKEN}&proxy=residential`,
    {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        url: url,
        browserWSEndpoint: true,
        cookies: true,
        ttl: 30000
      })
    }
  );
  return await response.json();
};

// Get the WebSocket endpoint after bot detection bypass
const { browserWSEndpoint, cookies } = await unblock("https://turo.com/");

// Connect to the browser
const browser = await playwright.chromium.connect(`${browserWSEndpoint}?token=${TOKEN}`);
const context = await browser.newContext();
const page = await context.newPage();

// Inject cookies if needed
// await context.addCookies(cookies);

await page.screenshot({ path: "screenshot.png" });
await browser.close();
```
      
      
**Python:**

```python
from playwright.sync_api import sync_playwright
import requests

TOKEN = "YOUR_API_TOKEN_HERE"

def unblock(url):
    response = requests.post(
        f"https://production-sfo.browserless.io/unblock?token={TOKEN}&proxy=residential",
        headers={"Content-Type": "application/json"},
        json={
            "url": url,
            "browserWSEndpoint": True,
            "cookies": True,
            "ttl": 30000
        }
    )
    return response.json()

with sync_playwright() as p:
    # Get the WebSocket endpoint after bot detection bypass
    response = unblock("https://turo.com/")
    browserWSEndpoint = response["browserWSEndpoint"]
    cookies = response.get("cookies", [])

    # Connect to the browser
    browser = p.chromium.connect(f"{browserWSEndpoint}?token={TOKEN}")
    context = browser.new_context()
    page = context.new_page()

    # Inject cookies if needed
    # context.add_cookies(cookies)

    page.screenshot(path="screenshot.png")
    browser.close()
```
      
    
  

## Still getting blocked?

For advanced bot detection and CAPTCHA solving, we recommend using [**BrowserQL**](/browserql/start) with the [`solve` mutation](/browserql/bot-detection/solving-captchas). It automatically detects and solves CAPTCHAs (reCAPTCHA, Cloudflare, and others) with built-in stealth capabilities.

The `/unblock` API works best when combined with our [Browserless Residential Proxy Service](/rest-apis/proxies).

## Configuration options

The `/unblock` API supports shared [request configuration options](/rest-apis/request-configuration) that apply across REST endpoints:

- [**Waiting for things**](/rest-apis/request-configuration#waiting-for-things): Wait for [events](/rest-apis/request-configuration#waitforevent), [functions](/rest-apis/request-configuration#waitforfunction), [selectors](/rest-apis/request-configuration#waitforselector), or [timeouts](/rest-apis/request-configuration#waitfortimeout) before returning the response
- [**Navigation options**](/rest-apis/request-configuration#navigation-options): Customize navigation behavior with `gotoOptions`
- [**Continue on error**](/rest-apis/request-configuration#continue-on-error): Use `bestAttempt` to continue when async events fail or time out

## FAQ & Troubleshooting

<details>
<summary>Why am I getting a <code>401 Unauthorized</code> error?</summary>

Your API token is missing or invalid. Include it as a `?token=` query parameter or in the `Authorization` header. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>My request returns an empty response</summary>

The page may not have finished loading. Increase `waitForTimeout` or use `waitForSelector` to wait for specific content before extracting data.

</details>

## Next steps

  <a href="/rest-apis/intro" className="next-step-card-link">
    
      <h3 className="next-step-card-title">REST API overview</h3>
      introduction to all available endpoints
    
  </a>
  <a href="/rest-apis/api-playground" className="next-step-card-link">
    
      <h3 className="next-step-card-title">API playground</h3>
      test endpoints interactively
    
  </a>
