> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.nativeport.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.nativeport.ai/_mcp/server.

# Browserless

Fronts four single-shot actions on [Browserless](https://docs.browserless.io)'s REST API (`https://production-sfo.browserless.io`) — each one page load, one browser session, one response:

```
POST /browserless/screenshot   → https://production-sfo.browserless.io/screenshot
POST /browserless/pdf          → https://production-sfo.browserless.io/pdf
POST /browserless/content      → https://production-sfo.browserless.io/content
POST /browserless/scrape       → https://production-sfo.browserless.io/scrape
```

Browserless is a full browser-automation platform, not just these four actions — BrowserQL, two async job APIs (Agent Run and Crawl), and a session-oriented WebSocket product (BaaS) all sit behind the same account token every NativePort tenant shares. Only the four actions above are single-shot, single-session, and carry no id a foreign tenant could ever replay, so only those are mounted.

**BQL, Agent Run, Crawl, `/unblock`, `/function`, and BaaS are not exposed.** Any other Browserless path draws the gateway's own `404`. A leaked Agent Run/Crawl job id or a BaaS WebSocket URL would be reachable by any NativePort tenant, not just other Browserless customers — Browserless's own per-token checks protect its customer base from each other, not one NativePort tenant from another.

Unlike every other route on this gateway, `/browserless` doesn't just relay your request — it runs a closed **field policy** first: a fixed allow-set of query parameters and JSON body fields, everything else rejected before Browserless is ever touched.

## Credential handling

* **`token` is injected as the first query parameter**, forced regardless of anything you send — including a decoded-name evasion attempt (`t%6Fken=...`).
* **`Authorization` is dropped** before the request goes upstream, even when you also pass a `token` query value yourself.
* **`timeout` is forced to `30000`** (30 seconds) regardless of what you send. Every body-level timeout duplicate Browserless documents — `gotoOptions.timeout`, `waitForSelector.timeout`, `waitForFunction.timeout`, `waitForEvent.timeout`, and the top-level `waitForTimeout` — is stripped entirely (not merely overwritten), so no combination of sub-timeouts can push a session past that one bound.

## Field policy

Every JSON body field is explicitly admitted or rejected — there's no passthrough for anything the list below doesn't name.

**Admitted on all four routes:** `url`, `html`, `gotoOptions`, `waitForSelector`, `waitForFunction`, `waitForEvent`, `rejectResourceTypes`, `rejectRequestPattern`, `bestAttempt`.

**One extra field per route:** `options` on `/screenshot` and `/pdf`; `elements` on `/scrape`. `/content` admits only the common set above.

**Denied outright, at both the query and body level — a `400` before the request reaches Browserless:** `stealth` (which also triggers Browserless's own server-side CAPTCHA solving), `blockAds`, `blockAdsInclude`, `blockConsentModals`, `proxy`, `proxyCountry`, `externalProxyServer`, `launch`, `emulationOs`, and `profile` (a saved, token-scoped session — reachable by every tenant sharing the one Browserless token this gateway injects). Any other query parameter or body field the policy doesn't name is rejected the same way: default-deny, not default-allow. The query string admits nothing beyond the forced `token`/`timeout` pair.

A rejected call answers `400` with `{"error": "Rejected: <reason>."}` and is never forwarded — Browserless never sees it, and it's never billed.

## Screenshot

`POST /browserless/screenshot` — send `url` or `html` (not both). `options` carries Puppeteer-style screenshot settings (`type`: `png`/`jpeg`/`webp`, `fullPage`, `quality`, `clip`, and so on).

```bash
curl -X POST "$GATEWAY/browserless/screenshot" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com", "options": {"fullPage": true, "type": "png"}}' \
  --output screenshot.png
```

Response: binary image bytes, `image/png` by default (`image/jpeg` or `image/webp` when `options.type` requests it), relayed byte-for-byte.

## PDF

`POST /browserless/pdf` — send `url` or `html` (not both). `options` mirrors Puppeteer's PDF settings (`format`, `printBackground`, `displayHeaderFooter`, `pageRanges`, and so on).

```bash
curl -X POST "$GATEWAY/browserless/pdf" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com", "options": {"format": "A4", "printBackground": true}}' \
  --output output.pdf
```

Response: binary `application/pdf`, relayed byte-for-byte.

## Rendered HTML

`POST /browserless/content` — send `url` or `html` (not both); returns the fully rendered page, JavaScript included.

```bash
curl -X POST "$GATEWAY/browserless/content" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com"}'
```

Response: `text/html`, the rendered document.

## Structured scrape

`POST /browserless/scrape` — `elements` (required alongside `url` or `html`) lists the CSS selectors to capture: `[{"selector": "h1"}, ...]`.

```bash
curl -X POST "$GATEWAY/browserless/scrape" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com", "elements": [{"selector": "h1"}]}'
```

Response: `application/json`, one result set per selector (`text`, `html`, `attributes`, layout box).

## Waiting, navigation, and error handling

The common fields above cover Browserless's shared request configuration: `waitForSelector`/`waitForFunction`/`waitForEvent`/`gotoOptions.timeout` (their own `.timeout` sub-fields stripped, per the forced 30-second bound above) gate when the action runs; `gotoOptions` (minus `.timeout`) controls navigation (`waitUntil: load|domcontentloaded|networkidle0|networkidle2`); `rejectResourceTypes`/`rejectRequestPattern` block unwanted requests; `bestAttempt: true` returns whatever page state exists instead of failing when a wait times out. Field-level detail for all of these lives in [Browserless's own docs](https://docs.browserless.io/rest-apis/request-configuration).

## Billing

Billed a flat **\$0.004 per accepted, forwarded call**, charged synchronously *before* the request reaches Browserless — Browserless bills browser-session time regardless of outcome, so the charge lands the moment the gateway commits to forwarding and applies the same whether the call succeeds, Browserless answers a non-2xx, or the upstream request times out or fails outright. A call the field policy rejects (see above) is never charged.

There's no idempotency key on this route: a retry after a failed or timed-out call opens its own new Browserless session and is billed as its own, independent attempt.

Field-level request and response detail for the four admitted actions lives in [Browserless's REST API docs](https://docs.browserless.io/rest-apis/intro); the gateway modifies neither beyond the field policy described above.