Agent2Agent Protocol · v0.3.0

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.

JSON-RPC 2.0SSE streamingA2A v0.3.0Residential IPLive viewer URL
Two browser windows exchanging Agent2Agent JSON-RPC messages

How it works

  1. 1
    Your agent (LangGraph, CrewAI, Claude, custom script) sends a JSON-RPC message/stream request to agent.humanbrowser.cloud/a2a.
  2. 2
    The A2A server spawns a Chromium session behind a residential proxy and returns a viewerUrl in the first SSE frame.
  3. 3
    A browser-use planner drives the page — every step, action, and cost update is streamed back as status-update events.
  4. 4
    On terminal state (completed, failed, or canceled), the server emits an artifact-update with the natural-language answer and structured findings, then closes the stream.

Endpoints

GET
https://agent.humanbrowser.cloud/.well-known/agent-card.json

Public manifest. Lists the skills, supported transports, security schemes, and streaming capability. No auth required.

POST
https://agent.humanbrowser.cloud/a2a

JSON-RPC 2.0 entry point. Bearer-token auth required. Methods:

  • message/send — starts the task and answers within seconds, once the browser is up (state working, 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 is metadata.outcome.result. Finished tasks are kept for 30 minutes.
  • actions/<name> — custom actions (list_countries, get_page_diagnostics, get_screenshots, switch_proxy_country, …). The actions/ 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 + SSE
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" }
        ]
      }
    }
  }'
Node.js — runOnCloud helper
// 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'
Python — httpx streaming
# 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

Browser Task

Free-form natural-language web automation. Navigate, click, fill, scroll, read. Returns a natural-language answer plus structured findings.

Input: text or data
Example: "Find the cheapest flight from Bangkok to Tokyo next weekend on skyscanner.com"
login_and_scrape

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.

Input: text + sensitive data
Example: "Log in to quora.com and list questions in my feed"
fill_form

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.

Input: text + data field map
Example: "Open example.com/contact and submit { name, email, 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 }
        }
      ]
    }
  }
}
Best practice: rotate credentials on your end and store them in a secret manager. We recommend the 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 eventkindstateWhat it means
1st frametasksubmittedTask accepted. metadata.viewerUrl appears as soon as Chromium spawns.
status-updatestatus-updateworkingPlanner step or action. status.message.parts carries human-readable text.
status-updatestatus-updateinput-requiredThe 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-updateartifact-update—Final result. artifact.parts = natural-language text + structured data.
status-update (final)status-updatecompletedSuccess. final: true. Stream closes.
status-update (final)status-updatefailedPlanner reported failure or session crashed. final: true.
status-update (final)status-updatecanceledYou 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_TOKEN

Get 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.retryable is 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) and allow_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-required with 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 is actions/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 (TypeScript)
// 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 (Python)
# 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.

Top up — $20

Loading secure checkout…

More coins →