Docs
Errors & statuses
Two kinds of failure. A request Unbrowse refuses is an HTTP error with a code. A run that started but did not succeed is a normal response whose status says so. Check the run's status, not only the HTTP code.
Error shape
Refused REST requests return a JSON body with a stable code and a human message:
{ "error": { "code": "quota_exceeded", "message": "…" } }Branch on code. The message may change and sometimes carries the next step (a top-up link, how long to wait).
A run that fails comes back as a run, with status: "failed" and error: { code, message }:
{ "runId": "lrun_…", "status": "failed", "error": { "code": "rate_limited", "message": "…" }, "signIn": null }POST /api/v1/runs answers 200 for a finished run whether it succeeded or failed, and 202 while it is accepted, working or input_required. A site tool call (POST /api/v1/sites/<host>/call/<tool>) answers 202 for input_required, otherwise 200.
Run statuses
| status | meaning | what to do |
|---|---|---|
accepted, working | Still running. | Poll GET /api/v1/runs/<runId>. |
succeeded | The declared outcome was verified. A 200 from the site is not enough. | Use result. The only status that bills. |
input_required | Waiting on a requirement: a choice, a value, an approval. Not a failure. | Answer the open requirements on the same run: POST /api/v1/runs/<runId>/responses or MCP unbrowse.resume. Don't start a new run. |
failed | Did not succeed. error.code says why (below). | Act on the code. Never billed. |
outcome_unknown | A write may have landed but no answer came back. | Check the site before retrying. Automatic retries of the write stay off. |
cancelled | Stopped at your request. | A request already sent can't be unsent. Known effects are kept. |
Auth and billing
| code | HTTP | when | what to do |
|---|---|---|---|
unauthorized | 401 | No key, or a key Unbrowse doesn't know. | Send Authorization: Bearer ub_live_…. Mint one at MCP & keys. |
revoked | 403 | The key was revoked. | Mint a new key. |
forbidden | 403 | No grant on this workspace, or X-Unbrowse-End-User sent with a key that isn't an org key. | Ask the workspace owner, or use an org key. |
quota_exceeded | 402 | Free calls used up and no paid credits left. Checked before anything is sent. | Buy credits at Billing. Failed calls never bill. |
insufficient_paid_credits | 402 | The call costs more than the paid balance. | Buy credits. |
Request shape
| code | HTTP | when | what to do |
|---|---|---|---|
bad_request | 400 | The body isn't JSON. | Send a JSON object. |
unknown_argument | 422 | The tool doesn't take an argument you sent. The message lists the arguments it does take. | Rename or drop the argument. |
invalid_input | 400 / 422 | A run input matches no slot, or a choice isn't one of the options offered. | Use the names and options from the message. |
capability_not_found | 404 | The capability id you pinned doesn't exist. | Search again, or run by task. |
idempotency_conflict | 409 | The same Idempotency-Key was used with different input. | Use a new key per distinct call. |
not_in_scope | 403 | The connection is in a tool scope (/mcp/<slug>, ?scope=, ?apps=) and the tool or capability is outside it. | Use a connection whose scope includes it, or add the app or tool to the scope in Apps & tools. |
unknown_scope | 404 | No tool scope with that slug in this workspace. | Create it in Apps & tools or POST /api/v1/scopes. |
invalid_scope, too_many_scopes | 400 | A scope's slug isn't 1-40 lowercase letters, digits or dashes, it lists more than 200 apps and tools, or the workspace already has 20 scopes. | Fix the body, or delete a scope. |
not_found | 404 | No such route, site, tool or run. | Check the path. GET /api/v1/sites/<host> lists a site's tools. |
internal | 500 | Something broke on our side. | Retry once. If it repeats, report the runId. |
Capacity and limits
| code | HTTP | when | what to do |
|---|---|---|---|
browser_capacity | 429 | Every cloud browser is busy. | Retry in about 30 seconds, or finish an open browse session. |
too_many_sessions | 429 | Your workspace already has 3 browse sessions open. | Finish or close one. |
index_busy | 429 | An index job is already running for you. | Wait for it: GET /api/v1/index/<id>. |
index_limit | 429 | Daily index-job limit reached (5 a day). | Try again tomorrow. |
slot_unavailable, unavailable | 503 | A server is starting or failing over. | Retry after the Retry-After header (1 to 5 seconds). |
Why a run failed
These arrive as error.code on a run with status: "failed".
| code | when | what to do |
|---|---|---|
no_capability | No tool fits the task yet. result.next.tool is unbrowse.index. | POST /api/v1/index with the URL, then GET /api/v1/index/{jobId}. Done means status: done and indexed > 0. model_unavailable means the indexing model is out of credit, not that the site refused and not that the workspace quota is spent. unbrowse.browse.open is an MCP tool: call it only when it is in this session's tool list. There is no POST /api/v1/browse/open, and the CLI does not open a browser. |
session_or_permission | The site answered 401/403 or showed a sign-in page. The run carries signIn.url when you have no login saved. | Open signIn.url in the person's browser, save the login, then call again. Unbrowse signs in by itself from then on. |
mfa_required | The site asked for a second factor. Also carries signIn. | Same as above. Save a TOTP seed with the login to make it unattended. |
rate_limited | The site returned 429. The wait is in the message (the site's Retry-After, else 60 seconds). | Wait that long, then retry. |
challenge | A bot check appeared in an unattended run. | Run it again as interactive. |
challenge_budget_exhausted | Bot checks kept coming back. | Retry later. |
schema_or_auth_drift | The site returned a page where it used to return data: it changed, or the session is gone. | Browse the flow once more so Unbrowse re-learns it. |
outcome_not_verified | The site answered, but with an empty page or without the fields the tool promises. | Check your inputs. Don't treat it as success. |
not_found | The site returned 404 or 410 for these inputs. | Fix the inputs. |
upstream_error, network_error | The site erred or couldn't be reached. | Retry later. |
browser_capacity, render_failed | The page needed a browser and none could render it. | Retry in about 30 seconds. |
destination_denied | The URL is private, loopback, or outside the tool's site. | Don't retry. |
policy_denied | The task or page tried to move a secret somewhere it shouldn't go. | Don't retry. |
declined | A required field was declined. | Start a new run. |
worker_lost, resource_locked | The worker stopped, or another agent holds the same resource. | Retry. |
Answering an input_required run
| code | HTTP | when | what to do |
|---|---|---|---|
conflict | 409 | The run isn't waiting for input. | GET the run and act on its status. |
stale_revision | 409 | The run changed since you read it. | GET it again and send the current stateRevision. |
stale_requirement, expired_requirement | 409 | That requirement was replaced or expired (after 15 minutes). | GET the run and answer its current requirements. |
unknown_requirement, invalid_option, invalid_answer | 422 | The answer names no open requirement, or the value isn't one of the options. | Answer from the run's requirements. |
Logins
| code | HTTP | when | what to do |
|---|---|---|---|
credential_required | 412 | A browse autofill found no saved login for the site. The error carries a one-time url. | Open the url, save the login, repeat the action. The agent never sees the value. |
no_login_form | 422 | No login fields are visible on the page. | Go to the sign-in page first. |
request_closed | 409 | The one-time link was already used, declined, or expired (after 30 minutes). | Ask for a new link. |
Browse sessions
| code | HTTP | when | what to do |
|---|---|---|---|
session_expired | 410 | The session ended: idle for more than 5 minutes, finished, or the host restarted. | Open a new session. What it learned is kept. |
stale_ref | 409 | The @ref isn't in the latest snapshot. | Take a new snapshot and use its refs. |
site_unreachable, page_unresponsive | 504 | The page didn't load in 30 seconds, or the snapshot took longer than 15. | Wait, then retry. |
egress_unavailable | 502 | The residential network refused the connection from three IPs in a row. | Retry in a minute, or pass another country. |
blocked | 502 | A bot check or an empty page over both HTTP and the browser. | Don't retry soon. |
MCP
Tool failures come back as JSON-RPC errors. The string code is in error.data.code, the same code as in REST. The numeric code follows the HTTP status: 401 → -32001, 403 → -32003, 404 → -32004, anything else → -32000. On MCP, error.data.details also carries fields like retryAfter.
A run with status: "failed" is not a JSON-RPC error. It is a normal result with isError: true.
`-32042` (URL elicitation). If your client declares capabilities.elicitation.url at initialize, a missing login or an empty balance comes back as -32042, with data.elicitations[0].url. Show that link to the person. When they finish, call again. Clients without URL elicitation get the plain error, or a run result carrying signIn.
An unauthenticated request to /mcp gets 401 with a WWW-Authenticate header that points to OAuth discovery. OAuth clients sign in from there. Other clients send Authorization: Bearer ub_live_….