{"openapi":"3.1.0","info":{"title":"Banana Peel Browser Agent API","version":"0.1.0","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+ runners). 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"}},"servers":[{"url":"https://bananapeel.com/api","description":"Production Cloud Run (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`. 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/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":"Limit-only listing (no cursors yet). Prefer console for deep history.","security":[{"bearerAuth":[]},{"apiKey":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":50}}],"responses":{"200":{"description":"Run list (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"}}}}}}},"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":{"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-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]}}},"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 AES-256-GCM encrypted; 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"]},"description":"Defaults to all four 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"]}},"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"]},"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."}}},"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"}},"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."}}}}},"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-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","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) before any runner bills","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: runs never sit at running forever — default 20 min, then failed with error code run_timeout (BP_RUN_HARD_TTL_MS)","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"]}}