Pagination
List endpoints return one bounded page of results, newest first. GET /api/v1/runs accepts limit (1–100, default 50) plus a starting_after run-id cursor: pass next_starting_after (or the last row's id) from the previous page while has_more is true. It also supports filters — status, runner, routing, created_after / created_before — see Runs & history.
Shape
GET /api/v1/runs?limit=50
→ {
"object": "list",
"data": [ /* banana_peel.run/v1 rows, newest first */ ],
"has_more": true,
"first_id": "run_a1…",
"last_id": "run_z9…",
"next_starting_after": "run_z9…"
}
GET /api/v1/runs?limit=50&starting_after=run_z9… # next pageOne honesty rule to know: with filters active, a page is built by scanning a bounded window of history. If the window ends before the page fills, has_more stays true and next_starting_after points at the last scanned run — even when data comes back empty. Keep paginating with next_starting_after until has_more is false; nothing is silently skipped.
List contract (GET /api/v1/runs)
GET/api/v1/runs (listRuns)
List runs for the API key owner
Parameters
| Field | Type | Req | Description |
|---|---|---|---|
| limit | query: integer default 50 | — | — |
| starting_after | query: string | — | Run id cursor — return runs strictly after it in the newest-first order. Unknown ids yield an empty terminal page. |
| created_after | query: string (date-time) | — | ISO 8601 — only runs created strictly after this instant. |
| created_before | query: string (date-time) | — | ISO 8601 — only runs created strictly before this instant. |
| status | query: string running | needs_human | succeeded | failed | blocked | empty | cancelled | — | Effective run status (post watchdog fix-up). |
| runner | query: string | — | Executed runner slug (exact match). |
| routing | query: string smart | learning | learning-all | category | runner | chain | — | Routing class of the requested routing param: runner = pinned single runner, chain = runner array, category = SOC2/HIPAA/EU. |
| parent_id | query: string | — | Only the sub-runs of one long task (lt_…). Implies exclude_children=false. |
| exclude_children | query: boolean default true | — | DEFAULT true: long-task sub-runs are omitted from the top-level list so one fan-out does not bury user-initiated runs. Pass false to interleave them; they are always reachable via parent_id= or GET /v1/long-tasks/{id}/runs. |
Response
| Field | Type | Req | Description |
|---|---|---|---|
| object | "list" | yes | — |
| data | any[] | yes | — |
| data[] | object | — | 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. |
| data[].schema | "banana_peel.run/v1" | — | — |
| data[].id | string | — | — |
| data[].status | string succeeded | failed | running | blocked | empty | needs_human | cancelled | — | — |
| data[].progress | object | — | 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. |
| data[].required_action | object | — | MFA/OTP pause mirror (prompt/kind only; never the secret). |
| data[].model | string | — | — |
| data[].routing | any | — | — |
| data[].routing_note | string | — | — |
| data[].ranked_runners | string[] | — | — |
| data[].parent_id | string | — | Present on long-task sub-runs only — the parent long task (lt_…). See GET /v1/long-tasks/{id}/runs. |
| data[].entity_id | string | — | Present on long-task sub-runs — the manifest entity this run serves (__enumeration__ for the enumeration step). |
| data[].role | string enumeration | entity_fetch | verification | — | Role of a long-task sub-run within its parent. verification = a sampled spot-check confirming randomly chosen claimed entity ids exist. |
| data[].routing_summary | object | — | Compact "why was it routed there" summary — present on every brain-routed run (routing: "smart"/category and forced-runner decisions the brain recorded; absent on pinned/chain/learning runs, which have no decision to explain). In-flight polls carry the honest base (planned primary + decision reason); the terminal object adds per-attempt fault attribution, the policy version, and a budget-widening echo. The `summary` sentence is redacted before it leaves the API. The same object is embedded in response.completed / response.failed webhook payloads (data.banana_peel.routing_summary). For the full decision card (candidate EU table, site defense, widening steps + stop reasons) follow `explanation` — GET /v1/responses/{id}/explanation. |
| data[].runner | string | — | Executed runner slug |
| data[].engine | string | — | Execution backend behind the runner when distinct (e.g. browser-use-cloud) |
| data[].url | string | — | — |
| data[].output | any | — | 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. |
| data[].answer | string | — | 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. |
| data[].answer_present | boolean | — | 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. |
| data[].evidence | object | — | Evidence trail backing `answer`: verdict reason, final URL, artifact names. |
| data[].error | object | — | — |
| data[].quality | object | — | 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. |
| data[].deliverables | object | — | 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). |
| data[].block_reason | string | — | — |
| data[].timing_ms | number | — | — |
| data[].cost | object | — | — |
| data[].normalization | object | — | 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). |
| data[].custody | object | — | 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. |
| data[].session | object | — | 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. |
| data[].live_view | string | — | — |
| data[].replay_url | string | — | — |
| data[].screenshot | string | — | — |
| data[].screenshot_unavailable_reason | string | — | — |
| data[].artifacts | any[] | — | — |
| data[].steps | object[] | — | — |
| data[].fallback_from | string[] | — | — |
| data[].act_script | array | — | — |
| data[].created_at | string (date-time) | — | — |
| data[].finished_at | string (date-time) | — | — |
| has_more | boolean | yes | — |
| first_id | string | — | — |
| last_id | string | — | — |
| next_starting_after | string | — | Present when has_more — cursor for the next page. Usually equals last_id; after a filtered scan window it is the last scanned run id (data may even be empty). Always pass it (not last_id) when present. |
HTTP statuses
200— Run list page (canonical banana_peel.run/v1 rows)400— Invalid filter (invalid_list_filter)401— Invalid API key
Generated from OpenAPI. Do not hand-maintain this table.
Webhook deliveries — cursor pagination
GET /api/v1/webhooks/:id/deliveries supports cursor pagination: limit (1–100, default 20) plus starting_after (a delivery id from the previous page). The response includes has_more; pass the last row's id as starting_after to fetch the next page. Delivery history is retained for 7 days.
GET /api/v1/webhooks/wh_123/deliveries?limit=20&starting_after=whdel_456
→ {
"object": "list",
"data": [ /* banana_peel.webhook_delivery rows */ ],
"has_more": true
}Browserbase-compatible surface
The Browserbase-compatible sessions API preserves Browserbase's own pagination and query strings byte-for-byte (e.g. GET /api/v1/sessions?status=RUNNING) so the official SDK keeps working unchanged — see the Browserbase wrapper.