Errors

When a request to the Browser Agent API fails (/api/v1/responses, /api/v1/runs, wrappers), you get a nested error object. Branch your handling on error.code — it is stable; message text is not.

Error envelope

Browser Agent API error envelope. Branch on error.code (stable); the full code table with remediations lives at /docs/api/errors.

FieldTypeReqDescription
errorobject
error.messagestringyes
error.typestringyes
error.codestringyes
error.paramstring
error.doc_urlstring
error.hintstring
error.remediationstringShort next-step guidance for agents.
error.claim_urlstringPresent on account_unclaimed — open it to claim the account.

Generated from OpenAPI. Do not hand-maintain this table.

Browser Agent API shape

{
  "error": {
    "message": "Missing or invalid Banana Peel API key. …",
    "type": "authentication_error",
    "code": "invalid_api_key",
    "param": null,
    "doc_url": "https://bananapeel.com/docs/agents",
    "hint": "export BANANA_PEEL_API_KEY=bp_live_…",
    "remediation": "Run `npx -y @banana-peel/cli init --agent --json` to mint a key, or use the HTTP fallback …"
  }
}
  • code — stable machine id (full table below)
  • hint — present when a short recovery tip helps (auth, routing, credits)
  • remediation — next-step guidance for agents; present for every code in the tables below
  • doc_url — points at /docs/agents
  • param — JSON field to fix when relevant, else null

Request error codes

Returned as the envelope above with a non-2xx HTTP status. Validation failures fail free — no runner was attempted and nothing was billed. 429s carry a Retry-After header.

