---
title: TypeScript client
description: Use pols from Node.js with the typed TypeScript client, generated from the same OpenAPI spec as the API.
---

`@zuschaua/pols-client` is a typed client for the pols API. Its types are generated from the API's [OpenAPI spec](/api/), so they match the API exactly; on top of them it adds helpers for waiting, streaming and computer use.

## Install

The package is published on GitHub Packages. Point the `@zuschaua` scope at it in your project's `.npmrc`, then install it:

```ini .npmrc
@zuschaua:registry=https://npm.pkg.github.com
```

```sh
npm install @zuschaua/pols-client
```

GitHub Packages asks npm to authenticate even for reading; log in with a GitHub token that may read packages (`npm login --scope=@zuschaua --registry=https://npm.pkg.github.com`). If npm still cannot find the package, write to team@peweo.com for access.

## Create, run, clean up

```ts
import { createPolsClient } from "@zuschaua/pols-client";

const pols = createPolsClient({ apiKey: process.env.POLS_API_KEY! });

let sb = await pols.sandboxes.create({
  name: "ci-run",
  size: "large",
  env: { NODE_ENV: "test" },
  secrets: ["GITHUB_TOKEN"], // vault entries, set as environment variables
});
sb = await pols.sandboxes.waitFor(sb); // until it runs

for await (const ev of pols.execStream(sb.id, { command: ["bash", "-lc", "npm test"] })) {
  if (ev.type === "stdout") process.stdout.write(ev.data ?? "");
  if (ev.type === "stderr") process.stderr.write(ev.data ?? "");
  if (ev.type === "exit") console.log("exit code", ev.exit_code);
}

await pols.files.write(sb.id, "/home/user/notes.md", "# notes");
const bytes = await pols.files.read(sb.id, "/home/user/notes.md");

await pols.sandboxes.waitFor(await pols.sandboxes.stop(sb.id));
```

Lifecycle calls return as soon as the API accepted them; `waitFor` polls until the sandbox reached its desired state.

## What the client covers

| Area | Calls |
| --- | --- |
| Sandboxes | `pols.sandboxes.list`, `.get`, `.create`, `.fork`, `.stop`, `.resume`, `.delete`, `.stats`, `.waitFor` |
| Commands | `pols.exec` (collected output), `pols.execStream` (streamed events) |
| Files | `pols.files.read`, `pols.files.write` |
| Templates | `pols.templates.list`, `.get`, `.create`, `.delete` |
| Vault | `pols.vault.list` |
| Org | `pols.org`, `pols.orgStats`, `pols.usage`, `pols.apiKeys.list` |
| Computer use | `pols.computer.screenshot`, `.click`, `.doubleClick`, `.drag`, `.type`, `.key`, `.scroll`, `.action`; `pols.browser.cdp` |
| Access | `pols.ports.list`, `.publish`, `.unpublish`, `.link`; `pols.desktop.link`; `pols.sshKeys.list`, `.create`, `.delete`; `pols.ssh` |

For anything without a helper, `pols.raw` is the typed openapi-fetch client for every endpoint.

`createPolsClient` takes the `apiKey`, and optionally a `baseUrl` (default `https://api.pols.so`) and a custom `fetch`. It refuses a plain `http://` base URL other than localhost unless you set `allowInsecureHttp`. `waitFor` gives up after 5 minutes unless you pass `timeoutMs`, and throws when the sandbox ends in `error`.

## Errors

Failed calls throw a `PolsError` with the HTTP `status` and the API's stable error `code`, such as `quota_exceeded` or `not_found`. See [Errors](/reference/errors/) for what each code means.

## Computer use and the browser

```ts
import { chromium } from "playwright";

const png = await pols.computer.screenshot(sb.id); // Uint8Array, a PNG
await pols.computer.click(sb.id, 960, 540);
await pols.computer.type(sb.id, "hello");

const { websocket_url } = await pols.browser.cdp(sb.id); // connect within 5 minutes
const browser = await chromium.connectOverCDP(websocket_url);
```

See [Computer use](/agents/computer-use/).
