# CDP headless browser

Zyte API exposes a headless browser over the [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/) (CDP),
letting you connect with Playwright, Puppeteer, or any other CDP-compatible
library and drive that browser directly with your own scripts.

This is different from [browser requests](browser.md#zapi-browser), where Zyte API
drives the browser for you and returns HTML or a screenshot. With CDP you get
a live browser connection and full control over what happens inside it.

Use CDP when you need to:

- Run existing Playwright or Puppeteer scripts without rewriting them.
- Handle complex, branching, or stateful workflows that cannot be expressed
  as a fixed sequence of [actions](browser.md#zapi-actions), without deploying
  [custom browser scripts](../ide/index.md#zapi-scripts) to Zyte infrastructure.
- Navigate multi-step user flows, fill forms, click, scroll, and interact
  with JavaScript-heavy websites.
- Intercept and inspect network traffic during a session.
- Debug sessions using browser developer tools.

Zyte manages the browser infrastructure, proxy routing, TLS fingerprinting,
and CAPTCHA management, so you write scraping logic, not browser operations.

## CDP vs browser requests

The following table maps [browser request](browser.md#zapi-browser) features to
their CDP equivalent:

| Browser request feature                                      | CDP equivalent                                                                                                                              |
|--------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------|
| [Browser HTML](browser.md#zapi-browser-html)                 | `page.content()`                                                                                                                            |
| [Screenshot](browser.md#zapi-screenshot)                     | `page.screenshot()`                                                                                                                         |
| [Actions](browser.md#zapi-actions)                           | Not needed: call your browser library directly.                                                                                             |
| [Network capture](browser.md#zapi-network-capture)           | Your browser library’s network API, e.g. `page.on("response")` or `page.route()`.                                                           |
| [Request headers](browser.md#zapi-set-browser-headers)       | Your browser library’s request header API, e.g. `page.setExtraHTTPHeaders()`. Unlike browser requests, not limited to the `Referer` header. |
| [JavaScript toggle](browser.md#zapi-javascript)              | Your browser library’s API for it, e.g. Puppeteer’s `page.setJavaScriptEnabled()`.                                                          |
| [Geolocation](features.md#zapi-geolocation)                  | The `proxy_region` [query parameter](#cdp-query-parameters).                                                                                |
| [IP type](features.md#zapi-ip-type)                          | The `proxy_type` [query parameter](#cdp-query-parameters).                                                                                  |
| [Cookies](features.md#zapi-cookies)                          | Your browser library’s cookie API. See [Cookies](#cdp-cookies).                                                                             |
| [Client-managed sessions](features.md#zapi-session-id)       | Not supported. See [Sessions](#cdp-sessions).                                                                                               |
| [Server-managed sessions](features.md#zapi-session-contexts) | Not supported. See [Sessions](#cdp-sessions).                                                                                               |
| [Response headers](features.md#zapi-headers)                 | Your browser library’s network API, e.g. `page.on("response")`.                                                                             |
| [Metadata](features.md#zapi-metadata)                        | Not needed: track state in your own script.                                                                                                 |

### Sessions

CDP does not support [client-managed](features.md#zapi-session-id) or
[server-managed](features.md#zapi-session-contexts) sessions: a CDP connection
already gives you a single browser, with one IP address and one cookie jar,
for the life of the connection, so there is nothing to opt into for that.
What you cannot do is make a *separate* CDP connection, or an HTTP or browser
request, reuse the IP address or cookie jar of an existing or past CDP
session.

If you need that kind of continuity across multiple requests, use
[browser requests](browser.md#zapi-browser) with [sessions](features.md#zapi-sessions)
instead. Mind their own expiration limits: a [client-managed session](features.md#zapi-session-id) lasts at most 15 minutes, shorter than a single CDP
session’s maximum `ttl`, while a [server-managed session](features.md#zapi-session-contexts) lasts up to 4 hours.

Browser requests also benefit from ban-avoidance work Zyte may do behind
the scenes, at no extra cost to you, beyond what a single request needs.
CDP gives you none of that: every session starts clean. What you get in
exchange is full control over sessions, cookies, headers, navigation, and
retries, so you can tune ban avoidance yourself, at the cost of a
potentially lower success rate on sites where Zyte’s automatic measures
would otherwise help.

### Cookies

CDP does not expose the [requestCookies](https://docs.zyte.com/zyte-api/usage/reference.html#operation/extract/request/requestCookies) and
[responseCookies](https://docs.zyte.com/zyte-api/usage/reference.html#operation/extract/request/responseCookies) request fields. Instead, you get full read
and write access to cookies through your browser library’s own API, e.g.
Playwright’s `context.cookies()` and `context.addCookies()`, or
Puppeteer’s `page.cookies()` and `page.setCookie()`.

## Requirements

To use CDP with your [Zyte API account](https://app.zyte.com/account/signup/zyteapi):

1. Get a subscription or, if using a pay-as-you-go plan, set a spending limit.
   CDP sessions are not allowed on free trials. See [Zyte API pricing](../pricing.md#zapi-pricing).
2. Complete our [Know Your Customer procedure](../ide/index.md#kyc).

Once both requirements are met, CDP access is enabled automatically. See **Zyte
API › Browser CDP** in the [Zyte dashboard](https://app.zyte.com/) for
details.

## Connecting

Connect to the `https://browser.zyte.com/` endpoint with an `Authorization`
header set to `Basic <token>`, where `<token>` is your [Zyte API key](https://app.zyte.com/o/zyte-api/api-access) and a colon
(`YOUR_ZYTE_API_KEY:`) encoded in Base64.

### Quick start

### Python (Playwright)

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

CDP_ENDPOINT = "https://browser.zyte.com/"

auth = base64.b64encode(b"YOUR_ZYTE_API_KEY:").decode()
headers = {"Authorization": f"Basic {auth}"}

with sync_playwright() as p:
    browser = p.chromium.connect_over_cdp(CDP_ENDPOINT, headers=headers)
    page = browser.new_page()
    page.goto("https://quotes.toscrape.com/scroll")
    page.evaluate(
        """async () => {
            let quoteCount = 0;
            while (true) {
                window.scrollTo(0, document.body.scrollHeight);
                await new Promise((resolve) => setTimeout(resolve, 500));
                const newCount = document.querySelectorAll(".quote").length;
                if (newCount === quoteCount) break;
                quoteCount = newCount;
            }
        }"""
    )
    print(page.locator(".quote").count())
    browser.close()
```

### Python (scrapy-playwright)

`settings.py`
```python
from w3lib.http import basic_auth_header

ZYTE_API_KEY = "YOUR_ZYTE_API_KEY"
PLAYWRIGHT_CDP_URL = "https://browser.zyte.com/"
PLAYWRIGHT_CDP_HEADERS = {"Authorization": basic_auth_header(ZYTE_API_KEY, "")}
```

`spiders/quotes_toscrape_com.py`
```python
from scrapy import Request, Spider
from scrapy_playwright.page import PageMethod

SCROLL_TO_BOTTOM = """async () => {
    let quoteCount = 0;
    while (true) {
        window.scrollTo(0, document.body.scrollHeight);
        await new Promise((resolve) => setTimeout(resolve, 500));
        const newCount = document.querySelectorAll(".quote").length;
        if (newCount === quoteCount) break;
        quoteCount = newCount;
    }
}"""


class QuotesToScrapeComSpider(Spider):
    name = "quotes_toscrape_com"

    async def start(self):
        yield Request(
            "https://quotes.toscrape.com/scroll",
            meta={
                "playwright": True,
                "playwright_page_methods": [
                    PageMethod("evaluate", SCROLL_TO_BOTTOM),
                ],
            },
        )

    def parse(self, response):
        quote_count = len(response.css(".quote"))
        print(quote_count)
```

Output:

```none
100
```

## Query parameters

Configure a session with query parameters on the connect URL:

```text
https://browser.zyte.com/?ttl=600&proxy_region=GB&proxy_type=residential
```

| Parameter      | Description                                                                                                                                                                                                                                                                                          |
|----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `proxy_region` | [Geolocation](features.md#zapi-geolocation) of the session’s proxy, as the same 2-letter country code accepted by [geolocation](https://docs.zyte.com/zyte-api/usage/reference.html#operation/extract/request/geolocation). Default: the most fitting geolocation based on the first target website. |
| `proxy_type`   | [IP type](features.md#zapi-ip-type) of the session’s proxy, `datacenter` or `residential`. Default: the type that best avoids bans on the first target website.                                                                                                                                      |
| `ttl`          | Session time-to-live, in seconds, from 15 to 3600. Default: 60. See [Session duration](#cdp-session).                                                                                                                                                                                                |

## Pricing

Sessions are billed in tier units, each costing the same as one [browser
request](browser.md#zapi-browser) at the [tier](../pricing.md#tiers) of the domain of the first
`page.goto()` call. See [Pricing @ zyte.com](https://www.zyte.com/pricing/#pricing) for tier prices.

The number of tier units that a session consumed is the higher of:

- The number of `page.goto()` calls.
- The session duration divided by 15 seconds and rounded up.

A single page load, done in under 15s, costs 1 unit, i.e. the same as a regular
browser request. Longer or busier sessions cost more, whichever count is
higher.

A session is billed once it has started, regardless of how it ends: whether
you close the browser, delete it explicitly, let it run until its [ttl](#cdp-session) expires, or the connection between Zyte and the browser is
unexpectedly lost mid-session. A session is not billed if it never starts, or
if Zyte ends it for its own reasons, e.g. a server restart.

> [!NOTE]
> Sessions using a [residential](features.md#zapi-residential) proxy, either
> Zyte’s automatic choice for the target website or forced with
> `proxy_type=residential`, have a bandwidth cap per unit. Sessions that
> exceed it are charged per-GB on the overage. Typical sessions are not
> affected.

### Session duration

A session starts when you connect to the CDP endpoint and ends when Zyte
receives the `Browser.close` CDP command.

> [!WARNING]
> Playwright’s `browser.close()` does not send `Browser.close`:
> it only disconnects your script from the session, without ending it. To
> end a session before its `ttl`, send `Browser.close` explicitly:
> 
> ```javascript
> const cdp = await page.context().newCDPSession(page);
> await cdp.send("Browser.close");
> ```
> 
> Puppeteer’s `browser.close()` does send `Browser.close`, ending the
> session.

Whether you end the session explicitly or disconnect without doing so, e.g.
your script exiting or crashing, Zyte keeps the session, and its proxy
allocation, reserved for you until it reaches its `ttl`, and you are billed
for that time.

By default, a session’s `ttl` is 60 seconds. See [Query parameters](#cdp-query-parameters)
for how to set a different value. Zyte terminates the session once it reaches
its `ttl`, and you are not billed for any time beyond it.