CodeHTTPWhenWhat to do
invalid_api_key401Missing or invalid `Authorization: Bearer bp_live_…` / `x-api-key`.Run `npx -y @banana-peel/cli init --agent --json` to mint a key, or if npx is unavailable use the supported HTTP fallback: GET /api/v1/agent-signup/challenge → solve the instruction → POST /api/v1/agent-signup with {challenge_id, challenge_answer} (see /docs/agents/signup). Then pass Authorization: Bearer bp_live_…
account_unclaimed403Agent account froze after the trial window without a human claim.Open the claim_url, sign in, then keep using the same stored API key. Do not paste the key into chat.
invalid_json400Body is not parseable JSON.Send a JSON body with Content-Type: application/json.
payload_too_large413Request body exceeds the 1 MB create-body limit.Send a JSON body of at most 1 MB (live-run, Responses, Messages, session submit). Nothing was created or billed.
unsafe_url400Target URL failed the public-https / SSRF checks (hostname, DNS, or redirect).Provide a public http(s) URL. Private, link-local, metadata, and names that resolve to those addresses (including nip.io / sslip.io) are rejected on every path.
missing_input400Neither `input` nor `url` was provided.Provide input (task text or {url, goal}) and/or url.
missing_url400routing: "learning" without a resolvable target URL.Learning mode probes the runner pool against a specific domain — pass `url` (or include an https:// URL in the task text). Nothing was billed.
invalid_routing400Malformed `routing` value.Use routing: smart | named runner | SOC2|HIPAA|EU | [runner, …].
missing_output_schema400normalize enabled without a schema.Set output_schema (or text.format / response_format) when normalize is enabled. Build one in Console → Normalize.
invalid_output_schema400`output_schema` is not a usable JSON Schema / property map.Provide a JSON Schema object (with `properties`) or a property map like { "order_id": { "type": "string" } }. Build one in Console → Normalize; nothing was billed.
invalid_max_cost_usd400`max_cost_usd` is not a positive number.Pass max_cost_usd as a positive USD number (e.g. 2.50) — the per-run spend ceiling. Nothing was billed.
invalid_max_fallbacks400`max_fallbacks` is not an integer in 0–12.Pass max_fallbacks as an integer from 0 (first pick only) to 12. Default is 5 additional runners after the primary. Nothing was billed.
invalid_list_filter400A list query parameter is invalid: unknown `status` or `routing` filter value, or a `created_after` / `created_before` timestamp that is not ISO 8601.On GET /api/v1/runs use status=running|needs_human|succeeded|failed|blocked|empty|cancelled, routing=smart|learning|learning-all|category|runner|chain, and ISO-8601 timestamps (e.g. 2026-08-01T00:00:00Z) for created_after / created_before. Nothing was billed.
idempotency_key_mismatch400The `Idempotency-Key` header and the body `idempotency_key` are both set but differ.Send the same key in both places, or only one of them (the header is the conventional choice). Nothing was created or billed.
invalid_idempotency_key400Idempotency key longer than 255 characters.Use a key of at most 255 characters (a UUID is ideal). Nothing was created or billed.
idempotency_conflict422Idempotency key reused with a DIFFERENT request body within its 24h window.A key replays its original run only for an identical request — this usually means a retry loop is mutating its payload. Use a fresh key for the new request, or resend the exact original body to replay. Nothing was created or billed.
idempotency_in_flight409Duplicate create with the same idempotency key while the original create is still being admitted.Honor Retry-After (~1s) and resend the identical request — you will receive the original run once it exists. No duplicate was created and nothing was billed.
invalid_credential400Credential create/update body failed validation.Provide domain, username, and password as strings within the documented length caps — see /docs/api/credentials.
credential_not_found404`credential_id` does not exist in this workspace.Use a vault credential id owned by this API key — list yours with GET /api/v1/credentials or create one with POST /api/v1/credentials (also Console → Vault).
invalid_custody400Malformed `custody` policy string.Use custody: any | trusted_runners | deck_only | pinned:<runner>[,<runner>…].
custody_unsatisfiable409Credential custody policy and requested routing share no runner.The credential custody policy and the requested routing have no runner in common. Loosen the policy (Console → Vault or PATCH /api/v1/credentials/{id}) or route within the allowed set; nothing was billed.
insufficient_credits402Workspace balance cannot cover the run.Top up in Console → Billing (face-value credits).
response_not_found404Unknown response id for this workspace.Poll the id returned from create; ids are per-workspace.
explanation_not_found404No routing decision card exists for this run.Decision cards are recorded for routing: "smart" runs (the default). Pinned runners, chains, and races have no routing decision to explain.
not_waiting_for_input409POST …/input while the run is not waiting on a human answer.Poll GET /api/v1/responses/{id} until status is requires_action, then POST {"answer": "…"} to /api/v1/responses/{id}/input.
artifact_expired410Artifact bytes aged out of the retention window.Copied screenshots and artifacts are retained for the workspace window (default 7 days). Re-run the task if you still need the files.
runner_unavailable503Pinned runner (or every runner in a chain) is unavailable.Pin another runner or use routing: smart.
saturated429Per-instance live-run concurrency cap reached.Honor Retry-After and reduce concurrency.
learning_capacity429Learning fan-out capacity saturated on this instance.Learning fan-out capacity on this instance is saturated. Honor Retry-After, or stagger learning creates; smart creates are unaffected.
rate_limited429Too many requests — including more concurrent Learning-family runs (`routing: "learning"` + `"learning-all"`, one shared cap) than the per-account limit (default 3).Honor Retry-After and back off. For Learning-family creates ("learning" / "learning-all"), wait for an in-flight Learning run to finish (poll GET /v1/responses/{id}) or cancel one before starting another.
enterprise_required403Custom retention on a non-Enterprise workspace.Custom retention is Enterprise-only. See /docs/security/retention or contact sales.
invalid_retention400Retention value not one of the self-serve windows.Use retention "24h", "7d", or "30d" on an Enterprise workspace.
retention_not_self_serve400Requested window is outside the self-serve set.Self-serve Enterprise retention is "24h", "7d", or "30d". Zero-retention and longer windows are negotiated contract terms — contact sales.
feature_disabled403The requested feature is disabled on this deployment. Currently: multi-entity long-task orchestration (POST /api/v1/long-tasks) — every task runs as a single run.Submit each task as its own run via POST /api/v1/responses — smart routing treats every task the same. Existing lt_… objects stay readable (GET /api/v1/long-tasks, …/{id}, …/{id}/runs). Nothing was created or billed.
long_task_not_found404Unknown long-task id for this workspace.Use the lt_… id returned from POST /api/v1/long-tasks; ids are per-workspace. List active ones with GET /api/v1/long-tasks.
not_multi_entity400The task does not look multi-entity, so it was rejected by the long-task submit gate.Single-shot tasks belong on POST /api/v1/responses (zero orchestration overhead). If you KNOW the job covers several entities, pass `expected_entities` (2–500). Nothing was billed.
invalid_expected_entities400`expected_entities` is outside the accepted range.Pass an integer between 2 and 500 — the number of entities (accounts, properties, …) you expect the job to cover, or omit it to let detection decide. Nothing was billed.
webhook_not_found404Unknown webhook id for this workspace.Use a webhook id owned by this API key — list yours with GET /api/v1/webhooks.
invalid_webhook_url400Endpoint URL failed the public-https / SSRF checks.Provide a public https URL. Private, metadata, .internal, and localhost/loopback hosts are rejected in production (http://localhost is allowed only in local development).
invalid_webhook_event400`events` contains an unknown event type.events must be a subset of response.completed, response.failed, response.requires_action, response.cancelled.
invalid_webhook_secret400Caller-supplied `secret` is too short.Pass secret as a string of at least 16 characters, or omit it and let Banana Peel mint a whsec_… secret (returned once).
webhook_limit400Account already has the maximum number of webhook endpoints.Each account can register at most 25 webhook endpoints. Delete unused endpoints (DELETE /api/v1/webhooks/{id}) or update one in place with PATCH.
webhook_delivery_not_found404Unknown delivery id for this endpoint.Use a delivery id from GET /api/v1/webhooks/{id}/deliveries for this endpoint.

Run outcomes vs transport errors

Not every failure is an HTTP error. A run that executed but didn't accomplish the task returns HTTP 200 (or 502) with status: "failed" on the response object — read error / banana_peel.error on that object, not the request-error envelope above. Always check status, not just the HTTP code. Terminal error.code values on the response object:

CodeHTTPWhenWhat to do
run_timeoutRun exceeded its active time contract: an explicit `time_budget_ms` plus grace, the legacy pinned/chain TTL, a dead-executor watchdog, or the absolute safety ceiling. See `banana_peel.time_budget_ms` for the contract that governed this run.Smart/category/learning runs without time_budget_ms are UNCAPPED — only a dead executor (stale progress) or the 24h safety ceiling stops them; pinned/chain runs keep the legacy hard TTL (default 20 min). The upstream runner hung or the background worker was interrupted. Retry the task; only attempts that actually executed were billed.
budget_exhaustedRun stopped at its spend ceiling (`max_cost_usd`, the $20 Learning default, or the routing plan budget).Attempts that executed are billed and their partial results are under banana_peel.routing_attempts. Raise max_cost_usd (or narrow the task) and retry if you want more attempts.
run_cancelledRun was cancelled via POST /api/v1/responses/{id}/cancel.Attempts that already executed are billed; nothing was dispatched after the cancel.
blockedTarget site blocked or challenged every attempt.Retry with routing: smart or a stealth/residential runner (steel, hyperbrowser, browserbase), or answer MFA via POST …/input.
run_failedRun executed but did not accomplish the task.Inspect banana_peel.screenshot / session.live_url, refine the task, and retry — routing: smart avoids the failing runner next time.
schema_violationRun finished but its output did not satisfy the supplied `output_schema` (missing/empty required keys, wrong types, or undeclared extra keys). The run is failed — never billed-and-claimed-successful on drifted keys.The nonconforming output is kept under banana_peel.output for inspection and the violations are listed in error.message. Attempts that executed are billed; the output-normalization add-on is not. Loosen the schema (mark fields optional / allow additionalProperties), improve the task instruction, or retry.

Agent signup fallback codes

GET /api/v1/agent-signup/challenge and POST /api/v1/agent-signup (see agent signup) use the same envelope with these codes, plus rate_limited (429) from the table above:

CodeHTTPWhenWhat to do
missing_user_agent400Challenge request without a User-Agent header.Send a User-Agent (bananapeel-cli/…, Cursor, Claude Code, Codex, or any HTTP client).
generic_user_agent400Signup from a generic client UA (curl, python-requests, …).Credit-granting signup requires a non-generic User-Agent (bananapeel-cli/…, Cursor, Claude Code, Codex). Set one on the signup POST.
missing_challenge400Signup POST without challenge_id / challenge_answer.Provide challenge_id and challenge_answer from GET /api/v1/agent-signup/challenge.
invalid_challenge400Challenge unknown, expired, already used, or bound to another IP.Request a fresh challenge (TTL is two minutes, one use, IP-bound) and retry the signup from the same client.
wrong_challenge_answer400challenge_answer did not solve the instruction.Re-read the challenge instruction and recompute the answer exactly, or request a new challenge.

Console billing endpoints

The signed-in console billing endpoints (POST /api/billing/checkout, POST /api/billing/portal) return the same error envelope: auth_required (401, sign in), invalid_credits_usd (400, top-up amount out of range), stripe_error (502, upstream Stripe failure — retry or contact support), internal_error (500).

Browserbase-compatible sessions

{ "statusCode": 401, "error": "Unauthorized", "message": "Unauthorized" }

/api/v1/sessions mirrors Browserbase's { statusCode, error, message } wire shape so the official SDK builds thrown errors from message as usual.

See also

Command Palette

Search for a command to run...