{
  "openapi": "3.1.0",
  "info": {
    "title": "Twin Browser API",
    "version": "1.0.0",
    "summary": "The browser execution layer for LLM agents.",
    "description": "Compile a browser task once, match re-phrased requests with a semantic dispatch cache, and replay deterministically with zero LLM calls. Bearer-key auth; the tenant is derived from the key. See /llms.txt for the agent-facing overview.\n\nAnti-ban controls (all optional, default-absent, backward compatible) are accepted on the run/job, screenshot, extract, and crawl request bodies: proxyRotate (boolean — force per-request proxy rotation), sessionKey (string — explicit account stickiness key), account (string — account/credential label for multiple sessions per host), platformProxy (boolean — set false to opt out of the platform sticky-proxy default), persistSession (boolean — persist/resume session cookies for the account)."
  },
  "servers": [
    {
      "url": "https://twin-browser.com/api/v1"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Per-tenant API key."
      }
    },
    "schemas": {
      "AdHocRun": {
        "type": "object",
        "required": [
          "url",
          "prompt",
          "success"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Authorized target URL (the authorization signal)."
          },
          "prompt": {
            "type": "string",
            "description": "The goal in natural language."
          },
          "success": {
            "type": "object",
            "description": "Structured success condition: one of {kind:\"statusText\",match}, {kind:\"urlIncludes\",value}, {kind:\"textVisible\",value}, {kind:\"extracted\"}, {kind:\"allOf\",conditions:[...]}, {kind:\"anyOf\",conditions:[...]}."
          },
          "proxy": {
            "type": "string",
            "description": "Optional outbound proxy URL."
          },
          "proxyRotate": {
            "type": "boolean",
            "description": "Anti-ban: force per-request proxy rotation (anonymous scraping)."
          },
          "sessionKey": {
            "type": "string",
            "description": "Anti-ban: explicit override for the account stickiness key."
          },
          "account": {
            "type": "string",
            "description": "Anti-ban: account/credential label (multiple sessions per host)."
          },
          "platformProxy": {
            "type": "boolean",
            "description": "Anti-ban: set false to opt out of the platform sticky-proxy default."
          },
          "persistSession": {
            "type": "boolean",
            "description": "Anti-ban: persist/resume browser session state (cookies) for this account."
          },
          "ignoreSession": {
            "type": "boolean",
            "description": "Force a FRESH login: skip restoring any stored session jar for this run (a new authenticated jar still persists on success). Use to re-establish a stale/expired session on demand."
          },
          "stealth": {
            "type": "boolean",
            "description": "Run in a full desktop browser profile (real Chrome, human-calibrated timing, residential egress) so an authorized run is not locked out by a bot check. Requires a Pro/Enterprise plan (else 403 code:\"plan_required\") AND \"authorized\": true; adds a per-success surcharge. Most tasks do not need it — reach for it when a run is actually turned away. POST /agent is a deprecated alias for a run with this set."
          },
          "authorized": {
            "type": "boolean",
            "description": "Authorization attestation, REQUIRED when stealth:true — assert you are authorized to access this target and accept the stealth terms. Recorded as the durable authorization artifact."
          },
          "hitl": {
            "type": "boolean",
            "description": "Human-in-the-loop: park the run (resumable) on a 2FA/approval wall it can't auto-resolve, returning { status:\"paused\", sessionId } instead of failing. A sign-in intent (credentials/account/login-worded goal) PARKS BY DEFAULT — set false to opt out. Resume via POST /runs/{id}/resume."
          }
        }
      },
      "NamedRun": {
        "type": "object",
        "required": [
          "target",
          "goal"
        ],
        "properties": {
          "target": {
            "type": "string",
            "description": "Authorized target."
          },
          "goal": {
            "type": "string",
            "description": "Named goal."
          },
          "proxy": {
            "type": "string",
            "description": "Optional outbound proxy URL."
          },
          "proxyRotate": {
            "type": "boolean",
            "description": "Anti-ban: force per-request proxy rotation (anonymous scraping)."
          },
          "sessionKey": {
            "type": "string",
            "description": "Anti-ban: explicit override for the account stickiness key."
          },
          "account": {
            "type": "string",
            "description": "Anti-ban: account/credential label (multiple sessions per host)."
          },
          "platformProxy": {
            "type": "boolean",
            "description": "Anti-ban: set false to opt out of the platform sticky-proxy default."
          },
          "persistSession": {
            "type": "boolean",
            "description": "Anti-ban: persist/resume browser session state (cookies) for this account."
          },
          "ignoreSession": {
            "type": "boolean",
            "description": "Force a FRESH login: skip restoring any stored session jar for this run (a new authenticated jar still persists on success). Use to re-establish a stale/expired session on demand."
          },
          "stealth": {
            "type": "boolean",
            "description": "Run in a full desktop browser profile (real Chrome, human-calibrated timing, residential egress) so an authorized run is not locked out by a bot check. Requires a Pro/Enterprise plan (else 403 code:\"plan_required\") AND \"authorized\": true; adds a per-success surcharge. Most tasks do not need it — reach for it when a run is actually turned away. POST /agent is a deprecated alias for a run with this set."
          },
          "authorized": {
            "type": "boolean",
            "description": "Authorization attestation, REQUIRED when stealth:true — assert you are authorized to access this target and accept the stealth terms. Recorded as the durable authorization artifact."
          },
          "hitl": {
            "type": "boolean",
            "description": "Human-in-the-loop: park the run (resumable) on a 2FA/approval wall it can't auto-resolve, returning { status:\"paused\", sessionId } instead of failing. A sign-in intent (credentials/account/login-worded goal) PARKS BY DEFAULT — set false to opt out. Resume via POST /runs/{id}/resume."
          }
        }
      },
      "CompileSkill": {
        "type": "object",
        "required": [
          "target",
          "goal"
        ],
        "properties": {
          "target": {
            "type": "string",
            "format": "uri"
          },
          "goal": {
            "type": "string"
          },
          "as": {
            "type": "string",
            "description": "Optional skill name."
          }
        }
      },
      "AsyncCallback": {
        "type": "object",
        "description": "Optional async-completion webhook. When set, the engine POSTs the job result to callbackUrl on completion, HMAC-signing the body with callbackSecret.",
        "properties": {
          "callbackUrl": {
            "type": "string",
            "format": "uri",
            "description": "Absolute http(s) URL to receive the completed result."
          },
          "callbackSecret": {
            "type": "string",
            "description": "Secret used to HMAC-sign the callback body (requires callbackUrl)."
          }
        }
      },
      "AsyncJob": {
        "allOf": [
          {
            "oneOf": [
              {
                "$ref": "#/components/schemas/AdHocRun"
              },
              {
                "$ref": "#/components/schemas/NamedRun"
              }
            ]
          },
          {
            "$ref": "#/components/schemas/AsyncCallback"
          }
        ]
      },
      "PremiumAgent": {
        "description": "A premium agent run (POST /agent): the same body as /run plus a REQUIRED \"authorized\": true attestation. The full anti-detection stack (stealth fleet + human mimicry + anti-ban sticky egress) is forced on server-side — you do not set stealth/humanize/platformProxy yourself.",
        "allOf": [
          {
            "oneOf": [
              {
                "$ref": "#/components/schemas/AdHocRun"
              },
              {
                "$ref": "#/components/schemas/NamedRun"
              }
            ]
          },
          {
            "type": "object",
            "required": [
              "authorized"
            ],
            "properties": {
              "authorized": {
                "type": "boolean",
                "enum": [
                  true
                ],
                "description": "Legal binding — assert you are authorized to access this target and accept the premium (stealth) terms. Required; the run records this attestation."
              }
            }
          }
        ]
      },
      "Screenshot": {
        "type": "object",
        "required": [
          "url"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Authorized target URL (the authorization signal)."
          },
          "fullPage": {
            "type": "boolean",
            "description": "Capture the full scrollable page (default viewport only)."
          },
          "selector": {
            "type": "string",
            "description": "Optional CSS selector to clip the capture to one element."
          },
          "proxy": {
            "type": "string",
            "description": "Optional outbound proxy URL."
          },
          "blockAssets": {
            "type": "boolean",
            "description": "Abort image/media/font requests to save bandwidth."
          },
          "type": {
            "type": "string",
            "enum": [
              "png",
              "jpeg"
            ],
            "description": "Encoding: png (default, lossless) or jpeg (lossy, smaller — best for LLM input)."
          },
          "quality": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "description": "JPEG quality 1-100 (default 70). Ignored for png."
          },
          "proxyRotate": {
            "type": "boolean",
            "description": "Anti-ban: force per-request proxy rotation (anonymous scraping)."
          },
          "sessionKey": {
            "type": "string",
            "description": "Anti-ban: explicit override for the account stickiness key."
          },
          "account": {
            "type": "string",
            "description": "Anti-ban: account/credential label (multiple sessions per host)."
          },
          "platformProxy": {
            "type": "boolean",
            "description": "Anti-ban: set false to opt out of the platform sticky-proxy default."
          },
          "persistSession": {
            "type": "boolean",
            "description": "Anti-ban: persist/resume browser session state (cookies) for this account."
          },
          "ignoreSession": {
            "type": "boolean",
            "description": "Force a FRESH login: skip restoring any stored session jar for this run (a new authenticated jar still persists on success). Use to re-establish a stale/expired session on demand."
          },
          "stealth": {
            "type": "boolean",
            "description": "Run in a full desktop browser profile (real Chrome, human-calibrated timing, residential egress) so an authorized run is not locked out by a bot check. Requires a Pro/Enterprise plan (else 403 code:\"plan_required\") AND \"authorized\": true; adds a per-success surcharge. Most tasks do not need it — reach for it when a run is actually turned away. POST /agent is a deprecated alias for a run with this set."
          },
          "authorized": {
            "type": "boolean",
            "description": "Authorization attestation, REQUIRED when stealth:true — assert you are authorized to access this target and accept the stealth terms. Recorded as the durable authorization artifact."
          }
        }
      },
      "Extract": {
        "type": "object",
        "required": [
          "url"
        ],
        "description": "Provide \"schema\" or \"fields\" to describe the output, OR rely on the extraction-template library: a per-host / generic-metadata template (see GET /templates) matches most URLs and returns structured data DETERMINISTICALLY (zero LLM, 0 metered cost). \"schema\"/\"fields\" are only required when \"template\" is false.",
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Authorized target URL (the authorization signal)."
          },
          "schema": {
            "type": "object",
            "description": "JSON-schema-like object describing the structured output. Optional when a template matches."
          },
          "fields": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Flat list of field names to extract. Optional when a template matches."
          },
          "template": {
            "oneOf": [
              {
                "type": "boolean"
              },
              {
                "type": "string"
              }
            ],
            "description": "Extraction template control: omit for per-host auto-select, false to disable (then schema/fields is required), or a template name to force one (see GET /templates). When a template supplies the result the response includes \"template\" and llm cost is 0."
          },
          "waitMs": {
            "type": "integer",
            "minimum": 0,
            "description": "Optional extra wait (ms) after load before reading, for JS-hydrated pages."
          },
          "blockAssets": {
            "type": "boolean",
            "description": "Override image/media/font blocking (default on when proxied)."
          },
          "prompt": {
            "type": "string",
            "description": "Optional natural-language extraction hint."
          },
          "proxy": {
            "type": "string",
            "description": "Optional outbound proxy URL."
          },
          "proxyRotate": {
            "type": "boolean",
            "description": "Anti-ban: force per-request proxy rotation (anonymous scraping)."
          },
          "sessionKey": {
            "type": "string",
            "description": "Anti-ban: explicit override for the account stickiness key."
          },
          "account": {
            "type": "string",
            "description": "Anti-ban: account/credential label (multiple sessions per host)."
          },
          "platformProxy": {
            "type": "boolean",
            "description": "Anti-ban: set false to opt out of the platform sticky-proxy default."
          },
          "persistSession": {
            "type": "boolean",
            "description": "Anti-ban: persist/resume browser session state (cookies) for this account."
          },
          "ignoreSession": {
            "type": "boolean",
            "description": "Force a FRESH login: skip restoring any stored session jar for this run (a new authenticated jar still persists on success). Use to re-establish a stale/expired session on demand."
          },
          "stealth": {
            "type": "boolean",
            "description": "Run in a full desktop browser profile (real Chrome, human-calibrated timing, residential egress) so an authorized run is not locked out by a bot check. Requires a Pro/Enterprise plan (else 403 code:\"plan_required\") AND \"authorized\": true; adds a per-success surcharge. Most tasks do not need it — reach for it when a run is actually turned away. POST /agent is a deprecated alias for a run with this set."
          },
          "authorized": {
            "type": "boolean",
            "description": "Authorization attestation, REQUIRED when stealth:true — assert you are authorized to access this target and accept the stealth terms. Recorded as the durable authorization artifact."
          }
        }
      },
      "Search": {
        "type": "object",
        "required": [
          "query"
        ],
        "properties": {
          "query": {
            "type": "string",
            "description": "The search query."
          },
          "depth": {
            "type": "string",
            "enum": [
              "shallow",
              "deep"
            ],
            "default": "shallow",
            "description": "shallow = synchronous ranked results; deep = async job that scrapes the top N matches for content (returns 202 { jobId }; poll GET /jobs/{id})."
          },
          "count": {
            "type": "integer",
            "minimum": 1,
            "maximum": 20,
            "default": 10,
            "description": "Number of ranked results. Results blend Brave (web + discussions + news + faq clusters) and Hacker News, deduped by URL; each result carries a `source` field (web | discussions | news | faq | hackernews)."
          },
          "fetchContent": {
            "type": "boolean",
            "default": false,
            "description": "Shallow only: also load + clean the content of the top results."
          },
          "topN": {
            "type": "integer",
            "minimum": 1,
            "maximum": 10,
            "default": 3,
            "description": "How many top results to fetch (shallow) / scrape (deep)."
          },
          "proxy": {
            "type": "string",
            "description": "Optional outbound proxy URL for content fetches."
          }
        }
      },
      "Map": {
        "type": "object",
        "required": [
          "url"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "A URL on the site to map (the origin is the target)."
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5000,
            "default": 1000,
            "description": "Max URLs to return."
          },
          "includeSubdomains": {
            "type": "boolean",
            "default": false,
            "description": "Include subdomains of the site."
          },
          "search": {
            "type": "string",
            "description": "Case-insensitive substring filter on the URL."
          },
          "scan": {
            "type": "boolean",
            "default": true,
            "description": "Perform the shallow link scan (false = sitemap-only)."
          }
        }
      },
      "Crawl": {
        "type": "object",
        "required": [
          "startUrl"
        ],
        "properties": {
          "startUrl": {
            "type": "string",
            "format": "uri",
            "description": "The URL to start crawling from (the authorization signal)."
          },
          "maxPages": {
            "type": "integer",
            "minimum": 1,
            "maximum": 1000,
            "default": 50,
            "description": "Max pages to read (also caps the credit reserve)."
          },
          "maxDepth": {
            "type": "integer",
            "minimum": 0,
            "default": 3,
            "description": "Max link depth from the start URL."
          },
          "includePaths": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Glob allowlist of path patterns, e.g. [\"/docs/**\"]."
          },
          "excludePaths": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Glob denylist of path patterns."
          },
          "sameDomainOnly": {
            "type": "boolean",
            "default": true,
            "description": "Stay on the start host."
          },
          "respectRobots": {
            "type": "boolean",
            "default": true,
            "description": "Honor robots.txt Disallow."
          },
          "concurrency": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5,
            "default": 3,
            "description": "Parallel page fetches."
          },
          "fields": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Optional per-page structured extract fields."
          },
          "extractSchema": {
            "type": "object",
            "description": "Optional per-page JSON Schema (use instead of fields)."
          },
          "prompt": {
            "type": "string",
            "description": "Optional extraction hint when fields/extractSchema is set."
          },
          "proxy": {
            "type": "string",
            "description": "Optional outbound proxy URL."
          },
          "proxyRotate": {
            "type": "boolean",
            "description": "Anti-ban: force per-request proxy rotation (anonymous scraping)."
          },
          "sessionKey": {
            "type": "string",
            "description": "Anti-ban: explicit override for the account stickiness key."
          },
          "account": {
            "type": "string",
            "description": "Anti-ban: account/credential label (multiple sessions per host) for a logged-in crawl."
          },
          "platformProxy": {
            "type": "boolean",
            "description": "Anti-ban: set false to opt out of the platform sticky-proxy default."
          },
          "persistSession": {
            "type": "boolean",
            "description": "Anti-ban: persist/resume browser session state (cookies) for this account."
          },
          "ignoreSession": {
            "type": "boolean",
            "description": "Force a FRESH login: skip restoring any stored session jar for this run (a new authenticated jar still persists on success). Use to re-establish a stale/expired session on demand."
          },
          "stealth": {
            "type": "boolean",
            "description": "Run in a full desktop browser profile (real Chrome, human-calibrated timing, residential egress) so an authorized run is not locked out by a bot check. Requires a Pro/Enterprise plan (else 403 code:\"plan_required\") AND \"authorized\": true; adds a per-success surcharge. Most tasks do not need it — reach for it when a run is actually turned away. POST /agent is a deprecated alias for a run with this set."
          },
          "authorized": {
            "type": "boolean",
            "description": "Authorization attestation, REQUIRED when stealth:true — assert you are authorized to access this target and accept the stealth terms. Recorded as the durable authorization artifact."
          },
          "callbackUrl": {
            "type": "string",
            "format": "uri",
            "description": "Optional completion webhook URL."
          },
          "callbackSecret": {
            "type": "string",
            "description": "Optional secret to HMAC-sign the callback body."
          }
        }
      },
      "Monitor": {
        "type": "object",
        "required": [
          "name",
          "check_type",
          "url",
          "interval_seconds",
          "callback_url"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Label for the monitor."
          },
          "check_type": {
            "type": "string",
            "enum": [
              "extract",
              "observe"
            ],
            "description": "How the watched value is read."
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "The page to watch."
          },
          "selector": {
            "type": "string",
            "description": "Optional CSS selector scoping an observe check to one element."
          },
          "watch_fields": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "For extract: fields to watch."
          },
          "nl_watch": {
            "type": "string",
            "description": "For extract: natural-language description of what to watch."
          },
          "interval_seconds": {
            "type": "integer",
            "minimum": 60,
            "description": "Check cadence in seconds (min 60)."
          },
          "callback_url": {
            "type": "string",
            "format": "uri",
            "description": "http(s) URL that receives the change webhook."
          },
          "callback_secret": {
            "type": "string",
            "description": "Optional secret to HMAC-sign the change webhook."
          },
          "proxy": {
            "type": "string",
            "description": "Optional outbound proxy URL."
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          }
        }
      }
    }
  },
  "paths": {
    "/run": {
      "post": {
        "operationId": "run",
        "summary": "Run a goal synchronously on an authorized target (~10 credits).",
        "description": "A SIGN-IN run (credentials/account/login-worded goal) that hits a 2FA/approval wall it can't auto-resolve PARKS by default: the 200 body is { status:\"paused\", sessionId, reason, challenge } instead of a completed run — resume it via POST /runs/{id}/resume (or hand a texted/typed code via the streaming /live/resume). Set hitl:false to opt out and get a plain needs-human miss instead. A free-text goal in the { target, goal } shape is a 400 (goal is a named/compiled skill; use { url, prompt, success } for free text).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/AdHocRun"
                  },
                  {
                    "$ref": "#/components/schemas/NamedRun"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Run result, OR { status:\"paused\", sessionId, reason, challenge, url, challengeUrl } when a sign-in run parked on a verification wall.\n\nFrom a PAUSED run there are three ways forward: POST /runs/{id}/resume with a code (a texted/typed code or an approval you completed out of band); POST /runs/{id}/input to click/type on the page yourself (watch it via GET /runs/{id}/stream); or POST /connect/sessions to get a link your END-USER opens to sign in by hand — the only one that works on sites which block automated login outright.\n\nA finished run also reports `engineRev`, and `failures[]` (each recovered/failed action with its error) when anything had to be retried or repaired.\n\nIf the run needed a credential you have not stored, the result carries `code: \"credential_missing\"` with `missingSecret` — store it via POST /secrets, or send the user through POST /connect/sessions.\n\nIf the run hit a wall automation cannot pass (a score-based anti-bot system, or a challenge that persisted), the result carries `code: \"connect_required\"` with a READY one-time `connectUrl` (+ `connectId`, `connectExpiresAt`) — hand it to your end-user, they sign in by hand once, and every later run restores the captured session. Opt out of the auto-mint with `connect:false` and mint your own via POST /connect/sessions. On hosts with repeated score-wall blocks the run may short-circuit to this response WITHOUT executing (reserve refunded, `policy.fastpath: \"score-wall\"`) — that is the policy engine saving you the solver cost."
          },
          "400": {
            "description": "Malformed body (e.g. a free-text goal in the named { target, goal } shape, or a bad success spec).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "Insufficient credits."
          },
          "403": {
            "description": "Unauthorized target."
          }
        }
      }
    },
    "/agent": {
      "post": {
        "operationId": "premium_agent",
        "deprecated": true,
        "summary": "DEPRECATED alias for POST /run with {\"stealth\":true} — prefer that. Forces the full anti-detection stack.",
        "description": "DEPRECATED — a thin alias for POST /run with {\"stealth\":true,\"humanize\":true,\"platformProxy\":true}. Prefer calling /run directly with \"stealth\": true. In ONE call it forces stealth-fleet routing, human-mimicry timing, and sticky residential egress. Requires the same REQUIRED \"authorized\": true attestation and Pro/Enterprise plan as any stealth /run — now enforced by /run itself (the single source of truth), so /run {stealth:true} and /agent gate identically. Billed like a stealth /run: higher-of(~10-credit floor, LLM COGS) plus a per-SUCCESS stealth surcharge.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PremiumAgent"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Run result (same shape as /run)."
          },
          "400": {
            "description": "Missing authorization attestation or malformed body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "Insufficient credits."
          },
          "403": {
            "description": "Plan gate — a Pro subscription is required (code:\"plan_required\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "501": {
            "description": "Stealth fleet not configured (fail closed — never billed as premium while run honestly)."
          }
        }
      }
    },
    "/live": {
      "post": {
        "operationId": "live",
        "summary": "Run a goal and stream the LIVE browser view back as Server-Sent Events (~10 credits).",
        "description": "Same body as /run, but the response is a live SSE screencast of the browser: an `event: meta` frame with { runId }, then `event: frame` JPEG frames, then a terminal `event: result`. The run is recorded, so a durable video is replayable afterwards via GET /runs/{id}/video. Metered like /run (higher-of the ~10-credit floor vs LLM COGS); settles once on the terminal frame or disconnect (failed/no-result → full refund).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/AdHocRun"
                  },
                  {
                    "$ref": "#/components/schemas/NamedRun"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "SSE stream (text/event-stream): meta → frame* → result."
          },
          "401": {
            "description": "Missing/invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "Insufficient credits."
          },
          "403": {
            "description": "Unauthorized target."
          }
        }
      }
    },
    "/live/resume": {
      "post": {
        "operationId": "live_resume",
        "summary": "Hand a 2FA code (or cancel) to a live run holding on an await-user step.",
        "description": "When a live /live run hits a login second factor it cannot auto-resolve, its SSE emits an `event: step` with `type:\"await-user\"` carrying a `token`, a `challenge` (sms-code | totp | email-code | app-approval) and a human `reason`. POST that `token` here with the user-supplied `code` (or `cancel:true`) to unblock the run — it continues on the SAME open SSE, no new connection. Not billed here (the parent run owns metering).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "token"
                ],
                "properties": {
                  "token": {
                    "type": "string",
                    "description": "The token from the await-user SSE event."
                  },
                  "code": {
                    "type": "string",
                    "description": "The verification code (required unless cancel:true)."
                  },
                  "cancel": {
                    "type": "boolean",
                    "description": "Abandon the wait instead of submitting a code."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ ok: true } — the run was unblocked."
          },
          "400": {
            "description": "Missing token, or no code and no cancel.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "No run is awaiting this token (timed out / already resumed)."
          }
        }
      }
    },
    "/runs": {
      "get": {
        "operationId": "list_runs",
        "summary": "List this tenant's runs (newest first, paginated).",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "mode",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "run",
                "job",
                "skill",
                "compile",
                "adapt"
              ]
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "running",
                "complete",
                "failed",
                "paused",
                "cancelled"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of run summaries."
          },
          "401": {
            "description": "Missing/invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/runs/{id}": {
      "get": {
        "operationId": "get_run",
        "summary": "Read a single run: status, success, masked action path, credits, video, timestamps.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Run detail (secret tokens in the path are redacted)."
          },
          "401": {
            "description": "Missing/invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such run for this tenant."
          }
        }
      }
    },
    "/runs/{id}/video": {
      "get": {
        "operationId": "get_run_video",
        "summary": "Stream a run's recorded video (only if the run was created with record:true).",
        "description": "Returns the recorded browser video bytes for a run that opted into recording. Served from durable storage (lazily persisted from the engine on first view). 404 when the run is not this tenant's, was not recorded, or the recording is no longer available.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Video bytes (video/*)."
          },
          "401": {
            "description": "Missing/invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such run, no recording, or recording expired."
          }
        }
      }
    },
    "/runs/{id}/resume": {
      "post": {
        "operationId": "resume_run",
        "summary": "Continue a paused (HITL) sync run after an out-of-band approval.",
        "description": "When a sync /run pauses for human action it returns `{ status:\"paused\", sessionId }`. Once the step is completed out of band (e.g. a phone approval), POST the `sessionId` here to finish the run on the same parked browser session. Charges the flat run floor on completion; still-paused → refunded and returns paused again. This is the continuation channel — a texted/typed CODE goes to POST /live/resume on the streaming path instead.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "sessionId"
                ],
                "properties": {
                  "sessionId": {
                    "type": "string",
                    "description": "The sessionId from the paused run response."
                  },
                  "code": {
                    "type": "string",
                    "description": "A texted/typed verification code, when the pause was a code-entry wall."
                  },
                  "secrets": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string"
                    },
                    "description": "Credentials for a run that paused with code:\"credential_missing\", e.g. { \"email\": \"...\", \"password\": \"...\" }. Merged into the PARKED session's vault so the same browser continues — no restart, no lost session state. Values are redacted from every frame/step/log, and are vaulted for next time unless the tenant has store_secrets off. Use this when YOUR app can collect the credential; when the password belongs to someone whose credentials you should not hold, mint a link with POST /connect/sessions instead."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ status:\"complete\", success, steps, path } or { status:\"paused\", sessionId, reason }."
          },
          "401": {
            "description": "Missing/invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "Insufficient credits."
          },
          "404": {
            "description": "No such run for this tenant."
          },
          "409": {
            "description": "Run is not paused, or the parked session is gone."
          }
        }
      }
    },
    "/runs/{id}/input": {
      "post": {
        "operationId": "control_paused_run",
        "summary": "Drive a paused run's browser by hand (click / type / key / scroll / goto).",
        "description": "The companion to /runs/{id}/resume. Resume hands back a verification CODE; this hands back an INTERACTION, so a person (or an agent reading a screenshot) can finish a step the agent could not — typically a sign-in on a site that blocks automated login, or one you hold no credential for.\n\nCoordinates are FRACTIONS of the viewport (0..1 from the top-left), not pixels: they are hit-tested server-side against the observed element map, so a click lands correctly whatever the viewer's size. Typed text is redacted from the stream and never stored.\n\nThere is no \"done\" call — the run re-checks the page after every interaction and continues by itself once the block clears.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "event"
                ],
                "properties": {
                  "event": {
                    "type": "object",
                    "description": "{ kind:\"click\", x, y } | { kind:\"fill\", x, y, text } | { kind:\"key\", key } | { kind:\"scroll\" } | { kind:\"goto\", url }",
                    "properties": {
                      "kind": {
                        "type": "string",
                        "enum": [
                          "click",
                          "fill",
                          "key",
                          "scroll",
                          "goto"
                        ]
                      },
                      "x": {
                        "type": "number",
                        "description": "Horizontal viewport fraction, 0..1"
                      },
                      "y": {
                        "type": "number",
                        "description": "Vertical viewport fraction, 0..1"
                      },
                      "text": {
                        "type": "string"
                      },
                      "key": {
                        "type": "string"
                      },
                      "url": {
                        "type": "string"
                      }
                    },
                    "required": [
                      "kind"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ ok: true, url } — the page URL after the interaction."
          },
          "400": {
            "description": "Invalid interaction shape."
          },
          "401": {
            "description": "Missing/invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such run for this tenant."
          },
          "409": {
            "description": "Run is not paused, or the parked session expired."
          }
        }
      }
    },
    "/runs/{id}/stream": {
      "get": {
        "operationId": "watch_paused_run",
        "summary": "Watch a paused run's browser live (SSE screencast) so you can drive it.",
        "description": "Server-sent events: `meta`, then `frame` (base64 JPEG) until you disconnect. Drives no agent and costs no credits — it mirrors a browser that is already open and waiting. Pair with POST /runs/{id}/input.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "text/event-stream: event: meta | frame | error."
          },
          "401": {
            "description": "Missing/invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such run for this tenant."
          },
          "409": {
            "description": "Run is not paused, or the parked session expired."
          }
        }
      }
    },
    "/connect/sessions": {
      "post": {
        "operationId": "create_connect_session",
        "summary": "Mint a link that lets a HUMAN sign into a site, so later runs are already logged in.",
        "description": "The answer to `code: \"credential_missing\"`, and to sites that hard-challenge automated logins from datacenter IPs and cannot be signed into by an agent at all.\n\nReturns a single-use `url`. Send your end-user there; they complete the sign-in (password, 2FA, CAPTCHA) in a Twin-Browser-hosted browser, and the resulting session is captured. Re-run the original task afterwards and it restores that session (`sessionRestored: true`) — no credentials, no 2FA.\n\nOAuth-shaped, for sites that offer no OAuth. The link expires in ~30 minutes and is scoped to that one host, and the end-user's PASSWORD is never stored — only the session.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "host"
                ],
                "properties": {
                  "host": {
                    "type": "string",
                    "description": "Site to connect, e.g. \"linkedin.com\" (a full URL also works)."
                  },
                  "account": {
                    "type": "string",
                    "description": "Label when several logins are kept per site; must match the `account` used on /run."
                  },
                  "redirectUri": {
                    "type": "string",
                    "description": "Absolute http(s) URL to return the user to once connected."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "{ id, url, host, account, expiresAt, ttlMinutes } — `url` is shown once."
          },
          "400": {
            "description": "Missing/invalid host, or a non-absolute redirectUri."
          },
          "401": {
            "description": "Missing/invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many pending connect sessions."
          }
        }
      },
      "get": {
        "operationId": "list_connect_sessions",
        "summary": "List connect links and whether each sign-in was completed.",
        "description": "Use this to check whether the user finished the sign-in before re-running the task. Never returns the token.",
        "responses": {
          "200": {
            "description": "{ sessions: [{ id, host, account, status, created_at, expires_at, connected_at, error }] }. status: pending | active | connected | failed | expired | cancelled."
          },
          "401": {
            "description": "Missing/invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/accounts": {
      "get": {
        "operationId": "list_accounts",
        "summary": "Which logins this tenant already has — metadata only, never a credential.",
        "description": "The discovery half of the credential system: every run-shaped endpoint accepts an `account` label to pick one of several logins for a host, and this endpoint tells you which labels exist. Read-only metadata — host, label, and a MASKED email preview. The ciphertext is never loaded; a credential leaves the system exactly once, at fill-time inside the browser.",
        "parameters": [
          {
            "name": "host",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Narrow to logins usable on one site (e.g. \"linkedin.com\"). Accounts with no host bound (\"any site\") are always included."
          },
          {
            "name": "url",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uri"
            },
            "description": "Alternative to host — a full URL whose hostname is used."
          }
        ],
        "responses": {
          "200": {
            "description": "{ accounts: [{ label, host, emailPreview, hasPassword, createdAt }], count }."
          },
          "401": {
            "description": "Missing/invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/sessions/import": {
      "post": {
        "operationId": "import_session",
        "summary": "Import a browser session captured on the user’s OWN machine (the capture extension).",
        "description": "The counterpart to the hosted connect flow, for popup-OAuth sign-ins (\"Continue with Google\") that only complete in a real local browser: the user signs in on their own machine and the companion extension ships the resulting cookies here. They are assembled into an encrypted session exactly as a hosted sign-in would be, so every later run on that host is `sessionRestored: true`. Cookies are the captured SESSION, never a password. Stored under both apex and www forms of the host.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "host",
                  "cookies"
                ],
                "properties": {
                  "host": {
                    "type": "string",
                    "description": "The site the session belongs to (e.g. \"linkedin.com\"). `url` or `target` are accepted aliases."
                  },
                  "account": {
                    "type": "string",
                    "description": "Optional account label, to keep several logins per host apart."
                  },
                  "cookies": {
                    "type": "array",
                    "description": "Browser cookies: [{ name, value, domain, path?, expires?/expirationDate?, httpOnly?, secure?, sameSite? }]. Non-usable entries are dropped; at least one usable cookie is required.",
                    "items": {
                      "type": "object"
                    }
                  },
                  "origins": {
                    "type": "array",
                    "description": "Optional localStorage origins in Playwright storageState form.",
                    "items": {
                      "type": "object"
                    }
                  },
                  "ttlDays": {
                    "type": "integer",
                    "description": "Optional session TTL override in days."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ ok: true, host, account?, cookieCount } — later runs on the host restore this session."
          },
          "400": {
            "description": "Missing host, empty cookies, or no usable cookie.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/tenants": {
      "post": {
        "operationId": "create_subtenant",
        "summary": "Reseller: create a subtenant under your account and mint its API key.",
        "description": "Requires the `tenants:provision` scope. `billing` decides who pays: \"self\" (the subtenant pays from its own balance; fund it with `credits`) or \"parent\" (your credit pool pays, each charge attributed to the subtenant). The API key is returned ONCE.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "billing": {
                    "type": "string",
                    "enum": [
                      "self",
                      "parent"
                    ],
                    "description": "Default \"self\"."
                  },
                  "plan": {
                    "type": "string"
                  },
                  "credits": {
                    "type": "integer",
                    "description": "Opening grant transferred from your balance (billing:\"self\" only)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "{ tenant: { id, name, slug, plan, billing }, apiKey, granted } — apiKey is shown once."
          },
          "400": {
            "description": "Invalid name/billing, or credits on a parent-billed subtenant."
          },
          "402": {
            "description": "Insufficient credits to fund the requested grant."
          },
          "403": {
            "description": "Key lacks the tenants:provision scope."
          },
          "409": {
            "description": "Nested resellers are not supported."
          }
        }
      },
      "get": {
        "operationId": "list_subtenants",
        "summary": "Reseller: list your subtenants with balances and what they cost you.",
        "responses": {
          "200": {
            "description": "{ subtenants: [{ id, name, billing, balance, billed_to_parent }] }."
          },
          "403": {
            "description": "Key lacks the tenants:provision scope."
          }
        }
      }
    },
    "/email-inbox": {
      "get": {
        "operationId": "list_email_inboxes",
        "summary": "List the connected IMAP inboxes used to auto-resolve emailed 2FA codes (host/user only).",
        "responses": {
          "200": {
            "description": "{ inboxes: [{ name, host, user }] } — never returns the app-password."
          },
          "401": {
            "description": "Missing/invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "connect_email_inbox",
        "summary": "Connect an IMAP inbox so sign-in runs auto-fill the emailed 2FA code (no HITL pause).",
        "description": "Stores an IMAP inbox (encrypted) that the engine's email-first path reads to fetch and fill an emailed verification code itself, so a login run resolves the 2FA wall without parking for a human. The inbox is VALIDATED against a real IMAP connection before it is stored (a wrong host/app-password is rejected 400). Use a provider app-password (e.g. Gmail: host imap.gmail.com, port 993). The app-password is write-only — never returned. Re-posting the same user updates it. This is the API-key equivalent of the dashboard's Connect-inbox action.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "host",
                  "user",
                  "pass"
                ],
                "properties": {
                  "host": {
                    "type": "string",
                    "description": "IMAP host, e.g. imap.gmail.com."
                  },
                  "port": {
                    "type": "integer",
                    "default": 993,
                    "description": "IMAP port (default 993)."
                  },
                  "user": {
                    "type": "string",
                    "description": "The inbox email address."
                  },
                  "pass": {
                    "type": "string",
                    "description": "App-password for the inbox (write-only; never returned)."
                  },
                  "secure": {
                    "type": "boolean",
                    "default": true,
                    "description": "Use TLS (default true)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ name, host, user } — the inbox was validated and stored."
          },
          "400": {
            "description": "Invalid host/user/pass, or the IMAP connection was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The connection check could not run right now — retry."
          }
        }
      },
      "delete": {
        "operationId": "delete_email_inbox",
        "summary": "Disconnect a stored IMAP inbox by name.",
        "parameters": [
          {
            "name": "name",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The inbox name from the list/connect response, e.g. email:you@gmail.com."
          }
        ],
        "responses": {
          "200": {
            "description": "{ name } — the inbox was removed."
          },
          "400": {
            "description": "Missing name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/jobs": {
      "post": {
        "operationId": "submit_job",
        "summary": "Submit a goal as an async job (~10 credits). Returns 202 { jobId }.",
        "description": "Optionally pass callbackUrl/callbackSecret to receive an HMAC-signed result webhook on completion. Like /run, a SIGN-IN job that hits a 2FA/approval wall PARKS by default — GET /jobs/{id} then reports status:\"paused\" with a sessionId (resume via POST /runs/{id}/resume); set hitl:false to opt out.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AsyncJob"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Job accepted; poll GET /jobs/{id}."
          },
          "402": {
            "description": "Insufficient credits."
          }
        }
      },
      "get": {
        "operationId": "list_jobs",
        "summary": "List this tenant's recent async jobs.",
        "responses": {
          "200": {
            "description": "Array of jobs."
          }
        }
      }
    },
    "/jobs/{id}": {
      "get": {
        "operationId": "get_job",
        "summary": "Poll an async job; settles credits on first terminal status.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Job status."
          },
          "404": {
            "description": "No such job."
          }
        }
      }
    },
    "/jobs/{id}/stream": {
      "get": {
        "operationId": "stream_job",
        "summary": "Stream an async job's status as Server-Sent Events (an alternative to polling).",
        "description": "Emits `event: status` frames ({ jobId, status, success, steps, error }) as the engine transitions the job, then a final authoritative frame, then closes. Billing settles once on close (complete → charge, failed/cancelled → refund); a client that disconnects early settles on its next poll.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "SSE stream (text/event-stream) of status frames."
          },
          "401": {
            "description": "Missing/invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such job."
          }
        }
      }
    },
    "/jobs/{id}/cancel": {
      "post": {
        "operationId": "cancel_job",
        "summary": "Cancel a running async job — full refund of the reserved credits.",
        "description": "Best-effort aborts the in-flight engine work, then flips the run to cancelled and refunds the reservation in full. Idempotent: cancelling an already-terminal job is a no-op that returns its current state with cancelled:false.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Cancellation result: { jobId, status, cancelled }."
          },
          "401": {
            "description": "Missing/invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such job."
          }
        }
      }
    },
    "/screenshot": {
      "post": {
        "operationId": "screenshot",
        "summary": "Capture a single page as a PNG (flat ~1 credit, no LLM cost).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Screenshot"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "PNG image bytes (image/png), or JSON { url } when the engine returns a hosted URL. Charge in x-credits-charged / credits_charged."
          },
          "402": {
            "description": "Insufficient credits."
          }
        }
      }
    },
    "/extract": {
      "post": {
        "operationId": "extract",
        "summary": "Extract structured JSON from a page with an LLM (metered, ~5-credit floor).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Extract"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Structured result + credits_charged."
          },
          "402": {
            "description": "Insufficient credits."
          }
        }
      }
    },
    "/templates": {
      "get": {
        "operationId": "list_templates",
        "summary": "List the extraction-template catalog used by /extract and /etl (public, no key).",
        "description": "Per-host + generic-metadata templates that extract structured data deterministically (zero LLM). Optional ?url= reports which template matches that URL.",
        "parameters": [
          {
            "name": "url",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uri"
            },
            "description": "If given, also report which template matches this URL."
          }
        ],
        "responses": {
          "200": {
            "description": "Catalog: { templates:[{ name, label, generic }], url?, matched? }."
          }
        }
      }
    },
    "/search": {
      "post": {
        "operationId": "search",
        "summary": "Web search across blended sources (Brave web/discussions/news/faq + Hacker News, deduped, each result tagged with source). Shallow (default): sync ranked results (~3 cr) + optional fetch (+1 cr/page). Deep (depth:\"deep\"): async job, scrapes top N (3 cr + 5 cr/page), returns 202 { jobId }.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Search"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Shallow: ranked results (+ cleaned content when requested) and credits_charged."
          },
          "202": {
            "description": "Deep: job accepted; poll GET /jobs/{id}."
          },
          "402": {
            "description": "Insufficient credits."
          },
          "503": {
            "description": "Search backend not configured.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/map": {
      "post": {
        "operationId": "map",
        "summary": "Discover all URLs of a site fast — sitemap + robots + shallow link scan (flat ~2 credits, no LLM).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Map"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "List of discovered URLs with source_counts and truncated flag."
          },
          "402": {
            "description": "Insufficient credits."
          }
        }
      }
    },
    "/crawl": {
      "post": {
        "operationId": "submit_crawl",
        "summary": "Crawl an entire site as an async job — billed per page (~3 credits/page). Returns 202 { jobId }.",
        "description": "Poll GET /jobs/{id} for the result (an array of pages). Optionally pass callbackUrl/callbackSecret for a completion webhook.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Crawl"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Crawl accepted; poll GET /jobs/{id}."
          },
          "402": {
            "description": "Insufficient credits."
          }
        }
      }
    },
    "/etl": {
      "post": {
        "operationId": "etl",
        "summary": "General ETL: extract → transform (clean + optional schema) → chunk → embed → load into the queryable content store.",
        "description": "Domain-agnostic ingest of any source (url | html | text), one or many per call. Embedding is ON by default (set embedding:false for store-only; store:false to return content without persisting). Billed per source: higher-of(~5-credit floor, metered LLM COGS) — only schema/fields extraction incurs LLM cost.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "A single URL to ingest."
                  },
                  "urls": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "uri"
                    },
                    "description": "Multiple URLs to ingest in one call."
                  },
                  "html": {
                    "type": "string",
                    "description": "Raw HTML to ingest (no fetch)."
                  },
                  "text": {
                    "type": "string",
                    "description": "Raw text to ingest (no fetch)."
                  },
                  "sources": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    },
                    "description": "Explicit source list; each entry has exactly one of {url|html|text} plus optional metadata."
                  },
                  "formats": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "text",
                        "markdown",
                        "html",
                        "json"
                      ]
                    },
                    "description": "Output formats (default ['text']); 'json' or schema/fields runs structured extraction."
                  },
                  "schema": {
                    "type": "object",
                    "description": "Optional JSON Schema for structured extraction."
                  },
                  "fields": {
                    "description": "Optional fields spec (array of names / {name,description} / object map)."
                  },
                  "prompt": {
                    "type": "string",
                    "description": "Optional instruction to guide structured extraction."
                  },
                  "embedding": {
                    "type": "boolean",
                    "description": "Embed chunks for semantic query (default true)."
                  },
                  "store": {
                    "type": "boolean",
                    "description": "Persist to the content store (default true)."
                  },
                  "collection": {
                    "type": "string",
                    "description": "Optional namespace to group + filter ingested documents."
                  },
                  "chunkSize": {
                    "type": "number",
                    "description": "Chunk size in chars (default 1200)."
                  },
                  "chunkOverlap": {
                    "type": "number",
                    "description": "Chunk overlap in chars (default 120)."
                  },
                  "proxy": {
                    "type": "string",
                    "description": "Optional proxy URL for url sources."
                  },
                  "waitMs": {
                    "type": "number",
                    "description": "Optional settle wait (ms) before reading url sources."
                  },
                  "metadata": {
                    "type": "object",
                    "description": "Optional metadata stored with every document in this call."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Per-document results { source, url, title, chunks, embedded, stored, document_id, data? } + totals + credits_charged."
          },
          "402": {
            "description": "Insufficient credits."
          }
        }
      }
    },
    "/etl/query": {
      "post": {
        "operationId": "etl_query",
        "summary": "Semantic search over content ingested via /etl — top-k similar chunks with source url/title (flat 1 credit, no LLM).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "query"
                ],
                "properties": {
                  "query": {
                    "type": "string",
                    "description": "Natural-language query to match against stored content."
                  },
                  "k": {
                    "type": "number",
                    "description": "Number of chunks to return (1-50, default 8)."
                  },
                  "collection": {
                    "type": "string",
                    "description": "Optional collection namespace to scope the search."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Top-k matches [{ content, url, title, similarity, document_id, chunk_index }] + credits_charged."
          },
          "402": {
            "description": "Insufficient credits."
          }
        }
      }
    },
    "/dispatch": {
      "post": {
        "operationId": "dispatch",
        "summary": "Semantic cache: match to a compiled skill (hit) or compile + cache (miss).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AdHocRun"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Dispatch result (cache hit or compiled)."
          }
        }
      }
    },
    "/skills": {
      "get": {
        "operationId": "list_skills",
        "summary": "List this tenant's compiled skills.",
        "responses": {
          "200": {
            "description": "Array of skills."
          }
        }
      },
      "post": {
        "operationId": "compile_skill",
        "summary": "Compile a skill — discover + minimize (~50 credits).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CompileSkill"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Compiled skill descriptor."
          }
        }
      }
    },
    "/skills/{name}/run": {
      "post": {
        "operationId": "run_skill",
        "summary": "Deterministically replay a compiled skill — no LLM (~1 credit).",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Replay result."
          }
        }
      }
    },
    "/library": {
      "get": {
        "operationId": "search_library",
        "summary": "Search the cross-tenant shared skill corpus — metadata only (free).",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Matching skills (metadata)."
          }
        }
      }
    },
    "/secrets": {
      "get": {
        "operationId": "list_secrets",
        "summary": "List this tenant's stored secret NAMES (write-only vault — values are never returned). Free.",
        "responses": {
          "200": {
            "description": "Secrets: { secrets:[{ name, created_at }] }."
          },
          "401": {
            "description": "Missing/invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "put_secret",
        "summary": "Store (encrypt) a named secret for this tenant, referenceable in prompts as {{secret:NAME}}. Free.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "value"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Secret name (1–64 chars of letters, digits, \"_\", \".\", \"-\")."
                  },
                  "value": {
                    "type": "string",
                    "description": "The secret value to encrypt at rest (never returned by GET)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Stored: { name }."
          },
          "400": {
            "description": "Invalid name/value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/secrets/{name}": {
      "delete": {
        "operationId": "delete_secret",
        "summary": "Delete a stored secret by name for this tenant. Free.",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted: { name }."
          },
          "401": {
            "description": "Missing/invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such secret for this tenant."
          }
        }
      }
    },
    "/cache/stats": {
      "get": {
        "operationId": "cache_stats",
        "summary": "Semantic-cache analytics for this tenant — hit rate + credits saved (free, no charge).",
        "description": "Reduces this tenant's runs over a rolling window into per-mode counts (skill = cache HIT, adapt = cross-tenant ADAPT, compile = MISS, run/job = uncached), a cache hit_rate = (skill+adapt)/(skill+adapt+compile), total credits_charged, and estimated_credits_saved (credits the cache HITs/ADAPTs saved versus cold compiles). Read-only metadata lookup — does not reserve or charge credits.",
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 365,
              "default": 30
            },
            "description": "Rolling window size in days (clamped 1..365)."
          }
        ],
        "responses": {
          "200": {
            "description": "Cache analytics: { window_days, totals:{ skill, adapt, compile, run, job, other, total }, hit_rate, credits_charged, estimated_credits_saved }."
          },
          "401": {
            "description": "Missing/invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/observe": {
      "post": {
        "operationId": "observe",
        "summary": "Serialize a page into indexed DOM state without acting (~1 credit).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Indexed DOM state."
          }
        }
      }
    },
    "/solve-captcha": {
      "post": {
        "operationId": "solve_captcha",
        "summary": "Solve a captcha blocking an in-flight session (~5 credits, charged only on success).",
        "description": "Asks the engine to solve a captcha in an already-open session (via 2captcha). No target URL — the session already holds the page context. Charges 5 credits ONLY when a captcha was actually solved; an unsolved attempt is fully refunded (credits_charged:0).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "sessionId"
                ],
                "properties": {
                  "sessionId": {
                    "type": "string",
                    "description": "The in-flight session holding the captcha-blocked page."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Result: { solved, type, credits_charged } (charged only when solved:true)."
          },
          "401": {
            "description": "Missing/invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "Insufficient credits."
          }
        }
      }
    },
    "/monitors": {
      "get": {
        "operationId": "list_monitors",
        "summary": "List this tenant's monitors (secrets excluded).",
        "responses": {
          "200": {
            "description": "Array of monitors."
          }
        }
      },
      "post": {
        "operationId": "create_monitor",
        "summary": "Create a monitor that watches a page on a schedule and pushes a signed webhook on change.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Monitor"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Monitor created."
          },
          "400": {
            "description": "Invalid monitor definition.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/monitors/{id}": {
      "get": {
        "operationId": "get_monitor",
        "summary": "Read a single monitor (secrets excluded).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Monitor detail."
          },
          "404": {
            "description": "No such monitor for this tenant."
          }
        }
      },
      "patch": {
        "operationId": "update_monitor",
        "summary": "Update / pause / resume a monitor (active, interval, callback, selector, watch spec).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Updated monitor."
          },
          "404": {
            "description": "No such monitor."
          }
        }
      },
      "delete": {
        "operationId": "delete_monitor",
        "summary": "Delete a monitor and its check history.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted."
          },
          "404": {
            "description": "No such monitor."
          }
        }
      }
    },
    "/monitors/{id}/history": {
      "get": {
        "operationId": "get_monitor_history",
        "summary": "A monitor's recent check history (changed/unchanged/error + value excerpts).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated check history."
          },
          "404": {
            "description": "No such monitor."
          }
        }
      }
    },
    "/pricing": {
      "get": {
        "operationId": "pricing",
        "summary": "Public credit rate card (no API key required).",
        "security": [],
        "parameters": [
          {
            "name": "region",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Credit rate card."
          }
        }
      }
    }
  }
}