SDK Overview

View as Markdown

The BCTRL SDK is a thin client. Each resource maps one-to-one to the HTTP API, each method makes a single request, and each request returns the response body unchanged. There’s no second model to learn: bctrl.runtimes.start(id) is exactly POST /v1/runtimes/{id}/start. Know the API, and you know the SDK.

Install

$npm install @bctrl/sdk
1import { Bctrl } from "@bctrl/sdk";
2
3const bctrl = new Bctrl({ apiKey: process.env.BCTRL_API_KEY! });

Omit apiKey and the client reads BCTRL_API_KEY from the environment. Create a key in the dashboard.

A complete run, end to end

Five calls take you from nothing to a recorded session. Read the comments - each step is a noun in the model below.

1import { Bctrl } from "@bctrl/sdk";
2import { chromium } from "playwright";
3
4const bctrl = new Bctrl({ apiKey: process.env.BCTRL_API_KEY! });
5
6// 1. Launch a cloud browser. Pass spaceId to scope it to a space.
7const runtime = await bctrl.runtimes.create({ type: "browser", name: "main" });
8
9// 2. Start it. This opens a run and returns a CDP connect URL.
10const { connectUrl, runId } = await bctrl.runtimes.start(runtime.id);
11
12// 3. Drive it yourself with Playwright over CDP.
13const browser = await chromium.connectOverCDP(connectUrl);
14const page = browser.contexts()[0]?.pages()[0] ?? (await browser.newPage());
15await page.goto("https://example.com");
16
17// 4. The run recorded everything - read it back.
18await bctrl.runs.events.list(runId);
19
20// 5. Stop the runtime.
21await bctrl.runtimes.stop(runtime.id);

The model

Four nouns, and they nest. A space is the boundary - it decides which storage, secrets, and AI credentials its runtimes can reach. A runtime is a cloud browser launched inside a space. Starting a runtime opens a run and hands back a connect URL. An invocation is what you create when you’d rather a hosted agent drive the runtime than do it yourself. The run is the thread that ties it together: whether you drove over CDP or an agent did, every event lands in the same run.

NounWhat it is
SpacesThe boundary. Scopes storage, vault, and AI access for the runtimes inside.
RuntimesCloud browsers. Create, configure, start, and stop them.
InvocationsHosted agent work BCTRL runs inside a runtime - Stagehand, browser-use, CAPTCHA.
RunsThe record of a runtime’s lifecycle: events, activity, live view, recording, and files.

One decision: who drives?

A runtime takes one controller at a time, and you pick which:

  • Drive it yourself. Take the connectUrl from start() and control the browser with Playwright or Puppeteer over CDP. See Connect with CDP.
  • Hand it to an agent. Create an invocation and let a hosted agent do the work in natural language. See Hosted agents.

While one controller holds the runtime, the other fails with runtime.controller_busy until the connection is released or the invocation finishes. Check for it with Bctrl.isControllerBusy(error).

Everything else mounts into a space

Vault, AI Providers, Proxies, Extensions, and Tools are reusable resources: define them once, mount them into a space, and every runtime there inherits them. Account is the organization layer above it all - API keys, subaccounts, and usage.

What the SDK guarantees

The SDK will never:

  • Wrap responses. Every method returns the route’s response body, unchanged.
  • Make a request you didn’t ask for - no hidden follow-ups, no background refresh, no implicit default-space lookup.
  • Bundle a browser driver. You bring your own Playwright or Puppeteer; the SDK never wraps them.

The few ergonomics it does add are explicit, so you always know where the work happens:

  • .iter() walks cursor pagination for you.
  • .streamUrl() returns an SSE endpoint for live events.
  • Typed errors - BctrlNotFoundError, BctrlRateLimitError, isControllerBusy, and more.
  • Pass a Zod schema to extract; the SDK converts it to JSON Schema on the wire.
  • createAndWait polls an invocation to completion - the name tells you it makes more than one request.