Plug your AI agent into a stealth cloud browser
Human Browser Cloud speaks the open Agent2Agent (A2A) protocol — JSON-RPC 2.0 over HTTPS with SSE streaming. Drop it in as a tool from LangGraph, CrewAI, Google ADK, OpenAI Agents SDK, or Claude. We spawn a Chromium on a residential IP, run your task, stream live progress back, and hand you a viewer URL a human can watch.

How it works
- 1Your agent (LangGraph, CrewAI, Claude, custom script) sends a JSON-RPC
message/streamrequest toagent.humanbrowser.cloud/a2a. - 2The A2A server spawns a Chromium session behind a residential proxy and returns a
viewerUrlin the first SSE frame. - 3A browser-use planner drives the page — every step, action, and cost update is streamed back as
status-updateevents. - 4On terminal state (
completed,failed, orcanceled), the server emits anartifact-updatewith the natural-language answer and structured findings, then closes the stream.
Endpoints
https://agent.humanbrowser.cloud/.well-known/agent-card.jsonPublic manifest. Lists the skills, supported transports, security schemes, and streaming capability. No auth required.
https://agent.humanbrowser.cloud/a2aJSON-RPC 2.0 entry point. Bearer-token auth required. Methods:
message/send— starts the task and answers within seconds, once the browser is up (stateworking,metadata.viewerUrl); the run continues in the background.message/stream— SSE; emit Task, then status-update / artifact-update events until terminal.tasks/get— snapshot a known task by id; poll every 3-5 s. The answer ismetadata.outcome.result. Finished tasks are kept for 30 minutes.actions/<name>— custom actions (list_countries, get_page_diagnostics, get_screenshots, switch_proxy_country, …). Theactions/prefix is required.tasks/cancel— cancel a running task; the underlying session is torn down.
Quick start
Three ways to call the same endpoint. All require HUMANBROWSER_API_TOKEN (sign up at humanbrowser.cloud).
curl -N -X POST https://agent.humanbrowser.cloud/a2a \
-H "Authorization: Bearer $HUMANBROWSER_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": "rpc-1",
"method": "message/stream",
"params": {
"message": {
"role": "user",
"messageId": "msg-1",
"parts": [
{ "kind": "text", "text": "Open ifconfig.me and report the IP address" }
]
}
}
}'// No SDK. Direct A2A (v0.3 JSON-RPC) over fetch().
const API = 'https://agent.humanbrowser.cloud/a2a';
const TOKEN = process.env.HUMANBROWSER_API_TOKEN;
const r = await fetch(API, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${TOKEN}` },
body: JSON.stringify({
jsonrpc: '2.0', id: 1, method: 'message/send',
params: { message: { role: 'user', messageId: 'msg-1', parts: [
{ kind: 'text', text: 'Open polymarket.com and report the top US politics market and odds' }
]}}
}),
});
const { result: task } = await r.json();
console.log(task.metadata.viewerUrl); // share with a human to watch live
console.log(task.metadata.sessionId); // poll tasks/get until terminal state
// Final result lands on tasks/get when state === 'completed'# pip install httpx
import os, json, uuid, httpx
API = "https://agent.humanbrowser.cloud/a2a"
TOKEN = os.environ["HUMANBROWSER_API_TOKEN"]
body = {
"jsonrpc": "2.0",
"id": f"rpc-{uuid.uuid4().hex[:8]}",
"method": "message/stream",
"params": {
"message": {
"role": "user",
"messageId": f"msg-{uuid.uuid4().hex[:8]}",
"parts": [
{"kind": "text", "text": "Visit reddit.com/r/programming and list top 5 post titles"}
],
}
},
}
with httpx.stream(
"POST", API,
headers={
"Authorization": f"Bearer {TOKEN}",
"Content-Type": "application/json",
"Accept": "text/event-stream",
},
json=body, timeout=None,
) as r:
r.raise_for_status()
event_type, data_lines = "message", []
for line in r.iter_lines():
if line == "":
if data_lines:
payload = json.loads("\n".join(data_lines))
result = payload.get("result") or {}
kind = result.get("kind")
if kind == "task":
print("viewer:", (result.get("metadata") or {}).get("viewerUrl"))
elif kind == "status-update":
print("status:", result["status"]["state"])
if result.get("final"):
break
elif kind == "artifact-update":
art = result["artifact"]
text = " ".join(p.get("text","") for p in art["parts"] if p["kind"]=="text")
print("artifact:", text)
event_type, data_lines = "message", []
continue
if line.startswith("event:"):
event_type = line[6:].strip()
elif line.startswith("data:"):
data_lines.append(line[5:].lstrip())Skills
Every skill is reachable through the same /a2a endpoint. The skill is chosen implicitly from the shape of the message parts.
Browser Task
Free-form natural-language web automation. Navigate, click, fill, scroll, read. Returns a natural-language answer plus structured findings.
Login & Scrape
Log in with provided credentials (sensitive DataPart), then run an authenticated scraping task. Credentials are injected at runtime and never echoed in artifacts or logs.
Fill Form
Open a URL and fill the form with structured data (DataPart with field map). Submit and report the resulting URL or confirmation message.
Sensitive credentials
To pass passwords, tokens, or 2FA codes, attach a DataPart with metadata.sensitive: true whose data is a flat map of string values, and write each key in the goal as a placeholder (<login>, <password>). The model only ever sees the placeholder; the real value is typed into the page at action time and is never written to artifacts, history or logs. Never put a secret in the goal text itself — the model reads the goal. URLs are not substituted, so write them plainly.
Combine with a profile in message.metadata to keep cookies between runs (so you only log in once).
{
"jsonrpc": "2.0",
"id": "rpc-1",
"method": "message/stream",
"params": {
"message": {
"role": "user",
"messageId": "msg-1",
"metadata": { "profile": "quora-me" },
"parts": [
{ "kind": "text", "text": "Log in to quora.com as <login> with <password> and list questions in my feed" },
{
"kind": "data",
"data": {
"login": "[email protected]",
"password": "hunter2"
},
"metadata": { "sensitive": true }
}
]
}
}
}profile field for sticky sessions — the cloud agent will reuse cookies and skip the login step on subsequent runs.Files: upload and download
To have the agent upload a file — an avatar, a creative, a PDF — attach a FilePart to message/send: {"kind":"file","file":{"name":"cover.jpg","mimeType":"image/jpeg","uri":"https://..."}}. Pass file.uri (a public http(s) URL the server fetches) or file.bytes (base64, up to 25 MB, at most 8 files per message). The file lands in the session and the agent sees it in its available_file_paths; it puts it on the page's <input type=file>with its native upload_file action, which works even when the input is hidden behind an Upload button.
If the file is only known by URL inside the goal text, the agent can fetch it itself with fetch_file(url). Two things the agent cannot do, and will say so instead of pretending: it has no Save-As, and it cannot read your local disk — give it a URL or bytes, never a path on your machine. Files a site downloads during the task (a real download, not an image opened in a tab) are added to available_file_paths automatically. The out-of-band actions/upload_file method still exists for callers that drive a session directly.
curl -X POST https://agent.humanbrowser.cloud/a2a \
-H 'Authorization: Bearer hb_live_xxxx' \
-H 'Content-Type: application/json' \
-d '{
"jsonrpc": "2.0", "id": 1, "method": "message/send",
"params": {
"message": {
"role": "user",
"parts": [
{ "kind": "text", "text": "Open the Page settings, set the attached image as the cover photo, save, and confirm it is visible." },
{ "kind": "file", "file": { "name": "cover.jpg", "mimeType": "image/jpeg", "uri": "https://media.example.com/f/abc123" } }
]
},
"metadata": { "profile": "my-fb-page" }
}
}'Fill form example
For deterministic form-filling, send a non-sensitive DataPart with the field map, then poll tasks/get for the confirmation in metadata.outcome.result.
{
"jsonrpc": "2.0",
"id": "rpc-1",
"method": "message/send",
"params": {
"message": {
"role": "user",
"messageId": "msg-1",
"parts": [
{ "kind": "text", "text": "Open https://example.com/contact, fill the form with these values, and submit." },
{
"kind": "data",
"data": {
"name": "Ilya Gordeev",
"email": "[email protected]",
"message": "Following up on our last call."
}
}
]
}
}
}Streaming events
Every message/stream response is a stream of JSON-RPC envelopes wrapped in SSE data: frames. The first frame is the Task; every subsequent frame is either a status-update or an artifact-update.
| SSE event | kind | state | What it means |
|---|---|---|---|
| 1st frame | task | submitted | Task accepted. metadata.viewerUrl appears as soon as Chromium spawns. |
| status-update | status-update | working | Planner step or action. status.message.parts carries human-readable text. |
| status-update | status-update | input-required | The agent needs an answer (OTP, magic link, decision); the question is in metadata.input_required. While it is still paused, reply with message/send carrying referenceTaskIds: [taskId] and metadata.in_reply_to: req_id (a ready resume_example is in the event). If the task has already ended, start a new task with the full context. |
| artifact-update | artifact-update | — | Final result. artifact.parts = natural-language text + structured data. |
| status-update (final) | status-update | completed | Success. final: true. Stream closes. |
| status-update (final) | status-update | failed | Planner reported failure or session crashed. final: true. |
| status-update (final) | status-update | canceled | You called tasks/cancel. final: true. |
Cost data (tokens_in, tokens_out, usd, model) is folded into metadata.cost on every status frame and on the final Task object.
Authentication
Every POST /a2a call must carry a Bearer token in the Authorization header:
Authorization: Bearer $HUMANBROWSER_API_TOKENGet a token by signing up at humanbrowser.cloud. Tokens are scoped to your account and counted against your plan. The Agent Card endpoint (/.well-known/agent-card.json) is unauthenticated so other agents can discover the manifest without credentials.
When the agent is stopped
Safety guards stop the agent before it repeats a purchase, loops on a dead click, or scrolls forever. Every stop is machine-readable, so your code can branch on it instead of parsing prose.
- Where:
tasks/get→metadata.outcome.blocks[]on a finished task,metadata.blocks[]while it is still working, and the same array in the result artifact's data part. - Shape:
{ code, retryable, how_to_retry, url, step, button?, count?, limit? }.outcome.retryableis true only when every block allows a plain re-run. - Codes:
submit_guard(a submit / pay / order button was clicked the maximum times on one page with no navigation — retryable: false, verify the submission first),repeat_action,no_effect_click,scroll_no_progress,navigate_loop(all retryable). - Overrides: per task, in
message.metadata.overrides:submit_max(1–10, default 2 clicks per submit button per page) andallow_resubmit: true(disables the submit guard for this task only — use it after you verified nothing was recorded). - Money-shaped stops: when the agent cannot verify whether a payment or order went through, it asks instead of guessing — the task moves to
input-requiredwith the question, and your reply resumes it.
Limits
- Task limits: 2 hours and 100 agent steps per task. Long jobs work better as several tasks on one
profile— they reuse the same logged-in browser. - Concurrent sessions: up to 10 per token (free plan: 2). Tasks on the same profile queue in one browser (up to 20 waiting); over the cap the task fails with
metadata.error.code: "session-cap". - Body size: 8 MB per JSON-RPC request — send larger files by
uri. - Models: chosen by the server (default
gpt-6-luna). The list you may pin isactions/list_models— pin one only with a measured reason. - Pricing: usage-based: $0.02 per agent step with the model included, $0.10 per browser-hour, $4/GB residential traffic, $0.005 per captcha. See pricing.
Interop
Because A2A is the protocol, you don't need our SDK. Drop the cloud browser into any framework as a node, tool, or sub-agent.
// LangGraph node — delegate any web step to the cloud browser-agent
export async function browserNode(state) {
const r = await fetch('https://agent.humanbrowser.cloud/a2a', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.HUMANBROWSER_API_TOKEN}`,
},
body: JSON.stringify({
jsonrpc: '2.0', id: 1, method: 'message/send',
params: {
message: {
role: 'user', messageId: 'msg-1',
parts: [{ kind: 'text', text: state.goal }],
},
},
}),
});
const { result: task } = await r.json();
// Poll tasks/get until task.status.state is terminal
return { ...state, browser_task_id: task.id, viewer_url: task.metadata.viewerUrl };
}# CrewAI tool — wrap the A2A endpoint as a tool any agent can call
from crewai.tools import tool
import os, httpx, time
@tool("HumanBrowser")
def human_browser(goal: str) -> str:
"""Run a stealth cloud browser-agent on a residential IP. Returns final text."""
api = "https://agent.humanbrowser.cloud/a2a"
token = os.environ["HUMANBROWSER_API_TOKEN"]
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
r = httpx.post(api, headers=headers, json={
"jsonrpc": "2.0", "id": 1, "method": "message/send",
"params": {"message": {"role": "user", "parts": [{"kind": "text", "text": goal}]}},
}, timeout=60).json()
task_id = r["result"]["id"]
# Poll tasks/get until terminal (completed | failed | canceled | input-required)
while True:
t = httpx.post(api, headers=headers, json={
"jsonrpc": "2.0", "id": 2, "method": "tasks/get",
"params": {"id": task_id},
}, timeout=30).json()["result"]
if t["status"]["state"] in ("completed", "failed", "canceled", "input-required"):
return (t.get("metadata", {}).get("outcome", {}) or {}).get("result", "")
time.sleep(2)For Google ADK, OpenAI Agents SDK, and Claude skills, point any A2A-compatible client at https://agent.humanbrowser.cloud/a2a with your bearer token. The Agent Card auto-advertises the three skills.
Ready to plug in?
Sign up, grab an API token, and your first task runs on a residential Chromium within seconds.