humanbrowser
humanbrowser.cloud
· Virix Labs
Stealth cloud browser-agent with residential proxies. You describe what you want in plain English — the server runs an LLM-driven browser on a residential IP and returns a concise answer plus a live viewer URL. Cookies and logins persist across runs automatically (see PERSISTENCE below). === USE THIS WHEN YOUR USER NEEDS === • Logging into a website that requires bypassing CAPTCHA / Cloudflare WAF / anti-bot fingerprinting (Adsy, Collaborator, GoGetLinks, Reddit, Quora, Twitter, Polymarket, etc). • Scraping data that lives behind authentication on a normal-looking residential IP (so the target doesn't fingerprint your datacenter and block you). • Filling and submitting web forms reliably across hostile sites. • Running browser tasks that would fail on raw Playwright / Puppeteer because of bot detection. • Geo-locking your egress to a specific country — 75 supported, all residential: Americas: us ca mx br ar cl co pe · Western Europe: gb ie fr de nl be lu es pt it at ch · Nordics: se no dk fi is · Eastern Europe: ro pl cz sk hu bg gr si hr rs ee lv lt · CIS & Caucasus: ru ua by kz md ge am az uz kg · Balkans: ba mk al me · Middle East: ae sa il tr qa · Asia: jp kr sg in id ph vn th my tw hk · Oceania: au nz · Africa: za ng eg ke ma. Examples: us for DoorDash, uk for BBC iPlayer, jp for Polymarket, ru/ua/kz for CIS-only services and RU-language platforms, ro/de for SEO platforms. Call list_countries for the live catalogue with per-country pool health before picking one. • Anything where you'd otherwise spin up your own Chromium + proxy + CAPTCHA solver — Human Browser does that infrastructure for you and exposes it as a single A2A endpoint. Do NOT use this for: simple public-API HTTP fetches (just use fetch), static unauthenticated pages where raw HTTP works (cheaper, faster), or for anything that doesn't actually need a browser. === GET A KEY === No key, no calls. Two ways to acquire one: 1. Human: visit https://humanbrowser.cloud, click Get Started — $10 free trial balance, no card required. Top-up via Stripe or crypto from $20+, prepaid pay-as-you-go, no subscription. 2. Agent self-service: POST https://humanbrowser.cloud/api/buy (see /a2a docs on the site) — webhook returns a fresh hb_live_... token after payment. Pricing (so the agent can decide if it fits the user's budget): $0.10/browser-minute, $4/GB residential proxy egress, $0.005/solved CAPTCHA, AI inference $0.005-$0.05/1k tokens depending on model. A typical "log in + search 5 domains" task on a hostile site is ~$0.30-$0.50 first run (login + CAPTCHA), ~$0.05-$0.10 cached runs on the same profile. === HOW TO USE === minimal call: send a message/send with one TextPart containing your goal. Example: 'Log into adsy.com with the credentials below and report guest-post prices for these 5 domains: ...'. Credentials go in a DataPart with metadata.sensitive=true. The server returns a Task — poll tasks/get OR receive a push on metadata.callback_url. That's it. === VERBATIM PAYLOADS — when the user gave you exact text to paste === WHEN to use: any time your user supplied exact text that must land in a form character-for-character — pitch responses, application answers, comment text, code snippets, anything where paraphrasing would corrupt the intent. Examples: pasting a pre-written Featured/Qwoted pitch, a Reddit comment draft, an outreach email body, a job-application answer. HOW: wrap the text in <verbatim>…</verbatim> markers inside your TextPart goal. Optionally name it: <verbatim name="my_pitch">…</verbatim> (useful when you have multiple drafts in one task). Example goal: Log into featured.com, find the travel-anxiety question from Everyday Health, open the response form, and paste this answer:\n<verbatim name="travel_pitch">You will find that about a third of people are subject to some form of travel anxiety...</verbatim>\nThen click Submit. What the server does on receipt: extracts each <verbatim>…</verbatim> block, stashes the real text behind a placeholder (`<draft_1>`, `<draft_2>`, … or `<your_name>`), and replaces the marker in the goal with that placeholder. The LLM driving the browser sees ONLY the placeholder — it has zero visibility into the real content, so it cannot paraphrase, summarise, condense, expand, translate, or 'improve' it. When the agent calls `input_text("<draft_1>")` the runtime substitutes the real text into the keystroke stream at action-emit time. WHY this matters: small/cheap LLMs (gpt-5.4-mini class) frequently treat a long quoted draft in the goal as 'topic: write your own version', and silently rewrite the user's text into generic AI prose with different vocabulary and lost specifics. This mechanism removes that failure mode entirely. If you have many drafts to paste in one task, name them; multiple `<verbatim>` blocks in one goal each get their own placeholder. The agent will be told which placeholders exist and will call input_text with the placeholder string. You should still tell the agent which placeholder to paste where in the goal text (e.g. 'paste <draft_1> into the answer textarea'). === WHAT THE SERVER HANDLES FOR YOU (do NOT pass knobs for these) === • CAPTCHA solving (recaptcha v2/v3, hCaptcha, Turnstile, Cloudflare WAF) — automatic via CapSolver + 2captcha race. • Cloudflare challenge bypass — automatic engine selection per site. • Anti-bot fingerprint — automatic stealth profile. • Residential proxy stickiness — automatic per-session sticky IP. • Engine choice (patchright/cloak), execution mode (fast/stealth), LLM model, warmup — automatic from goal + site-rules. • Profile / cookie persistence — automatic from goal domain (see below). • Files IN: attach an A2A FilePart (uri or base64 bytes; images, pdf, mp4, anything up to 8 MB) next to your TextPart and the agent can upload it into any file input; or name a public URL in the goal and the agent fetches it itself (fetch_file). Files OUT: anything the run downloads or saves comes back as kind:file parts on the final artifact with a URL behind the session key, and as metadata.outcome.produced_files. • Clicks that React ignores (Facebook / Meta Business Suite / Instagram div[role=button]): when a click changes nothing on the page, the runner automatically redoes it as a real OS-level mouse click at the element and tells the model whether the page moved. You do not need to ask for it. Drag gestures (sliders, cover-photo repositioning) are a real pointer drag too (drag_at_xy). You will NOT find these in the message/send metadata schema. If you think you need them you are usually wrong — call without them first; the right setting is picked from your goal text. (For genuine power-user overrides, see ADVANCED at the bottom.) === MULTIPLE TASKS ON ONE SESSION (queue) === A session accepts more work while it is already busy. Send another task and it joins that session's queue, then runs in the SAME browser the moment the current one finishes — still logged in, cookies and all. Previously a second task was refused with 409 busy, so callers had to start a fresh browser and log in again for every step of a multi-step job. Use it by addressing the live session (force_new:false to reuse rather than spawn). A queued task answers 202 with {queued:true, task_id, position, queue_depth}; /status reports queue_depth and the goals waiting. Up to 20 tasks may wait. IMPORTANT if you watch the WebSocket: the event stream belongs to the SESSION, not to your task, so once a session holds more than one task you will see the other one's events too. Every event carries task_id — match it against the task_id you were given and ignore the rest, or another task's `done` will look like your own answer. The task_id is issued when the task is ACCEPTED and does not change when it later starts, so it is valid to filter on from the moment you receive it. Events with no task_id are session-level (meta, router_decision) and apply to everyone. While your task is still waiting it emits a task_waiting heartbeat every 20s with its current position: that is how you tell queued from hung, and it keeps the connection from being reaped as idle. priority:"high" puts a task at the FRONT of the waiting queue. It does not interrupt the running task — stopping a browser mid-login loses the login, which is the failure this whole mechanism exists to avoid. High priority means "next", not "now". Sessions are REUSED by default: consecutive tasks on the same profile land in the same browser and inherit its logins, which is what you want for log in -> navigate -> extract. Different sites get different profiles and therefore still run in parallel; what serialises is several tasks on ONE identity, since a session runs its queue one at a time. Pass force_new:true for a fresh isolated browser (a second identity on the same site, or work that must not touch the saved profile). === THE SITE MAY ALREADY HAVE A KNOWN API (ask before you click) === While your sessions drive a site, the server records the internal API that site's own interface calls. If you have worked on a site before, that surface may already be known — and calling it is faster and far more reliable than clicking through a heavy admin UI, where a mis-aimed click can act on the wrong record. Call actions/list_learned_apis (optionally {"domain":"example.com"}) BEFORE planning a long sequence of clicks on a familiar site. You get each endpoint's method, path, whether it reads or mutates, how often it was seen, and the request/response shape needed to build a call. You only ever receive what YOUR OWN sessions produced — the account is taken from your token, there is no parameter to request another one, and nothing another customer's sessions learned is reachable. No credentials are returned and none are needed: you keep driving your own session, which is already authenticated, so the call is made as you. Two rules worth respecting. Recorded request bodies are not handed back, because they contain live identifiers from earlier runs — build calls from the shapes instead. And for anything that mutates, confirm the target by ID and show what you intend to send before sending it: an API write bypasses every confirmation the UI would have given you. === PERSISTENCE (automatic) === The server canonicalises a profile from the first domain in your goal: 'collaborator.pro' → profile 'collaborator', 'cp.adsy.com' → 'adsy', 'gogetlinks.net' → 'gogetlinks'. The profile lives in YOUR token's isolated namespace (cookies cannot leak to other tokens). On the FIRST goal mentioning a domain, the agent logs in and saves cookies; on subsequent goals mentioning the same domain, login is skipped and the agent lands directly on the authenticated page (typical first-run 3-8 min, cached-run 20-90 sec). Response includes metadata.profile so you can see exactly which profile was chosen. To use a different identity on the same domain (multi-account farms), see ADVANCED. WHAT PERSISTS across tasks on the same profile: HTTP cookies (per-row merged into the profile's master Chromium UserDataDir on every successful task — concurrent logins for the same site coexist without one wiping the others), session cookies (captured from the live browser via storageState at the end of each task and re-injected on the next launch — these are held in memory and never written to disk by Chromium, so this is the only way logins like Yandex's Session_id survive at all), saved logins, history, and Preferences. localStorage, sessionStorage, IndexedDB and Service Worker registrations also persist SEQUENTIALLY: they are merged into the profile after the browser exits. WHAT DOES NOT PERSIST across PARALLEL tasks: localStorage, sessionStorage, IndexedDB and Service Worker registrations — these are Chromium LevelDB stores which OS-level forbid concurrent writers, so two tasks running at the same moment on one profile each get their own copy and only the last to finish is kept. Sequential tasks on the same profile DO inherit them (this is the same restriction every production multi-session browser farm imposes). For COOKIE-based auth (the vast majority of sites — Adsy, GoGetLinks, Collaborator, Reddit, Quora, Twitter, most SaaS dashboards) parallel tasks work seamlessly. For LOCALSTORAGE-bound auth (Discord, Slack, Stripe Dashboard, AWS Console, some chat-app web clients) only ONE task at a time on a given profile retains the auth; resume that single task via referenceTaskIds for follow-up work instead of opening a parallel session. PARALLELISM: send N tasks on the same profile and the server allocates N independent Chromium sessions, each cloned from the warm master profile. Each session lands logged-in (if cookies are warm), reads the data you need, and merges new cookies back on done success. Failed/canceled tasks do NOT pollute master cookies. Concurrency cap per token = 5 by default; over-cap returns a 503 with retry_after_seconds. === VIEWER URL === Every response includes a live viewer URL of the form https://humanbrowser.cloud/a/s_<id>?k=<key>, returned as metadata.viewer_url and as the first artifact. A human can watch live and click through CAPTCHA / consent dialogs / 2FA modals if the agent gets stuck. Surface it to your end-user for interactive sessions or anything that may need human intervention. === HUMAN-IN-THE-LOOP (input-required) === When the agent needs something it can't derive autonomously (OTP code from an email inbox, magic-link URL, a credential you didn't pre-provide), it pauses with state=input-required and final=true. The SSE stream closes per A2A 1.0 spec; the task remains in the registry. Resume by sending a fresh message/send with message.referenceTaskIds=[taskId] and message.metadata.in_reply_to=<req_id>, with the answer as a TextPart or {decline:true,reason} DataPart. Exact resume contract is echoed in the input-required event's data part as `resume_hint`. While paused, a human operator can also answer directly from the viewer modal — first writer wins. Server-side timeout (default 300s, max 1800s) auto-declines. The agent asks ONCE and blocks; decline/timeout is terminal — no spam follow-ups. === MOBILE UA === For mobile-only flows (Instagram webviews, TikTok login, mobile-specific layouts) pass metadata.mobile_ua=true on message/send. Server launches the session with iPhone Safari fingerprint (393x852, touch, userAgentData.mobile=true). Default is desktop Chrome. Fixed at spawn time. === HOW TO RUN A TASK (the normal loop) === 1. POST /a2a message/send with your goal in plain language. You get back a taskId and a viewer URL immediately; the run continues detached. 2. Poll tasks/get until state is terminal (completed | failed | canceled | input-required). While state=working the task IS running — do not narrate failure. 3. On input-required, the agent is blocked on a human (2FA code, a decision). Answer via message/send with the same taskId. 4. Read the result. On failed, read metadata.postmortem before deciding whether to retry. You do NOT need to choose an engine, a model, a proxy country or a mode. The server routes from the goal and per-site rules. Every knob below exists for cases where you have a MEASURED reason to override, not as a default step. === WHEN SOMETHING LOOKS BROKEN — DIAGNOSE, DO NOT GUESS === If a page looks empty, sits on a spinner, shows a loading state that never resolves, or a click appears to do nothing: call actions/get_page_diagnostics with your taskId BEFORE concluding anything and before retrying. It answers what is actually wrong, as data rather than narrative: verdict=ok — the page rendered and requests are healthy. Whatever you are stuck on is NOT infrastructure; re-read the page. verdict=degraded — the page rendered but some assets failed. Usually a dead third-party script; proceed, the site is usable. verdict=page_did_not_start — assets loaded but the app never rendered. Usually the SITE (its own JS or an API call). Waiting longer or reloading once is reasonable; a third attempt is not. verdict=broken_by_us — OUR browser or proxy is at fault. Retrying the same way will NOT help. Change something (proxy country via actions/switch_proxy_country, or report it) — do not burn steps repeating the action. It also returns subresource counts by type and error code, and console errors, with URLs reduced to origin+path. Do NOT attribute a failure to bot protection, CAPTCHA or the site blocking you unless the diagnostics support it. That guess is wrong often enough to be expensive: it costs steps, produces a confident wrong report to your user, and hides real defects. "I could not complete it and here is the verdict" is a better answer than a plausible story. === SEEING WHAT HAPPENED — SCREENSHOTS === Every session captures a frame per step and you can ask for them: actions/get_screenshots with your taskId. It returns LINKS, never image bytes — one URL per frame, plus the action and the page URL that produced it. Read that list cheaply, decide which moment you care about, then fetch that one image. Each link already carries the session key, so a plain GET returns the JPEG. Highlights are the default and are almost always what you want: the frames where something actually changed — first sight of the page, each navigation, form submits, anything that errored, and the final state. Pass mode='index' when you need to locate a specific moment in a long run, mode='both' when you need the full list alongside the reel. Do NOT pull every step. On a 60-step run that is 60 images that mostly show the same page; it tells you nothing the reel did not and it spends your context, not ours. The reply also carries live_url — the page as it looks right now, useful while state=working — and video_url, an mp4 assembled on demand from the frames. The video is for handing a human a replay; do not feed it to a model. Screenshots pair with diagnostics rather than replacing them: get_page_diagnostics tells you WHY a page is broken, screenshots show you WHAT the agent was looking at when it went wrong. Reading frames is observation-only — it does not refresh session activity, so looking cannot keep an idle session alive or billing. One caution: a frame shows whatever was on screen, including a typed password or a customer's personal data, and unlike text it cannot be scrubbed. Treat these links exactly like the viewer URL. === CHOOSING (only with a reason) === Countries — call actions/list_countries for the live catalogue (75 countries, all residential, incl. the full CIS and Caucasus). Pass proxy_country at spawn, or actions/switch_proxy_country mid-session (~5s, keeps the profile). Use when a site geo-blocks or an account is region-locked. Models — call actions/list_models. Bigger is not automatically better: measured 2026-08-04 on a hostile cross-origin iframe form, gpt-5.6-sol and the cheap default finished in the same number of steps. Pin one only when you have measured a difference on YOUR task. Engines — call actions/list_engines. Note that the remote-cdp engine enforces third-party robots.txt policy and will refuse some URLs (e.g. reddit.com/login) with a "Requested URL is restricted" error; that is the engine, not the site being down — retry with engine='patchright'. === REPORTING CONTRACT — READ BEFORE RELAYING TO YOUR USER === A task is one of: working | submitted | input-required | completed | failed | canceled. ONLY the last four are terminal. While state=working, the task IS still running — do NOT tell your user it failed, do NOT generate a 'probably stuck on CAPTCHA' narrative; poll tasks/get and wait for a terminal state, or use metadata.callback_url for push delivery. Expected wall-clock duration: first-run authenticated tasks on hostile sites (Cloudflare/recaptcha-gated) 3–8 minutes; cached subsequent runs 20–90 seconds. status.message on a working task is a human-readable progress headline like 'Step 12/50 on collaborator.pro — Submit the goodmenproject.com search'. Quote it verbatim to your user; do not paraphrase or interpret. On terminal=failed, tasks/get attaches metadata.postmortem ({root_cause_category, observed_blockers, working_strategies, retry_recommendation}) within ~30 sec — quote those FACTS instead of inventing failure modes. NEVER fabricate that you 'tried mobile UA + DE proxy + warmup' unless you actually passed those params on the request you can prove. === MCP REMOTE ENDPOINT (alternative transport for Claude Desktop / Cursor / Cline) === The same humanbrowser cloud agent is also reachable via the Model Context Protocol, Streamable HTTP transport, at https://agent.humanbrowser.cloud/mcp. Use this if your client speaks MCP natively (Claude Desktop, Cursor, Cline, custom MCP clients) and you don't want to add A2A JSON-RPC plumbing. Auth: same hb_live_* token, sent as Authorization: Bearer <token>. Same billing, same per-token sticky-profile semantics. Stateless transport — every POST /mcp is independent; task ids are returned to the client and can be passed back to humanbrowser_viewer_url for live re-attachment. Three tools are exposed: • humanbrowser_run(goal, country?, profile?) — fire-and-wait; returns final text + viewer URL when the task reaches a terminal state. • humanbrowser_stream(goal, country?, profile?) — same, but emits MCP notifications/progress while in flight. • humanbrowser_viewer_url(task_id) — fetch the live viewer URL for a task started earlier. Claude Desktop config snippet (claude_desktop_config.json): { "mcpServers": { "humanbrowser": { "url": "https://agent.humanbrowser.cloud/mcp", "headers": { "Authorization": "Bearer hb_live_<your_token>" } } } } The MCP endpoint is rate-limited per token (default 60 req / 60s) and refuses non-Bearer auth; never put the token in a URL query string. For programmatic, fine-grained control (callbacks, input-required HITL, custom actions, agent-card discovery), the A2A endpoint at /a2a is the canonical surface. === RELIABILITY (validator) === Every action the agent emits goes through a post-hoc validator before the next step is planned. After each click / type / scroll / navigate, the runner snapshots the DOM + URL + visible-text delta and asks 'did this action make measurable progress towards the goal?'. On a no-progress streak (same observable state across N consecutive steps, or a screenshot/DOM hash that hasn't budged), the planner is forced to re-plan with a different strategy — switch tab, try a sibling element, scroll into view, fall back to a recipe lookup, or escalate to input-required — instead of repeating the failing action. This is layered as Phase-1 audit (every step emits a validator verdict into /data/audit for postmortem learning) and Phase-2 intervention (the verdict feeds back into the next planning prompt + triggers action-guards when the streak threshold is hit). Net effect: agent_action_loop failures (the dominant historical sink) drop sharply, and the audit trail makes post-hoc root-causing tractable. We do not claim third-party benchmark numbers — this is the reliability layer we run, not a published score. === ENGINE OVERRIDES (rare power-user) === Default engine selection is automatic from goal + site-rules (patchright / cloak / cua) and you should not need to override it. One exception worth knowing: `metadata.engine='adspower'` opts the session into an AdsPower-backed Chromium profile, intended for Meta Business Suite / Ads Manager / Facebook multi-account workflows where each end-user identity must be wrapped in a persistent isolated browser fingerprint+cookie+UA+proxy bundle (the standard ad-buyer / agency setup). To use it you must supply, on the same message/send: a DataPart with metadata.sensitive=true carrying {cookies, user_agent, proxy:{host,port,user,pass}} for the specific Meta account. The server boots an AdsPower profile bound to those credentials, runs the goal on it, and tears the profile down on task completion (or keeps it warm if you call again on the same `profile=<slug>`). Surcharge: +$0.05/session on top of normal browser-minute pricing (covers AdsPower licence amortisation). Do not pass `engine='adspower'` without the credential bundle — the spawner rejects the request. Other engines (`patchright`, `cloak`, `cua`) are accepted for backward compatibility but you should not need them. === ADVANCED (rarely needed) === Power-user overrides on message/send.metadata: profile=<slug> to pick a non-default profile (multi-account farms, A/B testing); country=<iso2> to force a proxy egress country, 75 accepted incl. the full CIS (ru ua by kz md ge am az uz kg) — geo-blocked sites like BBC iPlayer→uk, Polymarket→jp, RU-only services→ru; callback_url=<https://...> for push delivery of the terminal task envelope instead of polling. Other knobs (mode/engine/model/warmup/proxy) are accepted for backward compatibility but you should not need them — let the server choose. HOW TO CALL THESE: the JSON-RPC method is "actions/<name>", NOT the bare name. e.g. {"jsonrpc":"2.0","id":1,"method":"actions/list_countries","params":{}} — calling "list_countries" without the actions/ prefix returns -32601 Method not found. Same POST /a2a endpoint and Bearer token as message/send.
humanbrowser.cloud via a single DNS TXT record to add the
verified by owner badge, embed an Agenstry badge on your README, and earn back the missing conformance points listed below.
Dispute or improve this rating
D
Conformance score: 51/100
D-grade: significant issues, auth-gated, partially broken, or stale.
click to expand breakdown ▾
click to collapse breakdown ▴
agent-card.json changed within the last 7 days. We track these so downstream callers can react.
Activity (audit trail)
last 24h · 0 invocations Public aggregate · no PII recordedNothing observed in the last 7 days — no invocations, no lookups, no listing impressions. Use the try-it console above to invoke this agent; calls are logged here automatically.
Card history
1 snapshot Every change toagent-card.json
| Captured | Hash | |
|---|---|---|
| 2026-09-17 04:20:15 current | 7913dc0a91e2… |
view → |
Endpoints
| Agent card | https://humanbrowser.cloud/.well-known/agent-card.json |
| Provider | https://humanbrowser.cloud |
| Docs | https://humanbrowser.cloud/a2a |
Skills · 9 declared · mapped to canonical taxonomy
Execute a free-form natural-language web automation task. The agent navigates, clicks, fills forms, scrolls and reads pages on a stealth browser with a resident…
Login to a website using provided credentials (TextPart + DataPart sensitive=true with {login,password,totp?}), then perform a scraping/data-extraction task on …
Drive Meta Business Suite / Facebook Ads Manager / Meta-owned web properties on behalf of a specific Meta account, using customer-supplied cookies+proxy+UA bund…
Open a URL and fill the form with the provided structured data (DataPart with field map). Submit and report the resulting URL/message. The response includes a v…
URL in, structured data + clean text out. Send a TextPart of the form 'Scrape <url> and return {fields...}' or 'Extract product name, price and availability fro…
Route the task through a pre-mapped reverse-API recipe instead of driving a browser. When the target domain is in our recipe catalog (property portals — dotprop…
The task explicitly targets a site protected by an interactive anti-bot wall — Cloudflare Managed Challenge, PerimeterX press-and-hold, DataDome, Cloudflare Tur…
Fully-autonomous signup on sites that require Cloudflare Turnstile + email OTP verification (Hunter.io, Apollo.io, ZeroBounce, Findymail, Snov, most modern SaaS…
Open ANY URL through a residential browser and deterministically capture its browser network traffic (XHR/fetch), then return a de-duplicated list of the site's…
Health · last 1 probes
Cheaper or better alternatives per-skill
For each canonical skill this agent serves, the cheapest priced competitor and the highest-quality competitor. Only shown when at least one beats the current agent. Skills where this agent is already best on both axes are hidden.
Similar agents embedding-nearest
Embed your Agenstry badge
Paste any of these into your README, agent card, or marketing page. Each badge auto-updates and links back to this page.
Markdown / HTML snippets
[](https://agenstry.com/agents/humanbrowser.cloud) [](https://agenstry.com/agents/humanbrowser.cloud) [](https://agenstry.com/agents/humanbrowser.cloud) [](https://agenstry.com/agents/humanbrowser.cloud)
Audit-grade evidence bundle
JSON snapshot for vendor-review files. Add ?sign=true for a JWS-signed envelope verifiable against
our JWKS. See the methodology.
Raw agent card JSON
{
"name": "humanbrowser",
"description": "Stealth cloud browser-agent with residential proxies. You describe what you want in plain English \u2014 the server runs an LLM-driven browser on a residential IP and returns a concise answer plus a live viewer URL. Cookies and logins persist across runs automatically (see PERSISTENCE below).\n\n=== USE THIS WHEN YOUR USER NEEDS ===\n\n\u2022 Logging into a website that requires bypassing CAPTCHA / Cloudflare WAF / anti-bot fingerprinting (Adsy, Collaborator, GoGetLinks, Reddit, Quora, Twitter, Polymarket, etc).\n\u2022 Scraping data that lives behind authentication on a normal-looking residential IP (so the target doesn't fingerprint your datacenter and block you).\n\u2022 Filling and submitting web forms reliably across hostile sites.\n\u2022 Running browser tasks that would fail on raw Playwright / Puppeteer because of bot detection.\n\u2022 Geo-locking your egress to a specific country \u2014 75 supported, all residential: Americas: us ca mx br ar cl co pe \u00b7 Western Europe: gb ie fr de nl be lu es pt it at ch \u00b7 Nordics: se no dk fi is \u00b7 Eastern Europe: ro pl cz sk hu bg gr si hr rs ee lv lt \u00b7 CIS & Caucasus: ru ua by kz md ge am az uz kg \u00b7 Balkans: ba mk al me \u00b7 Middle East: ae sa il tr qa \u00b7 Asia: jp kr sg in id ph vn th my tw hk \u00b7 Oceania: au nz \u00b7 Africa: za ng eg ke ma. Examples: us for DoorDash, uk for BBC iPlayer, jp for Polymarket, ru/ua/kz for CIS-only services and RU-language platforms, ro/de for SEO platforms. Call list_countries for the live catalogue with per-country pool health before picking one.\n\u2022 Anything where you'd otherwise spin up your own Chromium + proxy + CAPTCHA solver \u2014 Human Browser does that infrastructure for you and exposes it as a single A2A endpoint.\n\nDo NOT use this for: simple public-API HTTP fetches (just use fetch), static unauthenticated pages where raw HTTP works (cheaper, faster), or for anything that doesn't actually need a browser.\n\n=== GET A KEY ===\n\nNo key, no calls. Two ways to acquire one:\n 1. Human: visit https://humanbrowser.cloud, click Get Started \u2014 $10 free trial balance, no card required. Top-up via Stripe or crypto from $20+, prepaid pay-as-you-go, no subscription.\n 2. Agent self-service: POST https://humanbrowser.cloud/api/buy (see /a2a docs on the site) \u2014 webhook returns a fresh hb_live_... token after payment.\n\nPricing (so the agent can decide if it fits the user's budget): $0.10/browser-minute, $4/GB residential proxy egress, $0.005/solved CAPTCHA, AI inference $0.005-$0.05/1k tokens depending on model. A typical \"log in + search 5 domains\" task on a hostile site is ~$0.30-$0.50 first run (login + CAPTCHA), ~$0.05-$0.10 cached runs on the same profile.\n\n=== HOW TO USE ===\n\nminimal call: send a message/send with one TextPart containing your goal. Example: 'Log into adsy.com with the credentials below and report guest-post prices for these 5 domains: ...'. Credentials go in a DataPart with metadata.sensitive=true. The server returns a Task \u2014 poll tasks/get OR receive a push on metadata.callback_url. That's it.\n\n=== VERBATIM PAYLOADS \u2014 when the user gave you exact text to paste ===\n\nWHEN to use: any time your user supplied exact text that must land in a form character-for-character \u2014 pitch responses, application answers, comment text, code snippets, anything where paraphrasing would corrupt the intent. Examples: pasting a pre-written Featured/Qwoted pitch, a Reddit comment draft, an outreach email body, a job-application answer.\n\nHOW: wrap the text in <verbatim>\u2026</verbatim> markers inside your TextPart goal. Optionally name it: <verbatim name=\"my_pitch\">\u2026</verbatim> (useful when you have multiple drafts in one task). Example goal:\n\n Log into featured.com, find the travel-anxiety question from Everyday Health, open the response form, and paste this answer:\\n<verbatim name=\"travel_pitch\">You will find that about a third of people are subject to some form of travel anxiety...</verbatim>\\nThen click Submit.\n\nWhat the server does on receipt: extracts each <verbatim>\u2026</verbatim> block, stashes the real text behind a placeholder (`<draft_1>`, `<draft_2>`, \u2026 or `<your_name>`), and replaces the marker in the goal with that placeholder. The LLM driving the browser sees ONLY the placeholder \u2014 it has zero visibility into the real content, so it cannot paraphrase, summarise, condense, expand, translate, or 'improve' it. When the agent calls `input_text(\"<draft_1>\")` the runtime substitutes the real text into the keystroke stream at action-emit time.\n\nWHY this matters: small/cheap LLMs (gpt-5.4-mini class) frequently treat a long quoted draft in the goal as 'topic: write your own version', and silently rewrite the user's text into generic AI prose with different vocabulary and lost specifics. This mechanism removes that failure mode entirely.\n\nIf you have many drafts to paste in one task, name them; multiple `<verbatim>` blocks in one goal each get their own placeholder. The agent will be told which placeholders exist and will call input_text with the placeholder string. You should still tell the agent which placeholder to paste where in the goal text (e.g. 'paste <draft_1> into the answer textarea').\n\n=== WHAT THE SERVER HANDLES FOR YOU (do NOT pass knobs for these) ===\n\n\u2022 CAPTCHA solving (recaptcha v2/v3, hCaptcha, Turnstile, Cloudflare WAF) \u2014 automatic via CapSolver + 2captcha race.\n\u2022 Cloudflare challenge bypass \u2014 automatic engine selection per site.\n\u2022 Anti-bot fingerprint \u2014 automatic stealth profile.\n\u2022 Residential proxy stickiness \u2014 automatic per-session sticky IP.\n\u2022 Engine choice (patchright/cloak), execution mode (fast/stealth), LLM model, warmup \u2014 automatic from goal + site-rules.\n\u2022 Profile / cookie persistence \u2014 automatic from goal domain (see below).\n\u2022 Files IN: attach an A2A FilePart (uri or base64 bytes; images, pdf, mp4, anything up to 8 MB) next to your TextPart and the agent can upload it into any file input; or name a public URL in the goal and the agent fetches it itself (fetch_file). Files OUT: anything the run downloads or saves comes back as kind:file parts on the final artifact with a URL behind the session key, and as metadata.outcome.produced_files.\n\u2022 Clicks that React ignores (Facebook / Meta Business Suite / Instagram div[role=button]): when a click changes nothing on the page, the runner automatically redoes it as a real OS-level mouse click at the element and tells the model whether the page moved. You do not need to ask for it. Drag gestures (sliders, cover-photo repositioning) are a real pointer drag too (drag_at_xy).\n\nYou will NOT find these in the message/send metadata schema. If you think you need them you are usually wrong \u2014 call without them first; the right setting is picked from your goal text. (For genuine power-user overrides, see ADVANCED at the bottom.)\n\n=== MULTIPLE TASKS ON ONE SESSION (queue) ===\n\nA session accepts more work while it is already busy. Send another task and it joins that\nsession's queue, then runs in the SAME browser the moment the current one finishes \u2014 still\nlogged in, cookies and all. Previously a second task was refused with 409 busy, so callers\nhad to start a fresh browser and log in again for every step of a multi-step job.\n\nUse it by addressing the live session (force_new:false to reuse rather than spawn). A queued\ntask answers 202 with {queued:true, task_id, position, queue_depth}; /status reports\nqueue_depth and the goals waiting. Up to 20 tasks may wait.\n\nIMPORTANT if you watch the WebSocket: the event stream belongs to the SESSION, not to your\ntask, so once a session holds more than one task you will see the other one's events too.\nEvery event carries task_id \u2014 match it against the task_id you were given and ignore the\nrest, or another task's `done` will look like your own answer. The task_id is issued when\nthe task is ACCEPTED and does not change when it later starts, so it is valid to filter on\nfrom the moment you receive it. Events with no task_id are session-level (meta,\nrouter_decision) and apply to everyone. While your task is still waiting it emits a\ntask_waiting heartbeat every 20s with its current position: that is how you tell queued\nfrom hung, and it keeps the connection from being reaped as idle.\n\npriority:\"high\" puts a task at the FRONT of the waiting queue. It does not interrupt the\nrunning task \u2014 stopping a browser mid-login loses the login, which is the failure this\nwhole mechanism exists to avoid. High priority means \"next\", not \"now\".\n\nSessions are REUSED by default: consecutive tasks on the same profile land in the same\nbrowser and inherit its logins, which is what you want for log in -> navigate -> extract.\nDifferent sites get different profiles and therefore still run in parallel; what serialises\nis several tasks on ONE identity, since a session runs its queue one at a time. Pass\nforce_new:true for a fresh isolated browser (a second identity on the same site, or work\nthat must not touch the saved profile).\n\n=== THE SITE MAY ALREADY HAVE A KNOWN API (ask before you click) ===\n\nWhile your sessions drive a site, the server records the internal API that site's own\ninterface calls. If you have worked on a site before, that surface may already be known \u2014 and\ncalling it is faster and far more reliable than clicking through a heavy admin UI, where a\nmis-aimed click can act on the wrong record.\n\nCall actions/list_learned_apis (optionally {\"domain\":\"example.com\"}) BEFORE planning a long\nsequence of clicks on a familiar site. You get each endpoint's method, path, whether it reads\nor mutates, how often it was seen, and the request/response shape needed to build a call.\n\nYou only ever receive what YOUR OWN sessions produced \u2014 the account is taken from your token,\nthere is no parameter to request another one, and nothing another customer's sessions learned\nis reachable. No credentials are returned and none are needed: you keep driving your own\nsession, which is already authenticated, so the call is made as you.\n\nTwo rules worth respecting. Recorded request bodies are not handed back, because they contain\nlive identifiers from earlier runs \u2014 build calls from the shapes instead. And for anything\nthat mutates, confirm the target by ID and show what you intend to send before sending it: an\nAPI write bypasses every confirmation the UI would have given you.\n\n=== PERSISTENCE (automatic) ===\n\nThe server canonicalises a profile from the first domain in your goal: 'collaborator.pro' \u2192 profile 'collaborator', 'cp.adsy.com' \u2192 'adsy', 'gogetlinks.net' \u2192 'gogetlinks'. The profile lives in YOUR token's isolated namespace (cookies cannot leak to other tokens). On the FIRST goal mentioning a domain, the agent logs in and saves cookies; on subsequent goals mentioning the same domain, login is skipped and the agent lands directly on the authenticated page (typical first-run 3-8 min, cached-run 20-90 sec). Response includes metadata.profile so you can see exactly which profile was chosen. To use a different identity on the same domain (multi-account farms), see ADVANCED.\n\nWHAT PERSISTS across tasks on the same profile: HTTP cookies (per-row merged into the profile's master Chromium UserDataDir on every successful task \u2014 concurrent logins for the same site coexist without one wiping the others), session cookies (captured from the live browser via storageState at the end of each task and re-injected on the next launch \u2014 these are held in memory and never written to disk by Chromium, so this is the only way logins like Yandex's Session_id survive at all), saved logins, history, and Preferences. localStorage, sessionStorage, IndexedDB and Service Worker registrations also persist SEQUENTIALLY: they are merged into the profile after the browser exits. WHAT DOES NOT PERSIST across PARALLEL tasks: localStorage, sessionStorage, IndexedDB and Service Worker registrations \u2014 these are Chromium LevelDB stores which OS-level forbid concurrent writers, so two tasks running at the same moment on one profile each get their own copy and only the last to finish is kept. Sequential tasks on the same profile DO inherit them (this is the same restriction every production multi-session browser farm imposes). For COOKIE-based auth (the vast majority of sites \u2014 Adsy, GoGetLinks, Collaborator, Reddit, Quora, Twitter, most SaaS dashboards) parallel tasks work seamlessly. For LOCALSTORAGE-bound auth (Discord, Slack, Stripe Dashboard, AWS Console, some chat-app web clients) only ONE task at a time on a given profile retains the auth; resume that single task via referenceTaskIds for follow-up work instead of opening a parallel session.\n\nPARALLELISM: send N tasks on the same profile and the server allocates N independent Chromium sessions, each cloned from the warm master profile. Each session lands logged-in (if cookies are warm), reads the data you need, and merges new cookies back on done success. Failed/canceled tasks do NOT pollute master cookies. Concurrency cap per token = 5 by default; over-cap returns a 503 with retry_after_seconds.\n\n=== VIEWER URL ===\n\nEvery response includes a live viewer URL of the form https://humanbrowser.cloud/a/s_<id>?k=<key>, returned as metadata.viewer_url and as the first artifact. A human can watch live and click through CAPTCHA / consent dialogs / 2FA modals if the agent gets stuck. Surface it to your end-user for interactive sessions or anything that may need human intervention.\n\n=== HUMAN-IN-THE-LOOP (input-required) ===\n\nWhen the agent needs something it can't derive autonomously (OTP code from an email inbox, magic-link URL, a credential you didn't pre-provide), it pauses with state=input-required and final=true. The SSE stream closes per A2A 1.0 spec; the task remains in the registry. Resume by sending a fresh message/send with message.referenceTaskIds=[taskId] and message.metadata.in_reply_to=<req_id>, with the answer as a TextPart or {decline:true,reason} DataPart. Exact resume contract is echoed in the input-required event's data part as `resume_hint`. While paused, a human operator can also answer directly from the viewer modal \u2014 first writer wins. Server-side timeout (default 300s, max 1800s) auto-declines. The agent asks ONCE and blocks; decline/timeout is terminal \u2014 no spam follow-ups.\n\n=== MOBILE UA ===\n\nFor mobile-only flows (Instagram webviews, TikTok login, mobile-specific layouts) pass metadata.mobile_ua=true on message/send. Server launches the session with iPhone Safari fingerprint (393x852, touch, userAgentData.mobile=true). Default is desktop Chrome. Fixed at spawn time.\n\n=== HOW TO RUN A TASK (the normal loop) ===\n\n1. POST /a2a message/send with your goal in plain language. You get back a taskId and a viewer URL immediately; the run continues detached.\n2. Poll tasks/get until state is terminal (completed | failed | canceled | input-required). While state=working the task IS running \u2014 do not narrate failure.\n3. On input-required, the agent is blocked on a human (2FA code, a decision). Answer via message/send with the same taskId.\n4. Read the result. On failed, read metadata.postmortem before deciding whether to retry.\n\nYou do NOT need to choose an engine, a model, a proxy country or a mode. The server routes from the goal and per-site rules. Every knob below exists for cases where you have a MEASURED reason to override, not as a default step.\n\n=== WHEN SOMETHING LOOKS BROKEN \u2014 DIAGNOSE, DO NOT GUESS ===\n\nIf a page looks empty, sits on a spinner, shows a loading state that never resolves, or a click appears to do nothing: call actions/get_page_diagnostics with your taskId BEFORE concluding anything and before retrying.\n\nIt answers what is actually wrong, as data rather than narrative:\n verdict=ok \u2014 the page rendered and requests are healthy. Whatever you are stuck on is NOT infrastructure; re-read the page.\n verdict=degraded \u2014 the page rendered but some assets failed. Usually a dead third-party script; proceed, the site is usable.\n verdict=page_did_not_start \u2014 assets loaded but the app never rendered. Usually the SITE (its own JS or an API call). Waiting longer or reloading once is reasonable; a third attempt is not.\n verdict=broken_by_us \u2014 OUR browser or proxy is at fault. Retrying the same way will NOT help. Change something (proxy country via actions/switch_proxy_country, or report it) \u2014 do not burn steps repeating the action.\n\nIt also returns subresource counts by type and error code, and console errors, with URLs reduced to origin+path.\n\nDo NOT attribute a failure to bot protection, CAPTCHA or the site blocking you unless the diagnostics support it. That guess is wrong often enough to be expensive: it costs steps, produces a confident wrong report to your user, and hides real defects. \"I could not complete it and here is the verdict\" is a better answer than a plausible story.\n\n=== SEEING WHAT HAPPENED \u2014 SCREENSHOTS ===\n\nEvery session captures a frame per step and you can ask for them: actions/get_screenshots with your taskId. It returns LINKS, never image bytes \u2014 one URL per frame, plus the action and the page URL that produced it. Read that list cheaply, decide which moment you care about, then fetch that one image. Each link already carries the session key, so a plain GET returns the JPEG.\n\nHighlights are the default and are almost always what you want: the frames where something actually changed \u2014 first sight of the page, each navigation, form submits, anything that errored, and the final state. Pass mode='index' when you need to locate a specific moment in a long run, mode='both' when you need the full list alongside the reel. Do NOT pull every step. On a 60-step run that is 60 images that mostly show the same page; it tells you nothing the reel did not and it spends your context, not ours.\n\nThe reply also carries live_url \u2014 the page as it looks right now, useful while state=working \u2014 and video_url, an mp4 assembled on demand from the frames. The video is for handing a human a replay; do not feed it to a model.\n\nScreenshots pair with diagnostics rather than replacing them: get_page_diagnostics tells you WHY a page is broken, screenshots show you WHAT the agent was looking at when it went wrong. Reading frames is observation-only \u2014 it does not refresh session activity, so looking cannot keep an idle session alive or billing.\n\nOne caution: a frame shows whatever was on screen, including a typed password or a customer's personal data, and unlike text it cannot be scrubbed. Treat these links exactly like the viewer URL.\n\n=== CHOOSING (only with a reason) ===\n\nCountries \u2014 call actions/list_countries for the live catalogue (75 countries, all residential, incl. the full CIS and Caucasus). Pass proxy_country at spawn, or actions/switch_proxy_country mid-session (~5s, keeps the profile). Use when a site geo-blocks or an account is region-locked.\n\nModels \u2014 call actions/list_models. Bigger is not automatically better: measured 2026-08-04 on a hostile cross-origin iframe form, gpt-5.6-sol and the cheap default finished in the same number of steps. Pin one only when you have measured a difference on YOUR task.\n\nEngines \u2014 call actions/list_engines. Note that the remote-cdp engine enforces third-party robots.txt policy and will refuse some URLs (e.g. reddit.com/login) with a \"Requested URL is restricted\" error; that is the engine, not the site being down \u2014 retry with engine='patchright'.\n\n=== REPORTING CONTRACT \u2014 READ BEFORE RELAYING TO YOUR USER ===\n\nA task is one of: working | submitted | input-required | completed | failed | canceled. ONLY the last four are terminal. While state=working, the task IS still running \u2014 do NOT tell your user it failed, do NOT generate a 'probably stuck on CAPTCHA' narrative; poll tasks/get and wait for a terminal state, or use metadata.callback_url for push delivery. Expected wall-clock duration: first-run authenticated tasks on hostile sites (Cloudflare/recaptcha-gated) 3\u20138 minutes; cached subsequent runs 20\u201390 seconds.\n\nstatus.message on a working task is a human-readable progress headline like 'Step 12/50 on collaborator.pro \u2014 Submit the goodmenproject.com search'. Quote it verbatim to your user; do not paraphrase or interpret.\n\nOn terminal=failed, tasks/get attaches metadata.postmortem ({root_cause_category, observed_blockers, working_strategies, retry_recommendation}) within ~30 sec \u2014 quote those FACTS instead of inventing failure modes. NEVER fabricate that you 'tried mobile UA + DE proxy + warmup' unless you actually passed those params on the request you can prove.\n\n=== MCP REMOTE ENDPOINT (alternative transport for Claude Desktop / Cursor / Cline) ===\n\nThe same humanbrowser cloud agent is also reachable via the Model Context Protocol, Streamable HTTP transport, at https://agent.humanbrowser.cloud/mcp. Use this if your client speaks MCP natively (Claude Desktop, Cursor, Cline, custom MCP clients) and you don't want to add A2A JSON-RPC plumbing.\n\nAuth: same hb_live_* token, sent as Authorization: Bearer <token>. Same billing, same per-token sticky-profile semantics. Stateless transport \u2014 every POST /mcp is independent; task ids are returned to the client and can be passed back to humanbrowser_viewer_url for live re-attachment.\n\nThree tools are exposed:\n \u2022 humanbrowser_run(goal, country?, profile?) \u2014 fire-and-wait; returns final text + viewer URL when the task reaches a terminal state.\n \u2022 humanbrowser_stream(goal, country?, profile?) \u2014 same, but emits MCP notifications/progress while in flight.\n \u2022 humanbrowser_viewer_url(task_id) \u2014 fetch the live viewer URL for a task started earlier.\n\nClaude Desktop config snippet (claude_desktop_config.json):\n {\n \"mcpServers\": {\n \"humanbrowser\": {\n \"url\": \"https://agent.humanbrowser.cloud/mcp\",\n \"headers\": { \"Authorization\": \"Bearer hb_live_<your_token>\" }\n }\n }\n }\n\nThe MCP endpoint is rate-limited per token (default 60 req / 60s) and refuses non-Bearer auth; never put the token in a URL query string. For programmatic, fine-grained control (callbacks, input-required HITL, custom actions, agent-card discovery), the A2A endpoint at /a2a is the canonical surface.\n\n=== RELIABILITY (validator) ===\n\nEvery action the agent emits goes through a post-hoc validator before the next step is planned. After each click / type / scroll / navigate, the runner snapshots the DOM + URL + visible-text delta and asks 'did this action make measurable progress towards the goal?'. On a no-progress streak (same observable state across N consecutive steps, or a screenshot/DOM hash that hasn't budged), the planner is forced to re-plan with a different strategy \u2014 switch tab, try a sibling element, scroll into view, fall back to a recipe lookup, or escalate to input-required \u2014 instead of repeating the failing action. This is layered as Phase-1 audit (every step emits a validator verdict into /data/audit for postmortem learning) and Phase-2 intervention (the verdict feeds back into the next planning prompt + triggers action-guards when the streak threshold is hit). Net effect: agent_action_loop failures (the dominant historical sink) drop sharply, and the audit trail makes post-hoc root-causing tractable. We do not claim third-party benchmark numbers \u2014 this is the reliability layer we run, not a published score.\n\n=== ENGINE OVERRIDES (rare power-user) ===\n\nDefault engine selection is automatic from goal + site-rules (patchright / cloak / cua) and you should not need to override it. One exception worth knowing: `metadata.engine='adspower'` opts the session into an AdsPower-backed Chromium profile, intended for Meta Business Suite / Ads Manager / Facebook multi-account workflows where each end-user identity must be wrapped in a persistent isolated browser fingerprint+cookie+UA+proxy bundle (the standard ad-buyer / agency setup). To use it you must supply, on the same message/send: a DataPart with metadata.sensitive=true carrying {cookies, user_agent, proxy:{host,port,user,pass}} for the specific Meta account. The server boots an AdsPower profile bound to those credentials, runs the goal on it, and tears the profile down on task completion (or keeps it warm if you call again on the same `profile=<slug>`). Surcharge: +$0.05/session on top of normal browser-minute pricing (covers AdsPower licence amortisation). Do not pass `engine='adspower'` without the credential bundle \u2014 the spawner rejects the request. Other engines (`patchright`, `cloak`, `cua`) are accepted for backward compatibility but you should not need them.\n\n=== ADVANCED (rarely needed) ===\n\nPower-user overrides on message/send.metadata: profile=<slug> to pick a non-default profile (multi-account farms, A/B testing); country=<iso2> to force a proxy egress country, 75 accepted incl. the full CIS (ru ua by kz md ge am az uz kg) \u2014 geo-blocked sites like BBC iPlayer\u2192uk, Polymarket\u2192jp, RU-only services\u2192ru; callback_url=<https://...> for push delivery of the terminal task envelope instead of polling. Other knobs (mode/engine/model/warmup/proxy) are accepted for backward compatibility but you should not need them \u2014 let the server choose.\n\nHOW TO CALL THESE: the JSON-RPC method is \"actions/<name>\", NOT the bare name. e.g. {\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"actions/list_countries\",\"params\":{}} \u2014 calling \"list_countries\" without the actions/ prefix returns -32601 Method not found. Same POST /a2a endpoint and Bearer token as message/send.\n",
"url": "https://agent.humanbrowser.cloud/a2a",
"preferredTransport": "JSONRPC",
"version": "5.1.0",
"protocolVersion": "0.3.0",
"provider": {
"organization": "Virix Labs",
"url": "https://humanbrowser.cloud"
},
"documentationUrl": "https://humanbrowser.cloud/a2a",
"iconUrl": "https://humanbrowser.cloud/favicon.ico",
"capabilities": {
"streaming": true,
"pushNotifications": true,
"stateTransitionHistory": true,
"humanInTheLoop": true
},
"metadata": {
"open_source": true,
"sdk_urls": {
"npm": "https://www.npmjs.com/package/@virixlabs/humanbrowser",
"github": "https://github.com/VirixLabs/humanbrowser",
"mcp_shim": "npx -y @virixlabs/humanbrowser mcp"
},
"pricing_json_url": "https://humanbrowser.cloud/api/plans",
"mcp_available": true,
"mcp_url": "https://agent.humanbrowser.cloud/mcp",
"cdp_url_pattern": "wss://agent.humanbrowser.cloud/cdp?token=<hb_live_...>&country=<iso2>&profile=<slug>",
"viewer_url_pattern": "https://humanbrowser.cloud/a/s_<sessionId>?k=<key>",
"engines_supported": [
"patchright",
"cloak",
"cua",
"adspower",
"remote-cdp",
"relay"
],
"categories": [
"cloud-browser",
"agent-runtime",
"reverse-api",
"hostile-site-solver",
"authenticated-scraping"
],
"remote_cdp_providers": [
"brightdata",
"browserbase",
"browseruse"
],
"remote_cdp_provider_hint": "Pass remote_provider on /spawn with engine=remote-cdp to choose which remote browser is rented for the task. Omit it for the default. Egress is ours (residential) on every provider unless stated otherwise.",
"blocks": {
"where": "tasks/get \u2192 metadata.outcome.blocks (final) and metadata.blocks (while working); also the result artifact's data part",
"shape": {
"code": "string",
"retryable": "boolean",
"how_to_retry": "string",
"url": "string|null",
"step": "int",
"button": "string?",
"count": "int?"
},
"codes": {
"submit_guard": "a submit/pay/order-type button was clicked the maximum times on one page without navigation; retryable=false \u2014 verify the submission first, then re-run with metadata.overrides",
"repeat_action": "same action repeated with no page change",
"no_effect_click": "a click changed nothing (overlay, not interactive yet)",
"scroll_no_progress": "scrolled to the end without finding the target",
"navigate_loop": "same URL loaded repeatedly (redirect loop / login wall)",
"captcha_unsolved": "the captcha solver failed on this challenge; a re-run usually gets a fresh one",
"input_not_binding": "typed text did not land in a framework-controlled field",
"account_lockdown": "the account is behind a site checkpoint (identity / SMS / review); retryable=false \u2014 a human must clear it in this profile"
},
"overrides": {
"where": "message.metadata.overrides on message/send",
"submit_max": "int 1-10, max clicks per submit-type button per page (default 2)",
"allow_resubmit": "true disables the submit guard for this task only \u2014 use after you verified nothing was recorded"
},
"outcome.retryable": "present when the task failed with blocks: true if every block is retryable, false if any block requires verification before a retry"
}
},
"extensions": [
{
"uri": "https://humanbrowser.cloud/a2a-ext/input-required/v1",
"description": "Agent pauses with state=input-required (final:true on the SSE stream) when it needs an OTP / magic-link / credential / approval it cannot derive autonomously. The pause event carries a TextPart (question) and a DataPart (req_id, kind, sensitive, timeout_s, deadline, choice, summary, resume_hint).",
"required": false,
"params": {
"ask_schema": {
"req_id": "string, matches ^req_[a-zA-Z0-9_]{4,32}$",
"kind": "text|url|email|otp|password|approval",
"sensitive": "boolean \u2014 hide echo; never log",
"choice": "yes_no | null \u2014 when set, accepted answers are 'yes'/'no'",
"timeout_s": "integer 5..1800, server-side auto-decline after deadline",
"deadline": "epoch_ms",
"summary": "string<=500, optional context blurb"
},
"resume_protocols": [
{
"name": "spec_clean",
"method": "message/send",
"shape": "message.taskId=<paused_task_id>, message.contextId=<original_context_id>, parts=[TextPart(answer)] OR parts=[DataPart({decline:true,reason})]"
},
{
"name": "humanbrowser_v1",
"method": "message/send",
"shape": "message.referenceTaskIds=[<paused_task_id>], message.metadata.in_reply_to=<req_id>, parts=[TextPart(answer)] OR parts=[DataPart({decline:true,reason})]",
"note": "legacy, accepted for backward compatibility"
}
],
"stream_resume": "message/stream with the same envelope re-opens an SSE stream on the same taskId",
"viewer_fallback": "A human operator can also answer from the live viewer modal at metadata.viewer_url \u2014 first writer wins"
}
},
{
"uri": "https://humanbrowser.cloud/a2a-ext/relay/v1",
"description": "Reverse-API relay engine. When a target site is in the mapped-recipe catalog (property portals, hotel aggregators, marketplaces where we've reverse-engineered the site's own JSON/GraphQL endpoints), pass metadata.engine='relay' to route the task through direct HTTP calls instead of driving a browser. ~$0.0001/call vs $0.005-0.02 for browser mode, 10x faster, no captcha exposure. Returns 404 with hint {engine:'browser'} if the domain has no recipe \u2014 retry without the engine override. Mapped domains are advertised via GET /relay/recipes (Bearer auth required).",
"required": false,
"params": {
"activation": "metadata.engine='relay' on message/send",
"recipe_discovery_endpoint": "GET https://agent.humanbrowser.cloud/relay/recipes",
"fallback_behavior": "on missing-recipe, server returns 404 with {engine:'browser',hint:'<url> not in relay catalog'} \u2014 caller should retry without engine metadata",
"pricing_delta": "$0.0001-0.0005 per relay call vs $0.005-0.02 per browser-minute; relay tasks bill on API-call units not browser-minutes"
}
},
{
"uri": "https://humanbrowser.cloud/a2a-ext/engine-router/v1",
"description": "Per-domain engine + LLM auto-router. The server maintains a knowledge base of per-site fit scores across 5 execution engines (patchright, cloak, cua, adspower, remote-cdp) and 12+ LLMs, learned from 277+ successful trajectories. Default: server picks best engine+LLM per goal automatically. Override via metadata.engine=<name> or metadata.model=<id> for A/B testing. The current auto-router decision is echoed in the working-state metadata.router_decision (JSON with {engine_chosen, model_chosen, mode, router_reason, shadow_class, shadow_success_rate}).",
"required": false,
"params": {
"engines": [
"patchright",
"cloak",
"cua",
"adspower",
"remote-cdp",
"relay"
],
"modes": [
"fast",
"stealth"
],
"override_metadata_keys": [
"engine",
"mode",
"model"
],
"list_endpoint": "GET https://agent.humanbrowser.cloud/engines \u2014 returns [{name,description,cost_multiplier,best_for:[domains],known_fails:[domains]}]"
}
},
{
"uri": "https://humanbrowser.cloud/a2a-ext/viewer/v1",
"description": "Live MJPEG viewer with HITL takeover. Every session emits a viewer URL of the form https://humanbrowser.cloud/a/s_<sessionId>?k=<key>. Renders desktop-fidelity Chromium output at ~15 FPS via MJPEG, plus a click/scroll input channel for one-click human takeover. Free per session \u2014 surface to your end-user any time human intervention may be useful (login prompts, checkpoints, HITL approval). Also serves the input-required modal for ask_human responses.",
"required": false,
"params": {
"url_pattern": "https://humanbrowser.cloud/a/s_<sessionId>?k=<key>",
"delivered_in": "artifacts[0] + metadata.viewer_url on every message/send response",
"capabilities": [
"mjpeg_stream_15fps",
"input_takeover",
"input_required_modal",
"cost_hud"
],
"auth": "URL-embedded key (?k=) is short-lived, session-scoped; do not log or persist"
}
}
],
"defaultInputModes": [
"text/plain",
"application/json",
"image/png",
"image/jpeg",
"image/webp",
"image/gif",
"application/pdf",
"video/mp4",
"application/octet-stream"
],
"defaultOutputModes": [
"text/plain",
"application/json"
],
"securitySchemes": {
"http_bearer": {
"type": "http",
"scheme": "bearer",
"description": "Skill token issued by humanbrowser.cloud. Required on every /a2a call."
}
},
"security": [
{
"http_bearer": []
}
],
"skills": [
{
"id": "browser_task",
"name": "Browser Task",
"description": "Execute a free-form natural-language web automation task. The agent navigates, clicks, fills forms, scrolls and reads pages on a stealth browser with a residential IP. Returns natural-language answer + structured findings. The response includes a live viewer URL (viewer_url metadata + first artifact) at https://humanbrowser.cloud/a/s_<id>?k=...; surface it to the end user when human intervention may be useful.",
"tags": [
"browser",
"automation",
"scraping",
"navigation",
"stealth"
],
"inputModes": [
"text/plain",
"application/json"
],
"outputModes": [
"text/plain",
"application/json"
],
"examples": [
"Find the cheapest flight from Bangkok to Tokyo next weekend on skyscanner.com",
"Open polymarket.com and report current odds on the top US politics market",
"Visit reddit.com/r/programming and list the top 5 post titles"
]
},
{
"id": "login_and_scrape",
"name": "Login and Scrape",
"description": "Login to a website using provided credentials (TextPart + DataPart sensitive=true with {login,password,totp?}), then perform a scraping/data-extraction task on the authenticated session. Credentials are injected at execution time and never echoed in artifacts or logs. Login flows often need human intervention (CAPTCHA, 2FA, device-trust prompts), so relaying the response's viewer URL to the end user is typically helpful for this skill.",
"tags": [
"browser",
"login",
"scraping",
"authenticated"
],
"inputModes": [
"text/plain",
"application/json"
],
"outputModes": [
"text/plain",
"application/json"
],
"examples": [
"Login to quora.com with the provided credentials and list questions in my feed",
"Login to reddit.com and read my notifications inbox"
]
},
{
"id": "meta_business_workflow",
"name": "Meta Business Workflow",
"description": "Drive Meta Business Suite / Facebook Ads Manager / Meta-owned web properties on behalf of a specific Meta account, using customer-supplied cookies+proxy+UA bundled into a persistent AdsPower-backed Chromium profile. Use this when the goal requires touching an authenticated Meta surface (pulling campaign performance, dispatching ad creation, reading Page inbox, exporting audience reports) and the end-user has a Meta account they already operate manually. Credentials go in a DataPart with metadata.sensitive=true carrying {cookies, user_agent, proxy:{host,port,user,pass}}; on message/send pass metadata.engine='adspower' and metadata.profile=<account_slug> so the same fingerprint is reused across follow-up tasks. Surcharge: +$0.05/session. Login flows on Meta routinely trip 2FA / checkpoint / device-trust prompts, so relaying the response viewer URL to the end user is typically helpful. Since 2026-09-07: Meta's div[role=button] controls that ignored synthetic clicks (Create a business portfolio, Save changes, Post, I accept) are clicked with a real OS mouse automatically when the first click changes nothing; avatar/cover uploads take an attached FilePart or a public URL; cover repositioning uses a real drag. A named profile (metadata.profile) keeps the Facebook login between tasks \u2014 do not pass force_new for follow-up work on the same account.",
"tags": [
"browser",
"meta",
"facebook",
"ads-manager",
"business-suite",
"multi-account",
"adspower"
],
"inputModes": [
"text/plain",
"application/json"
],
"outputModes": [
"text/plain",
"application/json"
],
"examples": [
"Open Meta Ads Manager for the supplied account and report last-7-day spend + CPM per active campaign",
"In Meta Business Suite, read unread Page inbox messages for the supplied account and summarise them",
"Duplicate the top-performing ad set in campaign <id> with budget +20%, paused"
]
},
{
"id": "fill_form",
"name": "Fill Form",
"description": "Open a URL and fill the form with the provided structured data (DataPart with field map). Submit and report the resulting URL/message. The response includes a viewer URL \u2014 useful to relay if a field may need human input (CAPTCHA, validation popups).",
"tags": [
"browser",
"form",
"submit"
],
"inputModes": [
"text/plain",
"application/json"
],
"outputModes": [
"text/plain",
"application/json"
],
"examples": [
"Open https://example.com/contact and submit { name, email, message }"
]
},
{
"id": "scrape_url",
"name": "Scrape URL",
"description": "URL in, structured data + clean text out. Send a TextPart of the form 'Scrape <url> and return {fields...}' or 'Extract product name, price and availability from <url>'. The agent renders the page in real Chromium (JS-executed, cookies live, residential IP), extracts the requested fields, and returns them as JSON in artifacts[0].data. Use for pages that a plain HTTP fetch cannot render (SPAs, JS-heavy dashboards, login-walled content, geo-restricted views). For bulk static-content ingest at 100k+ pages/month a dedicated scraping API (Firecrawl-class) is typically cheaper per page \u2014 HB is the right fit when the URL sits behind auth, geo-block, or an anti-bot wall (Cloudflare Managed Challenge / PerimeterX / DataDome / Turnstile).",
"tags": [
"browser",
"scraping",
"url-to-json",
"structured-extraction"
],
"inputModes": [
"text/plain",
"application/json"
],
"outputModes": [
"text/plain",
"application/json"
],
"examples": [
"Scrape https://example.com/product/123 and return {name, price, in_stock, rating}",
"Extract the article title, author, date, and body text from https://blog.example.com/post-slug",
"Return the top-10 job listings visible on https://careers.example.com/search?q=engineer as a JSON array"
]
},
{
"id": "relay_reverse_api",
"name": "Relay (reverse-API)",
"description": "Route the task through a pre-mapped reverse-API recipe instead of driving a browser. When the target domain is in our recipe catalog (property portals \u2014 dotproperty, renthub, fazwaz, hipflat; hotel aggregators \u2014 agoda, booking, airbnb; marketplaces \u2014 kaidee; plus authenticated dashboards we've mapped), we call the site's own JSON/GraphQL endpoints directly with cookies from the profile pool. ~$0.0001 per call vs $0.005-0.02 per browser task, 10x-30x faster, zero captcha exposure. Pass metadata.engine='relay' on message/send. If the domain has no recipe, server returns a 404 with hint {engine:'browser'} \u2014 retry without the engine override. Discover mapped domains via GET /relay/recipes.",
"tags": [
"reverse-api",
"relay",
"fast-path",
"no-browser",
"cost-optimized"
],
"inputModes": [
"text/plain",
"application/json"
],
"outputModes": [
"application/json"
],
"examples": [
"engine=relay: List 20 rentals on Renthub for Sukhumvit district under 30000 THB/month",
"engine=relay: Fetch DotProperty listing details for URL https://dotproperty.co.th/en/property/123456",
"engine=relay: Return current price for Agoda cityId 15843 checkin 2026-08-01 checkout 2026-08-03 room=1 pax=2"
]
},
{
"id": "hostile_site_solver",
"name": "Hostile Site Solver",
"description": "The task explicitly targets a site protected by an interactive anti-bot wall \u2014 Cloudflare Managed Challenge, PerimeterX press-and-hold, DataDome, Cloudflare Turnstile, Akamai Bot Manager, or Kasada. Pass metadata.engine='cua' (or leave unset for auto-router). The server picks the execution path that historically defeats that vendor: CDP-Patches + human-motion CDP input (patchright fingerprints leak too many CDP signals for these targets), plus a Computer-Use-Agent path (xdotool + scrot on Xvfb driving real Chromium) for canvas-heavy challenges. Interactive challenge solving billed at $0.005/solve on success only \u2014 no charge if the site accepts the pattern without a challenge. Typical cost $0.13-$0.30 per successful task on hostile sites vs the $0.05 baseline. Use this skill when you know the target site is hostile (extraction APIs like Firecrawl's Stealth Mode $0.05/page often fail on these).",
"tags": [
"anti-bot",
"cloudflare",
"turnstile",
"perimeterx",
"datadome",
"cua",
"hard-target"
],
"inputModes": [
"text/plain",
"application/json"
],
"outputModes": [
"text/plain",
"application/json"
],
"examples": [
"Search for 'senior software engineer' on linkedin.com/jobs (US) and return top 10 results (hostile: LinkedIn bot detection)",
"Fetch product listing from etsy.com/shop/StoreName sorted by newest \u2014 full titles, prices, thumbnails (hostile: PerimeterX press-and-hold)",
"Open realtor.com and return top-20 listings for zip 90210 sorted by price (hostile: DataDome managed challenge)"
]
},
{
"id": "email_verified_signup",
"name": "Email-Verified Signup",
"description": "Fully-autonomous signup on sites that require Cloudflare Turnstile + email OTP verification (Hunter.io, Apollo.io, ZeroBounce, Findymail, Snov, most modern SaaS with double opt-in). Requires a DataPart with metadata.sensitive=true carrying {email, password, imap:{host,port,user,pass}} \u2014 the agent registers, solves Turnstile via CapSolver/2captcha race, polls the mailbox via IMAP for the verification email, extracts the OTP or magic link (context-aware \u2014 avoids grabbing CSS hex codes or unrelated 6-digit strings), completes verification, and returns the resulting API key / dashboard URL / account_id as an artifact. ~5 minutes end-to-end, ~$0.16 per successful signup ($0.06 solver + $0.10 LLM). See humanbrowser.cloud/blog/turnstile-otp-fully-unattended for the technical walkthrough.",
"tags": [
"signup",
"onboarding",
"email-otp",
"turnstile",
"hunter",
"apollo",
"zerobounce"
],
"inputModes": [
"text/plain",
"application/json"
],
"outputModes": [
"text/plain",
"application/json"
],
"examples": [
"Sign up to hunter.io with the provided email/password and return the API key from the dashboard",
"Register on apollo.io with the provided credentials, verify the OTP from the inbox, and return the workspace ID",
"Create an account on snov.io using the credentials + IMAP details supplied and return the free-tier API token"
]
},
{
"id": "network_discovery",
"name": "Network endpoint discovery",
"description": "Open ANY URL through a residential browser and deterministically capture its browser network traffic (XHR/fetch), then return a de-duplicated list of the site's API endpoints with freshness signals (ids/timestamps like id/created_at/list_time). No LLM, no per-step billing; credential headers (cookie/authorization) are stripped. Use to reverse-engineer a site's private/data APIs \u2014 often fresher and richer than its public search \u2014 for any dynamic site (marketplaces, classifieds, SPAs). Params: url (required), country, duration_ms (<=90000, default 45000), scroll, reload, match_url (host/substring filter; omit to capture cross-host APIs), freshness_fields[].",
"tags": [
"browser",
"network",
"reverse-api",
"discovery",
"api-scout"
],
"inputModes": [
"text/plain",
"application/json"
],
"outputModes": [
"application/json",
"text/plain"
]
}
],
"additionalInterfaces": [
{
"url": "https://agent.humanbrowser.cloud/a2a",
"transport": "JSONRPC"
}
],
"customActions": [
{
"name": "switch_proxy_country",
"description": "[method: actions/switch_proxy_country] Switch the residential proxy egress country for the live session. Closes Chromium and relaunches it on a new sticky residential IP from the requested country (~5s). When a profile is in use, the chosen country is persisted onto the profile meta and inherited by future spawns. 75 countries are accepted, all residential \u2014 including the full CIS and Caucasus (ru ua by kz md ge am az uz kg), the Baltics (ee lv lt) and the Balkans (rs ba mk al me). Use this when a site geo-blocks (us for DoorDash, uk for BBC iPlayer), when an account is locked to a region (Polymarket -> jp, RU-only services -> ru), or when warming an identity with a stable household IP. Call list_countries first if unsure.",
"params": {
"type": "object",
"properties": {
"country": {
"type": "string",
"enum": [
"us",
"ca",
"mx",
"br",
"ar",
"cl",
"co",
"pe",
"gb",
"uk",
"ie",
"fr",
"de",
"nl",
"be",
"lu",
"es",
"pt",
"it",
"at",
"ch",
"se",
"no",
"dk",
"fi",
"is",
"ro",
"pl",
"cz",
"sk",
"hu",
"bg",
"gr",
"si",
"hr",
"rs",
"ee",
"lv",
"lt",
"ru",
"ua",
"by",
"kz",
"md",
"ge",
"am",
"az",
"uz",
"kg",
"ba",
"mk",
"al",
"me",
"ae",
"sa",
"il",
"tr",
"qa",
"jp",
"kr",
"sg",
"in",
"id",
"ph",
"vn",
"th",
"my",
"tw",
"hk",
"au",
"nz",
"za",
"ng",
"eg",
"ke",
"ma"
],
"description": "ISO-3166-alpha-2 country code (lowercase), e.g. us, gb, de, ru, ua, kz, ge, jp, ph. 75 residential geos incl. the full CIS and Caucasus; call list_countries for the live catalogue with per-country pool health."
}
},
"required": [
"country"
]
}
},
{
"name": "get_cost_snapshot",
"description": "[method: actions/get_cost_snapshot] Return the current accumulated cost breakdown for the live session (llm, bandwidth, browser-minutes, captcha) plus the current token balance. Useful for the caller to decide whether to continue vs abort mid-flow. Zero side effects \u2014 read-only, does not perturb the session.",
"params": {
"type": "object",
"properties": {}
}
},
{
"name": "list_engines",
"description": "[method: actions/list_engines] Return the catalog of execution engines the server can boot, with per-engine cost multiplier, fit-hints (which domain classes each is best for), and known-fail domains. Read-only. Reflects the current auto-router's decision table so the caller can make an informed engine override.",
"params": {
"type": "object",
"properties": {}
}
},
{
"name": "list_countries",
"description": "[method: actions/list_countries] Return the catalog of proxy egress countries the server can route through, including per-country IP pool health (last-hour tunnel-error rate, currently-online sessions, sticky-session TTL). Read-only. Use this to pick a healthy country before switch_proxy_country instead of blindly guessing. The catalogue currently spans 75 countries across the Americas, Europe (incl. the full CIS and Caucasus: ru ua by kz md ge am az uz kg), the Middle East, Asia, Oceania and Africa \u2014 all residential exits.",
"params": {
"type": "object",
"properties": {}
}
},
{
"name": "get_page_diagnostics",
"description": "[method: actions/get_page_diagnostics] Ask why the current page is or is not working. Returns a typed verdict, not a raw log: verdict (ok | degraded | page_did_not_start | broken_by_us), page_state (rendered | stuck_loading | blank), likely_cause as a plain sentence that says whose side the problem is on, plus subresource counts broken down by type and error code with sample URLs, and any console errors. URLs are reduced to origin+path \u2014 query strings are dropped because they carry tokens. Call this the moment a page looks empty, stuck on a spinner, or a click seems to do nothing, INSTEAD of guessing at bot protection: a verdict of broken_by_us means our browser or proxy is at fault and retrying the same way will not help.",
"params": {
"type": "object",
"properties": {
"taskId": {
"type": "string",
"description": "Task whose session to inspect (or pass sessionId)."
},
"sessionId": {
"type": "string",
"description": "Session to inspect, if you are not using a taskId."
},
"sessionKey": {
"type": "string",
"description": "Session password, when addressing by sessionId."
}
}
}
},
{
"name": "get_screenshots",
"description": "[method: actions/get_screenshots] See what the session actually looked like. Returns LINKS to the captured frames \u2014 never base64 image data \u2014 so the payload stays small enough to read: per frame you get the step number, why it was picked, the action that produced it, the page URL (origin+path, query strings dropped because they carry tokens), a one-line summary, and image_url. Also returns live_url (the page right now, works while the task is still running) and video_url (an mp4 assembled on demand from the frames, for a human to watch, not for a model to ingest). mode defaults to 'highlights' \u2014 the handful of frames where something changed: first view, each navigation, form submits, failures, final state. Use mode='index' to locate a specific moment in a long run and mode='both' for both lists; asking for every step on a long run is the wrong move, it returns near-identical images and spends your context. Read-only and observation-only: fetching frames does not refresh session activity and cannot keep an idle session billing. Links carry the session key, so treat them like the viewer URL \u2014 a screenshot can show a typed password or personal data.",
"params": {
"type": "object",
"properties": {
"taskId": {
"type": "string",
"description": "Task whose session to read frames from (or pass sessionId)."
},
"sessionId": {
"type": "string",
"description": "Session to read frames from, if you are not using a taskId."
},
"sessionKey": {
"type": "string",
"description": "Session password, when addressing by sessionId."
},
"mode": {
"type": "string",
"enum": [
"highlights",
"index",
"both"
],
"default": "highlights",
"description": "highlights (default) = only the frames that matter; index = every captured step with its action and url; both = the reel plus the full list."
}
}
}
},
{
"name": "list_models",
"description": "[method: actions/list_models] List the LLM models this deployment will accept on spawn. You normally do NOT need to pick one \u2014 the server routes by goal and site, and a bigger model is not automatically better: measured 2026-08-04, gpt-5.6-sol and the cheap default finished the same hostile-iframe task in the same number of steps. Choose explicitly only when you have a measured reason.",
"params": {
"type": "object",
"properties": {}
},
"returns": {
"type": "object",
"properties": {
"models": {
"type": "array",
"items": {
"type": "string",
"enum": [
"gpt-5.6-luna",
"gpt-5.6-sol",
"gpt-5.6-terra",
"gpt-5.5",
"gpt-5.5-pro",
"gpt-5.4",
"gpt-5.4-mini",
"gpt-5.4-nano",
"gpt-5.4-pro",
"gpt-5.2",
"gpt-5.1",
"gpt-5-pro",
"anthropic/claude-sonnet-4.6",
"anthropic/claude-haiku-4.5",
"anthropic/claude-opus-4.8",
"anthropic/claude-sonnet-4-6",
"anthropic/claude-haiku-4-5",
"anthropic/claude-opus-4-8",
"moonshotai/kimi-k2",
"minimax/minimax-m3",
"minimax/minimax-m2.5"
]
}
}
}
}
},
{
"name": "list_learned_apis",
"method": "actions/list_learned_apis",
"description": "[method: actions/list_learned_apis] Return the internal HTTP/GraphQL API that the server has learned about sites YOUR sessions have driven. While your session uses a site, the traffic its own interface makes is recorded; this gives that surface back as endpoints with method, path, whether they read or mutate, how often each was seen, and request/response shapes. Call this BEFORE planning a long click sequence on a site you have used before: on a heavy admin UI an API read is faster and cannot mis-click onto the wrong record, and it reports state as a value instead of as whatever the page happened to render. Scope is your own account only \u2014 the owner comes from your token and there is no parameter to name another, so another customer's learned APIs are not reachable. No credentials are returned and none are needed: you keep driving your own already-authenticated session, so the call is made as you. Recorded request BODIES are withheld because they contain live identifiers from earlier runs; build calls from the shapes. For anything that mutates, confirm the target by ID and show what you intend to send first \u2014 an API write bypasses every confirmation the UI would have given.",
"params": {
"type": "object",
"properties": {
"domain": {
"type": "string",
"description": "Optional. Restrict to one domain, e.g. 'direct.yandex.ru'. Omit to list everything learned for your account."
}
}
},
"returns": {
"type": "object",
"properties": {
"owner": {
"type": "string",
"description": "The account these APIs were learned for \u2014 always yours."
},
"count": {
"type": "integer"
},
"domains": {
"type": "array",
"description": "Per domain: base_url, confidence, last_updated, reads, writes, and the endpoint list.",
"items": {
"type": "object"
}
}
}
}
},
{
"name": "call_site_api",
"method": "actions/call_site_api",
"description": "[method: actions/call_site_api] Call the site's OWN internal API from inside your live session, instead of clicking its interface. Pair it with list_learned_apis, which tells you what that site's API looks like. Use it to verify what actually happened: after an edit, a read returns the stored value rather than whatever the page re-rendered, which is how 'saved' stops being a claim and becomes a fact. Credentials are never handed to you and never needed \u2014 the request is issued from within your page, so the browser attaches your cookies and the site's CSRF token is read live and used on the spot. Nothing is returned that anyone else could replay. Rules: same-origin only, because the call carries your session's cookies. Anything that mutates is a DRY RUN by default \u2014 you get back exactly what would be sent and must repeat with confirm:true to send it. Confirm the target by ID first: an API write bypasses every confirmation the interface would have shown you. REPLAY without holding a big body: if the request you need is large (a full campaign object, a long GraphQL query) you cannot paste it back \u2014 instead pass from_captured with the operation name you already triggered once in this session, and overrides as a map of dot-path to new value; HB pulls the full request it captured, applies only your overrides, and sends it. For a full-object mutation the captured body auto-serves as the safety baseline, so you cannot accidentally wipe a field you did not touch. Do the operation once (in the UI or via the app) so it is observed, then from_captured lets you repeat it with edits.",
"params": {
"type": "object",
"properties": {
"taskId": {
"type": "string",
"description": "Your task, used to find your session. Or pass sessionId."
},
"sessionId": {
"type": "string",
"description": "Session to call from, if you are not using taskId."
},
"sessionKey": {
"type": "string",
"description": "Session password, when addressing by sessionId."
},
"url": {
"type": "string",
"description": "Absolute URL on the SAME origin the session is on."
},
"method": {
"type": "string",
"description": "GET, POST, ... Defaults to POST."
},
"body": {
"description": "JSON body. For a GraphQL API this is the {operationName, variables, query} object."
},
"confirm": {
"type": "boolean",
"description": "Required to actually SEND a mutating call. Without it you get a dry run."
}
},
"required": [
"url"
]
},
"returns": {
"type": "object",
"properties": {
"status": {
"type": "integer"
},
"json": {
"description": "Parsed response when it is JSON."
},
"csrf_found": {
"type": "boolean",
"description": "Whether a CSRF token was located on the page. Never the token itself."
},
"dry_run": {
"type": "boolean",
"description": "True when nothing was sent because confirm was absent."
}
}
}
},
{
"name": "upload_file",
"method": "actions/upload_file",
"description": "[method: actions/upload_file] Put a file onto a page's file input \u2014 the one thing you cannot do by clicking. Use it to upload an image/creative, a document, an avatar. Pass source (an http(s) URL that HB downloads, or a data: URI / base64 of the bytes) and where to put it (selector of the <input type=file>, or label text near the upload control; omit both to use the page's first file input). HB writes the file into the session and sets it on the input natively, which works even when the input is hidden behind an 'Upload' button. It reports the filename actually attached. If the input only appears AFTER clicking an 'Add/Upload' button, click that first, then upload_file. HB runs on its own machine and cannot see your local disk \u2014 give a URL or base64, not a path. Alternative since 2026-09-07: attach the file to message/send as an A2A FilePart ({kind:file, file:{name, mimeType, uri|bytes}}) and the agent uploads it itself with upload_file; it can also fetch a public URL on its own with fetch_file.",
"params": {
"type": "object",
"properties": {
"taskId": {
"type": "string",
"description": "Your task, used to find your session. Or pass sessionId."
},
"sessionId": {
"type": "string",
"description": "Session to upload into, if not using taskId."
},
"sessionKey": {
"type": "string",
"description": "Session password, when addressing by sessionId."
},
"source": {
"type": "string",
"description": "An http(s) URL HB will download, or a data: URI / base64 of the file bytes."
},
"filename": {
"type": "string",
"description": "Name the site should see, e.g. creative.png. Optional."
},
"selector": {
"type": "string",
"description": "CSS selector of the <input type=file>. Optional."
},
"label": {
"type": "string",
"description": "Text near the upload control, if you have no selector. Optional."
}
},
"required": [
"source"
]
}
},
{
"name": "actions/discover_endpoints",
"description": "Deterministic browser network-endpoint discovery for any URL (see network_discovery skill)."
},
{
"name": "list_remote_providers",
"description": "[method: actions/list_remote_providers] Return the remote browsers this server can rent for engine=remote-cdp, and what distinguishes them. Read-only. Pass the chosen id as remote_provider on spawn; omit it to take the server default. Egress is ours (residential) on every provider. Measured 2026-08-28 on identical targets: browseruse exits residential with a self-consistent fingerprint, browserbase exits from a datacenter, brightdata is a static endpoint with no per-task rental.",
"params": {
"type": "object",
"properties": {}
}
}
]
}