{
  "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."
          },
          "files": {
            "type": "object",
            "description": "Files the agent can attach during this run: {name: {url}}, up to 5. Referenced when it finds a file input or an upload control — with exactly one file declared, any name it guesses resolves to it. Pair with POST /download to move an asset from one site to another."
          }
        }
      },
      "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."
          },
          "files": {
            "type": "object",
            "description": "Files the agent can attach during this run: {name: {url}}, up to 5. Referenced when it finds a file input or an upload control — with exactly one file declared, any name it guesses resolves to it. Pair with POST /download to move an asset from one site to another."
          }
        }
      },
      "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 top-to-bottom as one image (default: current viewport only). Use true for full-page layout/content understanding — e.g. handing the image to a vision model that needs the whole page at once — and false when only the above-the-fold view (or a `selector`-clipped element) matters."
          },
          "device": {
            "type": "string",
            "enum": [
              "laptop",
              "mobile"
            ],
            "description": "Named viewport preset: \"laptop\" (default, 1280x800 desktop viewport) or \"mobile\" (412x915 phone viewport, touch-enabled) — picks which CSS breakpoint a responsive page renders at. Overridden by an explicit `viewport`."
          },
          "viewport": {
            "type": "object",
            "description": "Explicit CSS viewport in pixels, overriding `device`'s size (its touch/mobile flags still apply if both are given). Use for a size neither preset covers.",
            "properties": {
              "width": {
                "type": "number"
              },
              "height": {
                "type": "number"
              }
            },
            "required": [
              "width",
              "height"
            ]
          },
          "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."
          },
          "settle": {
            "type": "boolean",
            "description": "Wait for the page to be VISUALLY finished before capturing — webfonts resolved, images decoded, entrance animations played out, two byte-identical frames. Default true. Set false to capture at the load event instead."
          },
          "settleMs": {
            "type": "integer",
            "minimum": 0,
            "description": "Ceiling (ms) on that readiness wait (default 8000). A quiet page settles well under it; this only bounds a page that never stops moving."
          },
          "waitMs": {
            "type": "integer",
            "minimum": 0,
            "description": "Extra wait (ms) applied AFTER the readiness settle, for something only you know about."
          },
          "animations": {
            "type": "string",
            "enum": [
              "disabled",
              "allow"
            ],
            "description": "disabled (default): finite CSS/Web animations are fast-forwarded to their end state and infinite ones frozen at capture time, so a long animation is still captured resolved. allow: capture whatever frame is live."
          },
          "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."
          }
        }
      },
      "Download": {
        "type": "object",
        "required": [
          "url"
        ],
        "description": "Fetch an authorized asset's raw bytes (a logo, image, PDF, or other document) via a single HTTP GET through the browser's own request context — not a page render. When this tenant has a saved session for the asset's host, it is applied automatically.",
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Direct URL of the asset. Its origin must be a valid http(s) target."
          },
          "referrer": {
            "type": "string",
            "description": "Optional Referer header — some CDNs hotlink-check this before serving an asset."
          },
          "proxy": {
            "type": "string",
            "description": "Optional outbound proxy URL."
          },
          "account": {
            "type": "string",
            "description": "Which stored login's session to use, when several are kept for this host (see GET /accounts)."
          },
          "maxBytes": {
            "type": "integer",
            "minimum": 1,
            "description": "Byte ceiling before refusing the download. Default 10 MB (10485760), hard-capped at 25 MB."
          },
          "stealth": {
            "type": "boolean",
            "description": "Route through the stealth fleet for an asset host that bot-walls plain requests. Requires a Pro/Enterprise plan (else 403 code:\"plan_required\"). Default false."
          }
        }
      },
      "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)."
          },
          "blockedDomains": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Never fetch requests to these hosts (subdomains included). Anti-bot, CAPTCHA and auth vendors are always allowed regardless."
          },
          "blockAds": {
            "type": "boolean",
            "default": false,
            "description": "Also abort the known ad / analytics / tracker hosts. Cuts proxy bandwidth and load time; leave off on guarded targets, where the absence of the usual third-party requests is itself unusual."
          },
          "captureXhr": {
            "type": "string",
            "description": "Collect the page's own background XHR/fetch responses whose URL matches this pattern (substring, `*` wildcard, or /regex/flags) and return them as \"capturedXhr\" — read a site's JSON API instead of its rendered DOM."
          },
          "prompt": {
            "type": "string",
            "description": "Optional natural-language extraction hint."
          },
          "rowSelector": {
            "type": "string",
            "description": "ROW EXPANSION: CSS selector for ONE row/card of a repeating listing. The engine expands it into every structurally-alike element on the page (same depth, tag, parent and grandparent tag, plus attribute similarity) and extracts PER ROW — so a 50-row listing is read from the rows instead of the whole page. Response carries \"rowsFound\"."
          },
          "rowPrompt": {
            "type": "string",
            "description": "ROW EXPANSION without a selector: describe ONE row in words (\"a product card\"). Costs a single metered LLM call to resolve a selector, which is then expanded exactly as \"rowSelector\". Ignored when \"rowSelector\" is given."
          },
          "rowFields": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Per-row field map { fieldName: cssSelectorRelativeToTheRow } read DETERMINISTICALLY off each row — zero LLM (an `a` yields its href, an `img` its src, anything else its trimmed text). Returns \"data\" as an array, one object per row. Max 50 fields."
          },
          "maxRows": {
            "type": "integer",
            "minimum": 1,
            "maximum": 500,
            "default": 100,
            "description": "Cap on expanded rows (default 100, hard ceiling 500)."
          },
          "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, Crawl-delay and Request-rate."
          },
          "startUrls": {
            "type": "array",
            "items": {
              "oneOf": [
                {
                  "type": "string"
                },
                {
                  "type": "object"
                }
              ]
            },
            "description": "RESUME a truncated crawl: pass the \"nextUrls\" from its response to continue exactly where it stopped, instead of re-crawling (and re-paying for) pages you already have."
          },
          "seenUrls": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "RESUME: the \"seenUrls\" from the previous response, so already-visited URLs are not queued again."
          },
          "followSelector": {
            "type": "string",
            "description": "Only discover links inside this CSS region, e.g. \".product-grid\" — the difference between crawling a catalogue and crawling the whole site chrome."
          },
          "allowPatterns": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Regular expressions tested against the full URL; a link must match at least one to be followed."
          },
          "denyPatterns": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Regular expressions tested against the full URL; a match is never followed (deny wins over allow)."
          },
          "respectNofollow": {
            "type": "boolean",
            "default": true,
            "description": "Skip links marked rel=\"nofollow\"."
          },
          "autothrottle": {
            "type": "boolean",
            "default": true,
            "description": "Learn a per-domain delay from response latency; double it (or honor Retry-After) when the site answers 429/403, relax as it recovers."
          },
          "maxBlockedRetries": {
            "type": "integer",
            "default": 1,
            "minimum": 0,
            "maximum": 3,
            "description": "Times a page that answered 401/403/407/429/444/503 is re-queued (after the backoff) before it is recorded as blocked."
          },
          "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. `dialogs[]` lists any native alert()/confirm()/prompt() the run encountered (type, message, whether it was accepted — accepted only when the goal authorizes a mutation) and `downloads[]` lists any file a CLICK triggered a real browser download for (base64, same size cap as POST /download) — both omitted when none occurred.\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 TYPED a stored credential and the site REFUSED it, the result carries `code: \"credential_rejected\"` with `credentialRejected: { host, secrets, phrase }` — `secrets` is the list of vault NAMES that were filled (never their values) and `phrase` is what the site said (\"incorrect\", \"not recognized\"). The run is stopped at that verdict instead of spending the rest of its steps on a page that will keep refusing. **Re-running with the same identifiers WILL be rejected again**: the values you hold are wrong, so the only things that change the outcome are storing a DIFFERENT value (POST /secrets), passing different ones in `secrets`, or minting a link with POST /connect/sessions so the person whose account it is signs in by hand. Do not retry unchanged, and do not re-store the same string.\n\nIf the run SIGNED UP for an account and the prompt gave no password, it used the tenant default new-account password (PUT /accounts/new-account-password) or a strong generated one, and filed the login under the site: the result carries `newAccount: { host, emailPreview, saved, reason, passwordSource, method? }` (method is set — e.g. \"google\" — when it signed up through a provider per PUT /accounts/signup-defaults) — never the password (read it with POST /accounts/reveal). A password in the prompt or in `secrets` is used instead, and none is generated when the tenant has turned off saving secrets.\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.\n\nOn a host with repeated CHALLENGE-wall blocks (Cloudflare/reCAPTCHA-v2/hCaptcha/Arkose — solvable in-session, unlike a score wall), a plain honest-fleet request AUTOMATICALLY upgrades to the stealth fleet instead of repeating a known miss — reported as `autoStealth: true` — but ONLY when this request already carries the SAME consent manual `stealth:true` requires (`\"authorized\": true`); it never spends stealth budget without that attestation. Pass `stealth:false` to opt out."
          },
          "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). `new_account` is { host, email_preview, saved, reason, password_source } when the run signed up for an account with the agent's own password, else null."
          },
          "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 `code: \"credential_rejected\"` (when the credential you hold is wrong and you have no better one), 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"
                }
              }
            }
          }
        }
      }
    },
    "/site": {
      "get": {
        "operationId": "inspect_site",
        "summary": "What we already know about a host — its anti-bot wall, your auth state there, and the run configuration both imply. Free.",
        "description": "Every blocked run writes back the anti-bot vendor the engine fingerprinted, keyed by host. That memory already steers POST /run automatically; this endpoint lets you ASK it BEFORE committing to a run, so a known wall is not rediscovered by paying for a run that was never going to finish.\n\n`wall.class` is the decision that matters. `score` — reCAPTCHA Enterprise/v3, DataDome, PerimeterX, Kasada, Akamai, Imperva: invisible, reputation-based, and no client-side lever reliably passes one; with nothing stored, the honest next step is POST /connect/sessions and a human. `challenge` — Cloudflare, reCAPTCHA v2, hCaptcha, Arkose: an actual widget, sometimes clearable in-session, and the case the stealth fleet is for. `passive` — a sensor cookie that did not block. `none` — nothing of yours has been blocked here yet, which is the absence of evidence rather than a clean bill.\n\n`recommend.body` is a ready-to-merge POST /run fragment. `recommend.withoutAttestation` states what the same host resolves to when you do NOT send `authorized: true`, so the difference the attestation makes is explicit rather than a surprise.\n\nRead-only: no browser, no reservation, no charge.",
        "parameters": [
          {
            "name": "host",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Host or full URL to inspect (e.g. \"app.acme.com\" or \"https://app.acme.com/login\"). `url` and `target` are accepted as aliases."
          },
          {
            "name": "account",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Scope the stored-session and stored-credential answer to one account label of several on the host."
          }
        ],
        "responses": {
          "200": {
            "description": "{ host, wall: { class: \"score\"|\"challenge\"|\"passive\"|\"none\", vendor, hits, lastOutcome }, auth: { session, account }, plan: { premium, stealthFleet }, recommend: { stealth, autoStealth, proxyType?, connect?, connectEndpoint?, withoutAttestation, body }, reason }."
          },
          "400": {
            "description": "Missing or unresolvable host.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "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, origin, createdByRun, createdAt, updatedAt }], count }. origin: \"user\" (a person saved it) | \"agent\" (a run created it by signing up; createdByRun names the run)."
          },
          "401": {
            "description": "Missing/invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/accounts/reveal": {
      "post": {
        "operationId": "reveal_account",
        "summary": "Read back a login the AGENT created — its email and password. Needs the accounts:reveal scope.",
        "description": "The one exception to the write-only vault. A run that signs up with no password in the prompt uses a generated password (or the tenant default) that nobody else holds; this returns it so the account can be handed to its owner. Only logins with origin \"agent\" are returned — a login a person saved answers 404 exactly like a missing one. Only a key explicitly created with \"Can read passwords of accounts the agent creates\" (scope `accounts:reveal`) may call it; the scope is in no default set and is not implied by a legacy empty scope list. Every call is audited (account.password_revealed) and rate-limited per tenant. Response is `cache-control: no-store`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "host": {
                    "type": "string",
                    "description": "The site, as GET /accounts returns it. With no label: the newest login the agent created there."
                  },
                  "label": {
                    "type": "string",
                    "description": "Which login on that host."
                  },
                  "id": {
                    "type": "string",
                    "description": "Or the login id."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ host, label, email, password, origin: \"agent\" }."
          },
          "400": {
            "description": "Neither host nor id given.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key lacks the accounts:reveal scope (code: scope_required).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No login created by the agent matches (code: not_found).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/accounts/signup-defaults": {
      "get": {
        "operationId": "get_signup_defaults",
        "summary": "How the agent creates an account when the prompt does not say.",
        "responses": {
          "200": {
            "description": "{ providers: [{ id: \"google\"|\"github\", on, ready, session, login }] (in the tenant's order), method (legacy), email, emailInboxConnected, warning?, emailWarning? }."
          },
          "401": {
            "description": "Missing/invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "set_signup_defaults",
        "summary": "Set the ordered sign-up providers (Google, GitHub) and the default email for new accounts.",
        "description": "`providers` is the ORDERED list a sign-up may take — `[{\"id\":\"github\",\"on\":true},{\"id\":\"google\",\"on\":true}]`, or a bare `[\"github\",\"google\"]` for \"these, on, in this order\". A sign-up takes the first enabled provider the site offers AND this workspace can sign in with (a captured session for its sign-in host via POST /connect/sessions, or a saved login for it); otherwise the form is filled. `method` (legacy): \"google\" = Google on and first, \"password\" = all off. `email` is used when the prompt names none; it should be a connected inbox (POST /email-inbox) so confirmation codes and links can be read — the response says whether it is. A method or email in the prompt always wins. Needs the `run` scope.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "providers": {
                    "type": "array",
                    "items": {
                      "oneOf": [
                        {
                          "type": "string",
                          "enum": [
                            "google",
                            "github"
                          ]
                        },
                        {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "string",
                              "enum": [
                                "google",
                                "github"
                              ]
                            },
                            "on": {
                              "type": "boolean"
                            }
                          }
                        }
                      ]
                    }
                  },
                  "method": {
                    "type": "string",
                    "enum": [
                      "password",
                      "google"
                    ],
                    "description": "Legacy; ignored when providers is given."
                  },
                  "email": {
                    "type": "string",
                    "nullable": true,
                    "description": "null or \"\" clears it"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The defaults after the change (same shape as GET)."
          },
          "400": {
            "description": "Invalid method or email, or nothing to change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/accounts/new-account-password": {
      "get": {
        "operationId": "get_new_account_password",
        "summary": "Whether a default password for new accounts is set. Never the value.",
        "responses": {
          "200": {
            "description": "{ set, updatedAt, whenUnset: \"generated per site\" }."
          },
          "401": {
            "description": "Missing/invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "set_new_account_password",
        "summary": "Set the default password a sign-up uses when the prompt gives none.",
        "description": "With a default set, every account the agent creates without a password in the prompt uses it; with none, each site gets its own strong generated password. Stored encrypted, never returned. Needs the `run` scope.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "password"
                ],
                "properties": {
                  "password": {
                    "type": "string",
                    "minLength": 8,
                    "maxLength": 128
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ set: true }."
          },
          "400": {
            "description": "Password too short/long or padded with spaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "clear_new_account_password",
        "summary": "Remove the default: sign-ups go back to a generated password per site.",
        "responses": {
          "200": {
            "description": "{ set: false }."
          },
          "401": {
            "description": "Missing/invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/warnings": {
      "get": {
        "operationId": "list_warnings",
        "summary": "List this tenant's open warnings — the general notifications surface. Free.",
        "description": "First kind: a run's LOGIN PROCESS FAILED — a credentialed login missing its stored secret (kind:\"credential_missing\"), a stored credential the SITE rejected when the run typed it (kind:\"credential_rejected\" — detail carries the vault NAMES that were refused; replacing the stored value is the only fix, re-storing the same one is not), a credential the engine declined to type on a host the site was never seen to hand its sign-in to (kind:\"credential_blocked\"), or a restored cookie session that turned out stale (kind:\"session_stale\"). Each row is DEDUPED per (kind, host, account): a broken account failing on every run produces one open warning, not a flood — a later failure refreshes it instead of adding a row. Acknowledge one via POST /warnings/{id}/ack once you've dealt with it; a later failure on the same identity raises a fresh one.",
        "parameters": [
          {
            "name": "all",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean"
            },
            "description": "Include already-acknowledged warnings (default: open only)."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ warnings: [{ id, kind, severity, host, account, run_id, message, detail, created_at, acknowledged_at }], count }. severity: info | warning | critical."
          },
          "401": {
            "description": "Missing/invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/warnings/{id}/ack": {
      "post": {
        "operationId": "acknowledge_warning",
        "summary": "Acknowledge (dismiss) one warning by id. Free.",
        "description": "Drops it from the default GET /warnings result. Does not delete the row — pass all:true on GET /warnings to still see it.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ id, acknowledged: true }."
          },
          "401": {
            "description": "Missing/invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such warning for this tenant."
          }
        }
      }
    },
    "/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). Waits for the page to be visually finished — fonts, images, entrance animations, pixel stability — before capturing.",
        "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."
          }
        }
      }
    },
    "/download": {
      "post": {
        "operationId": "download_asset",
        "summary": "Fetch an authorized asset's raw bytes — a logo, image, PDF, or other document (flat ~1 credit, no LLM cost).",
        "description": "A single HTTP GET through the browser's own request context, not a page render. Point it at a direct asset URL (typically one you already have from POST /extract's src/href output, or POST /map's URL list). When this tenant has a saved session for the asset's host, it is applied automatically — so a document behind a login downloads without a fresh sign-in.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Download"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON { data: <base64>, mime, bytes, status, url, sessionUsed, credits_charged } by default. Send an Accept header matching the asset's mime (or \"application/octet-stream\") to get the raw file bytes instead, with the charge in x-credits-charged."
          },
          "402": {
            "description": "Insufficient credits."
          },
          "403": {
            "description": "stealth:true requested without a Pro/Enterprise plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Asset exceeds maxBytes (code:\"asset_too_large\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/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."
          }
        }
      }
    }
  }
}