{"openapi":"3.1.0","info":{"title":"Banana Peel Browser Agent API","version":"2026-08-13","summary":"Smart routing for browser agents — OpenAI-compatible Responses API.","description":"OpenAI Responses–compatible API for browser agents with smart routing. OpenAI SDK drop-in: baseURL `{base}/api/v1`, model `banana-peel`, Authorization Bearer. Send a task with `routing` (smart = production; learning / learning-all = teach the router the best path — calibrate the brain so later smart runs pick the winner; named runner / category / list; defaults to smart), and Banana Peel executes on the best available runner (Browserbase, Steel, Browser Use, Skyvern, Hyperbrowser, Playwright, Deck — 250+ executable runner combinations across 50+ single runners — hosted agents, infrastructure providers, extraction engines and OSS frameworks). Thin Anthropic Messages adapter at POST /v1/messages. Category values SOC2/HIPAA/EU select MVP fixed runner pools (not full compliance attestation). Provider SDK wrappers (Browserbase/Steel/…) are separate from the OpenAI Responses surface.\n\nBase URL: https://bananapeel.com\nOpenAI compat docs: https://bananapeel.com/docs/api/openai\nAnthropic adapter docs: https://bananapeel.com/docs/api/anthropic\nAgent guide (golden path): https://bananapeel.com/docs/agents\nAgent-native signup: https://bananapeel.com/docs/agents/signup — `npx -y @banana-peel/cli init --agent --json`. Do not send agents to /keys.\nMCP server: https://bananapeel.com/docs/mcp\nllms.txt: https://bananapeel.com/llms.txt\nFull agent guide: https://bananapeel.com/llms-full.txt","contact":{"name":"Banana Peel","url":"https://bananapeel.com"},"termsOfService":"https://bananapeel.com/terms"},"servers":[{"url":"https://bananapeel.com/api","description":"Production (set BANANA_PEEL_BASE to override)"},{"url":"/api","description":"Same-origin relative (local dev / custom host)"},{"url":"https://bananapeel.com","description":"Bare-origin alias: /v1/* and /v2/* rewrite to /api/v1/* and /api/v2/* (so SDKs configured with the origin as baseURL still reach the API — e.g. /v1/sessions ≡ /api/v1/sessions). Always answers JSON, never HTML."}],"externalDocs":{"description":"For AI agents — integration guide","url":"https://bananapeel.com/docs/agents"},"tags":[{"name":"Browser Agent API","description":"Create and inspect browser-agent runs"},{"name":"Routing","description":"Smart routing, named runners, and categories — `routing` selects; `banana_peel.runner` reports who executed"},{"name":"Vault","description":"Credential vault management — create/list/update/revoke login credentials for use on runs via `credential_id`. Secret material is write-only: no endpoint ever returns it."},{"name":"Webhooks","description":"Outbound signed HTTP callbacks for Responses. At-least-once delivery; dedupe on event.id. Dual-signature 24h rotation grace. Distinct from Stripe inbound billing webhooks. See /docs/api/webhooks."},{"name":"Agent signup","description":"Anonymous agent free-account creation + claim-later"},{"name":"Wrappers","description":"Drop-in compatibility for provider SDKs"},{"name":"Platform","description":"Public platform health — ratios and timestamps only, never absolute volume"}],"paths":{"/v1/agent-signup/challenge":{"get":{"tags":["Agent signup"],"operationId":"agentSignupChallenge","summary":"Machine-solvable signup challenge (not a human CAPTCHA)","description":"Short-TTL (2 min), one-use, IP-bound puzzle. Solve `puzzle` then POST /v1/agent-signup. See https://bananapeel.com/docs/agents/signup.","responses":{"200":{"description":"challenge_id, instruction, puzzle, expires_in_seconds"},"429":{"description":"Rate limited"}}}},"/v1/agent-signup":{"post":{"tags":["Agent signup"],"operationId":"agentSignup","summary":"Create an anonymous free Banana Peel account for an agent","description":"Prefer `npx -y @banana-peel/cli init --agent --json` (stores the key locally; never paste it into chat). This endpoint is what the CLI calls. Requires a solved challenge. Creates the same free account a Console signup would (welcome credits, API key). api_key is returned once. Store it locally; never put it in LLM context. Share claim_url with the human. Do not send agents to /keys. Repeat signups from the same IP reuse the existing unclaimed account instead of granting another $10.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["challenge_id","challenge_answer"],"properties":{"challenge_id":{"type":"string"},"challenge_answer":{"type":"string"},"client":{"type":"string","enum":["cli","cursor","claude-code","codex","vscode","windsurf","claude-desktop"]},"email":{"type":"string","description":"Optional. Default agent_*@agents.bananapeel.com handle."}}}}}},"responses":{"200":{"description":"status=ready, account_id, api_key (once), claim_url, free_credit_usd, expires_at"},"400":{"description":"Missing/expired/wrong challenge"},"429":{"description":"Rate limited / welcome already granted to this IP in 24h (unclaimed accounts are reused, not 429)"}}}},"/v1/estimate":{"post":{"tags":["Browser Agent API"],"operationId":"estimateResponse","summary":"Dry-run cost estimate (no browser run)","description":"Resolve likely runner + catalog cost estimate. Optional normalize add-on (+$0.10) included when requested. Does not start a run. The response `budget` section reflects the spend ceiling the run would be dispatched under (`max_cost_usd` from the request, the $20 Learning default, or the routing plan budget).","security":[{"bearerAuth":[]},{"apiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateResponse"}}}},"responses":{"200":{"description":"Catalog estimate"},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/responses":{"post":{"tags":["Browser Agent API"],"operationId":"createResponse","summary":"Create a browser-agent response","description":"Create a browser-agent run. OpenAI Responses–compatible shape with Banana Peel details under `banana_peel`. `routing` defaults to `smart` when omitted. Provide `input` and/or `url`. `stream: true` returns OpenAI-style SSE. `background: true` (or Prefer: respond-async) returns 202 + poll (or register POST /v1/webhooks and receive signed events — prefer webhooks at production scale). `routing: \"learning\"` and `routing: \"learning-all\"` exist to teach the router the best path (calibrate the brain on this domain/task so later `smart` runs pick the winner) — they always return 202 and run in the background; Learning fan-out execution has a per-instance concurrency ceiling — saturated creates are refused with 429 `learning_capacity` + `Retry-After` (nothing is created or billed) — and each account may run a limited number of Learning-family runs concurrently (default 3, shared across both modes); creates beyond that are refused with 429 `rate_limited` + `Retry-After` (nothing is created or billed). `learning-all` sweeps the full catalog (250+ options) with no early stop — every dispatchable option attempts exactly once (catalog-only coming-soon options are skipped, never billed); every attempt bills (most expensive mode). Every run honors a spend ceiling: `max_cost_usd` (any mode), the $20 Learning default, or the $20 learning-all default — once executed attempts reach it, no NEW attempts are dispatched (in-flight ones finish and bill) and the run ends with error code `budget_exhausted` and partial results under `banana_peel.routing_attempts`. Run TIME is caller-controlled: optional `time_budget_ms` caps smart/learning-family runs; omitted = UNCAPPED (no run-level deadline — the task runs to completion; only true-hang protection stops it; prefer `background: true`). Optional `normalize` + `output_schema` (or OpenAI `text.format` / `response_format`) reshapes output (+$0.10 flat, charged only when the pass executes; the outcome is echoed under `banana_peel.normalization`). A supplied `output_schema` is a hard contract: success populates exactly those keys or the run fails with error code `schema_violation` (add-on not billed). Without a schema, `banana_peel.answer` / `answer_present` / `evidence` is the guaranteed stable read path. Credential custody is enforced: a vault `credential_id` applies its stored custody policy, and the optional `custody` field applies one to inline credentials — the runner candidate set is hard-filtered BEFORE routing, an unsatisfiable policy fails with 409 `custody_unsatisfiable` (nothing billed), and enforcement is echoed under `banana_peel.custody`. Idempotent retries: send an `Idempotency-Key` header (or body `idempotency_key`) — reuse within 24h with an identical body replays the original run (`Idempotent-Replayed: true`, original `created_at`); a different body is refused with 422 `idempotency_conflict`; a duplicate racing the original create gets 409 `idempotency_in_flight` + Retry-After. Runs debit credits: runner listed price, plus a 5% platform fee after your first top-up (0% while on trial credits).","security":[{"bearerAuth":[]},{"apiKey":[]}],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Client retry key (Stripe / IETF idempotency-key style; header names are case-insensitive). Equivalent to the body `idempotency_key` — if both are sent they must match (400 `idempotency_key_mismatch` otherwise). Keys are scoped to the workspace, shared across every run-creating endpoint (/v1/responses, /v1/messages, wrapper creates), and expire 24 hours after first use. Reuse with an IDENTICAL body (delivery-mode fields `stream`/`background` excluded) replays the original run — original `created_at`, `Idempotent-Replayed: true` response header, 200 when terminal / 202 while running. Reuse with a DIFFERENT body is refused with 422 `idempotency_conflict`. A duplicate sent while the original create is still being admitted is refused with 409 `idempotency_in_flight` + `Retry-After: 1` — retry to receive the original run. A create refused before its run exists (validation, credits, custody, capacity) does not consume the key."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateResponse"},"examples":{"minimal":{"summary":"Minimal create","value":{"model":"banana-peel","input":"Extract the title from https://example.com","routing":"smart"}},"with_normalize":{"summary":"Output normalization (+$0.10)","value":{"model":"banana-peel","input":"Extract the latest order from https://example.com/orders","routing":"smart","normalize":true,"output_schema":{"type":"object","properties":{"order_id":{"type":"string"},"total":{"type":"string"}},"required":["order_id","total"]}}}}}}},"responses":{"200":{"description":"Completed / failed / blocked JSON, or text/event-stream when stream:true","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResponseObject"}},"text/event-stream":{"schema":{"type":"string","description":"OpenAI Responses SSE events"}}}},"202":{"description":"Queued / still running (background)"},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"Insufficient credits","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflict — nothing was created and nothing was billed. Error code `custody_unsatisfiable`: the credential custody policy and the requested routing have no runner in common (loosen the policy in Console → Vault or PATCH /v1/credentials/{id}, or route within the allowed set). Error code `idempotency_in_flight`: a request with the same Idempotency-Key is still being admitted — honor `Retry-After` (~1s) and resend the identical request to receive the original run.","headers":{"Retry-After":{"description":"Present on `idempotency_in_flight`: seconds to wait before resending the identical request.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Idempotency conflict (error code `idempotency_conflict`): the Idempotency-Key was already used with DIFFERENT request parameters within its 24h window — almost always a client bug (a retry loop mutating its payload). Use a fresh key for the new request, or resend the exact original body to replay the original run. Nothing was created and nothing was billed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Backpressure — no run was created and nothing was billed; honor the `Retry-After` header (seconds) and retry. Error code `rate_limited`: this account already has the maximum concurrent Learning-family runs in flight (`\"learning\"` + `\"learning-all\"`, default 3). Error code `learning_capacity`: Learning fan-out execution capacity is saturated on this instance.","headers":{"Retry-After":{"description":"Seconds to wait before retrying the create.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"Run failed at transport layer"}}},"get":{"tags":["Browser Agent API"],"operationId":"listResponses","summary":"List recent responses","description":"OpenAI-shaped list of recent Responses for this workspace (same underlying runs as GET /v1/runs).","security":[{"bearerAuth":[]},{"apiKey":[]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","default":20,"minimum":1,"maximum":100}}],"responses":{"200":{"description":"List object with data[] of ResponseObject"},"401":{"description":"Invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/messages":{"post":{"tags":["Browser Agent API"],"operationId":"createAnthropicMessage","summary":"Anthropic Messages adapter (thin)","description":"Maps Anthropic Messages create onto the Responses browser-agent pipeline. Point Anthropic SDK baseURL at `{base}/api` with x-api-key bp_live_…. Honors the `Idempotency-Key` header / body `idempotency_key` with the same key space and replay / 422-conflict / 409-in-flight semantics as POST /v1/responses (errors here are Anthropic-shaped). Not a full Anthropic API. See /docs/api/anthropic.","security":[{"bearerAuth":[]},{"apiKey":[]}],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Client retry key — same key space and semantics as POST /v1/responses: identical-body reuse within 24h replays the original run (`Idempotent-Replayed: true`, original `created_at`); different body → 422 `idempotency_conflict`; duplicate racing the original create → 409 `idempotency_in_flight` + Retry-After. If the body also carries `idempotency_key`, the values must match (400 `idempotency_key_mismatch`)."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"model":{"type":"string","default":"banana-peel"},"max_tokens":{"type":"integer"},"system":{},"messages":{"type":"array"},"routing":{"description":"Banana Peel routing (default smart)"},"background":{"type":"boolean"},"normalize":{"type":"boolean"},"output_schema":{"type":"object","additionalProperties":true}}},"example":{"model":"banana-peel","max_tokens":1024,"messages":[{"role":"user","content":"Extract the title from https://example.com"}],"routing":"smart"}}}},"responses":{"200":{"description":"Anthropic-shaped message (+ banana_peel, response_id)"},"202":{"description":"Background / in progress"},"400":{"description":"Invalid request (Anthropic-shaped envelope). Includes `idempotency_key_mismatch` when the Idempotency-Key header and body `idempotency_key` are both set but differ."},"401":{"description":"Invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"In-flight idempotency duplicate (code `idempotency_in_flight`, Anthropic-shaped envelope): a request with the same Idempotency-Key is still being admitted. Honor `Retry-After` (~1s) and resend the identical request to receive the original run. Nothing was created or billed.","headers":{"Retry-After":{"description":"Seconds to wait before resending the identical request.","schema":{"type":"integer"}}}},"422":{"description":"Idempotency conflict (code `idempotency_conflict`, Anthropic-shaped envelope): the Idempotency-Key was already used with different request parameters within its 24h window. Use a fresh key or resend the exact original body. Nothing was created or billed."},"429":{"description":"Backpressure — same limits as POST /v1/responses (per-account Learning-family concurrency cap `rate_limited`, default 3; per-instance Learning fan-out ceiling `learning_capacity`). Nothing was created or billed. The envelope is Anthropic-shaped (`{type:\"error\",error:{type:\"rate_limit_error\",message,code}}`) and always carries the `Retry-After` header (seconds) — honor it and back off.","headers":{"Retry-After":{"description":"Seconds to wait before retrying the create.","schema":{"type":"integer"}}}}}}},"/v1/responses/{id}":{"get":{"tags":["Browser Agent API"],"operationId":"getResponse","summary":"Get a response by id","description":"Poll until terminal. When a target site asks for MFA/2FA/OTP, the run pauses immediately (`status: requires_action`) — it does not keep trying other runners. `required_action.type = submit_input` with kind, prompt, channel (totp|sms|email|unknown), message, and submit {method, path}. Captcha / bot-detection gates never pause — they solve, re-route, or end `blocked` (reason bot_challenge). Submit the code via POST /v1/responses/{id}/input `{ \"code\": \"123456\" }`. At production scale prefer POST /v1/webhooks (`response.requires_action` is time-sensitive) instead of spinning GET.","security":[{"bearerAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResponseObject"}}}},"401":{"description":"Invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Browser Agent API"],"operationId":"deleteResponse","summary":"Delete a response and purge copied artifacts","description":"Immediately deletes copied screenshots and artifact blobs for this run and tombstones the response. Subsequent GET returns 404. Unknown ids and other-workspace ids are also 404. Copied documents otherwise expire after 7 days (Enterprise: 24h / 7d / 30d). Provider-hosted session replays are not copied — see /docs/security/retention.","security":[{"bearerAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"object":{"type":"string","const":"response"},"deleted":{"type":"boolean","const":true}}}}}},"401":{"description":"Invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found (unknown, already deleted, or another workspace)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/responses/{id}/artifacts/{artifactId}":{"get":{"tags":["Browser Agent API"],"operationId":"getResponseArtifact","summary":"Download a captured run artifact","description":"Retrieve the bytes of a file the run captured (downloaded files, screenshots). Responds 302 with a short-lived signed URL (~10 min TTL) for cloud-stored artifacts, or streams the bytes directly. Artifact ids are listed under `banana_peel.artifacts[].id`; the `url` on each artifact already points here. Scoped to the API key's workspace — other-workspace and unknown ids return 404. Artifacts follow the run retention window (410 after expiry) and are purged by DELETE /v1/responses/{id}.","security":[{"bearerAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"artifactId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Artifact bytes (Content-Disposition: attachment)","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"302":{"description":"Redirect to a short-lived signed URL"},"401":{"description":"Invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown response/artifact id, another workspace, or bytes not captured (see error.message)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"Artifact expired per the retention window","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/responses/{id}/cancel":{"post":{"tags":["Browser Agent API"],"operationId":"cancelResponse","summary":"Cancel an in-flight response","description":"OpenAI-compatible cancel: returns the response object with `status: \"cancelled\"`. Stops dispatching new runner attempts / Learning batches immediately; runner attempts that already executed still bill (every-attempt billing), nothing new bills after the cancel. Where the upstream adapter supports it (e.g. Skyvern) the in-flight attempt is also aborted best-effort. Cancelling an already-terminal response is a no-op that returns the object unchanged.","security":[{"bearerAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Response object (status: cancelled, or unchanged if already terminal)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResponseObject"}}}},"401":{"description":"Invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/long-tasks":{"post":{"tags":["Browser Agent API"],"operationId":"createLongTask","summary":"Create a multi-entity long task (enumerate → fan-out → reconcile)","description":"Orchestrates jobs that span MANY entities behind one login (e.g. \"download the latest bill for ALL properties\"). The platform first enumerates the entities (grader-verified manifest), then fans out one idempotent sub-task per entity (each an ordinary response run, visible in your runs list with a parent_id), and reconciles per-entity outcomes with retries, forced runner rotation, and credential-safe login pacing. Always async: 202 + poll GET /v1/long-tasks/{id} or subscribe to long_task.* webhook events. Single-shot tasks are rejected with 400 `not_multi_entity` — use POST /v1/responses for those (zero orchestration overhead). Terminal statuses: complete | partial_complete (first-class, with an honest per-entity gap list — never a silent failure) | failed | cancelled. MFA pauses the task (`paused_interaction`); answer on the holding sub-task run via POST /v1/responses/{run_id}/input and the task resumes automatically.","security":[{"bearerAuth":[]},{"apiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LongTaskCreate"},"example":{"input":"Download the most recent bill PDF for ALL properties on this account.","url":"https://myaccount.example-utility.com","credential_id":"cred_abc123"}}}},"responses":{"202":{"description":"Long task accepted (status: enumerating). Poll or use webhooks.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LongTaskObject"}}}},"400":{"description":"Invalid request, or not multi-entity (use /v1/responses)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"tags":["Browser Agent API"],"operationId":"listLongTasks","summary":"List long tasks","description":"Newest first. No cursor or filters yet (unlike GET /v1/runs) — limit only.","security":[{"bearerAuth":[]},{"apiKey":[]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","default":20,"minimum":1,"maximum":100}}],"responses":{"200":{"description":"List of long task objects (newest first)","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","example":"list"},"data":{"type":"array","items":{"$ref":"#/components/schemas/LongTaskObject"}},"first_id":{"type":"string","nullable":true},"last_id":{"type":"string","nullable":true},"has_more":{"type":"boolean"}}}}}}}}},"/v1/long-tasks/{id}":{"get":{"tags":["Browser Agent API"],"operationId":"getLongTask","summary":"Long task progress: per-entity table, counts, gaps, spend","description":"The progress API. `entities[]` carries per-entity status (pending | dispatched | delivered | unfetchable | exhausted), attempts, the sub-task run id, artifact id when delivered, and the grader-verified reason when unfetchable. `gaps[]` is the honest not-delivered list. `events[]` is the audit log (dispatches, retries, credential cooling, discoveries, pauses).","security":[{"bearerAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Long task object","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LongTaskObject"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/long-tasks/{id}/cancel":{"post":{"tags":["Browser Agent API"],"operationId":"cancelLongTask","summary":"Cancel a long task","description":"No new sub-tasks dispatch; in-flight sub-tasks settle through their own cancellation path; the task lands `cancelled` with the per-entity ledger of what was already delivered. Already-terminal tasks return unchanged (no-op).","security":[{"bearerAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"202":{"description":"Cancellation requested"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/long-tasks/{id}/runs":{"get":{"tags":["Browser Agent API"],"operationId":"listLongTaskRuns","summary":"A long task's child runs (enumeration + per-entity fetches)","description":"Every sub-run the orchestrator dispatched for this task, oldest-first (execution order). Each row carries role (enumeration | entity_fetch), the manifest entity_id it serves, and entity_label when the manifest knows a human label. Children are bounded by the manifest, so this is a plain ordered list (no cursor). The same rows are reachable on the runs list via ?parent_id=.","security":[{"bearerAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Child-run list (oldest first)","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","const":"list"},"parent_id":{"type":"string"},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"object":{"type":"string","const":"banana_peel.run"},"created_at":{"type":"string"},"status":{"type":"string"},"runner":{"type":"string"},"cost_usd":{"type":"number"},"timing_ms":{"type":"number"},"parent_id":{"type":"string"},"role":{"type":"string","enum":["enumeration","entity_fetch"]},"entity_id":{"type":"string","description":"Manifest entity served (absent on the enumeration step)."},"entity_label":{"type":"string"}}}},"first_id":{"type":"string","nullable":true},"last_id":{"type":"string","nullable":true},"has_more":{"type":"boolean","const":false}},"required":["object","parent_id","data"]}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/responses/{id}/input":{"post":{"tags":["Browser Agent API"],"operationId":"submitResponseInput","summary":"Submit MFA/OTP or text for a paused run","description":"Answer a pending human-input challenge. Body: `{ \"code\": \"123456\" }` (OTP) or `{ \"answer\": \"…\" }`. The OTP is not stored in run history or echoed back. Prefer creating with `background: true` so you can poll + answer while the run waits.","security":[{"bearerAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","description":"OTP / MFA code"},"answer":{"type":"string","description":"Generic text / confirm reply"},"input":{"type":"string","description":"Alias of answer"}}},"example":{"code":"123456"}}}},"responses":{"200":{"description":"Accepted — keep polling GET /v1/responses/{id}"},"400":{"description":"Missing code/answer","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Run is not waiting for input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"tags":["Browser Agent API"],"operationId":"getResponseInput","summary":"Poll pending interaction (prompt/kind only)","security":[{"bearerAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Interaction view (never includes the secret)"}}}},"/v1/responses/{id}/explanation":{"get":{"tags":["Routing"],"operationId":"getResponseExplanation","summary":"Routing decision card for a smart-routed response","description":"Why smart routing picked the runner it did: the full candidate set with expected-utility decomposition (success probability, expected cost, expected latency, block risk), the chosen runner + machine-readable reason, whether the pick exploited known winners or explored, how much per-site evidence informed it, and — once the run finished — every attempt with fault attribution (platform faults are excluded from runner scores). The explanation also carries the site profile that informed the decision (`siteProfile`: detected defense — cloudflare | datadome | perimeterx | recaptcha | turnstile | akamai | none | unknown — and its defense cluster), the evidence depth per candidate (`priorLevel`: domain_task | domain | cluster_task | cluster | task | global), neighbor domains whose learned stats were transferred for cold starts (`neighborsUsed`), operator hint ids applied to the decision (`appliedHints`), and the per-domain exploration-quota state (`explorationQuota`). Recorded for `routing: \"smart\"` decisions; pinned runners and chains have none.","security":[{"bearerAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Route explanation (decision + outcome when terminal)"},"404":{"description":"Unknown response id, or no explanation recorded for it","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/runs":{"get":{"tags":["Browser Agent API"],"operationId":"listRuns","summary":"List runs for the API key owner","description":"Newest first (created_at desc). Cursor pagination: pass the last id (or `next_starting_after` when present) as `starting_after`; `has_more` says whether to keep going. Filters are honest post-filters over the indexed owner scan — when a bounded scan window ends before a filtered page fills, the response still has `has_more: true` and `next_starting_after` (the last SCANNED run id, even with an empty `data`), so pagination always makes forward progress and never silently drops history. `status` matches the effective status the API returns (watchdog-expired ghosts filter as failed). `runner` is the executed slug; `routing` is the routing CLASS of the request param (use runner= for a specific pinned slug).","security":[{"bearerAuth":[]},{"apiKey":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"starting_after","in":"query","required":false,"description":"Run id cursor — return runs strictly after it in the newest-first order. Unknown ids yield an empty terminal page.","schema":{"type":"string"}},{"name":"created_after","in":"query","required":false,"description":"ISO 8601 — only runs created strictly after this instant.","schema":{"type":"string","format":"date-time"}},{"name":"created_before","in":"query","required":false,"description":"ISO 8601 — only runs created strictly before this instant.","schema":{"type":"string","format":"date-time"}},{"name":"status","in":"query","required":false,"description":"Effective run status (post watchdog fix-up).","schema":{"type":"string","enum":["running","needs_human","succeeded","failed","blocked","empty","cancelled"]}},{"name":"runner","in":"query","required":false,"description":"Executed runner slug (exact match).","schema":{"type":"string"}},{"name":"routing","in":"query","required":false,"description":"Routing class of the requested routing param: runner = pinned single runner, chain = runner array, category = SOC2/HIPAA/EU.","schema":{"type":"string","enum":["smart","learning","learning-all","category","runner","chain"]}},{"name":"parent_id","in":"query","required":false,"description":"Only the sub-runs of one long task (lt_…). Implies exclude_children=false.","schema":{"type":"string"}},{"name":"exclude_children","in":"query","required":false,"description":"DEFAULT true: long-task sub-runs are omitted from the top-level list so one fan-out does not bury user-initiated runs. Pass false to interleave them; they are always reachable via parent_id= or GET /v1/long-tasks/{id}/runs.","schema":{"type":"boolean","default":true}}],"responses":{"200":{"description":"Run list page (canonical banana_peel.run/v1 rows)","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","const":"list"},"data":{"type":"array","items":{"$ref":"#/components/schemas/BananaPeelRun"}},"has_more":{"type":"boolean"},"first_id":{"type":"string","nullable":true},"last_id":{"type":"string","nullable":true},"next_starting_after":{"type":"string","description":"Present when has_more — cursor for the next page. Usually equals last_id; after a filtered scan window it is the last scanned run id (data may even be empty). Always pass it (not last_id) when present."}},"required":["object","data","has_more"]}}}},"400":{"description":"Invalid filter (invalid_list_filter)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/me":{"get":{"tags":["Browser Agent API"],"operationId":"getAccount","summary":"Account identity for this API key","description":"Returns account_id, created_at, and claim_url (when the key belongs to an unclaimed agent account). Used by `npx -y @banana-peel/cli whoami`. Works for env keys and credential-file keys. Does not return the API secret. Unclaimed-freeze does not apply — a frozen agent account still resolves.","security":[{"bearerAuth":[]},{"apiKey":[]}],"responses":{"200":{"description":"Account identity","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","const":"banana_peel.account"},"account_id":{"type":"string"},"created_at":{"type":"string","format":"date-time","nullable":true},"claim_url":{"type":"string","format":"uri","nullable":true},"claimable":{"type":"boolean"},"origin":{"type":"string","enum":["agent","console"]}},"required":["object","account_id"]}}}},"401":{"description":"Invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/workspace":{"get":{"tags":["Browser Agent API"],"operationId":"getWorkspace","summary":"Workspace retention policy","description":"Returns the document retention window for this API key. Non-enterprise workspaces are fixed at 7 days. See /docs/security/retention.","security":[{"bearerAuth":[]},{"apiKey":[]}],"responses":{"200":{"description":"Workspace retention view"},"401":{"description":"Invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"tags":["Browser Agent API"],"operationId":"updateWorkspace","summary":"Set Enterprise retention (24h / 7d / 30d)","description":"Enterprise self-serve: body `{ \"retention\": \"24h\"|\"7d\"|\"30d\" }` or `{ \"retention_days\": 1|7|30 }`. Non-enterprise → 403. Zero-retention and longer windows are contract terms, not PATCH values.","security":[{"bearerAuth":[]},{"apiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"retention":{"type":"string","enum":["24h","7d","30d"]},"retention_days":{"type":"integer","enum":[1,7,30]}}},"example":{"retention":"24h"}}}},"responses":{"200":{"description":"Updated workspace view"},"400":{"description":"Invalid retention (`invalid_retention` or `retention_not_self_serve`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not Enterprise (`enterprise_required`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/credentials":{"post":{"tags":["Vault"],"operationId":"createCredential","summary":"Create a vault credential","description":"Store a login credential in the account vault (same fields as Console → Vault). Username, password, and optional TOTP secret are encrypted at rest; the response returns id + metadata only — secret material is never returned by any endpoint. Use the returned id as `credential_id` on POST /v1/responses. The custody policy (any | trusted_runners | deck_only | pinned:<runner>[,…]; default trusted_runners) hard-limits which runners may touch the credential — see /docs/security/custody.","security":[{"bearerAuth":[]},{"apiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCredential"},"example":{"domain":"portal.example-utility.com","username":"jdoe@example.com","password":"hunter2-rotate-me","totp_secret":"JBSWY3DPEHPK3PXP","custody":"trusted_runners"}}}},"responses":{"201":{"description":"Created — id + metadata only (never the secret)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Credential"}}}},"400":{"description":"Invalid request (`invalid_credential` for missing/oversized fields, `invalid_custody` for a malformed policy)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"tags":["Vault"],"operationId":"listCredentials","summary":"List vault credentials (metadata only)","description":"All non-revoked credentials owned by the API key’s account: id, domain, masked username, mfa, custody policy, health, created_at, last_used. Never includes secret material.","security":[{"bearerAuth":[]},{"apiKey":[]}],"responses":{"200":{"description":"List object with data[] of Credential","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","const":"list"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Credential"}}}}}}},"401":{"description":"Invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/credentials/{id}":{"get":{"tags":["Vault"],"operationId":"getCredential","summary":"Get one credential (metadata only)","security":[{"bearerAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Credential metadata (never the secret)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Credential"}}}},"404":{"description":"Not found (`credential_not_found`) — unknown, revoked, or owned by another account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"tags":["Vault"],"operationId":"updateCredential","summary":"Update custody policy or rotate secrets","description":"Update the custody policy, rotate the password, or set/replace/remove the TOTP secret (`totp_secret: null` removes it). Returns metadata only.","security":[{"bearerAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"custody":{"type":"string","description":"any | trusted_runners | deck_only | pinned:<runner>[,<runner>…]"},"password":{"type":"string","description":"Rotate the stored password."},"totp_secret":{"type":["string","null"],"description":"Replace the TOTP secret, or null to remove it."},"mfa":{"type":"string","enum":["none","totp","sms-relay","passkey"]}}},"example":{"custody":"pinned:browserbase,steel"}}}},"responses":{"200":{"description":"Updated credential metadata","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Credential"}}}},"400":{"description":"Invalid field (`invalid_credential` / `invalid_custody`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found (`credential_not_found`) — unknown, revoked, or owned by another account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Vault"],"operationId":"deleteCredential","summary":"Revoke a credential","description":"Soft-deletes the credential: it disappears from lists and can no longer be used on runs. Ciphertext is retained for audit; it is never decrypted again.","security":[{"bearerAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted confirmation","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"object":{"type":"string","const":"banana_peel.credential"},"deleted":{"type":"boolean","const":true}}}}}},"404":{"description":"Not found (`credential_not_found`) — unknown, already revoked, or owned by another account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/webhooks":{"post":{"tags":["Webhooks"],"operationId":"createWebhook","summary":"Register an outbound webhook endpoint","description":"Create a per-account HTTPS endpoint. Banana Peel POSTs signed JSON when a response completes, fails, is cancelled, or requires human input (MFA). If `secret` is omitted, a `whsec_…` signing secret is minted and returned once — list/get never include it. https only (http://localhost allowed for development). Private, metadata, and .internal hosts are rejected. See /docs/api/webhooks.","security":[{"bearerAuth":[]},{"apiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWebhook"},"example":{"url":"https://example.com/webhooks/bananapeel","events":["response.completed","response.failed","response.requires_action","response.cancelled"]}}}},"responses":{"201":{"description":"Created — includes `secret` once","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookCreated"}}}},"400":{"description":"Invalid URL or events (`invalid_webhook_url` / `invalid_webhook_event`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"tags":["Webhooks"],"operationId":"listWebhooks","summary":"List webhook endpoints (never the secret)","description":"All outbound endpoints owned by the API key’s account: url, events, disabled, consecutive_failures, timestamps. Signing secrets are never returned.","security":[{"bearerAuth":[]},{"apiKey":[]}],"responses":{"200":{"description":"List object with data[] of WebhookEndpoint","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","const":"list"},"data":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEndpoint"}}}}}}},"401":{"description":"Invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/webhooks/{id}":{"get":{"tags":["Webhooks"],"operationId":"getWebhook","summary":"Get one webhook endpoint (never the secret)","security":[{"bearerAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Webhook metadata","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpoint"}}}},"404":{"description":"Not found (`webhook_not_found`) — unknown or owned by another account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"tags":["Webhooks"],"operationId":"updateWebhook","summary":"Update url, events, or enabled","description":"Patch the endpoint URL, subscribed events, or enabled/disabled. Re-enabling clears consecutive_failures. Signing secret is unchanged (use POST …/rotate).","security":[{"bearerAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWebhook"},"example":{"enabled":true}}}},"responses":{"200":{"description":"Updated webhook metadata","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpoint"}}}},"400":{"description":"Invalid field","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found (`webhook_not_found`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Webhooks"],"operationId":"deleteWebhook","summary":"Delete a webhook endpoint","security":[{"bearerAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted confirmation","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"object":{"type":"string","const":"banana_peel.webhook"},"deleted":{"type":"boolean","const":true}}}}}},"404":{"description":"Not found (`webhook_not_found`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/webhooks/{id}/rotate":{"post":{"tags":["Webhooks"],"operationId":"rotateWebhookSecret","summary":"Rotate the signing secret (returned once)","description":"Mints a new `whsec_…` secret. The previous secret remains valid for 24 hours (`old_secret_expires_at`). During grace, Banana Peel signs with the new secret and includes BOTH signatures: `X-Banana-Peel-Signature: t=<unix>,v1=<new>,v1=<old>` — verify against ANY v1. The new secret is returned once and never listed again.","security":[{"bearerAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Rotated — includes `secret` once plus `old_secret_expires_at`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookCreated"}}}},"404":{"description":"Not found (`webhook_not_found`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/webhooks/{id}/deliveries":{"get":{"tags":["Webhooks"],"operationId":"listWebhookDeliveries","summary":"List recent deliveries for an endpoint","description":"Paginated delivery history (newest first). `limit` 1–100 (default 20), `starting_after` = delivery id. Never includes signing secrets. Response body previews are capped. Other accounts’ ids 404.","security":[{"bearerAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"name":"starting_after","in":"query","schema":{"type":"string"},"description":"Delivery id cursor — return rows older than this id."}],"responses":{"200":{"description":"List of WebhookDelivery","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","const":"list"},"data":{"type":"array","items":{"$ref":"#/components/schemas/WebhookDelivery"}},"has_more":{"type":"boolean"},"first_id":{"type":"string","nullable":true},"last_id":{"type":"string","nullable":true}}}}}},"404":{"description":"Not found (`webhook_not_found`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/webhooks/{id}/deliveries/{delivery_id}":{"get":{"tags":["Webhooks"],"operationId":"getWebhookDelivery","summary":"Get one delivery (headers, payload preview, HTTP, latency)","description":"Includes request header summary, capped payload preview, last HTTP status, response body first 512 bytes, latency, and attempt count. No secrets.","security":[{"bearerAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"delivery_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Delivery detail","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookDelivery"}}}},"404":{"description":"Not found (`webhook_delivery_not_found` / `webhook_not_found`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/webhooks/{id}/deliveries/{delivery_id}/retry":{"post":{"tags":["Webhooks"],"operationId":"retryWebhookDelivery","summary":"Manually redeliver (same event.id)","description":"POSTs the frozen payload again with the same `event.id`. Use after fixing your endpoint. Does not mint a new event.","security":[{"bearerAuth":[]},{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"delivery_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Updated delivery after the attempt","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookDelivery"}}}},"404":{"description":"Not found (`webhook_delivery_not_found`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/rankings":{"get":{"tags":["Routing"],"operationId":"liveRankings","summary":"Live runner benchmark aggregated across all domains from real outcomes","responses":{"200":{"description":"Live runner benchmarks — one entry per runner, aggregated across all domains","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Rankings"}}}}}}},"/v1/capabilities":{"get":{"tags":["Routing"],"operationId":"runnerCapabilities","summary":"Runner capability matrix (canonical model) with live availability","description":"Each row includes `availability`: `live`, `suspended` (circuit-breaker cooldown — provider credits/quota exhausted, auth rejected, repeated hangs), or `probation` (cooldown expired, half-open re-probe) with a machine-readable `reason` and the `until` re-probe timestamp where known. `runner_counts` reports the honest pool size (total vs live). Suspended runners are already skipped by smart routing, fallback chains, and fan-outs — no client-side filtering is required.","responses":{"200":{"description":"Capability rows for runners, each with live `availability`, plus honest `runner_counts` (live vs total)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CapabilityMatrix"}}}}}}},"/status":{"get":{"tags":["Platform"],"operationId":"platformStatus","summary":"Public platform status (no auth)","description":"Powers the /status page and external monitors. Ratios and timestamps only — absolute run/runner volume is never exposed. Cached ~30s.","security":[],"responses":{"200":{"description":"Platform status snapshot","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlatformStatus"}}}}}}},"/v1/sessions":{"post":{"tags":["Wrappers"],"operationId":"browserbaseCreateSession","summary":"Browserbase-compatible session create (proxied)","description":"Creates a session: a live browser you hold open and drive yourself over CDP (Playwright/Puppeteer). Unlike a run (POST /v1/responses — a routed unit of work), a session carries no task and is not smart-routed. Drop-in for @browserbasehq/sdk. Authenticate with a Banana Peel key (Bearer / X-BB-API-Key). Path is /api/v1/sessions — NOT /api/wrappers/browserbase. Proxied to real Browserbase unless Deck routing headers/metadata are present.","security":[{"bearerAuth":[]},{"apiKey":[]}],"responses":{"201":{"description":"Browserbase session object"},"401":{"description":"Unauthorized (Browserbase error shape)"}}},"get":{"tags":["Wrappers"],"operationId":"browserbaseListSessions","summary":"Browserbase-compatible session list (proxied)","security":[{"bearerAuth":[]},{"apiKey":[]}],"responses":{"200":{"description":"Session list"}}}},"/wrappers/steel/tasks":{"post":{"tags":["Wrappers"],"operationId":"wrapperSteelTasks","summary":"Steel-shaped migration wrapper","security":[{"bearerAuth":[]},{"apiKey":[]}],"responses":{"200":{"description":"Normalized Banana Peel result"}}}},"/wrappers/steel/v1/sessions":{"post":{"tags":["Wrappers"],"operationId":"wrapperSteelSessionsCreate","summary":"Steel sessions proxy (steel-sdk baseURL drop-in)","description":"Creates a session — a live browser you drive yourself over CDP, always on Steel (no task, not smart-routed; for routed work use POST /v1/responses). Alias for official steel-sdk `/v1/sessions`. Point baseURL at `${BANANA_PEEL_BASE}/api/wrappers/steel`. Requires STEEL_API_KEY on the deployment.","security":[{"bearerAuth":[]},{"apiKey":[]}],"responses":{"200":{"description":"Upstream Steel session"},"503":{"description":"Steel sessions proxy not configured"}}},"get":{"tags":["Wrappers"],"operationId":"wrapperSteelSessionsList","summary":"Steel sessions list proxy","security":[{"bearerAuth":[]},{"apiKey":[]}],"responses":{"200":{"description":"Upstream Steel session list"}}}},"/wrappers/browser-use/run":{"post":{"tags":["Wrappers"],"operationId":"wrapperBrowserUseRun","summary":"Browser Use–shaped migration wrapper","security":[{"bearerAuth":[]},{"apiKey":[]}],"responses":{"200":{"description":"Normalized Banana Peel result"}}}},"/wrappers/skyvern/run":{"post":{"tags":["Wrappers"],"operationId":"wrapperSkyvernRun","summary":"Skyvern-shaped migration wrapper","security":[{"bearerAuth":[]},{"apiKey":[]}],"responses":{"200":{"description":"Normalized Banana Peel result"}}}},"/wrappers/hyperbrowser/task":{"post":{"tags":["Wrappers"],"operationId":"wrapperHyperbrowserTask","summary":"Hyperbrowser-shaped migration wrapper","security":[{"bearerAuth":[]},{"apiKey":[]}],"responses":{"200":{"description":"Normalized Banana Peel result"}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Authorization: Bearer bp_live_… (or bp_test_…)"},"apiKey":{"type":"apiKey","in":"header","name":"x-api-key","description":"Alternate: x-api-key: bp_live_…"}},"schemas":{"LongTaskCreate":{"type":"object","required":["input"],"properties":{"input":{"type":"string","description":"The multi-entity job in plain language (e.g. \"download the latest bill for ALL accounts\")."},"url":{"type":"string","description":"Portal URL."},"credential_id":{"type":"string","description":"Vault credential (cred_…) for the portal login."},"expected_entities":{"type":"integer","minimum":2,"maximum":500,"description":"Caller override when the text alone does not look multi-entity: \"I know this covers N entities\". Also threaded into the enumeration instruction and the manifest verifier as ground truth — provide it when you know K so short enumerations trigger an escalated search. Out of range → 400 invalid_expected_entities."},"verification":{"type":"string","enum":["cross_run"],"description":"High-stakes manifest verification (opt-in). \"cross_run\" runs a SECOND independent enumeration on a different runner up front and merges the union of both entity lists; manifest_verification reports method \"cross_run\" with an agreement score (set overlap) and a disagreement-derived confidence. The second enumeration bills like any run (one extra login + one extra enumeration). If the confirming enumeration fails, the task proceeds on the first grader-verified list — the opt-in never fails a job. Omit for the default single grader-verified enumeration. Unknown values → 400 invalid_verification."},"max_cost_usd":{"type":"number","description":"Total spend ceiling for the whole task."},"time_budget_ms":{"type":"integer","description":"Wall-clock ceiling for the whole task (ms). Omit for uncapped (recommended) — hang protection and the credential-safe login pacing still apply."}}},"LongTaskObject":{"type":"object","properties":{"id":{"type":"string","example":"lt_9f2c…"},"object":{"type":"string","example":"long_task"},"status":{"type":"string","enum":["enumerating","fanning_out","paused_interaction","complete","partial_complete","failed","cancelled"]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"request":{"type":"object","description":"Echo of the accepted request (input, url, credential_id, classified intent, verification opt-in when present).","properties":{"input":{"type":"string"},"url":{"type":"string"},"credential_id":{"type":"string"},"intent":{"type":"string"},"verification":{"type":"string","enum":["cross_run"]}}},"scale":{"type":"object","description":"How the multi-entity gate admitted the task.","properties":{"detected_by":{"type":"string","enum":["text","domain_prior","discovery","caller"]},"k_expected":{"type":"integer","nullable":true}}},"manifest_rev":{"type":"integer","description":"Starts at 0; bumps when mid-run discovery appends entities."},"manifest":{"type":"object","nullable":true,"description":"Manifest acceptance facts (spec §1). null until an enumeration is accepted.","properties":{"source_run_id":{"type":"string"},"verification":{"type":"object","properties":{"method":{"type":"string","enum":["grader","cross_run"]},"confidence":{"type":"number"},"agreement":{"type":"number"},"low_confidence":{"type":"boolean"}}}}},"manifest_verification":{"type":"object","description":"Alias of manifest.verification. How the entity list was accepted: method \"grader\" (verified above the confidence bar) or \"cross_run\". cross_run comes in two flavors: the PROACTIVE verification:\"cross_run\" opt-in reports \"agreement\" (set overlap of the two independent enumerations, 1.0 = identical) with confidence = min(leg confidences) × agreement and is graded on the normal confidence bar; the REACTIVE unanimity acceptance (a rejected claim later matched identically by a different runner, no agreement field) is always low_confidence: true — coverage may be partial and discovery can still append mid-run.","properties":{"method":{"type":"string","enum":["grader","cross_run"]},"confidence":{"type":"number"},"agreement":{"type":"number"},"low_confidence":{"type":"boolean"}}},"entities":{"type":"array","description":"Per-entity ledger (the progress table).","items":{"type":"object","properties":{"entity_id":{"type":"string"},"label":{"type":"string"},"status":{"type":"string","enum":["pending","dispatched","delivered","unfetchable","exhausted"]},"attempts":{"type":"integer","description":"Runner-class attempts only (environment failures do not count)."},"run_id":{"type":"string"},"artifact_id":{"type":"string","description":"Verified file artifact (delivered only) — GET /v1/responses/{run_id}/artifacts/{artifact_id}."},"terminal_reason":{"type":"string","description":"Grader-verified absence reason (unfetchable only)."},"last_error":{"type":"string"}}}},"counts":{"type":"object","properties":{"total":{"type":"integer"},"pending":{"type":"integer"},"dispatched":{"type":"integer"},"delivered":{"type":"integer"},"unfetchable":{"type":"integer"},"exhausted":{"type":"integer"}}},"gaps":{"type":"array","description":"Honest not-delivered list (what a partial_complete is missing and why).","items":{"type":"object","properties":{"entity_id":{"type":"string"},"status":{"type":"string"},"terminal_reason":{"type":"string"},"last_error":{"type":"string"}}}},"budget":{"type":"object","properties":{"max_usd":{"type":"number"},"spent_usd":{"type":"number","description":"Real sub-task spend to date, accrued regardless of outcome."},"per_entity_max_attempts":{"type":"integer","default":3}}},"time_budget_ms":{"type":"integer","nullable":true,"description":"Wall-clock ceiling. null + uncapped:true when omitted at create."},"uncapped":{"type":"boolean"},"session":{"type":"object","description":"Session facts: mode (reused = one login covers all sub-tasks; per_subtask = each sub-task logs in, paced by the credential bucket), established, the fresh-login count, and refreshes (proactive pre-expiry re-logins, one each). In reused mode the authenticated context continues across sub-tasks — and across runner classes where the runner documents accepting external session state. The stored session state itself is encrypted with the same key custody as vault credentials, never appears in any API response, webhook, or event, and is deleted when the task reaches a terminal state. The vendor session handle is never exposed.","properties":{"mode":{"type":"string","enum":["reused","per_subtask"]},"established":{"type":"boolean"},"logins":{"type":"integer"},"refreshes":{"type":"integer"}}},"interaction":{"type":"object","description":"Present while paused_interaction: the sub-task run holding the MFA prompt — answer via POST /v1/responses/{run_id}/input.","properties":{"run_id":{"type":"string"},"entity_id":{"type":"string"},"since":{"type":"string","format":"date-time"}}},"stop_reason":{"type":"string","description":"Why dispatch stopped early (budget_exhausted | time_budget_exhausted | enumeration_failed | cancelled)."},"cancel_requested":{"type":"boolean"},"events":{"type":"array","description":"Audit log — last 50 entries (dispatches, retries, credential_cooling, discoveries, pauses, terminals).","items":{"type":"object","properties":{"at":{"type":"string","format":"date-time"},"type":{"type":"string"},"entity_id":{"type":"string"},"message":{"type":"string"}}}}}},"CreateResponse":{"type":"object","properties":{"model":{"type":"string","default":"banana-peel"},"input":{"description":"Task text (string, OpenAI content array, or structured {url, goal, success_criteria, steps}). Required unless `url` alone is enough with goal/task.","oneOf":[{"type":"string"},{"type":"array"},{"type":"object"}]},"url":{"type":"string","format":"uri"},"instructions":{"type":"string"},"goal":{"type":"string"},"task":{"type":"string"},"credential_id":{"type":"string","description":"Optional vault credential id for the API key owner (create/list via /v1/credentials). After custody is satisfiable, secrets are decrypted and injected into the task for whichever runner routing selects (smart, steel, browserbase, …). Vault usage never pins routing to deck (deck_only is an opt-in custody policy; create default is trusted_runners). When routing does select Deck — pinned, smart, race, or learning — the credential is additionally attached through Deck’s native vault (registered per source, reused across runs), since Deck gates login-walled sources on an attached credential rather than the instruction text."},"stream":{"type":"boolean","description":"OpenAI Responses SSE stream (text/event-stream). Takes precedence over background for this request."},"background":{"type":"boolean","description":"Return 202 immediately and finish in the background. Preferred for human MFA/OTP relay (poll + POST …/input)."},"metadata":{"type":"object","additionalProperties":{"type":"string"},"description":"OpenAI metadata (string values); echoed on the response object."},"allow_interaction":{"type":"boolean","description":"Human-in-the-loop. Default on (response id is the relay channel). false opts out; true refuses non-relay runners."},"interaction_timeout_ms":{"type":"integer","description":"Max wait per MFA/OTP/text question (default 240000)."},"totp_secret":{"type":"string","description":"Base32 TOTP secret — OTP asks answered in-process (prefer vault credential_id)."},"totp_identifier":{"type":"string","description":"Skyvern-native TOTP inbox/phone identifier."},"totp_url":{"type":"string","description":"Skyvern-native TOTP poll URL."},"normalize":{"type":"boolean","description":"Paid output normalization (+$0.10 / run). Requires output_schema (or text.format / response_format schema). Build schemas in Console → Normalize."},"output_schema":{"type":"object","description":"Desired output JSON Schema (or property map). Used when normalize is true or when a schema is supplied via text.format / response_format. HARD CONTRACT: a terminal success must populate exactly these keys (required keys present, non-empty, right type; no undeclared extra data keys unless additionalProperties: true — run metadata like url/final_url is tolerated). A run whose output cannot satisfy the schema terminates status:failed with error.code schema_violation and the nonconforming output kept under banana_peel.output for inspection; the normalization add-on is not billed on violation. Without a schema, read the stable default envelope instead: banana_peel.answer / answer_present / evidence.","additionalProperties":true},"success_criteria":{"type":"string","description":"Explicit success criterion for the quality grader (e.g. \"an order number is present\")."},"steps":{"type":"array","items":{"type":"string"},"description":"Optional ordered steps for structured task input."},"credentials_ref":{"type":"string","description":"Alias for credential_id (vault credential)."},"custody":{"type":"string","description":"Credential custody policy for THIS request: any | trusted_runners | deck_only | pinned:<runner>[,<runner>…]. Hard-filters the runner candidate set before any routing / scoring / fan-out; when no runner survives, the request fails fast with 409 `custody_unsatisfiable` and nothing is billed. Use with inline credentials in the task text; vault credentials carry their own stored policy (both compose as AND). Enforcement is echoed under `banana_peel.custody`.","examples":["trusted_runners","deck_only","pinned:browserbase,steel"]},"text":{"type":"object","description":"OpenAI Responses text.format passthrough (json_schema → normalize).","additionalProperties":true},"response_format":{"type":"object","description":"OpenAI-style response_format (json_schema / json_object).","additionalProperties":true},"idempotency_key":{"type":"string","maxLength":255,"description":"Client retry key — equivalent to the `Idempotency-Key` HTTP header (send either; if both, they must match or the create fails with 400 `idempotency_key_mismatch`). Honored on sync, background (`background: true` / Prefer: respond-async), and streaming create; keys are per-workspace, shared across every run-creating endpoint, and expire 24h after first use. The key is reserved before dispatch: reuse with an identical body (`stream`/`background` excluded from the comparison) replays the existing run (original `created_at`, `Idempotent-Replayed: true` header) instead of starting a duplicate; reuse with a different body is refused with 422 `idempotency_conflict`; a duplicate racing the original create gets 409 `idempotency_in_flight`."},"routing":{"description":"Optional (default smart). Learning and learning-all exist to teach the router the best path: they calibrate the routing brain on a domain/task so later smart runs pick the winning runner. smart = production (router uses the brain to pick/fallback) | learning = paid calibration: probe the curated live ~54-single-runner pool on this task/URL (not the default for production traffic) | learning-all = paid thorough calibration: full-catalog sweep (250+ options, NO early stop; most expensive; do not use for every production job) | named runner (browserbase, steel, browser-use, skyvern, hyperbrowser, playwright, deck, …) | category MVP pool (SOC2/HIPAA/EU — fixed runner lists, not attestation) | ordered runner list. Learning probes batches of 9 (concurrency 8) and stops after the first wave containing a success; learning-all keeps the same waves but sweeps the full catalog (250+ options): every dispatchable option attempts once; coming-soon catalog rows are skipped (never billed); $20 default ceiling (a catalog-wide sweep typically hits it). Learning-family runs are paid calibration sweeps (executed attempts bill); winners ranked fastest+cheapest feed future smart routing.","oneOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"default":"smart","examples":["smart","learning","learning-all","browserbase","skyvern","SOC2",["steel","hyperbrowser","playwright"]]},"sla_profile":{"type":"string","enum":["balanced","cheapest","fastest","most_reliable"],"default":"balanced","description":"Smart-routing preference: how the router weighs cost, speed, and block-avoidance against success probability when choosing runners. Only affects routing: \"smart\"."},"max_fallbacks":{"type":"integer","minimum":0,"maximum":12,"default":5,"description":"Smart routing: how many additional runners after the primary (1 + N). Default 5 (up to 6 attempts unless spend, time, or a breaker stops earlier). 0 pins to the first pick only. Invalid values fail free with 400 invalid_max_fallbacks. Only affects routing: \"smart\" (and category). Staggered race still starts the primary first."},"max_cost_usd":{"type":"number","exclusiveMinimum":0,"description":"Per-run spend ceiling (USD), honored by every routing mode. Once executed attempts’ spend reaches the ceiling, no NEW attempts / batches / waves are dispatched; in-flight attempts finish (and bill) and the run ends with error code `budget_exhausted` plus partial results under `banana_peel.routing_attempts`. Learning runs default to $20 when omitted (an explicit max_cost_usd overrides the default in either direction). The active ceiling is echoed under `banana_peel.budget` and reflected by POST /v1/estimate.","examples":[2.5,0.25]},"time_budget_ms":{"type":"integer","minimum":30000,"description":"OPTIONAL run time budget (ms) for routing: \"smart\", \"category\", \"learning\", and \"learning-all\". When set it is THE orchestration ceiling: smart attempts share the window, learning sweeps stop dispatching new batches at it, and the run is reaped at budget + a 10-minute grace. When OMITTED the run is UNCAPPED — no run-level deadline exists; attempts defer to adapter-native leashes and per-attempt watchdogs, and only true-hang protection (no-progress watchdog, repeating-error detection, progress-staleness reaping, and a 24 h absolute safety ceiling) can stop the run. Uncapped runs should use background: true — a synchronous request is still bounded by the HTTP request timeout. Not accepted for pinned runner/chain routing (400 time_budget_unsupported). Values below 30000 fail free with 400 invalid_time_budget_ms; there is no upper bound. Spend ceilings (max_cost_usd) are independent. Alias: timeout_ms. The active contract is echoed as banana_peel.time_budget_ms (null + uncapped: true when uncapped).","examples":[1800000]}}},"CreateCredential":{"type":"object","required":["domain","username","password"],"properties":{"domain":{"type":"string","maxLength":253,"description":"Login site domain, e.g. portal.example.com (scheme/path stripped)."},"username":{"type":"string","maxLength":320},"password":{"type":"string","maxLength":1024,"description":"Stored envelope-encrypted: per-secret AES-256-GCM data key wrapped by Cloud KMS (automatic key rotation). Never returned by any endpoint."},"totp_secret":{"type":"string","maxLength":128,"description":"Optional base32 TOTP seed — OTP challenges are answered in-process at run time. Sets mfa: totp."},"custody":{"type":"string","default":"trusted_runners","description":"Custody policy: any | trusted_runners | deck_only | pinned:<runner>[,<runner>…]. See /docs/security/custody.","examples":["trusted_runners","deck_only","pinned:browserbase,steel"]},"mfa":{"type":"string","enum":["none","totp","sms-relay","passkey"],"description":"MFA mode; inferred as totp when totp_secret is set."},"source_id":{"type":"string","maxLength":120,"description":"Optional catalog source id when the site comes from the sources list."}}},"Credential":{"type":"object","description":"Vault credential metadata. Secret material (password / TOTP seed) is write-only and never appears here.","properties":{"id":{"type":"string","description":"cred_… — pass as credential_id on POST /v1/responses"},"object":{"type":"string","const":"banana_peel.credential"},"domain":{"type":"string"},"source_id":{"type":"string","nullable":true},"username_masked":{"type":"string","description":"Masked username for display (e.g. jdo***@example.com)."},"mfa":{"type":"string","enum":["none","totp","sms-relay","passkey"]},"custody":{"type":"string","description":"any | trusted_runners | deck_only | pinned:<runner>[,<runner>…]"},"health":{"type":"string","enum":["ok","stale","failed"]},"created_at":{"type":"string","format":"date-time"},"last_used":{"type":"string","format":"date-time","nullable":true}}},"CreateWebhook":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","description":"Public HTTPS callback URL. Private, metadata, .internal, and localhost/loopback hosts are rejected in production (http://localhost is allowed only in local development)."},"events":{"type":"array","items":{"type":"string","enum":["response.completed","response.failed","response.requires_action","response.cancelled","long_task.progress","long_task.interaction_required","long_task.completed","long_task.partial","long_task.failed"]},"description":"Defaults to all event types when omitted."},"secret":{"type":"string","minLength":16,"description":"Optional signing secret. If omitted, Banana Peel mints `whsec_…` and returns it once."}}},"UpdateWebhook":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"type":"string","enum":["response.completed","response.failed","response.requires_action","response.cancelled","long_task.progress","long_task.interaction_required","long_task.completed","long_task.partial","long_task.failed"]}},"enabled":{"type":"boolean","description":"Set false to disable delivery."},"disabled":{"type":"boolean"}}},"WebhookEndpoint":{"type":"object","description":"Registered outbound webhook. The signing secret is write-only and never appears here.","properties":{"id":{"type":"string","description":"wh_…"},"object":{"type":"string","const":"banana_peel.webhook"},"url":{"type":"string"},"events":{"type":"array","items":{"type":"string"}},"disabled":{"type":"boolean"},"disabled_reason":{"type":"string","nullable":true},"consecutive_failures":{"type":"integer"},"last_delivery_at":{"type":"string","format":"date-time","nullable":true},"last_http_status":{"type":"integer","nullable":true},"old_secret_expires_at":{"type":"string","format":"date-time","nullable":true,"description":"When set, the previous signing secret still verifies until this instant (24h rotation grace)."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"WebhookCreated":{"allOf":[{"$ref":"#/components/schemas/WebhookEndpoint"},{"type":"object","properties":{"secret":{"type":"string","description":"Signing secret (`whsec_…`) — returned once at create/rotate."},"old_secret_expires_at":{"type":"string","format":"date-time","nullable":true,"description":"On rotate: the previous secret remains valid until this timestamp (24h). Absent on create."},"warning":{"type":"string"}}}]},"WebhookEvent":{"type":"object","description":"Outbound webhook POST body. Delivered at least once — store `id` and ignore duplicates. `data` is the same object as GET /v1/responses/{id}, redacted (no secrets). Delivery order is not guaranteed; `sequence` is monotonic per response so you can drop stale events. Treat the current GET /responses/:id state as authoritative.","required":["id","object","api_version","type","sequence","created_at","data"],"properties":{"id":{"type":"string","description":"evt_… — canonical idempotency key. The same id may arrive more than once (retries / redelivery). Header X-Banana-Peel-Event-Id is a convenience copy."},"object":{"type":"string","const":"banana_peel.event"},"api_version":{"type":"string","description":"Date-version of the event envelope (currently 2026-08-13).","example":"2026-08-13"},"type":{"type":"string","enum":["response.completed","response.failed","response.requires_action","response.cancelled","long_task.progress","long_task.interaction_required","long_task.completed","long_task.partial","long_task.failed"]},"sequence":{"type":"integer","description":"Monotonic per response. Incremented once per event emitted for that response (shared across endpoints). Drop events with sequence less than the highest you have already applied for that response.id."},"created_at":{"type":"string","format":"date-time"},"data":{"allOf":[{"$ref":"#/components/schemas/ResponseObject"}],"description":"Redacted response object. For requires_action, includes required_action.submit_input (kind + prompt) so the customer can POST /v1/responses/{id}/input — never the secret the user will type. On brain-routed runs, completed/failed events carry data.banana_peel.routing_summary — runner, strategy, reason code, one redacted sentence, per-attempt fault attribution, policy version, widening echo, and the path to the full decision card — so \"why was it routed there\" needs no second fetch."}}},"WebhookDelivery":{"type":"object","description":"One attempt-history row for an outbound event. Payload/response previews are capped; secrets are never stored.","properties":{"id":{"type":"string","description":"whdel_…"},"object":{"type":"string","const":"banana_peel.webhook_delivery"},"webhook_id":{"type":"string"},"event_id":{"type":"string","description":"Frozen evt_… — redelivery reuses this id."},"type":{"type":"string"},"response_id":{"type":"string"},"sequence":{"type":"integer"},"status":{"type":"string","enum":["pending","delivered","failed"]},"attempt":{"type":"integer"},"next_attempt_at":{"type":"string","format":"date-time","nullable":true},"last_http_status":{"type":"integer","nullable":true},"last_error":{"type":"string","nullable":true},"last_latency_ms":{"type":"integer","nullable":true},"response_body_preview":{"type":"string","nullable":true,"description":"First 512 bytes of the customer endpoint’s response body."},"request_headers":{"type":"object","additionalProperties":{"type":"string"},"nullable":true,"description":"Sent headers except the HMAC signature."},"redelivery_count":{"type":"integer"},"payload_preview":{"type":"string","description":"Truncated frozen event JSON (GET one only)."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"Error":{"type":"object","description":"Browser Agent API error envelope. Branch on error.code (stable); the full code table with remediations lives at /docs/api/errors.","properties":{"error":{"type":"object","required":["message","type","code"],"properties":{"message":{"type":"string"},"type":{"type":"string"},"code":{"type":"string"},"param":{"type":"string","nullable":true},"doc_url":{"type":"string"},"hint":{"type":"string"},"remediation":{"type":"string","description":"Short next-step guidance for agents."},"claim_url":{"type":"string","description":"Present on account_unclaimed — open it to claim the account."}}}}},"ResponseObject":{"type":"object","properties":{"id":{"type":"string"},"object":{"type":"string","const":"response"},"created_at":{"type":"integer","description":"Unix seconds"},"status":{"type":"string","enum":["completed","failed","in_progress","requires_action","cancelled"]},"required_action":{"nullable":true,"type":"object","description":"Present when status is requires_action — owner secrets only (MFA/OTP/text/confirm). Never emitted for captcha / bot challenges. Visuals (live_view / screenshot / replay_url) are included when available.","properties":{"type":{"type":"string","const":"submit_input"},"submit_input":{"type":"object","properties":{"kind":{"type":"string","enum":["otp","text","confirm"]},"prompt":{"type":"string"},"created_at":{"type":"integer"},"channel":{"type":"string","enum":["totp","sms","email","unknown"],"description":"Best-effort delivery channel inferred from the prompt."},"message":{"type":"string","description":"Human-readable ask (same as prompt)."},"submit":{"type":"object","description":"How to send the code.","properties":{"method":{"type":"string","const":"POST"},"path":{"type":"string","description":"/api/v1/responses/{id}/input"}}},"live_view":{"type":"string","nullable":true},"screenshot":{"type":"string","nullable":true},"replay_url":{"type":"string","nullable":true}}}}},"incomplete_details":{"nullable":true,"type":"object"},"model":{"type":"string"},"confidence":{"nullable":true,"type":"number"},"output":{"type":"array","items":{"type":"object"}},"output_text":{"type":"string","description":"ALWAYS a plain string, on every runner: freeform answers verbatim; structured results (including output_schema-normalized ones) as pretty-printed JSON. The structured value itself is under banana_peel.output. Guaranteed never a character-indexed map ({\"0\":\"T\",\"1\":\"h\",…}) and never double-encoded JSON-in-a-string — outputs are repaired/unwrapped at the canonical normalization choke point before the envelope is built or persisted."},"usage":{"type":"object","description":"Approximate tokens (char/4); billing is USD under banana_peel.billing","properties":{"input_tokens":{"type":"integer"},"output_tokens":{"type":"integer"},"total_tokens":{"type":"integer"}}},"metadata":{"type":"object","additionalProperties":{"type":"string"}},"error":{"nullable":true,"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"remediation":{"type":"string"}}},"banana_peel":{"$ref":"#/components/schemas/BananaPeelRun"}}},"BananaPeelSession":{"type":"object","nullable":true,"description":"Metadata about the browser this run used (live view, replay, CDP connect). A record of the run’s browser — not a standalone session you manage. Unused fields are null.","properties":{"id":{"type":"string","nullable":true},"provider_id":{"type":"string","nullable":true},"live_url":{"type":"string","nullable":true},"replay_url":{"type":"string","nullable":true},"connect_url":{"type":"string","nullable":true},"selenium_remote_url":{"type":"string","nullable":true},"region":{"type":"string","nullable":true},"status":{"type":"string","nullable":true}}},"BananaPeelArtifact":{"type":"object","description":"A file captured from the run (downloads, screenshots, recordings). Captured artifacts are hosted by Banana Peel: `url` points at the authenticated retrieval endpoint (GET /v1/responses/{id}/artifacts/{artifactId}, same API-key auth), which 302-redirects to a short-lived signed URL (~10 min TTL) or streams the bytes. Artifacts follow the run retention window (default 7 days) and are purged on response delete / account deletion. Per-file capture cap is 30 MB; up to 10 artifacts per run. When bytes could not be captured, `url` falls back to the provider-hosted URL (typically session-gated and short-lived) and `unavailable_reason` says why.","properties":{"id":{"type":"string","nullable":true},"name":{"type":"string"},"content_type":{"type":"string","description":"Sniffed from magic bytes when the provider declares a generic type."},"size":{"type":"integer"},"url":{"type":"string","nullable":true,"description":"Authenticated Banana Peel retrieval URL for captured artifacts; provider URL only when capture failed."},"kind":{"type":"string","enum":["download","screenshot","recording","log","other"]},"unavailable_reason":{"type":"string","description":"Present when the bytes are not retrievable from Banana Peel (oversize, not captured, …)."}}},"BananaPeelRun":{"type":"object","description":"Canonical banana_peel.run/v1 — superset over all runners. `runner` is the executed runner slug; `routing` is what you requested; `engine` is the execution backend when distinct.","properties":{"schema":{"type":"string","const":"banana_peel.run/v1"},"id":{"type":"string"},"status":{"type":"string","enum":["succeeded","failed","running","blocked","empty","needs_human","cancelled"]},"progress":{"nullable":true,"type":"object","description":"Live in-flight visibility (only while status is in_progress / requires_action): runner attempts so far, current Learning batch, and runner spend to date. Superseded by routing_attempts on the terminal object.","properties":{"attempts":{"type":"array","items":{"type":"object","properties":{"runner":{"type":"string"},"status":{"type":"string","description":"running | succeeded | failed | blocked"},"started_at":{"type":"string","format":"date-time"},"finished_at":{"type":"string","format":"date-time"},"cost_usd":{"type":"number"},"timing_ms":{"type":"number"}}}},"attempts_started":{"type":"integer"},"attempts_finished":{"type":"integer"},"batch":{"type":"object","description":"Present for Learning-mode batch probes (1-based current batch).","properties":{"current":{"type":"integer"},"total":{"type":"integer"},"size":{"type":"integer"}}},"spend_usd":{"type":"number","description":"Runner spend so far (pre-fee)."},"updated_at":{"type":"string","format":"date-time"}}},"required_action":{"nullable":true,"type":"object","description":"MFA/OTP pause mirror (prompt/kind only; never the secret)."},"model":{"type":"string"},"routing":{},"routing_note":{"type":"string","nullable":true},"ranked_runners":{"type":"array","items":{"type":"string"}},"parent_id":{"type":"string","description":"Present on long-task sub-runs only — the parent long task (lt_…). See GET /v1/long-tasks/{id}/runs."},"entity_id":{"type":"string","description":"Present on long-task sub-runs — the manifest entity this run serves (__enumeration__ for the enumeration step)."},"role":{"type":"string","enum":["enumeration","entity_fetch"],"description":"Role of a long-task sub-run within its parent."},"routing_summary":{"type":"object","nullable":true,"description":"Compact \"why was it routed there\" summary — present on every brain-routed run (routing: \"smart\"/category and forced-runner decisions the brain recorded; absent on pinned/chain/learning runs, which have no decision to explain). In-flight polls carry the honest base (planned primary + decision reason); the terminal object adds per-attempt fault attribution, the policy version, and a budget-widening echo. The `summary` sentence is redacted before it leaves the API. The same object is embedded in response.completed / response.failed webhook payloads (data.banana_peel.routing_summary). For the full decision card (candidate EU table, site defense, widening steps + stop reasons) follow `explanation` — GET /v1/responses/{id}/explanation.","properties":{"runner":{"type":"string","nullable":true,"description":"Executed runner (terminal) or the planned primary (in-flight)."},"strategy":{"type":"string","nullable":true,"description":"explore | exploit | forced | fallback (additive enum; widened runs report explore)."},"reason":{"type":"string","nullable":true,"description":"Machine reason code: exploit_expected_utility, exploit_recent_winner, explore_cold_domain, explore_low_confidence, explore_floor, explore_streak_breaker, explore_budget_widening, forced_by_caller, fallback_static_chain, … (additive enum)."},"summary":{"type":"string","description":"One redacted plain-words sentence — the same headline the console decision card shows."},"policy_version":{"type":"string","description":"Routing policy version that made the decision (terminal runs)."},"attempts":{"type":"array","description":"Executed attempts in dispatch order.","items":{"type":"object","properties":{"runner":{"type":"string"},"status":{"type":"string"},"attribution":{"type":"string","description":"Fault attribution from the reward scorer (runner | platform | environment | orchestration | task) — only runner-attributed outcomes affect runner scores."}}}},"widening":{"type":"object","description":"Present when the run widened past the planned candidate set (budget-governed widening).","properties":{"dispatches":{"type":"integer","description":"Widened dispatches executed."},"stop_reason":{"type":"string","nullable":true,"description":"widening_exhausted | pool_exhausted | null (null when the run ended for run-level reasons)."}}},"explanation":{"type":"string","description":"Path to the full decision card: /api/v1/responses/{id}/explanation."}}},"runner":{"type":"string","nullable":true,"description":"Executed runner slug"},"engine":{"type":"string","nullable":true,"description":"Execution backend behind the runner when distinct (e.g. browser-use-cloud)"},"url":{"type":"string","nullable":true},"output":{"description":"Run result in canonical shape, identical across runners: a plain string for freeform tasks, or a structured object/array when the runner returned real JSON (or output_schema was requested). Normalized at a single choke point before persist + envelope build: char-exploded strings ({\"0\":\"T\",\"1\":\"h\",…}) are reassembled and JSON-encoded strings unwrapped, so the shape never varies by runner. With an output_schema this is a HARD CONTRACT: a terminal success populates exactly the schema keys, or the run is failed with error.code schema_violation."},"answer":{"type":"string","nullable":true,"description":"GUARANTEED READ PATH (stable default envelope): best-effort distilled answer string, always present as a key. Runner outputs name their keys differently run to run (current_outstanding_balance vs result vs prose-in-summary) — `answer` is the stable projection, so clients without an output_schema never chase keys. Null exactly when answer_present is false. For legitimately empty results the answer states the empty result (e.g. the \"No Order History Found\" evidence); for download tasks it lists the delivered files."},"answer_present":{"type":"boolean","description":"Delivery-verdict bit: was the REQUESTED ANSWER actually delivered? True on every genuine success — including a legitimate empty/zero result (\"0 orders\" IS the answer; empty-result ≠ failure). False on failed/blocked runs and on hollow completions (output that only narrates login/click actions without the requested value — those are status failed, never billed-and-claimed-successful). Meaningful on terminal states; false while still running."},"evidence":{"nullable":true,"type":"object","description":"Evidence trail backing `answer`: verdict reason, final URL, artifact names.","properties":{"quality_reason":{"type":"string","nullable":true},"final_url":{"type":"string","nullable":true},"artifacts":{"type":"array","items":{"type":"string"}}}},"error":{"nullable":true,"type":"object"},"quality":{"nullable":true,"type":"object","description":"Delivery verdict from the honesty layer (deterministic checks + QA grader): did the run deliver the requested answer? Terminal `status` follows this verdict — not the runner’s mechanical exit code — and the routing scoreboard/brain learn from the same verdict-based labels.","properties":{"verdict":{"type":"string","enum":["succeeded","blocked","failed","empty"]},"confidence":{"type":"number"},"reason":{"type":"string"},"source":{"type":"string","enum":["deterministic","grader"]},"answer_present":{"type":"boolean","description":"Mirrored to banana_peel.answer_present (see there)."}}},"deliverables":{"nullable":true,"type":"object","description":"Delivery verdict for file-deliverable tasks: claimed downloads vs files actually captured as retrievable artifacts. Null for text-only tasks. A run that claims downloads but delivers zero captured files is demoted to failed (verdict not_delivered) — in-session download URLs expire with the browser session and are not deliverables. In structured output, `download_succeeded` is renamed `download_triggered` (the in-session click) with a sibling `file_delivered` boolean (actual capture).","properties":{"claimed":{"type":"integer","description":"Files the run output claims were downloaded."},"delivered":{"type":"integer","description":"File artifacts actually captured and retrievable."},"verdict":{"type":"string","enum":["delivered","partial","not_delivered"]},"note":{"type":"string"}}},"block_reason":{"type":"string","nullable":true},"timing_ms":{"type":"number","nullable":true},"cost":{"nullable":true,"type":"object"},"normalization":{"type":"object","description":"Output normalization echo — present whenever normalization was requested. `applied: true` means the reshape pass executed (and the +$0.10 add-on was billed); `applied: false` includes a short `reason` and `addon_usd: 0` (never charged for a pass that did not run, and never charged when the output_schema contract was violated — see error.code schema_violation).","properties":{"requested":{"type":"boolean","const":true},"applied":{"type":"boolean"},"reason":{"type":"string","nullable":true,"description":"Why the pass did not apply; null when applied."},"addon_usd":{"type":"number","description":"Add-on billed for this run (0.1 when applied, else 0)."}}},"custody":{"type":"object","description":"Custody enforcement echo — present whenever a credential custody policy applied to this run (vault credential and/or request `custody`). The runner candidate set was hard-filtered to `allowed_runners` BEFORE routing; no fallback, race, wave, or learning probe left that set. Persisted on the run record for audit.","properties":{"policy":{"type":"string","description":"Composed policy label (any | trusted_runners | deck_only | pinned:<runner> — joined with + when composed)."},"source":{"type":"string","enum":["vault","request","vault+request"]},"credential_id":{"type":"string","nullable":true,"description":"Vault credential id; null for inline credentials."},"allowed_runners":{"type":"array","items":{"type":"string"},"description":"Effective allowed-runner set after the filter (capped at 40 slugs)."},"allowed_runner_count":{"type":"integer"},"candidates_before_filter":{"type":"integer"},"enforced":{"type":"boolean","const":true}}},"session":{"$ref":"#/components/schemas/BananaPeelSession"},"live_view":{"type":"string","nullable":true},"replay_url":{"type":"string","nullable":true},"screenshot":{"type":"string","nullable":true},"screenshot_unavailable_reason":{"type":"string","nullable":true},"artifacts":{"type":"array","items":{"$ref":"#/components/schemas/BananaPeelArtifact"}},"steps":{"type":"array","items":{"type":"object"}},"fallback_from":{"type":"array","items":{"type":"string"}},"act_script":{"nullable":true,"type":"array"},"created_at":{"type":"string","format":"date-time"},"finished_at":{"type":"string","format":"date-time","nullable":true}}},"RankingTrendBucket":{"type":"object","description":"Per-day relative traffic bucket. Volume is published as shares only — absolute per-day run counts are not part of the public contract.","properties":{"day":{"type":"string","description":"UTC day, YYYY-MM-DD"},"share":{"type":"number","description":"Runner's share of that day's routed attempts (0–1)."},"success_rate":{"type":"number","nullable":true,"description":"Runner's own success rate for the day; null when it had no attempts."}}},"RunnerAvailability":{"type":"object","description":"Live availability from the runner-health circuit breaker. `suspended` runners are skipped by smart routing, fallback chains, fan-outs, and races until `until`; `probation` is half-open — the cooldown expired, the runner is eligible again, and the next routed attempt is the re-probe (reason/detail/until describe the suspension it is recovering from). Pure surfacing — routing already excludes suspended runners, so you never need to filter yourself; use this to explain pool coverage or to avoid pinning a suspended runner.","properties":{"status":{"type":"string","enum":["live","suspended","probation"]},"reason":{"type":"string","nullable":true,"enum":["provider_credits_exhausted","provider_payment_required","provider_auth_rejected","provider_key_missing","repeated_hangs","suspended",null],"description":"Machine-readable suspension reason; null when live."},"detail":{"type":"string","nullable":true,"description":"Human-readable circuit-breaker detail; null when live."},"until":{"type":"string","format":"date-time","nullable":true,"description":"When the suspension cooldown ends/ended and the runner becomes re-probe eligible (in the past for probation); null when live or unknown."}}},"RunnerPoolCounts":{"type":"object","description":"Honest pool size: `total` advertised runners, `live` fully healthy, `suspended` inside a circuit-breaker cooldown right now (skipped by routing), `probation` half-open (eligible; next attempt re-probes). total = live + suspended + probation.","properties":{"total":{"type":"integer"},"live":{"type":"integer"},"suspended":{"type":"integer"},"probation":{"type":"integer"}}},"RankingRow":{"type":"object","description":"Runner-level aggregate. Volume fields are relative-only (traffic_share, traffic_delta_pct); the absolute-count fields (successes/failures/blocks/total/window_runs) were removed from the contract.","properties":{"runner":{"type":"string"},"label":{"type":"string"},"mode":{"type":"string","enum":["infra","agent","oss"],"nullable":true},"availability":{"$ref":"#/components/schemas/RunnerAvailability"},"success_rate":{"type":"number"},"failed_rate":{"type":"number"},"blocked_rate":{"type":"number"},"rank_score":{"type":"number","description":"Wilson lower bound (95%) on lifetime success rate — default sort key."},"domains":{"type":"integer","description":"Distinct domains with recorded outcomes."},"traffic_share":{"type":"number","description":"Share of recent-window routed attempts (0–1)."},"traffic_delta_pct":{"type":"number","nullable":true,"description":"Traffic change, last 7 trend days vs the 7 before (%); null without a base."},"is_new":{"type":"boolean","description":"True when the runner has current-week traffic but no prior-week base."},"low_sample":{"type":"boolean","description":"Qualitative small-sample hint (never the count itself)."},"avg_latency_ms":{"type":"number","nullable":true},"p50_latency_ms":{"type":"number","nullable":true},"p95_latency_ms":{"type":"number","nullable":true},"avg_cost_usd":{"type":"number","nullable":true},"last_seen_at":{"type":"string","format":"date-time","nullable":true},"trend":{"type":"array","items":{"$ref":"#/components/schemas/RankingTrendBucket"}}}},"RankingTask":{"type":"object","properties":{"task":{"type":"string","description":"Task intent (extract, search, login_mfa, …)"},"share":{"type":"number","description":"Share of recorded task outcomes."},"top_runners":{"type":"array","items":{"type":"object","properties":{"runner":{"type":"string"},"share":{"type":"number"},"success_rate":{"type":"number"}}}}}},"Rankings":{"type":"object","properties":{"object":{"type":"string","const":"list"},"live":{"type":"boolean"},"group_by":{"type":"string","const":"runner"},"updated_at":{"type":"string","format":"date-time"},"trend_days":{"type":"integer"},"data":{"type":"array","items":{"$ref":"#/components/schemas/RankingRow"},"description":"Ordered by rank_score descending."},"tasks":{"type":"array","items":{"$ref":"#/components/schemas/RankingTask"}},"note":{"type":"string"}}},"CapabilityMatrix":{"type":"object","description":"Capability rows carry live `availability` (live | suspended | probation + machine-readable reason + `until`) from the runner-health circuit breaker, and `runner_counts` makes the advertised pool honest (live vs total).","properties":{"object":{"type":"string","const":"list"},"schema":{"type":"string","const":"banana_peel.capabilities/v1"},"all_capabilities":{"type":"array","items":{"type":"string","enum":["navigate","act","extract","cdp_connect","live_view","replay","screenshot","downloads","steps","cost","proxy","persist_profile","human_input","totp"]}},"named_runners":{"type":"array","items":{"type":"string"},"example":["browserbase","steel","browser-use","skyvern","hyperbrowser","anchor","deck","playwright","auto"]},"smart_default_chain":{"type":"array","items":{"type":"string"},"example":["browserbase","steel","hyperbrowser","browser-use","skyvern","anchor","deck","playwright"]},"categories":{"type":"array","items":{"type":"string"},"example":["SOC2","HIPAA","EU"]},"runner_counts":{"$ref":"#/components/schemas/RunnerPoolCounts"},"data":{"type":"array","items":{"type":"object","properties":{"slug":{"type":"string"},"mode":{"type":"string"},"capabilities":{"type":"array","items":{"type":"string"}},"notes":{"type":"string","nullable":true},"availability":{"$ref":"#/components/schemas/RunnerAvailability"}}},"example":[{"slug":"agentql","mode":"extract","capabilities":["extract","cost"],"notes":null},{"slug":"airtop","mode":"infra","capabilities":["navigate","act","extract","cdp_connect","screenshot","downloads","steps","cost","persist_profile","live_view"],"notes":null},{"slug":"anchor","mode":"infra","capabilities":["navigate","act","extract","cdp_connect","live_view","replay","screenshot","downloads","steps","cost","proxy","persist_profile","human_input","totp"],"notes":null},{"slug":"apify","mode":"extract","capabilities":["extract","cost"],"notes":null},{"slug":"asteroid","mode":"agent","capabilities":["navigate","act","extract","replay","screenshot","downloads","steps","cost","human_input","totp"],"notes":null},{"slug":"browser-use","mode":"agent","capabilities":["navigate","act","extract","replay","screenshot","downloads","steps","cost","human_input","totp"],"notes":null},{"slug":"browser-use-oss","mode":"oss","capabilities":["navigate","act","extract","screenshot","downloads","steps","cost"],"notes":null},{"slug":"browserbase","mode":"infra","capabilities":["navigate","act","extract","cdp_connect","live_view","replay","screenshot","downloads","steps","cost","proxy","persist_profile","human_input","totp"],"notes":"Also backs the optional Browserbase sessions wrapper."},{"slug":"browserbase-stealth","mode":"infra","capabilities":["navigate","act","extract","cdp_connect","live_view","replay","screenshot","downloads","steps","cost","proxy","persist_profile","human_input","totp"],"notes":null},{"slug":"browserless","mode":"infra","capabilities":["navigate","act","extract","cdp_connect","screenshot","downloads","steps","cost"],"notes":null},{"slug":"camoufox","mode":"oss","capabilities":["navigate","act","extract","screenshot","downloads","steps","cost"],"notes":null},{"slug":"chrome-devtools-mcp","mode":"oss","capabilities":["navigate","act","extract","screenshot","downloads","steps","cost"],"notes":null},{"slug":"claude-computer-use","mode":"agent","capabilities":["navigate","act","extract","screenshot","downloads","steps","cost","human_input","totp"],"notes":null},{"slug":"cloudflare-browser","mode":"extract","capabilities":["extract","cost"],"notes":"Read-oriented edge rendering."},{"slug":"crawl4ai","mode":"oss","capabilities":["navigate","extract","steps","cost"],"notes":null},{"slug":"crawlee","mode":"oss","capabilities":["navigate","extract","screenshot","steps","cost"],"notes":null},{"slug":"deck","mode":"agent","capabilities":["navigate","act","extract","replay","screenshot","steps","cost","human_input","totp"],"notes":"Runner hosted agent — not the Banana Peel API spine."},{"slug":"decodo","mode":"extract","capabilities":["extract","cost"],"notes":null},{"slug":"diffbot","mode":"extract","capabilities":["extract","cost"],"notes":null},{"slug":"exa","mode":"extract","capabilities":["extract","cost"],"notes":null},{"slug":"exa-research","mode":"agent","capabilities":["extract","steps","cost"],"notes":"Research agent — extract/search only; no page actions, no credential custody."},{"slug":"firecrawl","mode":"extract","capabilities":["extract","cost"],"notes":null},{"slug":"google-gemini-agent","mode":"agent","capabilities":["navigate","act","extract","screenshot","downloads","steps","cost","human_input","totp"],"notes":null},{"slug":"hyperagent","mode":"oss","capabilities":["navigate","act","extract","screenshot","downloads","steps","cost"],"notes":null},{"slug":"hyperbrowser","mode":"infra","capabilities":["navigate","act","extract","cdp_connect","live_view","screenshot","downloads","steps","cost","persist_profile","human_input","totp"],"notes":null},{"slug":"jina-reader","mode":"extract","capabilities":["extract","cost"],"notes":null},{"slug":"kernel","mode":"infra","capabilities":["navigate","act","extract","cdp_connect","screenshot","downloads","steps","cost","persist_profile"],"notes":null},{"slug":"lightpanda","mode":"infra","capabilities":["navigate","extract","cdp_connect","steps","cost"],"notes":"No screenshots on this engine."},{"slug":"llm-scraper","mode":"oss","capabilities":["navigate","extract","screenshot","steps","cost"],"notes":null},{"slug":"magnitude","mode":"oss","capabilities":["navigate","act","extract","screenshot","downloads","steps","cost"],"notes":null},{"slug":"manus","mode":"agent","capabilities":["navigate","act","extract","steps","cost"],"notes":null},{"slug":"midscene","mode":"oss","capabilities":["navigate","act","extract","screenshot","downloads","steps","cost"],"notes":null},{"slug":"nodriver","mode":"oss","capabilities":["navigate","act","extract","screenshot","downloads","steps","cost"],"notes":null},{"slug":"notte","mode":"extract","capabilities":["extract","cost"],"notes":null},{"slug":"openai-chatgpt-agent","mode":"agent","capabilities":["navigate","act","extract","screenshot","downloads","steps","cost","human_input","totp"],"notes":null},{"slug":"parallel","mode":"extract","capabilities":["extract","cost"],"notes":null},{"slug":"parallel-task","mode":"agent","capabilities":["extract","steps","cost"],"notes":"Research agent — extract/search only; no page actions, no credential custody."},{"slug":"playwright","mode":"oss","capabilities":["navigate","act","extract","screenshot","downloads","steps","cost"],"notes":null},{"slug":"playwright-mcp","mode":"oss","capabilities":["navigate","act","extract","screenshot","downloads","steps","cost"],"notes":null},{"slug":"puppeteer","mode":"oss","capabilities":["navigate","act","extract","screenshot","downloads","steps","cost"],"notes":null},{"slug":"scrapegraph-ai","mode":"oss","capabilities":["navigate","extract","steps","cost"],"notes":null},{"slug":"scraperapi","mode":"extract","capabilities":["extract","cost"],"notes":null},{"slug":"scrapfly","mode":"extract","capabilities":["extract","cost"],"notes":null},{"slug":"scrapingbee","mode":"extract","capabilities":["extract","cost"],"notes":null},{"slug":"selenium-grid","mode":"oss","capabilities":["navigate","act","extract","screenshot","downloads","steps","cost"],"notes":null},{"slug":"seleniumbase","mode":"oss","capabilities":["navigate","act","extract","screenshot","downloads","steps","cost"],"notes":null},{"slug":"serpapi","mode":"extract","capabilities":["extract","cost"],"notes":null},{"slug":"serper","mode":"extract","capabilities":["extract","cost"],"notes":null},{"slug":"skyvern","mode":"agent","capabilities":["navigate","act","extract","replay","screenshot","downloads","steps","cost","human_input","totp"],"notes":null},{"slug":"stagehand","mode":"oss","capabilities":["navigate","act","extract","screenshot","downloads","steps","cost","persist_profile"],"notes":null},{"slug":"steel","mode":"infra","capabilities":["navigate","act","extract","cdp_connect","live_view","screenshot","downloads","steps","cost","proxy","persist_profile","human_input","totp"],"notes":null},{"slug":"tavily","mode":"extract","capabilities":["extract","cost"],"notes":null},{"slug":"tinyfish","mode":"agent","capabilities":["navigate","act","extract","steps","cost"],"notes":null},{"slug":"zenrows","mode":"extract","capabilities":["extract","cost"],"notes":null}]}}},"PlatformStatus":{"type":"object","description":"Public platform health snapshot — ratios and timestamps only, never absolute run/runner volume.","properties":{"generatedAt":{"type":"string","format":"date-time"},"overall":{"type":"string","enum":["operational","degraded","outage","unknown"]},"components":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","enum":["web","api","runners"]},"label":{"type":"string"},"level":{"type":"string","enum":["operational","degraded","outage","unknown"]},"detail":{"type":"string","description":"Ratios and timestamps only — never absolute volume."}}}},"history":{"type":"array","description":"Daily run-success history, oldest → newest.","items":{"type":"object","properties":{"day":{"type":"string","description":"UTC day, YYYY-MM-DD."},"successRate":{"type":"number","nullable":true,"minimum":0,"maximum":1}}}},"incidents":{"type":"array","items":{},"description":"Always empty today."}}}}},"webhooks":{"response.completed":{"post":{"tags":["Webhooks"],"operationId":"onResponseCompleted","summary":"response.completed","description":"Banana Peel POSTs this event when a run succeeds. Verify X-Banana-Peel-Signature (`t=<unix>,v1=<hmac>` over `{t}.{raw_body}`; during rotation grace the header may contain two v1 values — accept ANY). Return 2xx within 10 seconds to acknowledge (verify → enqueue → 2xx; process async). Delivered at least once — dedupe on event.id. See /docs/api/webhooks.","parameters":[{"name":"X-Banana-Peel-Signature","in":"header","required":true,"schema":{"type":"string"},"description":"t=<unix>,v1=<hmac_sha256_hex>[,v1=<previous hmac> during 24h rotation grace]"},{"name":"X-Banana-Peel-Event-Id","in":"header","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"2XX":{"description":"Return any 2xx within 10 seconds to acknowledge; otherwise Banana Peel retries. Process the event asynchronously after verify + enqueue."}}}},"response.failed":{"post":{"tags":["Webhooks"],"operationId":"onResponseFailed","summary":"response.failed","description":"Run failed, blocked, or hard-TTL run_timeout. Same envelope and signature as response.completed.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"2XX":{"description":"Return any 2xx to acknowledge."}}}},"response.requires_action":{"post":{"tags":["Webhooks"],"operationId":"onResponseRequiresAction","summary":"response.requires_action","description":"Time-sensitive owner-secret input (MFA/OTP only — never captcha). `data.required_action.submit_input` has kind + prompt plus live_view / screenshot / replay_url; POST the answer to /v1/responses/{id}/input. The secret the user will type is never included. May fire more than once per run (multiple MFA rounds). Bot challenges end blocked or re-route instead.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"2XX":{"description":"Return any 2xx to acknowledge."}}}},"response.cancelled":{"post":{"tags":["Webhooks"],"operationId":"onResponseCancelled","summary":"response.cancelled","description":"Caller cancelled via POST /v1/responses/{id}/cancel.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"2XX":{"description":"Return any 2xx to acknowledge."}}}},"long_task.progress":{"post":{"tags":["Webhooks"],"operationId":"onLongTaskProgress","summary":"long_task.progress","description":"A long-task entity settled (delivered / retry / unfetchable / exhausted) or the manifest verified. data is the long-task object (GET /v1/long-tasks/{id}): per-entity table, counts, gaps, budget. Repeating by design — one delivery per (entity, outcome). Same envelope and signature as response.* events.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"2XX":{"description":"Return any 2xx to acknowledge."}}}},"long_task.interaction_required":{"post":{"tags":["Webhooks"],"operationId":"onLongTaskInteractionRequired","summary":"long_task.interaction_required","description":"A long-task sub-task hit MFA / human input and the task paused (status paused_interaction). data.interaction.run_id is the holding CHILD run — answer via POST /v1/responses/{run_id}/input; the task resumes automatically. Fires once per holding run. Short retry schedule (time-sensitive).","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"2XX":{"description":"Return any 2xx to acknowledge."}}}},"long_task.completed":{"post":{"tags":["Webhooks"],"operationId":"onLongTaskCompleted","summary":"long_task.completed","description":"Terminal: every entity delivered or grader-verified unfetchable, none exhausted. Fires once per task.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"2XX":{"description":"Return any 2xx to acknowledge."}}}},"long_task.partial":{"post":{"tags":["Webhooks"],"operationId":"onLongTaskPartial","summary":"long_task.partial","description":"Terminal, first-class: some entities delivered, some did not — data.gaps lists exactly which and why. Never reported as a silent failure. Fires once per task.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"2XX":{"description":"Return any 2xx to acknowledge."}}}},"long_task.failed":{"post":{"tags":["Webhooks"],"operationId":"onLongTaskFailed","summary":"long_task.failed","description":"Terminal: no entities delivered (or enumeration failed). A CANCELLED long task also arrives as this event type with data.status: \"cancelled\" — read data.status to distinguish. Fires once per task.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"2XX":{"description":"Return any 2xx to acknowledge."}}}}},"x-banana-peel":{"canonical_schema":"banana_peel.run/v1","api_version":"2026-08-13","base_url":"https://bananapeel.com","openapi_json":"https://bananapeel.com/openapi.json","openapi_yaml":"https://bananapeel.com/openapi.yaml","llms_txt":"https://bananapeel.com/llms.txt","llms_full_txt":"https://bananapeel.com/llms-full.txt","docs_agents":"https://bananapeel.com/docs/agents","docs_mcp":"https://bananapeel.com/docs/mcp","mcp_package":"npx -y @banana-peel/mcp","named_runners":["browserbase","steel","browser-use","skyvern","hyperbrowser","anchor","deck","playwright","auto"],"smart_default_chain":["browserbase","steel","hyperbrowser","browser-use","skyvern","anchor","deck","playwright"],"routing_modes":["smart","learning","learning-all","SOC2","HIPAA","EU"],"learning":{"batch_size":9,"concurrency":8,"bills_every_attempt":true,"default_max_cost_usd":20,"attempt_max_cost_usd":2,"account_concurrency_default":3},"learning_all":{"batch_size":9,"concurrency":8,"bills_every_attempt":true,"no_early_stop":true,"default_max_cost_usd":20,"attempt_max_cost_usd":2,"catalog":"250+","account_concurrency_default":3},"categories":["SOC2","HIPAA","EU"],"capabilities":["navigate","act","extract","cdp_connect","live_view","replay","screenshot","downloads","steps","cost","proxy","persist_profile","human_input","totp"],"capability_matrix":[{"slug":"agentql","mode":"extract","capabilities":["extract","cost"],"notes":null},{"slug":"airtop","mode":"infra","capabilities":["navigate","act","extract","cdp_connect","screenshot","downloads","steps","cost","persist_profile","live_view"],"notes":null},{"slug":"anchor","mode":"infra","capabilities":["navigate","act","extract","cdp_connect","live_view","replay","screenshot","downloads","steps","cost","proxy","persist_profile","human_input","totp"],"notes":null},{"slug":"apify","mode":"extract","capabilities":["extract","cost"],"notes":null},{"slug":"asteroid","mode":"agent","capabilities":["navigate","act","extract","replay","screenshot","downloads","steps","cost","human_input","totp"],"notes":null},{"slug":"browser-use","mode":"agent","capabilities":["navigate","act","extract","replay","screenshot","downloads","steps","cost","human_input","totp"],"notes":null},{"slug":"browser-use-oss","mode":"oss","capabilities":["navigate","act","extract","screenshot","downloads","steps","cost"],"notes":null},{"slug":"browserbase","mode":"infra","capabilities":["navigate","act","extract","cdp_connect","live_view","replay","screenshot","downloads","steps","cost","proxy","persist_profile","human_input","totp"],"notes":"Also backs the optional Browserbase sessions wrapper."},{"slug":"browserbase-stealth","mode":"infra","capabilities":["navigate","act","extract","cdp_connect","live_view","replay","screenshot","downloads","steps","cost","proxy","persist_profile","human_input","totp"],"notes":null},{"slug":"browserless","mode":"infra","capabilities":["navigate","act","extract","cdp_connect","screenshot","downloads","steps","cost"],"notes":null},{"slug":"camoufox","mode":"oss","capabilities":["navigate","act","extract","screenshot","downloads","steps","cost"],"notes":null},{"slug":"chrome-devtools-mcp","mode":"oss","capabilities":["navigate","act","extract","screenshot","downloads","steps","cost"],"notes":null},{"slug":"claude-computer-use","mode":"agent","capabilities":["navigate","act","extract","screenshot","downloads","steps","cost","human_input","totp"],"notes":null},{"slug":"cloudflare-browser","mode":"extract","capabilities":["extract","cost"],"notes":"Read-oriented edge rendering."},{"slug":"crawl4ai","mode":"oss","capabilities":["navigate","extract","steps","cost"],"notes":null},{"slug":"crawlee","mode":"oss","capabilities":["navigate","extract","screenshot","steps","cost"],"notes":null},{"slug":"deck","mode":"agent","capabilities":["navigate","act","extract","replay","screenshot","steps","cost","human_input","totp"],"notes":"Runner hosted agent — not the Banana Peel API spine."},{"slug":"decodo","mode":"extract","capabilities":["extract","cost"],"notes":null},{"slug":"diffbot","mode":"extract","capabilities":["extract","cost"],"notes":null},{"slug":"exa","mode":"extract","capabilities":["extract","cost"],"notes":null},{"slug":"exa-research","mode":"agent","capabilities":["extract","steps","cost"],"notes":"Research agent — extract/search only; no page actions, no credential custody."},{"slug":"firecrawl","mode":"extract","capabilities":["extract","cost"],"notes":null},{"slug":"google-gemini-agent","mode":"agent","capabilities":["navigate","act","extract","screenshot","downloads","steps","cost","human_input","totp"],"notes":null},{"slug":"hyperagent","mode":"oss","capabilities":["navigate","act","extract","screenshot","downloads","steps","cost"],"notes":null},{"slug":"hyperbrowser","mode":"infra","capabilities":["navigate","act","extract","cdp_connect","live_view","screenshot","downloads","steps","cost","persist_profile","human_input","totp"],"notes":null},{"slug":"jina-reader","mode":"extract","capabilities":["extract","cost"],"notes":null},{"slug":"kernel","mode":"infra","capabilities":["navigate","act","extract","cdp_connect","screenshot","downloads","steps","cost","persist_profile"],"notes":null},{"slug":"lightpanda","mode":"infra","capabilities":["navigate","extract","cdp_connect","steps","cost"],"notes":"No screenshots on this engine."},{"slug":"llm-scraper","mode":"oss","capabilities":["navigate","extract","screenshot","steps","cost"],"notes":null},{"slug":"magnitude","mode":"oss","capabilities":["navigate","act","extract","screenshot","downloads","steps","cost"],"notes":null},{"slug":"manus","mode":"agent","capabilities":["navigate","act","extract","steps","cost"],"notes":null},{"slug":"midscene","mode":"oss","capabilities":["navigate","act","extract","screenshot","downloads","steps","cost"],"notes":null},{"slug":"nodriver","mode":"oss","capabilities":["navigate","act","extract","screenshot","downloads","steps","cost"],"notes":null},{"slug":"notte","mode":"extract","capabilities":["extract","cost"],"notes":null},{"slug":"openai-chatgpt-agent","mode":"agent","capabilities":["navigate","act","extract","screenshot","downloads","steps","cost","human_input","totp"],"notes":null},{"slug":"parallel","mode":"extract","capabilities":["extract","cost"],"notes":null},{"slug":"parallel-task","mode":"agent","capabilities":["extract","steps","cost"],"notes":"Research agent — extract/search only; no page actions, no credential custody."},{"slug":"playwright","mode":"oss","capabilities":["navigate","act","extract","screenshot","downloads","steps","cost"],"notes":null},{"slug":"playwright-mcp","mode":"oss","capabilities":["navigate","act","extract","screenshot","downloads","steps","cost"],"notes":null},{"slug":"puppeteer","mode":"oss","capabilities":["navigate","act","extract","screenshot","downloads","steps","cost"],"notes":null},{"slug":"scrapegraph-ai","mode":"oss","capabilities":["navigate","extract","steps","cost"],"notes":null},{"slug":"scraperapi","mode":"extract","capabilities":["extract","cost"],"notes":null},{"slug":"scrapfly","mode":"extract","capabilities":["extract","cost"],"notes":null},{"slug":"scrapingbee","mode":"extract","capabilities":["extract","cost"],"notes":null},{"slug":"selenium-grid","mode":"oss","capabilities":["navigate","act","extract","screenshot","downloads","steps","cost"],"notes":null},{"slug":"seleniumbase","mode":"oss","capabilities":["navigate","act","extract","screenshot","downloads","steps","cost"],"notes":null},{"slug":"serpapi","mode":"extract","capabilities":["extract","cost"],"notes":null},{"slug":"serper","mode":"extract","capabilities":["extract","cost"],"notes":null},{"slug":"skyvern","mode":"agent","capabilities":["navigate","act","extract","replay","screenshot","downloads","steps","cost","human_input","totp"],"notes":null},{"slug":"stagehand","mode":"oss","capabilities":["navigate","act","extract","screenshot","downloads","steps","cost","persist_profile"],"notes":null},{"slug":"steel","mode":"infra","capabilities":["navigate","act","extract","cdp_connect","live_view","screenshot","downloads","steps","cost","proxy","persist_profile","human_input","totp"],"notes":null},{"slug":"tavily","mode":"extract","capabilities":["extract","cost"],"notes":null},{"slug":"tinyfish","mode":"agent","capabilities":["navigate","act","extract","steps","cost"],"notes":null},{"slug":"zenrows","mode":"extract","capabilities":["extract","cost"],"notes":null}],"notes":["Primary integration: POST /api/v1/responses (OpenAI Responses drop-in)","OpenAI SDK: baseURL {base}/api/v1, model banana-peel, Bearer bp_live_… — see /docs/api/openai","Anthropic thin adapter: POST /api/v1/messages — see /docs/api/anthropic","routing defaults to smart (production). Learning and learning-all teach the router the best path — calibrate the brain so later smart runs pick the winner. learning = live ~54-single-runner pool with early stop (paid calibration, $20); learning-all = full catalog (250+ options), no early stop (thorough paid calibration, $20 default ceiling; coming-soon skipped; do not use for every production job)","stream:true → OpenAI Responses SSE; background:true → 202 + poll or outbound webhooks","Outbound webhooks: POST/GET /api/v1/webhooks, GET/PATCH/DELETE /api/v1/webhooks/{id}, POST …/rotate (24h dual-signature grace), GET …/deliveries, POST …/deliveries/{id}/retry — at-least-once signed events response.completed|failed|requires_action|cancelled (event.id canonical dedupe; sequence per response). Terminal retries over 24h via /api/cron/webhooks; requires_action short-retries then stops when the MFA window closes. Sender timeout 10s. /docs/api/webhooks","Output normalization: normalize + output_schema (or text.format / response_format) → reshape (+$0.10, billed only when the pass runs; outcome echoed under banana_peel.normalization)","output_schema is a hard contract: terminal success populates exactly the schema keys or the run fails with error code schema_violation (nonconforming output kept for inspection; add-on not billed)","Guaranteed read path without a schema: banana_peel.answer (distilled string), answer_present (was the requested answer delivered — a verified empty result like \"0 orders\" counts as delivered), evidence (verdict reason + final URL + artifacts). Terminal status follows the delivery verdict, never raw runner exit codes; hollow completions (login narration without the answer) fail","MCP: /docs/mcp — stdio tools create_response, get_response, submit_input, …","MFA/OTP: When a target site asks for MFA, 2FA, or an OTP, the run pauses immediately and asks the user for the code. It does not keep trying other runners. Poll GET /api/v1/responses/:id until status is requires_action (or handle the response.requires_action webhook). MCP create_response waits until that pause and returns human_action_required plus required_action. Submit the code with POST /api/v1/responses/{id}/input  { \"code\": \"123456\" }  (or { \"answer\": \"…\" } for text/confirm). MCP: submit_input({ id, code }). Then keep polling until completed or failed. CAPTCHA and other bot-detection gates (reCAPTCHA, hCaptcha, Turnstile, DataDome, Cloudflare challenge, “prove you are human”) are solved, re-routed, or end blocked with reason bot_challenge. They never become a human text prompt. Cursor/MCP: ASK THE HUMAN IN CHAT NOW for the site MFA/OTP code. Call submit_input with their code. Do not retry the task or pick another runner. Docs: https://bananapeel.com/docs/guides/mfa","Cancel: POST /v1/responses/{id}/cancel → status cancelled; executed attempts bill, nothing new after","Delete: DELETE /v1/responses/{id} purges copied screenshots/artifacts immediately; later GET is 404 — /docs/security/retention","Identity: GET /v1/me — account_id + created_at + claim_url for the API key (CLI whoami); no secret returned","Retention: copied documents 7 days default; GET/PATCH /v1/workspace (Enterprise 24h|7d|30d; zero by contract)","In-flight visibility: GET /v1/responses/{id} exposes banana_peel.progress (attempts, Learning batch, spend) while in_progress","Routing transparency: brain-routed responses carry banana_peel.routing_summary (runner, strategy, reason code, redacted one-sentence summary, per-attempt fault attribution, policy_version, widening echo) in the envelope AND in response.completed/failed webhook payloads; full decision card at GET /v1/responses/{id}/explanation — /docs/api/routing-explanations","Validation fails free: structurally unusable requests → 400 (missing_input, missing_url for learning/learning-all, invalid_output_schema, invalid_custody, invalid_max_cost_usd, invalid_max_fallbacks, invalid_time_budget_ms, time_budget_unsupported) before any runner bills","Time budget: time_budget_ms (optional; smart/category/learning/learning-all) — set: the run time ceiling (reaped at budget + 10-min grace); OMITTED: the run is UNCAPPED (no run-level deadline; adapter leashes + per-attempt watchdogs + true-hang protection only, 24 h absolute safety ceiling; use background: true). Echoed as banana_peel.time_budget_ms (null + uncapped: true when uncapped). Spend caps are independent","Spend ceiling: max_cost_usd (any mode; Learning defaults to $20, learning-all to $20) — executed attempts reaching it stop NEW dispatch; run ends with error code budget_exhausted + partial results; ceiling echoed under banana_peel.budget and by POST /v1/estimate","Backpressure: 429 rate_limited when an account exceeds its concurrent Learning-family run limit (learning + learning-all, default 3); 429 learning_capacity when instance fan-out capacity is saturated — nothing created or billed. EVERY 429 (including via the /v1/messages adapter and stream:true creates) carries a Retry-After header (seconds); honor it and back off — /docs/api/rate-limits","Runner availability: GET /v1/capabilities rows carry availability (live | suspended | probation + machine-readable reason + until) from the runner-health circuit breaker, plus honest runner_counts (total vs live vs suspended vs probation); suspended runners are auto-skipped by routing — no client-side filtering needed","Custody enforced: vault credentials carry a policy (any | trusted_runners | deck_only | pinned:<runner>; create default trusted_runners, deck_only is opt-in); request custody covers inline credentials; hard pre-filter on the candidate set — conflict → 409 custody_unsatisfiable (nothing billed); echo under banana_peel.custody — /docs/security/custody","Vault management: POST/GET /api/v1/credentials, GET/PATCH/DELETE /api/v1/credentials/{id} — create returns id + metadata; secrets are write-only (never returned); use the id as credential_id on /v1/responses — injected at run time onto the runner routing selects (not a Deck source, not pinned to deck) — /docs/api/credentials","Hard TTL: pinned/chain runs default 20 min then failed with error code run_timeout (BP_RUN_HARD_TTL_MS); smart/learning-family runs are UNCAPPED unless time_budget_ms is set — reaped only on progress staleness (dead worker) or the 24 h safety ceiling","Browserbase wrapper: /api/v1/sessions (not /api/wrappers/browserbase)","Runs vs sessions: a run is a routed unit of work (POST /v1/responses, billed per attempt); a session is a live CDP browser you drive yourself (no task, not smart-routed) — /docs/concepts/sessions","Do not use /api/v2/* — mock playground, not the product API","Do not use POST /api/live/run — console dispatcher only"]}}