Cloud browser

Claude's browser tool

Claude's browser toolset (browser_toolset_20260801) is a client toolset: Claude names an action, and your code has to perform it and report back in an exact shape. One endpoint does that part against a cloud browser session, so your agent loop only forwards blocks in both directions.

The loop

Create a session, then for each turn: send the conversation to Claude, post the assistant message's content to POST /api/browser/sessions/{id}/claude-tools, and append data.tool_results as the next user message. Stop when Claude stops asking for tools. The API key needs the browser:write scope.

import Anthropic from "@anthropic-ai/sdk";
const anthropic = new Anthropic();
const endpoint = "https://scrappy.gg/api/browser/sessions/" + sessionId + "/claude-tools";
const tools = [{ type: "browser_toolset_20260801" }];
const messages = [{ role: "user", content: "Open example.com/docs and tell me how to get started." }];
for (;;) {
const res = await anthropic.messages.create({
model: process.env.CLAUDE_MODEL, // a model that supports the browser toolset
max_tokens: 4096,
tools,
messages,
});
messages.push({ role: "assistant", content: res.content });
if (res.stop_reason !== "tool_use") break;
const out = await fetch(endpoint, {
method: "POST",
headers: { Authorization: "Bearer " + process.env.SCRAPPY_API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({ content: res.content, options: { allowedHosts: ["example.com"] } }),
}).then((r) => r.json());
// If you declared tools of your own, append their tool_results to this same message.
messages.push({ role: "user", content: out.data.tool_results });
}

contentis the assistant message's content array, at most 200 blocks. Everything else goes in the optional options object described below.

What comes back

The response's data has tool_results, failedAt (an index or null), pending and complete (see Human approval) and elapsedMs. tool_results holds one tool_result for each browser call, in order, each carrying toolset_name: "browser". It can go to the API as it is. Blocks that are not browser calls, such as text or your own tools, are ignored, and you answer those yourself.

The request answers 200 even when calls failed. A click that missed is an answer for Claude to read, not an HTTP failure. The one exception is a body outside the documented bounds, which is a 400 INVALID_INPUT.

The batch rule

One turn can carry several calls. They run one after another. After the first failure, every later call in the turn is answered with is_error: true and the text Not executed: an earlier action in this turn failed. data.failedAt is the index of the call that failed. This keeps Claude from typing into a form that a failed click never opened.

LimitValueWhat happens past it
Time per turn90 secondsA call that would start past the budget is answered with an error. wait and hold_key are cut short to fit.
Browser calls per turn50The 51st is answered with an error and the rest get the halt text.

Tabs

tab_id is the session's own tab id, the same value every tab parameter in the browser API takes. Results carry a browser_state block with the full tab inventory and the active tab. A tab that a page opened itself, such as a popup, is reported as tab_opened. The active tab is remembered per session between requests.

File downloads show up in the same block as download_started, download_completed and download_failed, each with a download_id you can fetch from GET /api/browser/sessions/{id}/downloads/{downloadId}.

Screenshots and coordinates

Screenshots are scaled so their longest edge is options.maxImageEdge: 1568 by default, 256 to 2000 allowed. Claude's click coordinates are in that scaled frame, and they are scaled back to page coordinates before the click is sent. Keep maxImageEdge constant within a conversation. zoom re-renders a region at up to 4x, so small text stays legible. imageFormat and imageQuality default to jpeg at 80.

Refs and read_page

read_page renders the accessibility tree as indented role "name" [ref_N] lines. It shows visible elements by default. interactive narrows that to controls and all adds elements outside the viewport. Output is capped at 50,000 characters, and an open modal is kept even past the cap.

The refs are the same ref_N handles /read-page and /findreturn, so you can mix Claude's calls with your own API calls on the same session. A ref is stable for the element's lifetime. When the page has re-rendered the element, the result tells Claude the ref is stale and to re-read the page.

Dialog policy

The toolset has no action for native alert, confirm or prompt dialogs, and an open one blocks its tab. The executor answers them for Claude according to options.dialogPolicy and says so in the result text.

  • dismiss(default) cancels the dialog, so nothing is ever confirmed on Claude's behalf.
  • accept confirms it. A prompt()is answered with the page's own default text.
  • beforeunload is always accepted.

A dialog is answered whether it opened during a call, between calls, or was the reason a call timed out. In the last case a click is reported as done and a read is run again. type, key and hold_key are reported as interrupted and possibly incomplete, so Claude checks the field.

Keep Claude on your sites with allowedHosts

Navigation always refuses anything that is not http(s), and private or internal hosts. With options.allowedHosts (up to 200 entries, subdomains included) every other host is refused too. Write entries as example.com, *.example.com, https://example.com/ or example.com:8443.

The list is enforced after every call, not only on navigate. If a click, form post or script takes a tab off the list, that tab is cleared and the call fails. Any other tab on a site that is not listed is closed, including popups that opened between calls.

Human approval

To have a person confirm actions before they happen, set options.approval to { "members": [...] }. Members are action names or one of three groups:

GroupCovers
inputClicks, drags, mouse buttons, type, key, hold_key, form_input, file_upload, javascript_exec
navigationnavigate, new_tab, close_tab
allEvery action
  1. Calls run in order until the first gated one. That call and everything after it are not run.
  2. The response has complete: false and pending: { index, tool_use_id, name, input }. Do not send anything to Claude yet. Show the call to a person.
  3. Post the same content again with options.resume = { from: pending.index, approved: [tool_use_id] }, or denied in place of approved. Execution continues from that call and may stop again at the next gated one.
  4. Append each response's tool_results to the ones you already have. When complete is true, send them all to Claude.
async function runTurn(content, options, askPerson) {
const results = [];
let resume;
for (;;) {
const out = await post({ content, options: { ...options, resume } });
results.push(...out.data.tool_results);
if (out.data.complete) return results; // now send them to Claude
const call = out.data.pending; // { index, tool_use_id, name, input }
const ok = await askPerson(call);
resume = ok
? { from: call.index, approved: [call.tool_use_id] }
: { from: call.index, denied: [call.tool_use_id] };
}
}
// Gate every click, keystroke and navigation:
await runTurn(res.content, { approval: { members: ["input", "navigation"] } }, confirmInUi);

A denied call is answered with an error saying the user declined, and the rest of the turn gets the halt text. Keep approval and documents identical across the posts of one turn.

Optional actions and file uploads

javascript_exec, read_console, read_network and file_upload are off until you list them in options.enabledOptional. Enable the same ones in the toolset's configs on the Claude side, or Claude never sees them. If you disabled actions in configs, list them in options.disabled.

For file_upload, stage the files in options.documents, keyed by the id Claude will use, and tell Claude those ids in your prompt. Each entry is a url or base64 content with a filename. Up to 20 documents can be staged.

{
"content": [ /* the assistant message's content */ ],
"options": {
"enabledOptional": ["file_upload"],
"documents": {
"resume": { "url": "https://example.com/files/resume.pdf", "filename": "resume.pdf" },
"cover": { "content": "<base64>", "filename": "cover.txt", "mimeType": "text/plain" }
}
}
}
  • Claude can only choose among the staged ids. File paths are refused.
  • The target can be the file input or the styled button a site shows in its place. The executor finds the hidden input behind it.
  • Several ids attach together only to an input that allows several files.
  • A single URL document can be up to 100MB. Several at once are limited to 9MB each.