> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zode.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Browser and desktop

> Drive Chrome in a sandbox over CDP with Playwright or Puppeteer, and control its Linux desktop with screenshots, clicks and keys.

A sandbox can run Chrome, which you drive from outside over the Chrome DevTools Protocol (CDP), and a Linux desktop, which you control with screenshots and input actions. Each is a switch on the sandbox. The browser is on every plan; the desktop needs Pro, Team or Enterprise. See [Browser and desktop](https://docs.zode.dev/concepts/browser-and-desktop.md).

## Switch them on

Pass `browser` or `desktop` to `Sandbox.create` or `sbx.fork`, or switch a running sandbox:

```ts theme={null}
const sbx = await Sandbox.create({ browser: true });
await sbx.desktop.enable();
console.log(sbx.browser.enabled, sbx.desktop.enabled); // true true
await sbx.browser.disable();
```

Left out, `create` uses the workspace default and `fork` copies the parent. `enable()` and `disable()` resolve with the updated sandbox. `enabled` reads the record as of the last call; call `sbx.refresh()` to re-read it.

If the host cannot switch on a feature you asked for at create or fork, the call still succeeds and `sbx.featureError` holds the reason. On a create that feature stays off; on a fork it keeps the parent's value. Retry with `enable()`. The next call that re-reads the record clears `featureError`.

```ts theme={null}
const sbx = await Sandbox.create({ browser: true, desktop: true });
if (sbx.featureError) console.warn(sbx.featureError);
```

| Error | When |
| - | - |
| `invalid_state` (409) | The sandbox is not running |
| `plan_required` (402) | The plan does not include it: the desktop on Free |
| `feature_unavailable` (501) | The sandbox's host cannot run it yet |
| `node_error` (502) | The host did not start it; the message says why |

Switching the browser off keeps its profile, cookies included, for the next time it is on.

## Browser

| Member | What it does |
| - | - |
| `sbx.browser.enabled` | Whether Chrome is on, per the record |
| `sbx.browser.enable()` / `disable()` | Switch Chrome on or off |
| `sbx.browser.info()` | `{ cdp_url, transport, expires_at }` |
| `sbx.browser.cdpUrl()` | `info().cdp_url`, a browser-level CDP WebSocket URL |

The URL looks like `wss://eu-1.nodes.zode.dev/cdp/...`. `transport` is `direct` when it points at the sandbox's host and `relay` when it goes through `api.zode.dev`. The ticket in the URL is good for opening a connection for 5 minutes (`expires_at`); a connection you opened stays open after that. Ask for a new URL only to reconnect. While Chrome is off, both calls throw `browser_off` (409).

### Playwright

```ts theme={null}
import { chromium } from "playwright";
import { Sandbox } from "@zodedev/sdk";

const sbx = await Sandbox.create({ browser: true });
const browser = await chromium.connectOverCDP(await sbx.browser.cdpUrl());
const context = browser.contexts()[0] ?? (await browser.newContext());
const page = await context.newPage();
await page.goto("https://example.com");
console.log(await page.title());
await browser.close(); // disconnects; Chrome keeps running in the sandbox
```

### Puppeteer

```ts theme={null}
import puppeteer from "puppeteer-core";
import { Sandbox } from "@zodedev/sdk";

const sbx = await Sandbox.get("sbx_ilvsd743b36wqi26");
const browser = await puppeteer.connect({ browserWSEndpoint: await sbx.browser.cdpUrl() });
const page = await browser.newPage();
await page.goto("https://example.com");
await page.screenshot({ path: "example.png" });
await browser.disconnect();
```

Code running inside the sandbox reaches the same Chrome at `http://127.0.0.1:9222`.

## Desktop

| Member | What it does |
| - | - |
| `sbx.desktop.enabled` | Whether the desktop is on, per the record |
| `sbx.desktop.enable()` / `disable()` | Switch the desktop on or off |
| `sbx.desktop.info()` | Screen size, such as `{ width: 1280, height: 800 }` |
| `sbx.desktop.screenshot()` | The screen as PNG bytes (`Uint8Array`) |
| `sbx.desktop.click(x, y, { button?, double? })` | Click; `button` is `"left"` (default), `"middle"` or `"right"`; `double` defaults to `false` |
| `sbx.desktop.move(x, y)` | Move the pointer |
| `sbx.desktop.type(text)` | Type text as keystrokes |
| `sbx.desktop.key(keys)` | Press a key combination, such as `"ctrl+l"`, `"ctrl+shift+t"` or `"Return"` |
| `sbx.desktop.scroll(x, y, { dx?, dy? })` | Scroll at a point by notches; positive is down or right |
| `sbx.desktop.viewUrl({ control? })` | A live view to embed: `{ url, transport, expires_at }` |

Coordinates are screen pixels from the top left. While the desktop is off, these calls throw `desktop_off` (409).

```ts theme={null}
import { writeFile } from "node:fs/promises";

await sbx.desktop.enable();
await writeFile("before.png", await sbx.desktop.screenshot());
await sbx.desktop.click(640, 400);
await sbx.desktop.key("ctrl+l");
await sbx.desktop.type("https://example.com");
await sbx.desktop.key("Return");
await sbx.desktop.scroll(640, 400, { dy: 5 });
```

### Live view

`viewUrl()` returns a page URL to put in an iframe. Its ticket opens the page for 1 hour; an open page keeps its connection. By default the view is watch-only. `viewUrl({ control: true })` lets the viewer's mouse, keyboard, clipboard and file transfers reach the desktop. To switch modes, ask for a new URL.

```ts theme={null}
const { url } = await sbx.desktop.viewUrl({ control: false });
// <iframe src={url} allow="clipboard-read; clipboard-write; fullscreen; autoplay"></iframe>
```

Leave out the iframe's `sandbox` attribute; the page needs scripts and storage. A page with a Content-Security-Policy needs `frame-src https://*.nodes.zode.dev https://api.zode.dev`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.