TypeScript SDK
This page covers how to install, configure, and use the official @web-agent/sdk Node / browser package: open a session, start a run, and stream events.
npm install @web-agent/sdk
# or pnpm add @web-agent/sdk / yarn add @web-agent/sdk / bun add @web-agent/sdkWorks in Node 20+ and modern browsers.
Don't ship a server-grade
wa_key to the browser. Keys grant project-wide access; ship them only to server-side code or to environments where you trust the runtime.
One entry point: Client
User-facing API products live on the same Client:
import { Client } from "@web-agent/sdk";Create a client with new Client({ apiKey, projectId }); both apiKey and projectId are required — the examples below read them from environment variables.
| Resource | Product | Use case |
|---|---|---|
client.sessions / messages / events | DoAnything (open-ended) | Free-form input; the agent picks the path. |
client.deepResearch | DeepResearch (research → report) | Standalone API. |
client.webSearch | WebSearch (query → results) | Synchronous by default (wait: true). |
client.track | Track (monitor → snapshot) | Long-lived monitors with webhook delivery. |
DoAnything — open-ended runs
import { Client } from "@web-agent/sdk";
const client = new Client({
apiKey: process.env.WEBAGENT_API_KEY!,
projectId: process.env.WEBAGENT_PROJECT_ID!,
});
const session = await client.sessions.create({
instructions: "Search Hacker News for the top 5 stories today.",
});
const run = session.runs[0]!; // session-create implicitly queues the first run
for await (const event of client.events.stream(session.id, run.id)) {
console.log(event.type, event.data);
if (event.type === "run.completed") break;
}Wire fields stay snake_case to match the API exactly; method names are camelCase.
Resume + heartbeats
for await (const event of client.events.stream(session.id, run.id, {
lastEventId: "142",
includeHeartbeats: false,
})) {
if (event.type === "run.completed") break;
}Backed by fetch with manual SSE parsing — works in Node 20+, Bun, Cloudflare Workers, and modern browsers.
Follow-up run vs. inflight message
// 1. Push into the current run's chat queue
await client.messages.send(session.id, run.id, {
content: "Also include the comment count for each.",
});
// 2. Start a NEW run in the SAME session
const followup = await client.sessions.createRun(session.id, {
instructions: "Click into the first post and summarise it.",
});Answer an input request
await client.messages.intervene(session.id, run.id, {
kind: "answer_input_request",
input_request_id: "ir_01HXX",
response: { solved: true },
});The kind discriminator lets the same endpoint handle take-control / release-control too — see Take Control.
Cancel / stop / list
await client.sessions.cancelRun(session.id, run.id, { reason: "user_cancelled" });
await client.sessions.stop(session.id, { force: false });
const list = await client.sessions.list({ status: "running", limit: 20 });
list.items.forEach((s) => console.log(s.id, s.status));DeepResearch — research → report
DeepResearch is a standalone API — the path carries no project segment (/v1/deep_research); the project tenant resolves from the Bearer token.
const run = await client.deepResearch.run({
topic: "Open-source vector DB landscape 2026",
depth: "deep", // light / standard / deep
requireOutlineApproval: true, // outline HITL gate (default on)
});
// Subscribe to events (DR uses the DoAnything SSE channel) + respond to the gate
for await (const event of client.events.stream(
run.session_id as string, run.run_id as string,
)) {
if (event.type === "run.input_request") {
await client.deepResearch.intervene(run.run_id as string, {
requestId: (event.data as { request_id: string }).request_id,
response: "approve", // or { action: "approve_with_edits", edits: [...] }
});
}
if (event.type === "run.completed") break;
}
// Pull the three-piece artifact set (final.md / citations.json / confidence.json)
const artifacts = await client.deepResearch.listArtifacts(run.run_id as string);
const final = await client.deepResearch.getArtifact(
run.run_id as string, artifacts[0]!.id as string,
);WebSearch — query → results
WebSearch is project-scoped. run() defaults to wait: true: the server blocks for up to 30s and returns the full result once the search completes; past 30s it returns 202 — call get(runId) to poll.
// Synchronous (default)
const result = await client.webSearch.run({
queries: ["best TypeScript ORM 2026"],
engines: ["tavily"],
summarize: true,
});
// Async
const pending = await client.webSearch.runAsync({
queries: ["best TypeScript ORM 2026"],
});
const detail = await client.webSearch.get(pending.run_id as string);
// Refine (re-run within the same run)
await client.webSearch.refine(pending.run_id as string, {
text: "add site:reddit.com and re-run",
});Track — long-lived monitors
Track creates a long-running monitor. It checks the target pages repeatedly on a schedule (cron / interval / event), saving a snapshot each time; when a change meets the trigger condition, it notifies you through the channel you configured (such as a webhook).
const mon = await client.track.create({
intent:
"Notify me when the iPhone 17 Pro listing on apple.com goes below $999",
schedule: { kind: "interval", interval_seconds: 3600 },
notifyChannel: { kind: "callback_url", url: "https://hooks.example.com/track" },
});
// Lifecycle controls — pause / resume / refine via patch:
await client.track.pause(mon.id as string, { reason: "manual review" });
await client.track.resume(mon.id as string);
await client.track.refine(mon.id as string, {
triggerDsl: { op: "lt", field: "price", value: 999 },
});
// Manually run one check (bypasses the schedule):
const outcome = await client.track.runNow(mon.id as string);
// Snapshot history (newest first):
const snaps = await client.track.listSnapshots(mon.id as string);
const snap = await client.track.getSnapshot(
mon.id as string,
snaps.items[0]!.id as string,
);
// Webhook delivery history + re-send a failed delivery:
const deliveries = await client.track.listDeliveries(mon.id as string, {
includePayload: true,
});
await client.track.retryDelivery(
mon.id as string,
deliveries.items[0]!.id as number,
);
// Cancel terminates the monitor (terminal state):
await client.track.cancel(mon.id as string); // equivalent: client.track.delete(...)Alignment HITL (optional)
If the supervisor needs you to disambiguate intent, the monitor moves to pending_clarification and emits an alignment.input_request event. Answer with intervene():
await client.track.intervene(mon.id as string, {
requestId: "req_align_1",
response: "SKU A",
});You can also push free-text guidance into the alignment queue via client.track.message(monId, { content: "…" }).
Errors
import {
ApiError,
InsufficientCreditsError,
RateLimitedError,
UnauthorizedError,
} from "@web-agent/sdk";
try {
await client.sessions.create({ instructions: "…" });
} catch (err) {
if (err instanceof InsufficientCreditsError) {
console.log("top up:", err.detail, err.extra);
} else if (err instanceof ApiError) {
console.log(err.code, err.statusCode, err.detail);
} else {
throw err;
}
}Every error class subclasses ApiError and exposes code / statusCode / detail / extra matching the API error envelope.
Types
DoAnything request / response types are top-level exports:
import type {
Session, Run, Event, EventType,
CreateSessionRequest, CreateRunRequest, InterveneRequest,
RunStatus, SessionStatus, TerminalReason,
} from "@web-agent/sdk";DR / DS / WS responses come back as Record<string, unknown> (the OpenAPI envelope verbatim) — index by key (run.run_id / run.status). Each resource also exports its own option types (DRRunOptions / DSRunOptions / WSRunOptions).
Next steps
- Python SDK — same surface in Python.
- Errors & retries — recommended retry policy, idempotency keys.
- DoAnything — lifecycle, profiles, workspaces.