@boxline/sdk 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,495 @@
1
+ # Boxline Node SDK
2
+
3
+ Cloud browsers and shell sandboxes for AI agents. Each session is an isolated machine with a real Chrome and, if you
4
+ ask for it, a bash shell with Python and Node, sharing one `/workspace` disk.
5
+
6
+ ```bash
7
+ npm install @boxline/sdk # add playwright-core (or playwright) to drive the browser with Playwright
8
+ export BOXLINE_API_KEY=bxl_...
9
+ ```
10
+
11
+ The client talks to `https://api.boxline.dev`; set `BOXLINE_API_URL` (or `baseUrl`) for another API, e.g.
12
+ `http://localhost:8080` for a local one.
13
+
14
+ Node 18 or newer (it uses the global `fetch`). The client also works in browsers, with `credentials: "include"`
15
+ instead of an API key, on the platform's own sites.
16
+
17
+ - [Sessions and Playwright](#sessions-and-playwright)
18
+ - [Browser, shell and files together](#browser-shell-and-files-together)
19
+ - [The page APIs](#the-page-apis)
20
+ - [Web search](#web-search)
21
+ - [Mouse, keyboard and computer use](#mouse-keyboard-and-computer-use)
22
+ - [An agent run with variables](#an-agent-run-with-variables)
23
+ - [Limits, Continue and messages](#limits-continue-and-messages)
24
+ - [Tasks and structured output](#tasks-and-structured-output)
25
+ - [Secrets and saved login details](#secrets-and-saved-login-details)
26
+ - [A script with useModel](#a-script-with-usemodel)
27
+ - [CAPTCHAs](#captchas)
28
+ - [Browser settings and extensions](#browser-settings-and-extensions)
29
+ - [Webhooks](#webhooks)
30
+ - [Lists and pages](#lists-and-pages)
31
+ - [Errors, retries and time limits](#errors-retries-and-time-limits)
32
+ - [Every method](#every-method)
33
+ - [Examples](#examples)
34
+
35
+ ## Sessions and Playwright
36
+
37
+ ```ts
38
+ import { Boxline } from "@boxline/sdk";
39
+ import { chromium } from "playwright-core";
40
+
41
+ const bx = new Boxline(); // BOXLINE_API_KEY; BOXLINE_API_URL (default https://api.boxline.dev)
42
+ const session = await bx.sessions.create({ timeout: 300 });
43
+
44
+ const browser = await chromium.connectOverCDP(session.connectUrl!); // a signed URL: treat it like a password
45
+ const page = browser.contexts()[0]!.pages()[0]!;
46
+ await page.goto("https://example.com");
47
+ console.log(await page.title());
48
+
49
+ // The actions API drives the same browser without Playwright, one HTTP call per list of actions.
50
+ const { title, content } = await session.content("markdown");
51
+ await session.release();
52
+ ```
53
+
54
+ `session.liveUrl` is a page where a person can watch and take over. `session.pause()` saves the browser and files and
55
+ stops billing; touching the session resumes it.
56
+
57
+ ## Browser, shell and files together
58
+
59
+ One session has a browser, a shell and a disk they share: browser downloads land in `<workspace>/downloads`, the shell
60
+ starts in the workspace, and the files API reads and writes it.
61
+
62
+ ```ts
63
+ const s = await bx.sessions.create({ shell: true });
64
+ const browser = await chromium.connectOverCDP(s.connectUrl!);
65
+ const page = browser.contexts()[0]!.pages()[0]!;
66
+ await page.goto("http://127.0.0.1:4800");
67
+ await page.click("#download"); // the browser downloads a CSV
68
+
69
+ const file = await s.files.waitFor("downloads/*.csv"); // files: wait until it has finished writing
70
+ const r = await s.exec(`mkdir -p output && python3 - <<'EOF'
71
+ import csv
72
+ total = sum(float(row["revenue"]) for row in csv.DictReader(open("downloads/report.csv")))
73
+ open("output/total.txt", "w").write(f"{total:.2f}")
74
+ EOF`); // the shell: Python adds up a column
75
+ const total = (await s.files.readText("output/total.txt")).trim(); // files: read what the shell wrote
76
+
77
+ await s.actions([{ action: "fill", selector: "#total", value: total }, { action: "click", selector: "#send" }]);
78
+ await s.release();
79
+ ```
80
+
81
+ The same mix works inside an agent run (`shell: true`: the agent uses `browser_*` tools and `bash`) and inside a
82
+ script (`require("child_process")` and `require("fs")` next to `page` and `step()`): see
83
+ [agent-mixed.ts](examples/agent-mixed.ts) and [script-mixed.ts](examples/script-mixed.ts).
84
+
85
+ ## The page APIs
86
+
87
+ One call each; the page renders in a real browser inside a sandbox.
88
+
89
+ ```ts
90
+ const page = await bx.fetch("https://example.com", { format: "markdown", links: true });
91
+ const png = await bx.screenshot("https://example.com", { fullPage: true }); // Uint8Array
92
+ const pdf = await bx.pdf("https://example.com", { paper: "A4" });
93
+ const { data } = await bx.extract<{ plans: { name: string; price: string }[] }>({
94
+ url: "https://example.com/pricing",
95
+ schema: { type: "object", properties: { plans: { type: "array", items: { type: "object", properties: { name: { type: "string" }, price: { type: "string" } } } } } },
96
+ });
97
+ const job = await bx.crawl.wait((await bx.crawl.start({ url: "https://example.com/docs", maxPages: 20 })).id);
98
+ ```
99
+
100
+ A page that cannot load throws `PageUnreachableError` (502 `page_unreachable`) or `PageTimeoutError` (504
101
+ `page_timeout`).
102
+
103
+ ## Web search
104
+
105
+ ```ts
106
+ const { results, cached } = await bx.search({ query: "playwright connectOverCDP", limit: 5, country: "US", recency: "year" });
107
+ for (const r of results) console.log(r.title, r.url, r.publishedAt ?? "");
108
+
109
+ // fetch: also open the top pages (true = 3, or 0–5) in a sandboxed browser and get them as Markdown.
110
+ const withPages = await bx.search({ query: "boxline docs", limit: 3, fetch: 2 });
111
+ console.log(withPages.results[0].content?.slice(0, 200), withPages.results[0].error);
112
+ ```
113
+
114
+ The same search by the same project within an hour comes from the cache (`cached: true`) and is not counted; the plan
115
+ includes `searchesPerMonth` (`bx.usage()` shows `searches`). The query text goes to the search provider (Brave) with the
116
+ platform's key, nothing about you: keep passwords and personal data out of it. `SearchUnavailableError` (503
117
+ `search_unavailable`) when search is not set up on the server.
118
+
119
+ ## Mouse, keyboard and computer use
120
+
121
+ Coordinates are CSS pixels of the session's viewport (1280×720 by default). Everything acts on the page through the
122
+ browser, never on the machine's desktop, and moves in straight lines (`steps` spreads a move over evenly spaced points).
123
+
124
+ ```ts
125
+ await s.mouse.move(400, 300, { steps: 10 });
126
+ await s.mouse.click(400, 300, { button: "right", count: 2, modifiers: ["Shift"] });
127
+ await s.mouse.drag("#card", "#done-column", { steps: 20 }); // selectors mean the element's centre
128
+ await s.mouse.drag([{ x: 100, y: 100 }, { x: 200, y: 150 }, { x: 300, y: 100 }]); // or a path
129
+ await s.mouse.down(); await s.mouse.up();
130
+ await s.hover("#menu");
131
+ await s.keyboard.key("ControlOrMeta+A"); // Playwright's key names; ctrl, cmd, Return work too
132
+ console.log(await s.cursor()); // {x, y}
133
+ const shot = await s.screenshot({ maxWidth: 640, cursor: true }); // {data, mimeType, width, height, scale}
134
+
135
+ // Every result says what happened; a bare string is a plain-English step.
136
+ const results = await s.actions([{ action: "goto", url: "https://example.com" }, "click More information"]);
137
+ console.log(results.map((r) => r.text));
138
+ ```
139
+
140
+ **Computer use.** `session.computer()` runs ONE action exactly as a computer-use model's tool gave it (Claude's
141
+ `computer` tool input, or one OpenAI `computer_call` action) and returns the screen after it, so you can drive a
142
+ session from your own computer-use loop:
143
+
144
+ ```ts
145
+ const screen = await s.computer({ action: "left_click", coordinate: [512, 300] }, { maxWidth: 1024 }); // Anthropic shape
146
+ await s.computer({ type: "keypress", keys: ["CTRL", "A"] }, { screenshot: false }); // OpenAI shape
147
+ // screen: {ok, text, screenshot (base64), width, height, scale, cursor, url, title}; send screen.screenshot back to the model
148
+ ```
149
+
150
+ With `maxWidth` the screenshot is scaled down and the action's coordinates are read in its pixels: use the same
151
+ `maxWidth` on every call. A point outside the screen throws `OutOfViewportError`. Or let the agent do it:
152
+ `bx.agent.run({ task, mode: "computer" })` (models with `supportsComputerUse` in `bx.agent.models()`); its steps carry the
153
+ model's `thought`, also streamed as `thought` events.
154
+
155
+ ## An agent run with variables
156
+
157
+ The model sees `%email%` and `%password%`, never the values. A value is typed only where the model types text or
158
+ picks an option, and `origins` limits it to fields on those sites (recommended for passwords).
159
+
160
+ ```ts
161
+ const run = await bx.agent.run({
162
+ task: "Sign in to https://example.com with %email% and %password%, then open the billing page.",
163
+ variables: {
164
+ email: "ada@example.com",
165
+ password: { value: process.env.SITE_PASSWORD!, origins: ["https://example.com"] },
166
+ },
167
+ });
168
+ for await (const event of bx.agent.stream(run.id)) {
169
+ if (event.type === "tool") console.log(event.name, event.input); // placeholders, never values
170
+ if (event.type === "done") console.log(event.status, event.result);
171
+ }
172
+ ```
173
+
174
+ `agent.takeover(id)` and `agent.handBack(id, note)` hand the browser to a person and back; `agent.wait(id)` polls
175
+ until the run ends.
176
+
177
+ ## Limits, Continue and messages
178
+
179
+ A run stops at the first of its limits: `maxSteps` (default 30; `null` for none), `maxCostUsd` (optional, model cost
180
+ in USD), `maxConsecutiveErrors` tool errors in a row (default 5), the same call with the same result 5 times, or its
181
+ session's time (`timeout`, for the run's own session). At a step, cost, error or no-progress limit it ends with an
182
+ `errorCode` (`max_steps`, `max_cost`, `too_many_errors`, `no_progress`), `resultText` says what is done and what is
183
+ left, and `continuable.until` says how long it can be continued: its session is kept for 10 minutes.
184
+
185
+ ```ts
186
+ const run = await bx.agent.run({ task: "Turn every video in the workspace into 30-second clips", shell: true, maxSteps: 20, maxCostUsd: 2 });
187
+ let done = await bx.agent.wait(run.id);
188
+ if (done.continuable) {
189
+ console.log(done.resultText); // what is done, what is left
190
+ const next = await bx.agent.continueRun(done.id, { maxSteps: 30, instruction: "The downloads are done; do the clips." });
191
+ done = await bx.agent.wait(next.id); // next.continuedFrom === done.id, same session
192
+ }
193
+ ```
194
+
195
+ A run that had `variables` needs them again on `continueRun` (their values are never stored). `NotContinuableError`:
196
+ the run did not stop at a limit, was continued already, or its window passed.
197
+
198
+ Tell a working run something without taking the browser: it reads the message at its next step, and a run waiting for
199
+ your help (`ask_user_for_help`) takes it as the answer.
200
+
201
+ ```ts
202
+ await bx.agent.sendMessage(run.id, "Also open page C and include its heading in the answer.");
203
+ ```
204
+
205
+ ## Tasks and structured output
206
+
207
+ A task is a saved agent run: an instruction with `%name%` variables, an output schema, browser settings, a saved login,
208
+ a model and, if you like, a schedule. Run it by hand or on its schedule; every run is an agent run.
209
+
210
+ ```ts
211
+ interface Books { category: string; books: { title: string; price: number }[] }
212
+
213
+ const task = await bx.tasks.create({
214
+ name: "Books by category",
215
+ instruction: "Open https://books.toscrape.com, open the category %category% and return the first 3 books with their prices",
216
+ variables: [{ name: "category", default: "Travel" }],
217
+ output: {
218
+ type: "object",
219
+ properties: {
220
+ category: { type: "string" },
221
+ books: { type: "array", items: { type: "object", properties: { title: { type: "string" }, price: { type: "number" } }, required: ["title", "price"] } },
222
+ },
223
+ required: ["category", "books"],
224
+ },
225
+ schedule: { cron: "0 9 * * MON-FRI", timezone: "Europe/London", enabled: false },
226
+ });
227
+
228
+ const run = await bx.tasks.run<Books>(task.id, { variables: { category: "Poetry" } });
229
+ const done = await bx.tasks.waitForRun(run); // done.result: Books | null
230
+ console.log(done.status, done.result?.books, done.resultText);
231
+
232
+ for await (const r of bx.tasks.runs(task.id, { status: ["failed", "missed"] })) console.log(r.id, r.errorCode ?? r.reason);
233
+ await bx.tasks.update(task.id, { schedule: { enabled: true } }); // null removes a field, e.g. { schedule: null }
234
+ ```
235
+
236
+ - **Structured output** works on one agent run too: `bx.agent.run({ task, output: schema })`, then
237
+ `bx.agent.wait<T>(id)`. `result` is the JSON answer and `resultText` a one-sentence summary. An answer that still
238
+ does not match after one repair try fails the run with `errorCode: "output_invalid"` (`ErrorCode.outputInvalid`).
239
+ - **Variables**: plain ones are written into the instruction (and kept with the run); `{ name, secret: true, origins }`
240
+ is never stored, must come with every run, and is typed without the model seeing it. A task with a secret variable
241
+ cannot have a schedule. A run missing a value throws `MissingVariablesError`.
242
+ - **Schedules**: five-field cron (at most every 5 minutes) read in `timezone`. A scheduled run is `queued` until it
243
+ starts; a time that comes while a run is still going is `skipped`, and times the platform was down for are `missed`
244
+ (`reason`, `missedCount`). The plan limits tasks and schedules switched on (`PlanLimitError`).
245
+ - `waitForRun` takes the run from `run()` or `(taskId, taskRunId)`, with `{pollMs, timeoutMs}`; there is no GET for
246
+ one task run, so it watches the task's unfinished runs.
247
+
248
+ ## Secrets and saved login details
249
+
250
+ Project secrets are write-only: the value is sealed when stored and never returned or shown. Each secret's `scope` says
251
+ where it may be used: `"agent"` (the default: only the AI, as `%NAME%`), `"shell"` (only as `$NAME` in shells) or `"all"`.
252
+
253
+ ```ts
254
+ await bx.secrets.create({ name: "GITHUB_TOKEN", value: process.env.GITHUB_TOKEN!, scope: "shell" });
255
+ await bx.secrets.create({ name: "SITE_PASSWORD", value: process.env.SITE_PASSWORD!, origins: ["https://example.com"] });
256
+
257
+ // In a shell: exported as $GITHUB_TOKEN, and shown as %GITHUB_TOKEN% wherever it appears in the output.
258
+ const s = await bx.sessions.create({ shell: true, env: { REGION: "eu" }, secrets: ["GITHUB_TOKEN"] });
259
+ await s.exec("gh repo list --limit 3");
260
+ await s.exec("./deploy.sh", { secrets: ["DEPLOY_KEY"] }); // this one command only
261
+
262
+ // For the AI: typed as %SITE_PASSWORD% only on its sites, never shown to the model.
263
+ await s.step("type %SITE_PASSWORD% into the password field", { secrets: ["SITE_PASSWORD"] });
264
+ await bx.agent.run({ task: "Sign in to https://example.com with %SITE_PASSWORD%", secrets: ["SITE_PASSWORD"] });
265
+
266
+ // A saved login's details with 2FA: %login.username%, %login.password% and %login.otp% in sessions started with it.
267
+ await bx.contexts.setLogin(context.id, { origin: "https://example.com", username: "ada@example.com", password, totpSecret });
268
+
269
+ for await (const e of bx.secrets.audit({ name: "GITHUB_TOKEN" })) console.log(e.at, e.action, e.actor, e.usedBy?.type);
270
+ ```
271
+
272
+ - **Exported secrets can be read by anything that runs in the shell**, including an agent's commands that a web page
273
+ tries to steer. Export only what you accept that for; keep passwords at scope `"agent"` with `origins`. Hiding
274
+ values in output is a guard against accidents, not a boundary.
275
+ - `runScript(code, { secrets, login: true })` lets the script's `step()` calls use secrets and the saved login's
276
+ details (the values never enter the machine). In a session with Chrome extensions, steps and scripts with secrets
277
+ need `allowWithExtensions: true`, as agent runs with variables do.
278
+ - Errors: `SecretExistsError` (use `update`), `SecretNotAllowedError` (the scope does not allow that use),
279
+ `TooManySecretValuesError` (the session hides as many values as it can: start a new one), `MachineTooOldError`
280
+ (during a deploy), `PlanLimitError` (beyond the plan's `maxSecrets`).
281
+
282
+ ## A script with useModel
283
+
284
+ `runScript` runs Playwright code inside the session (it needs `shell: true`). `page`, `context`, `browser`, `env`,
285
+ `require`, and the plain-English helpers `step()`, `extract()` and `useModel()` are in scope.
286
+
287
+ ```ts
288
+ const s = await bx.sessions.create({ shell: true });
289
+ const { stdout, exitCode } = await s.runScript(`
290
+ useModel("claude-haiku-4-5"); // every later step() and extract() uses it
291
+ await page.goto("https://example.com/signup");
292
+ await step("type %email% into the email field");
293
+ await step("click Continue", { model: "claude-sonnet-5" }); // this call only
294
+ const plans = await extract("the plan names and prices"); // returns the data itself
295
+ console.log(JSON.stringify(plans));
296
+ `, { env: { email: "ada@example.com" }, onData: (_stream, text) => process.stdout.write(text) });
297
+ ```
298
+
299
+ ## CAPTCHAs
300
+
301
+ By default (`captcha: "ask"`) the platform notices a CAPTCHA that waits for a person and hands over: agent runs and
302
+ steps pause, `session.data.attention` says which one, and a person solves it in the live view. `"ignore"` carries on;
303
+ `"solve"` (paid plans) tries to solve it first. Only automate sites you are allowed to.
304
+
305
+ ```ts
306
+ const s = await bx.sessions.create({ captcha: "ask" });
307
+ const stop = s.onCaptcha(({ state, kind, url }) => console.log(`CAPTCHA ${state}: ${kind} on ${url}`)); // tell someone: s.liveUrl
308
+ await s.goto("https://example.com/signup");
309
+ await s.waitForHuman({ timeoutMs: 5 * 60_000 }); // throws CaptchaTimeoutError if nobody solves it
310
+ stop();
311
+ ```
312
+
313
+ A plain-English step that waits too long throws `CaptchaTimeoutError` (409 `captcha_timeout`).
314
+
315
+ ## Browser settings and extensions
316
+
317
+ Ad and tracker blocking and cookie-banner answers are on every plan; set them when a session starts, or change them
318
+ while it runs:
319
+
320
+ ```ts
321
+ const s = await bx.sessions.create({ blockAds: true, cookieBanners: "reject" }); // "reject" is the default; "off" leaves banners alone
322
+ await s.goto("https://example.com");
323
+ await s.refresh();
324
+ console.log(s.data.blockedRequests); // refused inside the machine, before any proxy (collected about every 30 s)
325
+ await s.update({ blockAds: false, cookieBanners: "off" });
326
+ await bx.fetch("https://example.com", { blockAds: true }); // screenshot, pdf, extract, crawl.start and agent.run take it too
327
+ ```
328
+
329
+ Chrome extensions (Manifest V3, plan feature `extensions`): upload the zip of the extension's folder once, then start
330
+ sessions with it (at most 10, at start only).
331
+
332
+ ```ts
333
+ const ext = await bx.extensions.upload("./my-extension.zip"); // or a Uint8Array / Buffer / ArrayBuffer / Blob, at most 10 MB
334
+ const s = await bx.sessions.create({ extensions: [ext.id] }); // loaded before the session is returned
335
+ for await (const e of bx.extensions.list()) console.log(e.id, e.name, e.version, e.permissions);
336
+ await bx.extensions.delete(ext.id);
337
+ ```
338
+
339
+ - **An extension sees every page and every value typed in the session** (agent variables such as passwords too),
340
+ and can send them anywhere. Upload only extensions you trust, and keep secrets in sessions without extensions. An
341
+ agent run with `variables` in a session with extensions throws `VariablesWithExtensionsError` unless you pass
342
+ `allowWithExtensions: true`.
343
+ - The upload is checked before it is stored: `InvalidExtensionError` says why (not a zip, Manifest V2, a native
344
+ binary, a `debugger` or `nativeMessaging` permission…), `PayloadTooLargeError` over 10 MB, `LimitReachedError`
345
+ beyond 100 extensions, `ExtensionDeniedError` for one the operator blocks. Uploads carry an `Idempotency-Key`, so a
346
+ retry never stores one twice.
347
+
348
+ ## Webhooks
349
+
350
+ Signed HTTPS callbacks when a session ends, an agent run, task run or crawl finishes, or a CAPTCHA waits for a person.
351
+
352
+ ```ts
353
+ const endpoint = await bx.webhooks.create({ url: "https://example.com/webhooks/boxline", events: ["session.ended", "agent_run.finished"] });
354
+ // endpoint.secret ("whsec_…") is shown only now: store it with your other secrets.
355
+ ```
356
+
357
+ In your receiver, verify every delivery against the **raw** body, and drop events you have handled already (a
358
+ delivery can arrive more than once):
359
+
360
+ ```ts
361
+ import { verifyWebhook, WebhookSignatureError } from "@boxline/sdk";
362
+
363
+ // body: the raw bytes (Buffer) exactly as received; re-serialised JSON will not match.
364
+ try {
365
+ const event = verifyWebhook(body, req.headers["boxline-signature"], process.env.BOXLINE_WEBHOOK_SECRET!);
366
+ if (!(await alreadyHandled(event.id))) await handle(event); // dedupe by the id in the signed body
367
+ res.writeHead(200).end();
368
+ } catch (err) {
369
+ if (err instanceof WebhookSignatureError) res.writeHead(400).end(err.reason); // malformed, timestamp_out_of_range, no_matching_signature
370
+ else throw err;
371
+ }
372
+ ```
373
+
374
+ - The signature's timestamp must be within 5 minutes of your clock (`{toleranceSeconds}` changes it), which stops
375
+ replays of old deliveries.
376
+ - `bx.webhooks.rotateSecret(id)` returns a new secret; for 24 hours deliveries are signed with both, so switch at your
377
+ pace. `verifyWebhook` takes a list too: `[newSecret, oldSecret]`.
378
+ - `bx.webhooks.test(id)` sends a `webhook.test` event now; `bx.webhooks.test(id, {type: "agent_run.waiting"})` sends a
379
+ made-up sample of that type (marked `test: true`). `bx.webhooks.deliveries(id, {status: "failed"})` lists
380
+ deliveries (newest first) with each attempt's status and `errorCode`; `retryDelivery(id, deliveryId)` sends one again.
381
+ - `bx.webhooks.eventTypes()` lists every event type with a description; `events: ["*"]` subscribes to all of them,
382
+ also types added later, so ignore types you do not know. `verifyWebhook<WebhookEventPayload>(…)` types the event as
383
+ a union keyed by `type`: `if (event.type === "agent_run.waiting") event.data.consoleUrl`.
384
+ - `webhookSignatureHeader(secret, body)` signs a body the way the API does, for your receiver's tests.
385
+ - Endpoints must be public HTTPS (`WebhookUrlNotAllowedError`); a plan has `webhookEndpoints` of them.
386
+
387
+ ## Lists and pages
388
+
389
+ Every list takes `limit` and `after`. Await it for one page, or iterate it for everything (the SDK follows `next`):
390
+
391
+ ```ts
392
+ const page = await bx.sessions.list({ status: ["RUNNING", "PAUSED"], limit: 50 });
393
+ console.log(page.data.length, page.total, page.next);
394
+
395
+ for await (const s of bx.sessions.list({ status: "RUNNING" })) console.log(s.id);
396
+ for await (const e of session.events({ types: ["console", "error"] })) console.log(e.text);
397
+ for await (const p of page.iterPages()) console.log(p.data.length);
398
+ ```
399
+
400
+ Lists: `sessions.list`, `sessions.events`, `sessions.pages`, `contexts.list`, `apiKeys.list`, `agent.list`,
401
+ `crawl.list`, `extensions.list`, `tasks.list`, `tasks.runs`, `secrets.list`, `secrets.audit`, and a crawl's pages (`crawl.get(id, {after})`, or
402
+ `for await (const p of bx.crawl.pages(id))`).
403
+
404
+ ## Errors, retries and time limits
405
+
406
+ - **Typed errors.** Every failure is a `BoxlineError` with `status`, `code` (stable, snake_case), `message`,
407
+ `requestId` (the API's id: quote it when asking for support) and `clientRequestId` (yours, if you sent one).
408
+ Subclasses: `RateLimitError` (`retryAfter`), `FeatureNotInPlanError`, `ProjectSuspendedError`,
409
+ `IdempotencyMismatchError`, `IdempotencyInProgressError`, `InvalidCursorError`, `CaptchaTimeoutError`,
410
+ `PageUnreachableError`, `PageTimeoutError`, `ModelRefusedError` (extract), `SearchUnavailableError`,
411
+ `OutOfViewportError`, `WebhookUrlNotAllowedError`, `WebhooksUnavailableError`, `WebhookDisabledError`,
412
+ `PayloadExpiredError`, `WebhookSignatureError` (from verifyWebhook), `VariablesWithExtensionsError`,
413
+ `InvalidExtensionError`, `PayloadTooLargeError`, `LimitReachedError`, `ExtensionDeniedError`, `CrossSiteRequestError`,
414
+ `MissingVariablesError`, `PlanLimitError`, `SecretExistsError`, `SecretNotAllowedError`, `TooManySecretValuesError`,
415
+ `MachineTooOldError`, `NotContinuableError`, `TooManyMessagesError`, `SessionNotRunningError`, `AuthenticationError`,
416
+ `NotFoundError`, and
417
+ `BoxlineConnectionError` /
418
+ `BoxlineTimeoutError` when no answer came back. `ErrorCode` has the codes.
419
+ - **Retries.** GETs, and the calls that create or start something (sessions, bulk, agent runs, continued runs, messages
420
+ to runs, crawls, API keys, contexts, extension uploads, tasks, task runs), are retried after a network error, a time-out, 429 and 5xx: 2 retries by default, exponential backoff
421
+ from 0.5 s to 8 s with jitter, or what `Retry-After` / `RateLimit-Reset` say (up to 60 s; longer waits go to you as a
422
+ `RateLimitError`). Other POSTs (exec, actions, fetch…) are never retried: they could run twice.
423
+ - **Idempotency keys.** The SDK sends a new `Idempotency-Key` with every create, and the same one on its retries, so a
424
+ retry never starts a second session or run. Pass your own to make your own retries safe:
425
+ `bx.sessions.create(params, { idempotencyKey: "job-42" })`. The same key with another body is an
426
+ `IdempotencyMismatchError` (for a new API key: also when another caller used it).
427
+ - **Time limits.** `timeoutMs` per request (default 120 s); long calls (exec, `files.waitFor`, plain-English steps)
428
+ get the time they need.
429
+
430
+ ```ts
431
+ const bx = new Boxline({ maxRetries: 3, timeoutMs: 60_000 });
432
+ await bx.sessions.get(id, { timeoutMs: 5_000, maxRetries: 0, clientRequestId: "trace-42" });
433
+ const patient = bx.withOptions({ maxRetries: 6 });
434
+ ```
435
+
436
+ Every request carries `Boxline-SDK: node/<version>`.
437
+
438
+ ## Every method
439
+
440
+ Every public API operation has a method (the Python SDK has the same names in snake_case; the full list is
441
+ `docs/sdk-methods.json` in the platform repository). Session methods take the id first; a `Session` object has the
442
+ same methods without it (`session.pause()`).
443
+
444
+ | Area | Methods |
445
+ |---|---|
446
+ | Account | `me`, `hasFeature`, `auth.signup`, `auth.login`, `auth.logout`, `project.trajectories`, `project.setTrajectories`, `project.settings`, `project.setSettings`, `apiKeys.list`, `apiKeys.create`, `apiKeys.revoke` |
447
+ | Webhooks | `webhooks.create`, `list`, `get`, `update`, `delete`, `rotateSecret`, `test`, `deliveries`, `retryDelivery`; `verifyWebhook` (no request) |
448
+ | Sessions | `sessions.create`, `get`, `list`, `update`, `release`, `pause`, `resume`, `move`, `extend`, `rotateProxy`, `rotateUrls`, `live`, `bulk` |
449
+ | Browser | `sessions.actions`, `sessions.computer`; on a session: `goto`, `click`, `hover`, `fill`, `type`, `press`, `scroll`, `wait`, `select`, `elements`, `evaluate`, `content`, `screenshot`, `cursor`, `upload`, `tabs`, `newTab`, `switchTab`, `closeTab`, `back`, `forward`, `reload`, `step`, `extract`, `exportCookies`, `computer`, `mouse.move`/`moveBy`/`click`/`down`/`up`/`drag`, `keyboard.key`/`type`/`press` |
450
+ | Shell and scripts | `sessions.exec`, `execStream`, `runScript`, `restartShell` |
451
+ | Files | `sessions.files.list`, `read`, `readText`, `write`, `delete`, `waitFor` |
452
+ | Logs | `sessions.events`, `streamEvents`, `pages`, `recording`, `recordingFrame`; on a session: `waitForHuman`, `onCaptcha` |
453
+ | Saved logins | `contexts.create`, `get`, `list`, `rename`, `delete`, `setLogin`, `deleteLogin` |
454
+ | Secrets | `secrets.create`, `list`, `get`, `update`, `delete`, `audit` |
455
+ | Extensions | `extensions.upload`, `list`, `get`, `delete` |
456
+ | Web | `fetch`, `screenshot`, `pdf`, `extract`, `search`, `crawl.start`, `get`, `list`, `cancel`, `pages`, `wait` |
457
+ | Agent | `agent.models`, `run`, `get`, `list`, `takeover`, `handBack`, `cancel`, `continueRun`, `sendMessage`, `stream`, `wait` |
458
+ | Tasks | `tasks.create`, `list`, `get`, `update`, `delete`, `run`, `runs`, `waitForRun` |
459
+ | Usage | `usage`, `stats`, `pricing`, `openapi`, `health` |
460
+
461
+ ## Examples
462
+
463
+ In `examples/`. Most use the small example site (`npx tsx examples/test-site.ts`), which only a local API's sessions can
464
+ reach, so run them with `BOXLINE_API_URL=http://localhost:8080`; `search.ts` and `quickstart.ts` work anywhere.
465
+
466
+ | File | Shows |
467
+ |---|---|
468
+ | `quickstart.ts` | a session, Playwright over CDP, the actions API |
469
+ | `mixed-download-process-fill.ts` | Playwright download → Python in the shell → files API → a form filled with actions |
470
+ | `agent-mixed.ts` | one agent run using the browser and the shell, streamed step by step |
471
+ | `script-mixed.ts` | a script mixing Playwright, `child_process`, `fs`, `useModel` and `step()` |
472
+ | `page-apis.ts` | fetch, screenshot, pdf, crawl, extract, a page that cannot load |
473
+ | `search.ts` | web search, with the top pages fetched as Markdown, and the cache |
474
+ | `computer-use.ts` | mouse and keyboard (drag, hover, double-click, keys), each action's `text`, `session.computer()` in both shapes |
475
+ | `agent-computer.ts` | an agent run in computer mode, with its thoughts streamed |
476
+ | `webhook-receiver.ts` | a receiver (Node `http`) that verifies each delivery and drops duplicates |
477
+ | `webhooks.ts` | endpoints, deliveries and their history, re-sending, rotating the secret, a test event |
478
+ | `agent-variables.ts` | an agent run with `%email%` / `%password%` limited to one site |
479
+ | `tasks.ts` | a task with an output schema on a demo shop: run with a variable, waited for, its history, changed, deleted; an agent run with `output` |
480
+ | `secrets.ts` | a secret exported into a shell (its length checked, its value hidden in output), changed, audited, deleted; login details on a saved login |
481
+ | `script-use-model.ts` | `useModel`, `step()` and `extract()` in a script |
482
+ | `captcha.ts` | noticing a CAPTCHA and handing it to a person |
483
+ | `block-ads.ts` | a session that blocks ads and trackers, and its `blockedRequests` count |
484
+ | `extension.ts` | a tiny MV3 extension built as a zip, uploaded, loaded into a session, and deleted |
485
+ | `list-sessions.ts` | cursor pages and `for await` |
486
+ | `reliability.ts` | idempotency keys, request ids, typed errors, time limits |
487
+
488
+ ```bash
489
+ pnpm --filter @boxline/sdk build # in the platform repository: the examples import @boxline/sdk
490
+ BOXLINE_API_KEY=bxl_… npx tsx examples/quickstart.ts
491
+ ```
492
+
493
+ ---
494
+
495
+ This repository holds the Boxline Node SDK (@boxline/sdk). It is copied from Boxline's main repository on every change. Issues and pull requests are welcome here; accepted changes are made there and arrive with the next copy.