# WebAgent — Full Documentation (EN) --- # WebAgent WebAgent is the web action layer in Qoni. It lets agents search, extract, operate webpages, track changes, and run controlled web tasks. After reading these docs, you can integrate WebAgent into your application, create a session, submit a run, and stream execution results. WebAgent is not a traditional crawler SDK. It is designed for LLM agent task execution: developers provide an instruction, while WebAgent manages runtime resources, page state, retries, structured results, and the run lifecycle. The Console and SDKs are clients for the same API. ## Toward the agentic web Qoni's goal is to **make Agents first-class citizens of the Web**. WebAgent is not only a background click simulator, and not only a renderer for humans; it places Human, Agent, and Web in the same auditable collaboration layer, so an Agent can read, act, produce results, and render state back to people within an explicit permission boundary. ## When to use WebAgent - Your agent needs real-time web data instead of relying only on model training data or a fixed knowledge base. - You need search, extraction, browser actions, and long-running tasks behind auditable APIs. - You want the Console, SDKs, and backend services to share the same REST API contract. - You need a session / run model for state, event streaming, or steps that require user confirmation. ## When not to use WebAgent - The task only needs your own backend APIs and does not need open web access. - You need large-scale offline crawling, warehouse synchronization, or search-index construction. - The target site's terms do not allow automated access and you do not have the required authorization. - You have not defined API keys, project scope, run budget, and failure handling. ## Core capabilities | Capability | Description | | --- | --- | | DoAnything API | Provide a natural-language instruction. WebAgent chooses tools, runs steps, and returns a result. | | Shaped APIs | Dedicated API contracts for artifact-shaped workflows such as DeepResearch, WebSearch, and Track. | | Session / run model | A session owns runtime resources. A run represents one instruction or follow-up action. | | Event stream | Subscribe to run state, output chunks, errors, and user-confirmation requests through SSE. | | SDK and raw HTTP | Python, TypeScript, and cURL docs use the same API semantics. | ## Documentation entry points - [What is WebAgent](/en/webagent/getting-started/what-is-webagent) explains WebAgent's role, boundaries, and API shape. - [Quickstart](/en/webagent/getting-started/quickstart) runs the first run with Python, TypeScript, or cURL. - [Authentication & API keys](/en/webagent/getting-started/authentication) explains `wa_` keys, project scope, and rotation. - [DoAnything](/en/webagent/features/do-anything) explains the session, run, event, and profile lifecycle. - [Errors & Retries](/en/webagent/reference/errors-and-retries) covers error codes, retry policy, and idempotency. - [API Reference](/en/webagent/reference/) covers base URL, auth, errors, rate limits, and pagination. - [Vibecoding](/en/webagent/guides/vibecoding) shows how to give the docs and OpenAPI spec to an IDE-resident LLM. --- # What is WebAgent WebAgent is a general-purpose agent platform for the browser — an API that lets programs complete tasks in a real browser, from a single atomic search to a goal that runs for a month. ## The four APIs WebAgent exposes **4 peer APIs**; pick whichever matches what you want to do: - **DoAnything** — give one natural-language instruction; the agent picks its own tools and path and completes the task in a browser. The artifact shape is open-ended. - **DeepResearch** — give a research topic; it runs multiple retrieval rounds and cross-checks, and produces a cited, confidence-scored report (`final.md` + citations + confidence). - **WebSearch** — give a batch of queries; it fetches across engines, deduplicates, reranks, and returns structured search results (optional summary). - **Track** — give a monitoring intent; it re-fetches on a schedule, compares against a baseline, and notifies you on change (a snapshot stream + change notifications). Shared capabilities: - **Profiles** — reusable login state across sessions. No re-logging in every run. - **Workspaces** — a persistent file system the agent can read and write to. - **Schedules** — cron, interval, event-triggered, or autonomous (the agent decides when next to run). - **SSE event stream** — the same `run.*` events that drive the Console, streamed directly to your code. ## What it is not - Not a low-code automation builder. There is no canvas. You wire up tasks in code (or via the Console as a prototyping aid). - Not a hosted LLM API. Bring your task; WebAgent picks an LLM and pays the bill on a credits model. ## Three product surfaces: Console / OpenAPI / SDK All APIs are exposed through the same three surfaces, with 1:1 capability parity and a shared resource layer / event stream / billing: | You can use … | … to do | |---|---| | The [REST API (OpenAPI)](/en/webagent/reference/) | Anything. Console and SDKs are just clients. `api.eak.eazo.ai/v1/...` + `Authorization: Bearer wa_...` | | Python or TypeScript [SDK](/en/webagent/sdk/python) | Same surface, idiomatic types, retries, streaming, `wait_for_done`. | | The [Console](https://dashboard.qoni.ai) | Prototype tasks visually; non-developers welcome; *Get Code* dialog hands you working snippets. | **API-developer-first** — the product is the API. Console is a convenience layer, not a separate product surface; no Console-only privileged endpoints. ## Resource model The 4 APIs share one event stream and billing, but each has its own resource model. DoAnything uses **session + run**: ``` Session (one container; holds a browser, profile, workspace) └── Run #1 status: completed (one instruction; lifecycle has 7 states) └── Run #2 status: running (a follow-up instruction in the same session) └── events: SSE stream (status_changed, message, action.*, screenshot, …) ``` A **session** owns the runtime resources (browser, profile, workspace). Each **run** is one instruction; you can submit follow-up runs against the same session and they share state. The run lifecycle has seven states (`pending`, `running`, `awaiting_input`, `paused`, `done`, `failed`, `canceled`); see [DoAnything](/en/webagent/features/do-anything). DeepResearch / WebSearch / Track differ: DeepResearch / WebSearch are standalone runs (one-shot artifact, no session); Track uses long-lived monitors. Each feature page covers its own. ## Next steps - [Quickstart](/en/webagent/getting-started/quickstart) — 5 minutes from sign-up to first SSE event. - [Authentication & API keys](/en/webagent/getting-started/authentication) — how `wa_` keys work and how to scope them. - [DoAnything](/en/webagent/features/do-anything) — the core resource model. --- # Quickstart Five minutes. Sign up, install the SDK, start a run, and watch events stream back. ## Step 0 — Get an API key (30 sec) 1. Sign up at [dashboard.qoni.ai](https://dashboard.qoni.ai). 2. Open **Settings → API Keys → Create**. 3. Copy the `wa_…` key. **It is shown once.** If you lose it, revoke and create again. ```bash export WEBAGENT_API_KEY=wa_xxxxxxxxxxxxxxxxxxxxxxxx export WEBAGENT_PROJECT_ID=proj_xxxxxxxxxxxxxxxxxxxxxxxx ``` ::: tip Project ID Project-scoped paths look like `/v1/projects/{pid}/…` (DoAnything / WebSearch / Track). Find your project ID in the Console URL after **Project Switcher → your project**. Standalone endpoints (DeepResearch) resolve the project from your Bearer token. ## Step 1 — Install the SDK (30 sec) ```bash pip install web-agent-sdk ``` ```bash npm install @web-agent/sdk ``` ```bash # nothing to install ``` ## Step 2 — Run a task (90 sec) WebAgent is **4 peer APIs** — DoAnything, DeepResearch, WebSearch, Track. Pick whichever matches what you want to do. The walkthrough below uses **DoAnything** as the demo — all 4 APIs share the same mechanism: `Client` → start a task → stream events to a terminal state. The other three appear in Step 3. `Client` opens a DoAnything session and streams events to terminal. ```python import asyncio from web_agent.v1 import Client from web_agent.v1.types import CreateSessionRequest async def main(): async with Client( api_key="wa_demo_xxxxxxxxxxxxxxxx", project_id="proj_demo_0001", ) as client: session = await client.sessions.create(CreateSessionRequest( instructions="Search Hacker News for the top 5 stories today, return them as a list.", )) run = session.runs[0] async for event in client.events.stream(session.id, run.id): print(event.type, event.data) if event.type == "run.completed": break asyncio.run(main()) ``` ```typescript import { Client } from "@web-agent/sdk"; const client = new Client({ apiKey: "wa_demo_xxxxxxxxxxxxxxxx", projectId: "proj_demo_0001", }); const session = await client.sessions.create({ instructions: "Search Hacker News for the top 5 stories today, return them as a list.", }); const run = session.runs[0]!; for await (const event of client.events.stream(session.id, run.id)) { console.log(event.type, event.data); if (event.type === "run.completed") break; } ``` ```bash curl https://api.eak.eazo.ai/v1/projects/proj_demo_0001/do_anything/sessions \ -H "Authorization: Bearer wa_demo_xxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "instructions": "Search Hacker News for the top 5 stories today, return them as a list." }' ``` ::: tip Same code as the Console The Console's **Get Code** dialog hands you exactly this snippet, with your real API key and current form values pre-filled. Open `dashboard.qoni.ai/new`, fill the form, click **Get Code**, and you skip the typing. ## Step 3 — DeepResearch / WebSearch / Track (60 sec) These 3 APIs share the same `Client` / endpoints / error envelope as DoAnything. When you want a research report / search results / change monitoring, use the matching API: ```python async with Client(api_key="wa_...", project_id="proj_demo") as client: run = await client.deep_research.run( topic="Open-source vector DB landscape 2026", depth="deep", ) print(run["run_id"]) ``` ```python async with Client(api_key="wa_...", project_id="proj_demo") as client: # wait=true (default): blocks ≤30s synchronously result = await client.web_search.run( queries=["best Python ORM 2026"], ) for hit in result["results"]["results"]: print(hit["title"], hit["url"]) ``` ```python async with Client(api_key="wa_...", project_id="proj_demo") as client: mon = await client.track.create( intent="Notify me when the Apple stock dips below $200", schedule={"kind": "interval", "interval_seconds": 3600}, notify_channel={"kind": "callback_url", "url": "https://hooks.example.com/track"}, ) print(mon["id"]) ``` These 3 APIs share the same auth / error envelope / event channel as DoAnything — see [Python SDK](/en/webagent/sdk/python) / [TypeScript SDK](/en/webagent/sdk/typescript). ## Step 4 — Watch it in the Console (30 sec) Open `https://dashboard.qoni.ai/sessions/` (replace with the id you printed in step 2). You'll see the same chat log plus a live browser preview iframe — exactly what your stream is showing, rendered. ## Next steps - [Run that asks you to confirm something](/en/webagent/features/do-anything#input-request) — `run.input_request`. - [Save login state across sessions](/en/webagent/features/do-anything#profiles) — Profiles. - [Schedule a run every morning](/en/webagent/reference/) — Schedules. - [Browse the full API](/en/webagent/reference/) — every endpoint, every field. ## Troubleshooting | Symptom | Cause | Fix | |---|---|---| | `401 unauthorized` | Wrong, expired, or revoked key | **Settings → API Keys** → create a new one | | `402 insufficient_credits` | Free quota used up | **Settings → Billing → Add credits** | | SSE stalls > 60 s | Network drop or proxy buffering | Reconnect with `Last-Event-ID` — see [Events & SSE](/en/webagent/features/do-anything#events) | | `429 rate_limit_exceeded` | Burst over plan concurrency | Back off + retry; Dev plan defaults to 5–10 concurrent sessions | --- # Authentication & API keys This page covers WebAgent's API key format, how to send it on every request, and the create / revoke / rotate flow. Every request carries a bearer token in the `Authorization` header: ```http Authorization: Bearer wa_xxxxxxxxxxxxxxxxxxxxxxxx ``` ## Key shape - **Prefix:** `wa_` — short for *web agent*. (Mirrors Stripe's `sk_*`.) - **Length:** 28+ chars after the prefix. Treat them as opaque. - **Scope:** one project. Multi-project? Create one key per project. - **Visibility:** shown **once** at creation. Lose it → revoke and create again. - **Revocation:** soft-delete with a one-hour grace window so in-flight requests don't 401 mid-run. ## Where to keep it - **Local dev:** environment variable, `.env.local` (already in `.gitignore`). - **Production:** your secret manager (Vault, AWS Secrets Manager, GCP Secret Manager, …). - **Never** commit a key to git. The Console's *Get Code* dialog uses a placeholder by default. ## Multi-project A single user can have many projects. The API path encodes the project: ```http GET /v1/projects/{project_id}/do_anything/sessions ``` There is no `X-Project-Id` header. The path makes the tenant explicit so a stray `curl` to a different project ID is a different URL — no silent cross-tenant calls. ## Rotation You can keep two valid keys at once. Common rotation flow: 1. **Create** a new key in **Settings → API Keys**. 2. **Deploy** with the new key. 3. **Revoke** the old one. Old key keeps working for one hour; deploy completes; old key 401s after the grace window. ## Errors | Status | Code | Meaning | |---|---|---| | 401 | `unauthorized` | Missing, malformed, expired, or revoked-past-grace | | 403 | `forbidden` | Key valid but doesn't have access to that project | | 429 | `rate_limit_exceeded` | Per-key concurrency or per-minute limit hit | ## Next steps - [Pricing & Credits](/en/webagent/getting-started/pricing) — what each run costs. - [DoAnything](/en/webagent/features/do-anything) — what a request actually creates. --- # Pricing & Credits WebAgent runs on **credits** — a pre-paid USD balance. Each run drains the balance as it executes. The Console header shows a live `$X.XX` and hovering reveals the breakdown. ## Two buckets | Bucket | Source | Expires | |---|---|---| | **Monthly** | Your subscription's included credits | End of billing period | | **Additional** | One-off top-ups + auto-recharge | Never | Runs drain monthly first, then additional. You'll never lose top-up money to a month-end reset. ## Four cost lines per run Every run records four costs separately so you can build dashboards: - `llm_cost_usd` — LLM tokens consumed - `browser_cost_usd` — browser-pool seconds - `proxy_cost_usd` — proxy bandwidth (when applicable) - `total_cost_usd` — the sum, also what drains your balance You can read them on `SessionResponse` and on every `run.cost_update` SSE event. ## Plans | Plan | Monthly | Includes | Concurrency | |---|---|---|---| | Free | $0 | $50–100 trial credits during early access | 1–2 | | Dev | ~$29 | $30 credits | 5–10 | | Business | ~$299 | $400 credits + team seats | 50–100 | | Scaleup | ~$999 | $1,400 credits + dedicated queue + region pinning | 250+ | ::: tip Subject to ±30% adjustment Numbers above are baseline. Pricing is finalised before public launch; early-access users get the locked-in rate. ## Auto-recharge To avoid 402s on a long Sunday-night run, enable auto-recharge in **Settings → Billing**: - **Threshold** — top up when balance falls below `$X`. - **Amount** — top up by `$Y` each time. - **Monthly cap** — never spend more than `$Z` of recharge per calendar month. ## Per-run duration limit You can cap a run's duration. Useful for cron jobs you'd rather have fail-fast than runaway: ```python await client.sessions.create(CreateSessionRequest( instructions="...", max_duration_minutes=30, )) ``` When the duration limit is hit the run is terminated. The credits already spent are still billed. ## Next steps - [Authentication](/en/webagent/getting-started/authentication) — how to keep keys safe. - [DoAnything](/en/webagent/features/do-anything) — what a run actually executes. --- # DoAnything DoAnything is WebAgent's open-ended API. You give one natural-language instruction, and the agent picks its own tools, decides its own path, runs the steps, and does its best to reach the goal in a browser environment. Unlike DeepResearch / WebSearch / Track — those three are shaped APIs with a fixed artifact shape and a quality contract — DoAnything has **no fixed artifact shape**: you may not fully know what the goal looks like in the end, so the agent improvises. It therefore makes no quality contract; when you want a contract, use the matching shaped API. The 4 APIs are peers — pick whichever fits what you want. ## When to use it - The goal is undefined or compound, and the agent needs to decide how to do it. - The task spans multiple steps and pages, and may need login state, human confirmation, or long-running execution. - You accept "best effort" rather than a fixed-shape result contract. When you want a fixed-shape artifact, use [DeepResearch](/en/webagent/features/deep-research) / [WebSearch](/en/webagent/features/web-search) / [Track](/en/webagent/features/track). ## Resource model A **session** is one runtime container — it owns the browser, the profile (cookies / login state), and the workspace (file system). A **run** is one instruction that runs inside a session. You can fire follow-up runs at the same session; they share the state the previous run left behind. ```text project └── session id: sess_… ├── browser, profile, workspace └── run id: run_… ├── instructions "Search Hacker News..." ├── status running | done | … └── events (SSE) run.status_changed, run.message, … ``` ## Lifecycle (seven states) ```mermaid stateDiagram-v2 [*] --> pending pending --> running running --> awaiting_input : run.input_request awaiting_input --> running : POST /intervene running --> paused : POST /pause paused --> running : POST /resume running --> done running --> failed pending --> canceled running --> canceled : POST /cancel awaiting_input --> canceled paused --> canceled ``` | State | Meaning | Next moves | |---|---|---| | `pending` | Accepted, queued for an agent slot | → `running`, `canceled` | | `running` | Agent is actively working | → `done`, `failed`, `awaiting_input`, `paused`, `canceled` | | `awaiting_input` | Agent paused itself; needs you to answer | → `running` (via `intervene`) | | `paused` | You paused it (manual) | → `running` (via `resume`), `canceled` | | `done` | Completed successfully; `output` populated | terminal | | `failed` | Hit an error; `error.code` and `error.detail` populated | terminal | | `canceled` | You canceled (or scheduled max-duration tripped) | terminal | A run in any non-terminal state holds session resources. Cap with `max_duration_minutes` to bound that. ## Submitting a run The first run is submitted together with the `CreateSessionRequest` when you create a session. Request fields: | Field | Type | Required | Notes | |---|---|---|---| | `instructions` | string | Yes | The instruction in plain English. Up to 10 000 chars. | | `max_duration_minutes` | int | No | 1–10 080 (one week). | | `recording` | object | No | `{enabled, quality, capture_during_take_control}`; omit for off. | | `keep_alive` | bool | No | When the run ends, keep the session warm for follow-up runs. | | `allowed_actions` | string[] | No | Whitelist of tool actions the agent may call. Empty = all allowed. | | `profile_id` | string | No | Reuse cookies/auth from a [Profile](/en/webagent/features/profiles). | The full schema is in the [OpenAPI spec](/openapi/v1.json). Example: ```python from web_agent import Client from web_agent.v1.types import CreateSessionRequest, RecordingConfigRequest session = await client.sessions.create(CreateSessionRequest( instructions="Find the top 5 Show HN posts from the last 24 hours.", max_duration_minutes=10, recording=RecordingConfigRequest(enabled=True), keep_alive=True, )) run = session.runs[0] ``` ## Follow-up runs ```python from web_agent.v1.types import CreateRunRequest followup = await client.sessions.create_run( session.id, CreateRunRequest( instructions="Now click into the first post and summarise the discussion.", ), ) ``` The follow-up runs in the same browser, with the same cookies, against the same DOM the previous run left. ## Events {#events} Every run emits a Server-Sent Events stream: ```http GET /v1/projects/{pid}/do_anything/sessions/{sid}/runs/{rid}/events Authorization: Bearer wa_… ``` Eleven event types (the envelope is the same; `data` shape varies): | Type | When | |---|---| | `run.status_changed` | State transition | | `run.message` | Agent or user message in the chat thread | | `run.action.started` | Agent invoked a tool | | `run.action.completed` | Tool returned | | `run.action.failed` | Tool threw | | `run.screenshot` | New browser frame (`url` is short-lived) | | `run.input_request` | Agent paused; needs you to answer | | `run.input_request_resolved` | Your `intervene` was accepted | | `run.cost_update` | Per-step cost delta | | `run.completed` | Terminal; `output` populated | | `stream.heartbeat` | Every ~15 s; harmless | Reconnect cleanly: ```http GET …/events Last-Event-ID: 142 ``` The server replays events with `id > 142` so you don't miss anything. ## Input request (human in the loop) {#input-request} When the agent hits a captcha, a 2FA prompt, or any judgment call, it emits `run.input_request`: ```json { "type": "run.input_request", "data": { "input_request_id": "ir_01HXX…", "prompt": "I see a 'Verify you're human' challenge. Solve it for me?", "schema": { "type": "object", "properties": { "solved": { "type": "boolean" } } } } } ``` You answer via `POST /intervene`: ```python await client.messages.intervene( session.id, run.id, input_request_id="ir_01HXX…", response={"solved": True}, ) ``` The run transitions back to `running`. The whole cycle is one round-trip; no polling. ## Take Control {#take-control} Some steps the agent cannot finish on its own — a captcha, 2FA, a password only a person should type. For these you can **hand the browser to a human**: an interactive live browser opens, the person clicks and types, and control is then handed back to the agent. This differs from an input request above: an input request is the agent asking a question that you answer with JSON; Take Control is **a human directly operating the agent's browser**. Take Control uses the same `intervene` endpoint, with `kind` selecting the action. ### 1. Request control ```http POST /v1/projects/{pid}/do_anything/sessions/{sid}/runs/{rid}/intervene Content-Type: application/json { "kind": "take_control", "reason": "captcha" } ``` It is commonly triggered after the agent emits a `run.input_request` (captcha / login needed), but you can request it at any time. ### 2. Get the control URL, hand it to a person The backend issues a standalone control URL, delivered via the `run.take_control_pending` event (`data` carries `standalone_control_url` / `exp` / `reason`). It is an unbranded live-browser page — you can **forward it straight to your end user**. In that page, the person clicks and types directly on the agent's browser: solving captchas, logging in, handling judgment calls. After you request control the agent does **not** stop immediately — only once someone actually opens the URL and connects does the run move to `paused` and the agent pause (event `run.user_paused`). If no one connects, the URL expires after about 5 minutes and `run.take_control_expired` fires. ### 3. Release control When the person is done, call `intervene` again: ```http { "kind": "release_control", "trigger": "client_release" } ``` The run returns to `running`, and the agent re-observes the current page and continues (event `run.user_released`). The control page also auto-releases after about 30s idle. To re-issue an expired URL, use `{ "kind": "refresh_control_url" }`. ### Related events | Event | Meaning | |---|---| | `run.take_control_pending` | The control URL is issued, awaiting a connection; `data.standalone_control_url` is the page for the human | | `run.user_paused` | Someone connected and is operating; the agent is paused | | `run.user_released` | Control handed back; the agent continues | | `run.take_control_expired` | No one connected; the URL timed out | > SDK note: the Python / TypeScript SDK `intervene()` currently covers only `answer_input_request`. Take Control's `take_control` / `release_control` / `refresh_control_url` go through the HTTP intervene endpoint directly. See the [OpenAPI spec](/openapi/v1.json) for the full fields. ## Profiles {#profiles} A **profile** is a single, whole browser identity (one per user, accumulating their login state across every site). Reference one when creating a session: ```python await client.sessions.create(CreateSessionRequest( instructions="Open my LinkedIn inbox and reply to the latest message.", profile_id="prof_alice", )) ``` The first time, set up the Profile manually in the Console (log into whichever sites you need). Future sessions reuse the same one. See [Profiles](/en/webagent/features/profiles). ## Workspaces A **workspace** is a persistent file system. The agent can read and write files; you fetch them via signed URL after the run is done. Useful for "scrape this site, write a CSV, hand it back." ## Next steps - [API Reference](/en/webagent/reference/) — every field. - [Authentication](/en/webagent/getting-started/authentication) — keys, scopes, rotation. - [Vibecoding](/en/webagent/guides/vibecoding) — how to provide all of this to your IDE's LLM. --- # DeepResearch DeepResearch is one of WebAgent's typed APIs. You give it a topic; it runs multiple rounds of retrieval, cross-checks sources, and produces a Markdown research report with citations and confidence. Unlike DoAnything: DoAnything has an open-ended output shape — the agent decides what to return; DeepResearch has a **fixed output shape** — always a research report — so it can offer a field-stable result contract (`final_md` + citations + confidence). When you know you want a report, use this API. ## When to use it - The output you want is clearly a research report, not the result of an open-ended task. - You need traceable citations and measurable coverage, not just a summary. - The task can run for a few minutes to tens of minutes — DeepResearch is a long-running job. When you want "run a batch of keywords and get structured hits" rather than a written report, use [WebSearch](/en/webagent/features/web-search). When you want an open-ended task, use [DoAnything](/en/webagent/features/do-anything). ## Run a research job HTTP endpoint: ```http POST /v1/projects/{pid}/deep_research/runs Authorization: Bearer wa_... ``` Request fields: | Field | Type | Required | Description | |---|---|---|---| | `topic` | string | Yes | Research topic | | `depth` | string | No | `light` / `standard` / `deep` — controls the number of retrieval rounds and cost. Defaults to `standard` | | `output_format` | string | No | Report shape. Defaults to `report` | | `target_audience` | string | No | Who the report is written for; affects the depth of the prose | | `require_outline_approval` | bool | No | When `true`, pauses at the outline stage for your approval — see below. Defaults to `true` | | `max_duration_minutes` | int | No | Duration backstop | | `domain_whitelist` / `domain_blacklist` | string[] | No | Restrict / exclude retrieval domains | | `callback_url` | string | No | URL called back on a terminal state | For the full schema, see `CreateResearchRequest` in the [OpenAPI spec](/openapi/v1.json). Example: ```python from web_agent.v1 import Client async with Client(api_key="wa_...", project_id="proj_demo") as client: run = await client.deep_research.run( topic="The 2026 open-source vector database landscape", depth="standard", require_outline_approval=False, ) print(run["run_id"], run["status"]) ``` ## Phases A research run moves through these phases in order. While `status` is still `running`, `phase` tells you where it is: | phase | Meaning | |---|---| | `brief` | Parses the topic, settles the research goal | | `plan` | Plans the retrieval paths | | `hitl_outline` | Outline is ready, waiting for your approval (appears only when `require_outline_approval=true`) | | `gather` | Multiple rounds of retrieval, extracting sources | | `crosscheck` | Cross-checks across sources | | `synthesize` | Drafts the report with citations and confidence | ## Outline approval (human in the loop) When `require_outline_approval=true`, the run pauses at the `hitl_outline` phase and waits for you to confirm the outline before continuing. This lets you correct the direction before any retrieval cost is spent. ```python run = await client.deep_research.run(topic="...", require_outline_approval=True) # run is paused at hitl_outline; approve or adjust the outline via intervene await client.deep_research.intervene(run["run_id"], response={"approved": True}) ``` When you don't need this step, set `require_outline_approval=False` and the run proceeds straight to a terminal state. ## Result Once a run reaches a terminal state, the `result` field (`ResearchResultPayload`) contains: | Field | Description | |---|---| | `final_md` | The research report body, Markdown | | `final_artifact_id` | The id of the report as an artifact; use the artifacts endpoints to fetch the original file | | `citations_count` | Number of citations | | `confidence_summary` | Confidence summary | | `partial_sections` | Sections already produced when the run did not finish | Fetch an artifact: ```python artifacts = await client.deep_research.list_artifacts(run["run_id"]) blob = await client.deep_research.get_artifact(run["run_id"], artifacts[0]["id"]) ``` ## Async and long-running jobs DeepResearch is synchronous by default, but the `deep` tier can run for a long time. When you need it not to block, use async mode and subscribe to the event stream to follow along: ```python run = await client.deep_research.run_async(topic="...", depth="deep") async for event in client.deep_research.events.stream(run["run_id"]): print(event.type, event.data) ``` The event stream endpoint `…/deep_research/runs/{run_id}/events` follows the [SSE conventions](/en/webagent/reference/#sse-conventions): events carry a numeric `id`, and after a disconnect you resume with `Last-Event-ID`. Other lifecycle operations: `cancel` (cancel), `feedback` (rate the result), `send_followup` (add requirements on top of an existing report), `refine` (re-run based on feedback). ## Next steps - [WebSearch](/en/webagent/features/web-search) — use this when you want structured hits, not a written report - [DoAnything](/en/webagent/features/do-anything) — use DoAnything for open-ended tasks - [Pricing & quota](/en/webagent/getting-started/pricing) — how `depth` affects cost - [API Overview](/en/webagent/reference/) — shared conventions; the full field set is in the OpenAPI spec --- # WebSearch WebSearch is one of WebAgent's typed APIs. You give it a batch of queries; it fetches results across search engines, deduplicates, reranks, optionally attaches summaries, and returns a structured list of hits. Unlike DeepResearch: DeepResearch produces a written report and runs for a few minutes to tens of minutes; WebSearch produces a **structured result list**, blocking synchronously by default and returning in seconds. Use this when you want "hits" rather than a "report". ## When to use it - You want a batch of programmatically processable search results (title / url / snippet), not a written report. - You want to run multiple queries in a single call and get a unified list deduplicated across engines. - You need a response in seconds and can accept synchronous blocking. When you want a written research report, use [DeepResearch](/en/webagent/features/deep-research). When you want the agent to decide its own retrieval paths and operate across pages, use [DoAnything](/en/webagent/features/do-anything). ## Run a search HTTP endpoint: ```http POST /v1/projects/{pid}/web_search/runs Authorization: Bearer wa_... ``` Request fields: | Field | Type | Required | Description | |---|---|---|---| | `queries` | string[] | Yes | A batch of queries | | `engines` | string[] | No | Restrict the search engines; omit = the default engine set | | `max_results_per_query` | int | No | Maximum results to fetch per query; omit for the server default | | `rerank` | bool | No | Unified rerank of results across engines | | `summarize` | bool | No | Attach summaries to results; enabling it increases cost and latency | | `freshness` | string | No | Restrict the recency of results | | `site_whitelist` / `site_blacklist` | string[] | No | Restrict / exclude sites | | `language` | string | No | Preferred result language | | `objective` | string | No | A description of the retrieval intent, used for reranking and summarization | For the full schema, see `WebSearchRequest` in the [OpenAPI spec](/openapi/v1.json). Example: ```python from web_agent.v1 import Client async with Client(api_key="wa_...", project_id="proj_demo") as client: run = await client.web_search.run( queries=["best Python ORM 2026", "SQLAlchemy vs Tortoise"], max_results_per_query=10, rerank=True, ) for hit in run["results"]["results"]: print(hit["final_rank"], hit["title"], hit["url"]) ``` ## Result The run's `results` field (`WebSearchResults`): | Field | Description | |---|---| | `results` | The list of hits, each a `SearchResultItem` | | `total_unique_results` | Total number of results after deduplication | | `is_summarized` | Whether summaries were attached (true when `summarize=true`) | | `engine_answer` | The answer returned directly by the engine (provided by some engines) | Each `SearchResultItem`: | Field | Description | |---|---| | `title` / `url` | Title and link | | `canonical_url` | The normalized URL; deduplication is keyed on this | | `snippet` | The summary fragment returned by the engine | | `summary` | The summary generated by WebAgent (present when `summarize=true`) | | `final_rank` / `original_rank` | Rank after / before reranking | | `source_engine` | Which engine the hit came from | | `source_query` | Which query the hit corresponds to | | `dedup_count` | How many engines/queries this result was hit by — higher means more trustworthy | ## Async and continuation WebSearch returns synchronously by default. With a large number of queries or `summarize` enabled, it takes longer; you can subscribe to the event stream to follow along: ```http GET /v1/projects/{pid}/web_search/runs/{run_id}/events ``` The event stream follows the [SSE conventions](/en/webagent/reference/#sse-conventions). Other operations: `cancel` (cancel), `refine` (re-run the same batch of queries with a new intent), `send_followup` (add queries). ## Next steps - [DeepResearch](/en/webagent/features/deep-research) — use this when you want a written report - [Track](/en/webagent/features/track) — use this when you want to "continuously monitor changes to a query" - [API Overview](/en/webagent/reference/) — shared conventions; the full field set is in the OpenAPI spec --- # Track Track is one of WebAgent's typed APIs. You describe a monitoring intent; it repeatedly fetches data from web pages on a schedule, compares against a baseline, and notifies you through your chosen channel when a change hits. Unlike DoAnything / DeepResearch / WebSearch: those "run once, produce a result". Track's resource is a **monitor** — a long-lived object that runs itself repeatedly on a schedule, producing one run per tick. ## When to use it - What you care about is how something **changes over time**, not a one-off result right now. - You want to be pushed a notification when a change happens, rather than polling yourself. - The monitoring can stay running long-term — a monitor exists until you delete it. When you only want a one-off result, use [WebSearch](/en/webagent/features/web-search) or [DoAnything](/en/webagent/features/do-anything). ## Create a monitor HTTP endpoint: ```http POST /v1/projects/{pid}/track/monitors Authorization: Bearer wa_... ``` Request fields: | Field | Type | Required | Description | |---|---|---|---| | `intent` | string | Yes | The monitoring intent, natural language | | `notify_channel` | object | Yes | The notification channel — see [notify_channel](#notify-channel) below | | `schedule` | object | No | The trigger schedule — see [schedule](#schedule) below. Omit = use the default schedule | | `target_urls` | string[] | No | Restrict which pages to monitor; omit and the agent finds them itself | | `extraction_schema` | object | No | Specifies which structured fields to extract from the page | | `trigger_dsl` | object | No | The trigger condition — only when met does it count as a "change hit" | | `stop_condition_dsl` | object | No | The stop condition — once met, the monitor stops automatically | | `profile_id` | string | No | Reuse the login state of a [Profile](/en/webagent/features/profiles) | For the full schema, see `CreateMonitorRequest` in the [OpenAPI spec](/openapi/v1.json). `schedule` and `notify_channel` are nested objects, structured as described in the two sections below. ## schedule `schedule.kind` decides when the monitor runs: | kind | Description | |---|---| | `interval` | Fixed interval; pair with `interval_seconds` | | `cron` | Cron expression; pair with `cron` | | `event` | Triggered by an external event; pair with `event_filter` | | `autonomous` | The agent decides itself when the next run should be | ## notify_channel `notify_channel.kind` decides where a change hit is pushed: | kind | Description | |---|---| | `callback_url` | POSTs to your URL; pair with `url` | | `global_webhook` | Reuses a project-level webhook; pair with `webhook_id` | | `console_inbox` | Pushes to the Console inbox, suited for a human to read | ## Example Create a monitor with `intent` + `schedule` + `notify_channel` together: ```python from web_agent.v1 import Client async with Client(api_key="wa_...", project_id="proj_demo") as client: monitor = await client.track.create( intent="Notify me when Apple's stock price drops below $200", schedule={"kind": "interval", "interval_seconds": 3600}, notify_channel={"kind": "callback_url", "url": "https://hooks.example.com/track"}, ) print(monitor["id"], monitor["status"]) ``` ## Baseline and change detection A monitor's first run establishes a **baseline** (`baseline_extracted`). Every run afterward is compared against the baseline, and a notification is sent only when `trigger_dsl` is met. `last_tick_at` / `last_tick_n` record the time and sequence number of the most recent run, and `consecutive_failures` counts consecutive failures — too many consecutive failures and the monitor enters an error state. ## Lifecycle Once created, a monitor exists long-term until you delete it. Common operations: | Operation | Description | |---|---| | `pause` / `resume` | Pause / resume scheduling | | `run_now` | Run once immediately, without waiting for the schedule | | `cancel` / `delete` | Stop / delete the monitor | | `patch` | Change the intent, schedule, channel, etc. | | `refine` | Adjust the monitoring intent in natural language | | `list_runs` / `get_run` | Query historical runs | | `list_deliveries` / `retry_delivery` | Query notification delivery records; failed ones can be redelivered | | `intervene` / `message` | Respond when the monitor hits a login, captcha, or other situation needing human intervention | The event stream endpoint `…/track/monitors/{mid}/events` follows the [SSE conventions](/en/webagent/reference/#sse-conventions); follow each tick of the monitor in real time. ## Next steps - [Profiles](/en/webagent/features/profiles) — reuse login state when monitoring sites that require a login - [WebSearch](/en/webagent/features/web-search) — use this when you only want a one-off result - [API Overview](/en/webagent/reference/) — shared conventions; the full field set is in the OpenAPI spec --- # Profiles A **Profile** is a **single, whole browser identity**: cookies, localStorage, and login state for every site are all stored in one Profile. It is **not split per site** — one user maps to one Profile, and when the agent opens the browser with it, that person's login state for all sites is present. When creating a session or a monitor, you reference it by `profile_id`, and the agent opens the browser carrying that identity — no need to log in again each time. Without a Profile, every session is a blank slate — when it hits a page that requires a login, it can only stop. ::: tip Don't split Profiles per site A Profile is whole. Log into whichever sites you need within the **same** Profile; every later task shares that one Profile. Do not create a separate Profile per site. ## When to use it - The task needs to access sites that require a login (email, social platforms, internal systems). - You want multiple sessions / monitors to share the same login state, rather than each logging in on its own. - The login state needs to be reused across tasks and across days. When the task only accesses public pages and needs no login, you don't need a Profile. ## Create and log in a Profile To install a login state into a Profile for the first time, we recommend doing it in the [Console](https://dashboard.qoni.ai): the Console opens a controlled browser where you log in normally, pass captchas, and make whatever settings you want, and the login state lands in the Profile. This step inherently needs a human, and the Console is the smoothest form for it. After that, this Profile can be referenced repeatedly from code. You can also manage Profile resources via the API: ```http POST /v1/projects/{pid}/profiles create a profile GET /v1/projects/{pid}/profiles list profiles GET /v1/projects/{pid}/profiles/{id} get one PATCH /v1/projects/{pid}/profiles/{id} rename, etc. DELETE /v1/projects/{pid}/profiles/{id} delete ``` Fields you can pass on creation: | Field | Type | Description | |---|---|---| | `name` | string | A recognizable name for the Profile, usually one per user | | `customer_user_id` | string | Associate the Profile with a user in your business system | Interactive login goes through `POST /v1/projects/{pid}/profiles/login` and `POST /v1/projects/{pid}/profiles/{id}/confirm` — see the [OpenAPI spec](/openapi/v1.json) for the exact request bodies. > The API also has a `cookie_domains` field; it only serves the legacy "bring-your-own-cookies" B2B path and is not used by a whole Profile — you can ignore it. ## Using a Profile in a session / monitor Once you have a `profile_id`, reference it when creating a session: ```python from web_agent.v1 import Client from web_agent.v1.types import CreateSessionRequest async with Client(api_key="wa_...", project_id="proj_demo") as client: session = await client.sessions.create(CreateSessionRequest( instructions="Open the LinkedIn inbox and reply to the latest message.", profile_id="prof_alice", )) ``` [Track](/en/webagent/features/track) monitors also accept `profile_id`; use it when monitoring sites that require a login. ## Profile state A Profile's `state` and these fields reflect whether it is still usable: | Field | Description | |---|---| | `state` | The Profile's current state | | `last_used_at` | The most recent time it was used by a session / monitor | | `last_alive_at` | The most recent time the login state was confirmed still valid | | `last_failure_reason` | The reason for the most recent usage failure | Cookies and login state expire. If a run fails because the login is no longer valid, `last_failure_reason` explains why — at that point you need to go back to the Console and log in to that Profile again. Making this step observable costs less than letting the run fail repeatedly on the login page. ## Next steps - [DoAnything](/en/webagent/features/do-anything) — how `profile_id` enters a session - [Track](/en/webagent/features/track) — reuse login state for long-term monitoring - [API Overview](/en/webagent/reference/) — shared conventions; the full field set is in the OpenAPI spec --- # Errors & retries This page lists WebAgent's error codes and tells you which ones to retry, which to surface to the user, and which to fix in your own code. Every error response has the same shape: ```json { "code": "rate_limit_exceeded", "detail": "Per-key concurrency limit (10) reached.", "extra": { "limit": 10, "active": 10 } } ``` The HTTP status tells you the *category*; the `code` field is the **stable contract** — switch on it, never on `detail` (English prose, may change). ## Code matrix | Status | Code | Retry? | What to do | |---|---|---|---| | 400 | `bad_request` | ❌ | Fix the request body. Check the OpenAPI spec for the field. | | 401 | `unauthorized` | ❌ | Key is missing, malformed, expired, or revoked past its 1-hour grace. Create a new one. | | 402 | `insufficient_credits` | ❌ | Top up via **Settings → Billing**, or enable auto-recharge. | | 402 | `budget_exceeded` | ❌ | Project budget cap hit. Raise the cap or split the work. | | 403 | `forbidden` | ❌ | Key valid, but the project doesn't grant it access to this resource. | | 403 | `safety_boundary_violated` | ❌ | The agent refused on safety grounds. Read `extra.reason`; reword the instruction. | | 404 | `session_not_found`, `run_not_found`, `profile_not_found`, … | ❌ | The id is wrong or the resource was deleted. | | 409 | `conflict` | ❌ | State mismatch (e.g. `cancel` on a terminal run). Re-read state and decide. | | 422 | `validation_error` | ❌ | Schema-level — `extra.errors[]` lists the offending fields. | | 429 | `rate_limit_exceeded` | ✅ | Honour `Retry-After`; exponential back-off if absent. | | 429 | `too_many_concurrent_sessions` | ✅ | Wait for an in-flight session to free up, or upgrade plan. | | 5xx | `internal_error` | ✅ | Same call, exponential back-off. Capped at 3–5 attempts. | | (network) | — | ✅ | Connection reset / timeout — retry idempotently. | ✅ = safe to retry without reasoning. ❌ = will keep failing until you change something. ## Retry policy we recommend ```python import time, random def with_retries(fn, *, attempts=4, base=0.5, cap=8.0): for i in range(attempts): try: return fn() except WebAgentError as e: if e.code not in {"rate_limit_exceeded", "too_many_concurrent_sessions", "internal_error"}: raise # not safe to retry if i == attempts - 1: raise sleep_s = min(cap, base * 2**i) + random.uniform(0, 0.25) time.sleep(e.retry_after_seconds or sleep_s) ``` The Python and TypeScript SDKs ship this loop by default; the table above is for when you're calling the API directly. ## Idempotency keys {#idempotency} Every mutating endpoint (`POST /sessions`, `POST /sessions/{sid}/runs`, `POST /messages`, …) accepts an `Idempotency-Key` header: ```http POST /v1/projects/proj_demo_0001/do_anything/sessions Idempotency-Key: 9b2f7c1e-…-uuid ``` Replay the same UUID within 24 hours and you'll get the **same response back** (same `session_id`, same status code) — even after a network blip. Generate one UUID per logical action, not per retry. ## Inside the run lifecycle Errors during agent execution don't always fail the run — many are recoverable: - **Tool error** — emitted as `run.action.failed`; the agent decides to retry the tool, choose a different one, or fail the whole run. - **Captcha / 2FA** — emitted as `run.input_request`; you answer via `POST /intervene`. - **Hard cap hit** — run transitions to `failed` with `error.code = budget_exceeded` or `duration_exceeded`. Spent credits are billed. - **Safety refusal** — run transitions to `failed` with `error.code = safety_boundary_violated`. No credits charged for the refused step. You see all four through the [SSE stream](/en/webagent/features/do-anything#events). ## SSE-specific failure modes | Symptom | Cause | Fix | |---|---|---| | Stream stalls > 60 s | Network drop or proxy buffering | Reconnect with `Last-Event-ID: `. | | `Last-Event-ID` ignored | Buffer expired (older than 1 hour) | Re-fetch run state via `GET /sessions/{sid}/runs/{rid}` and resume from current. | | Duplicate events on reconnect | At-least-once delivery | Dedupe by event `id` (monotonic per-run). | ## Next steps - [API Overview](/en/webagent/reference/) — every endpoint shares these conventions. - [DoAnything](/en/webagent/features/do-anything) — the lifecycle states above are normative. --- # Python SDK This page covers how to install, configure, and use the official `web-agent-sdk` Python package: open a session, start a run, and stream events. ```bash pip install web-agent-sdk ``` Requires Python 3.10+. The SDK is async-first (`asyncio` / `anyio`). ## One entry point: `Client` User-facing API products live on the same `Client`: ```python from web_agent.v1 import Client ``` Constructing a `Client` takes `api_key` and `project_id`. Both are optional — when omitted, they default to the environment variables `$WEBAGENT_API_KEY` / `$WEBAGENT_PROJECT_ID` respectively. | Resource | Product | Use case | |---|---|---| | `client.sessions / messages / events` | DoAnything (open-ended) | Free-form input; the agent picks the path. | | `client.deep_research` | DeepResearch (research → report) | Standalone API. | | `client.web_search` | WebSearch (query → results) | Synchronous by default (`wait=true`). | | `client.track` | Track (monitor → snapshot) | Long-lived monitors with webhook delivery. | > The package name is `web-agent-sdk` (hyphen) but the import is `web_agent` — same convention as `python-dateutil` → `dateutil`. ## DoAnything — open-ended runs ```python import asyncio from web_agent.v1 import Client from web_agent.v1.types import CreateSessionRequest async def main(): async with Client( api_key="wa_demo_xxxxxxxxxxxxxxxx", project_id="proj_demo_0001", ) as client: session = await client.sessions.create(CreateSessionRequest( instructions="Search Hacker News for the top 5 stories today, return them as a list.", )) run = session.runs[0] # session-create implicitly queues the first run async for event in client.events.stream(session.id, run.id): print(event.type, event.data) if event.type == "run.completed": break asyncio.run(main()) ``` ### Follow-up run vs. inflight message ```python # 1. Push a message into the *current* run's chat queue # (agent peeks the queue at the next ReAct boundary) 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 # (reuses browser, profile, workspace; previous run must be terminal) from web_agent.v1.types import CreateRunRequest new_run = await client.sessions.create_run( session.id, CreateRunRequest(instructions="Click into the first post and summarise it."), ) ``` ### Answer an input request ```python await client.messages.intervene( session.id, run.id, input_request_id="ir_01HXX", response={"solved": True}, ) ``` ### Cancel / stop / list ```python await client.sessions.cancel_run(session.id, run.id, reason="user_cancelled") await client.sessions.stop(session.id, force=False) # soft stop session listing = await client.sessions.list(status="running", limit=20) for s in listing.items: print(s.id, s.status) ``` ### Heartbeats and resume `stream()` filters heartbeats by default; pass `include_heartbeats=True` for connection-health UIs. Resume an interrupted stream with `Last-Event-ID`: ```python client.events.stream(session.id, run.id, last_event_id="142") ``` ## 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. ```python async with Client(api_key="wa_...", project_id="proj_demo") as client: run = await client.deep_research.run( topic="Open-source vector DB landscape 2026", depth="deep", # light / standard / deep require_outline_approval=True, # outline HITL gate (default on) ) print(run["run_id"], run["status"]) ``` Subscribe to events (DR uses the DoAnything SSE channel) and respond to the outline gate: ```python async for event in client.events.stream( run["session_id"], run["run_id"], ): if event.type == "run.input_request": # outline ready, awaiting human approval await client.deep_research.intervene( run["run_id"], request_id=event.data["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) artifacts = await client.deep_research.list_artifacts(run["run_id"]) final = await client.deep_research.get_artifact( run["run_id"], artifacts[0]["id"], ) ``` ## WebSearch — query → results WebSearch is a project-scoped API. `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(run_id)` to poll. ```python # Synchronous (default) result = await client.web_search.run( queries=["best Python ORM 2026"], engines=["tavily"], summarize=True, ) for hit in result["results"]["results"]: print(hit["title"], hit["url"]) # Async pending = await client.web_search.run_async(queries=["best Python ORM 2026"]) detail = await client.web_search.get(pending["run_id"]) # Refine (re-run within the same run) await client.web_search.refine( pending["run_id"], 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). ```python 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}, notify_channel={"kind": "callback_url", "url": "https://hooks.example.com/track"}, ) # Lifecycle controls — pause / resume / refine via patch: await client.track.pause(mon["id"], reason="manual review") await client.track.resume(mon["id"]) await client.track.refine(mon["id"], trigger_dsl={"op": "lt", "field": "price", "value": 999}) # Manually run one check (bypasses the schedule); inspect that check's result: outcome = await client.track.run_now(mon["id"]) # Pull the snapshot history (newest first): snapshots = await client.track.list_snapshots(mon["id"]) snap = await client.track.get_snapshot(mon["id"], snapshots["items"][0]["id"]) # Inspect webhook delivery history + re-send a failed delivery: deliveries = await client.track.list_deliveries(mon["id"], include_payload=True) await client.track.retry_delivery(mon["id"], deliveries["items"][0]["id"]) # Cancel terminates the monitor (terminal state): await client.track.cancel(mon["id"]) # equivalent: await client.track.delete(mon["id"]) ``` ### Alignment HITL (optional) If the supervisor needs you to disambiguate intent (e.g. "did you mean SKU A or SKU B?"), the monitor moves to `pending_clarification` and emits an `alignment.input_request` event. Answer with `intervene()`: ```python await client.track.intervene( mon["id"], request_id="req_align_1", response="SKU A", ) ``` You can also push free-text guidance into the alignment queue at any time via `client.track.message(mon_id, content="…")`. ## Errors The SDK raises typed exceptions you can catch by class: ```python from web_agent.v1 import ( UnauthorizedError, InsufficientCreditsError, RateLimitedError, ) try: await client.sessions.create(CreateSessionRequest(instructions="…")) except InsufficientCreditsError as e: print("top up:", e.detail, e.extra) ``` Every exception subclasses `ApiError` and carries `code` / `detail` / `extra` matching the [API error envelope](/en/webagent/reference/#errors). | Exception class | HTTP | `code` | |---|---|---| | `UnauthorizedError` | 401 | `unauthorized` | | `ForbiddenError` | 403 | `forbidden`, `safety_boundary_violated` | | `NotFoundError` | 404 | `*_not_found` | | `ConflictError` | 409 | `conflict` | | `ValidationError` | 422 | `validation_error` | | `RateLimitedError` | 429 | `rate_limit_exceeded` | | `InsufficientCreditsError` | 402 | `insufficient_credits` | | `BudgetExceededError` | 402 | `budget_exceeded` | ## Type stubs DoAnything resources (`Session`, `Run`, `Event`, etc.) are dataclasses re-exported from `web_agent.v1`: ```python from web_agent.v1 import Session, Run, Event, RunStatus ``` DR / DS / WS responses are returned as `dict[str, Any]` (the OpenAPI envelope verbatim) — index by key (`run["run_id"]` / `run["status"]`). `mypy --strict` is supported. ## Next steps - [TypeScript SDK](/en/webagent/sdk/typescript) — same surface in JS/TS. - [Errors & retries](/en/webagent/reference/errors-and-retries) — recommended retry policy, idempotency keys. - [DoAnything](/en/webagent/features/do-anything) — lifecycle, profiles, workspaces. --- # 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. ```bash npm install @web-agent/sdk # or pnpm add @web-agent/sdk / yarn add @web-agent/sdk / bun add @web-agent/sdk ``` Works 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`: ```typescript 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 ```typescript 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 ```typescript 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 ```typescript // 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 ```typescript 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](/en/webagent/features/do-anything#input-request). ### Cancel / stop / list ```typescript 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. ```typescript 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. ```typescript // 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). ```typescript 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()`: ```typescript 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 ```typescript 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](/en/webagent/reference/#errors). ## Types DoAnything request / response types are top-level exports: ```typescript import type { Session, Run, Event, EventType, CreateSessionRequest, CreateRunRequest, InterveneRequest, RunStatus, SessionStatus, TerminalReason, } from "@web-agent/sdk"; ``` DR / DS / WS responses come back as `Record` (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](/en/webagent/sdk/python) — same surface in Python. - [Errors & retries](/en/webagent/reference/errors-and-retries) — recommended retry policy, idempotency keys. - [DoAnything](/en/webagent/features/do-anything) — lifecycle, profiles, workspaces. --- # cURL & raw HTTP This page documents the common request patterns for WebAgent over raw HTTP — usable from any language that can send HTTPS / JSON, no official SDK required. ## Request structure Every call shares the same structure. Read the specification below first; each `curl` later on this page is one instance of it. ### Base URL and paths ``` https://api.eak.eazo.ai ``` Paths for project-scoped endpoints are all prefixed with the project: ``` /v1/projects/{project_id}/do_anything/sessions /v1/projects/{project_id}/do_anything/sessions/{session_id} /v1/projects/{project_id}/do_anything/sessions/{session_id}/runs/{run_id}/events ``` ### Path parameters The following parameters appear as required by each endpoint's path; when present, they are mandatory: | Parameter | Form | Source | |---|---|---| | `{project_id}` | `proj_`-prefixed string | Console → Project Switcher; the project is the isolation unit for project-scoped endpoints | | `{session_id}` | `sess_`-prefixed string | the `id` in the "Create a session" response | | `{run_id}` | `run_`-prefixed string | a run `id` in the session response's `runs[]`, or `latest_run_id` | ### Authentication Every request carries a bearer token: ``` Authorization: Bearer ``` `` is a `wa_`-prefixed API key, created in Console → Settings → API Keys — see [Authentication](/en/webagent/getting-started/authentication). ### Request body Mutating endpoints (`POST`) carry `Content-Type: application/json` with a JSON body. The full field set for each endpoint is defined by the [OpenAPI spec](/openapi/v1.json). ## Create a session The examples below use placeholder values (`proj_demo_0001` / `wa_demo_…` / `sess_demo_0001` / `run_demo_0001`); for real calls, substitute your own values per "Request structure" above. ```bash curl https://api.eak.eazo.ai/v1/projects/proj_demo_0001/do_anything/sessions \ -H "Authorization: Bearer wa_demo_xxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "instructions": "Find the top 5 stories on Hacker News right now." }' ``` ## Stream events (SSE) ```bash curl -N \ -H "Authorization: Bearer wa_demo_xxxxxxxxxxxxxxxx" \ "https://api.eak.eazo.ai/v1/projects/proj_demo_0001/do_anything/sessions/sess_demo_0001/runs/run_demo_0001/events" ``` To resume after a drop, add the last id you received: ```bash curl -N \ -H "Authorization: Bearer wa_demo_xxxxxxxxxxxxxxxx" \ -H "Last-Event-ID: 142" \ "…/events" ``` ## Send a follow-up message ```bash curl https://api.eak.eazo.ai/v1/projects/proj_demo_0001/do_anything/sessions/sess_demo_0001/runs/run_demo_0001/messages \ -H "Authorization: Bearer wa_demo_xxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "content": "Also include the comment count for each." }' ``` ## Answer an input request ```bash curl https://api.eak.eazo.ai/v1/projects/proj_demo_0001/do_anything/sessions/sess_demo_0001/runs/run_demo_0001/intervene \ -H "Authorization: Bearer wa_demo_xxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "kind": "answer_input_request", "input_request_id": "ir_01HXX", "response": { "solved": true } }' ``` The `kind` discriminator selects the variant — same endpoint also handles `take_control` / `release_control` (see [Take Control](/en/webagent/features/do-anything#input-request)). ## Cancel a run ```bash curl -X POST https://api.eak.eazo.ai/v1/projects/proj_demo_0001/do_anything/sessions/sess_demo_0001/runs/run_demo_0001/cancel \ -H "Authorization: Bearer wa_demo_xxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "reason": "user_cancelled" }' ``` ## List sessions ```bash curl "https://api.eak.eazo.ai/v1/projects/proj_demo_0001/do_anything/sessions?status=running&limit=20" \ -H "Authorization: Bearer wa_demo_xxxxxxxxxxxxxxxx" ``` ## Errors Any non-2xx response is JSON with a stable `code`: ```json { "code": "insufficient_credits", "detail": "Project balance below the minimum required ($0.50).", "extra": { "balance_usd": "0.12", "required_usd": "0.50" } } ``` See [API Overview → Errors](/en/webagent/reference/#errors) for the full list. ## Idempotency ```bash curl … -H "Idempotency-Key: $(uuidgen)" ``` Replaying the same key returns the same response — safe to retry on network errors. --- # API Overview This page covers the conventions every WebAgent endpoint shares. For per-endpoint field details, see the [OpenAPI 3.1 spec](/openapi/v1.json) — every SDK and the Console's *Get Code* dialog generate from it. ## Base URL ``` https://api.eak.eazo.ai ``` All resource paths are scoped to a project: ``` /v1/projects/{project_id}/... ``` ## Authentication Bearer token in the `Authorization` header. See [Authentication & API keys](/en/webagent/getting-started/authentication). ```http Authorization: Bearer wa_xxxxxxxxxxxxxxxxxxxxxxxx ``` ## Errors JSON body, HTTP status, and a stable `code` you can switch on: ```json { "code": "session_not_found", "detail": "Session sess_demo_0001 not found.", "extra": { "session_id": "sess_demo_0001" } } ``` | Status | Common codes | |---|---| | 400 | `bad_request` | | 401 | `unauthorized` | | 402 | `insufficient_credits`, `budget_exceeded` | | 403 | `forbidden`, `safety_boundary_violated` | | 404 | `session_not_found`, `run_not_found`, `profile_not_found`, … | | 409 | `conflict` | | 422 | `validation_error` | | 429 | `rate_limit_exceeded`, `too_many_concurrent_sessions` | | 5xx | `internal_error` | ## Rate limits - Per-key concurrent sessions — set by your plan. - Per-key request rate — sliding-window; `429` with `Retry-After` on breach. - Per-project monthly credit budget — soft warning at 80%, hard stop at 100%. ## Pagination List endpoints are cursor-paginated: ```http GET /v1/projects/{pid}/do_anything/sessions?limit=50&cursor=eyJ… ``` Response includes `next_cursor` (or `null` at the end). Limits cap at 100 per page. ## Idempotency Mutating endpoints (`POST /sessions`, `POST /messages`, …) accept an optional `Idempotency-Key` header. Send the same UUID and you'll get the same response back; safe to retry. ## SSE conventions Streaming endpoints (e.g. `…/events`) emit JSON-encoded SSE events with a numeric `id`. To reconnect cleanly, send `Last-Event-ID: ` and the server replays everything after that id. ## Next steps - [OpenAPI spec](/openapi/v1.json) — every endpoint, every field, machine-readable. - [DoAnything](/en/webagent/features/do-anything) — the resource model. - [Errors & retries](/en/webagent/reference/errors-and-retries) — full error-code matrix. --- # Vibecoding with WebAgent If you're writing code with an LLM in your IDE — Cursor, Claude Code, Aider, Continue, or another tool — add the complete WebAgent docs and OpenAPI schema to its context before asking it to implement an integration. This keeps the model from relying on stale knowledge or guessing fields. ## Prepare the context | Site resource | Purpose | Recommended use | |---|---|---| | [`/en/webagent/llms.txt`](/en/webagent/llms.txt) | Index of page titles and one-line descriptions | Let the model locate relevant pages first | | [`/en/webagent/llms-full.txt`](/en/webagent/llms-full.txt) | Complete documentation merged into one Markdown file | Download it or attach it as long-context input | | [`/openapi/v1.json`](/openapi/v1.json) | OpenAPI 3.1 schema | Download it and use it as the authority for fields, requests, and responses | These resources use paths on the current documentation site and do not depend on a separate documentation domain. Before copying the rules below, open or download the resources you need and add them to the IDE session or project context. ## Drop-in prompt Copy this into your IDE's system prompt, rules file, or first message: ``` You are integrating WebAgent. Treat the complete WebAgent documentation and OpenAPI 3.1 schema I provide as authoritative. If the context does not define a required endpoint or field, identify the gap instead of guessing. API conventions: - Base URL: https://api.eak.eazo.ai - Bearer auth: header `Authorization: Bearer wa_…` - Path-scoped to project: /v1/projects/{project_id}/... - Wire fields are snake_case. Decimals are JSON strings ("10.00", not 10.00). - Most mutations accept Idempotency-Key. SDK packages: - Python: `pip install web-agent-sdk` - TypeScript: `npm install web-agent-sdk` Both SDKs auto-reconnect SSE streams via Last-Event-ID. Prefer them over hand-rolled HTTP unless asked otherwise. Read the attached OpenAPI schema before writing code, then use the complete documentation to confirm the integration flow. ``` ## Cursor `.cursor/rules/webagent.md`: ```markdown --- description: Conventions for integrating with WebAgent globs: ["**/*.{ts,tsx,py}"] --- Read the complete documentation and OpenAPI schema provided with the project before writing WebAgent code. Treat the OpenAPI schema as authoritative for fields, requests, and responses — never guess. If the context does not define a required interface, ask before continuing. Use the official SDKs (`web-agent-sdk`) unless the user asks for raw HTTP. ``` ## Claude Code Add to your project's `CLAUDE.md`: ```markdown ## WebAgent integration Read the complete WebAgent documentation and OpenAPI schema attached to the project first. SDK: `web-agent-sdk` (Python and TypeScript). Treat the OpenAPI schema as authoritative for fields, requests, and responses. Wire fields are snake_case. Stream run events via the SDK's `.stream()` helper, which handles `Last-Event-ID` reconnection. If the context does not define a required interface, ask instead of guessing. ``` ## Refresh the context When the WebAgent API or SDK version changes, download the complete documentation and OpenAPI schema again from the site resources on this page. Do not keep a retired external documentation domain in long-lived IDE rules. ## Console "Get Code" dialog The fastest way to get a working snippet: open the [Console](https://dashboard.qoni.ai/new), fill the form, click **Get Code**. You get four tabs (Prompt for an LLM agent / Python / TypeScript / cURL), each pre-filled with your real key and current configuration. Paste into your editor. ## Why this works - The documentation and schema travel with the project or session instead of depending on a fixed external documentation domain. - The full-docs file is Markdown, not HTML, so LLMs can parse it directly. - The OpenAPI spec is the same single source of truth our SDKs and Console are generated from. No drift. ## Next steps - [Quickstart](/en/webagent/getting-started/quickstart) — first run in five minutes. - [API Reference](/en/webagent/reference/) — interactive, with Try-it.