{
  "openapi": "3.1.0",
  "info": {
    "title": "CrawlForge REST API",
    "version": "1.0.0",
    "description": "31 metered web tools behind one API key, sent as the X-API-Key header. Every call is priced in credits: x-credits on an operation is its base price, and a tool with a per-unit or conditional price explains it in x-credits-note and x-extra. Credits are deducted only after a call succeeds. Every error response, whatever its status, charges nothing. A successful response reports credits_used and credits_remaining. Each tool also answers GET with the self-description this document was generated from. Full reference, including each tool's response shape: https://www.crawlforge.dev/docs/api-reference",
    "termsOfService": "https://www.crawlforge.dev/terms",
    "contact": {
      "name": "CrawlForge",
      "email": "simon@crawlforge.dev"
    }
  },
  "servers": [
    {
      "url": "https://www.crawlforge.dev/api/v1",
      "description": "Production"
    }
  ],
  "security": [
    {
      "ApiKeyAuth": []
    }
  ],
  "tags": [
    {
      "name": "tools",
      "description": "The web tools. POST runs a tool and is metered; GET describes it and is free."
    }
  ],
  "paths": {
    "/tools/agent": {
      "post": {
        "operationId": "agent",
        "summary": "Autonomous research/extraction from a natural-language prompt — no URLs required. Plans search queries, fetches and filters relevant pages, and returns a prose or structured answer under hard safety caps. Agent runs can exceed the REST API's ~50s window (the MCP tool allows 120s) — keep maxSteps/maxUrls small, or use the CrawlForge MCP server for long runs.",
        "tags": [
          "tools"
        ],
        "x-credits": 18,
        "x-credits-note": "8 per run, plus 5 for each walled page (challenge, 403/429, empty shell, timeout) the agent gets by retrying it in a stealth browser, at most 2 a run; a retry blocked again is free. The published 18 is the ceiling reserved before the run; the charge is what ran, and the response reports it as credits_used.",
        "x-extra": {
          "availability": {
            "hosted_api": true,
            "mcp_server": true,
            "note": "Runs on the CrawlForge execution backend. Agent runs can exceed the REST API's ~50s window (the MCP tool allows up to 120s wall-clock) — keep maxSteps/maxUrls small, or use the CrawlForge MCP server for long runs. model \"pro\" is not available via the REST API."
          }
        },
        "x-docs-url": "https://www.crawlforge.dev/docs/api-reference/tools/agent",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AgentRequest"
              },
              "example": {
                "prompt": "Find the current pricing tiers for the top 3 MCP web-scraping providers",
                "maxSteps": 3,
                "maxUrls": 5
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's result; 18 credits charged (see x-credits-note and x-extra for add-ons).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolSuccess"
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR: the body does not match the request schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "MISSING_API_KEY or INVALID_API_KEY.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "INSUFFICIENT_CREDITS: the balance does not cover the tool's base price. X-Auto-Recharge-Triggered says whether an automatic recharge was started; retry once it lands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMIT_EXCEEDED: your plan's rate limit. Retry-After: 60.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "TOOL_ERROR, INTERNAL_ERROR or CREDIT_DEDUCTION_FAILED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "default": {
            "description": "Any other error. Which of 403, 404, 413, 422, 501, 502, 503 and 504 a tool can return depends on what it does (fetching a page, calling the execution backend, reading stored results); the code vocabulary is listed on the ErrorResponse schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "describe_agent",
        "summary": "Describe agent",
        "description": "The tool's self-description: its price, its request schema and an example body. No API key is needed.",
        "tags": [
          "tools"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The ToolInfo this document was generated from.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolInfo"
                }
              }
            }
          }
        }
      }
    },
    "/tools/analyze_content": {
      "post": {
        "operationId": "analyze_content",
        "summary": "Analyze web content for language, keywords, sentiment, and readability",
        "tags": [
          "tools"
        ],
        "x-credits": 3,
        "x-docs-url": "https://www.crawlforge.dev/docs/api-reference/tools/analyze-content",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AnalyzeContentRequest"
              },
              "example": {
                "url": "https://example.com/article",
                "analyze_sentiment": true,
                "extract_keywords": true,
                "detect_language": true,
                "analyze_readability": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's result; 3 credits charged (see x-credits-note and x-extra for add-ons).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolSuccess"
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR: the body does not match the request schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "MISSING_API_KEY or INVALID_API_KEY.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "INSUFFICIENT_CREDITS: the balance does not cover the tool's base price. X-Auto-Recharge-Triggered says whether an automatic recharge was started; retry once it lands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMIT_EXCEEDED: your plan's rate limit. Retry-After: 60.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "TOOL_ERROR, INTERNAL_ERROR or CREDIT_DEDUCTION_FAILED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "default": {
            "description": "Any other error. Which of 403, 404, 413, 422, 501, 502, 503 and 504 a tool can return depends on what it does (fetching a page, calling the execution backend, reading stored results); the code vocabulary is listed on the ErrorResponse schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "describe_analyze_content",
        "summary": "Describe analyze_content",
        "description": "The tool's self-description: its price, its request schema and an example body. No API key is needed.",
        "tags": [
          "tools"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The ToolInfo this document was generated from.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolInfo"
                }
              }
            }
          }
        }
      }
    },
    "/tools/batch_scrape": {
      "post": {
        "operationId": "batch_scrape",
        "summary": "Fetch up to 50 URLs concurrently in one synchronous call, returning title, text, and optional CSS-extracted fields per URL",
        "tags": [
          "tools"
        ],
        "x-credits": 5,
        "x-credits-note": "5 per URL attempted (skipped URLs are not charged)",
        "x-extra": {
          "execution_model": {
            "mode": "synchronous",
            "time_budget": "20s of fetching per batch; URLs not started in time return status \"skipped\" and are not charged",
            "robots": "URLs disallowed by the target's robots.txt, URLs on a host CrawlForge has permanently blocked or that asked us to back off, and URLs whose host declares a Crawl-delay too long for the batch budget all return status \"skipped\" (with the reason in error) and are not charged; the reasons are summarized in notes",
            "crawl_delay": "A robots.txt Crawl-delay is honoured per host: URLs on that host are fetched one at a time, spaced by the delay, while other hosts keep the full concurrency",
            "per_url_timeout": "8s",
            "binary_content": "A PDF or other binary served with a 2xx status fails that URL (status \"failed\", error naming the content type) instead of being decoded as text; use process_document for it",
            "javascript": "Pages are fetched without JavaScript execution; screenshots and JS rendering require a browser — use the CrawlForge MCP server (npm: crawlforge-mcp-server)",
            "results_retention": "The full result set is stored for 24h and retrievable via get_batch_results using the returned batch_id"
          }
        },
        "x-docs-url": "https://www.crawlforge.dev/docs/api-reference/tools/batch-scrape",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BatchScrapeRequest"
              },
              "example": {
                "urls": [
                  {
                    "url": "https://example1.com/products",
                    "id": "site1"
                  },
                  {
                    "url": "https://example2.com/products",
                    "id": "site2"
                  }
                ],
                "batch_config": {
                  "concurrency": 5
                },
                "extraction_template": {
                  "fields": [
                    {
                      "name": "title",
                      "selector": "h1"
                    },
                    {
                      "name": "price",
                      "selector": ".price"
                    },
                    {
                      "name": "canonical",
                      "selector": "link[rel=\"canonical\"]",
                      "attribute": "href"
                    }
                  ]
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's result; 5 credits charged (see x-credits-note and x-extra for add-ons).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolSuccess"
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR: the body does not match the request schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "MISSING_API_KEY or INVALID_API_KEY.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "INSUFFICIENT_CREDITS: the balance does not cover the tool's base price. X-Auto-Recharge-Triggered says whether an automatic recharge was started; retry once it lands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMIT_EXCEEDED: your plan's rate limit. Retry-After: 60.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "TOOL_ERROR, INTERNAL_ERROR or CREDIT_DEDUCTION_FAILED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "default": {
            "description": "Any other error. Which of 403, 404, 413, 422, 501, 502, 503 and 504 a tool can return depends on what it does (fetching a page, calling the execution backend, reading stored results); the code vocabulary is listed on the ErrorResponse schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "describe_batch_scrape",
        "summary": "Describe batch_scrape",
        "description": "The tool's self-description: its price, its request schema and an example body. No API key is needed.",
        "tags": [
          "tools"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The ToolInfo this document was generated from.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolInfo"
                }
              }
            }
          }
        }
      }
    },
    "/tools/browser_session": {
      "post": {
        "operationId": "browser_session",
        "summary": "Drive a browser across several calls, keeping the page, its cookies and its login in between. The loop is: open a session on a URL, snapshot it to list the interactive elements as refs (@e1, @e2 ...), act on those refs, read the content (the whole page by default, onlyMainContent: true for the main article block alone), close. Because the page stays open you can look before each step instead of committing to a whole chain up front, so a wrong selector costs one call rather than all of them. Not for a page that renders without interaction (scrape), and not for an interaction you can write out in advance — that is one scrape_with_actions call.",
        "tags": [
          "tools"
        ],
        "x-credits": 3,
        "x-credits-note": "Priced per operation, not per call: open 3, read 2, and snapshot, act, screenshot, close and list 1 each. The published 3 is the ceiling — it is what an unrecognised operation costs and what is reserved before the body is read, and the charge drops to the operation's own price. A login-then-read flow (open, snapshot, act, act, read, close) costs 9.",
        "x-extra": {
          "credits_cost_by_operation": {
            "note": "Nothing in a session is free: close and list are bookkeeping and still cost 1. An absent or unrecognised operation is charged the flat 3.",
            "open": 3,
            "snapshot": 1,
            "act": 1,
            "read": 2,
            "screenshot": 1,
            "close": 1,
            "list": 1
          },
          "session_lifetime": {
            "ttl_seconds": 600,
            "activity_ttl_seconds": 300,
            "note": "A session dies 600s after it opens or 300s after its last use, whichever comes first — both adjustable on open. Close it when you are done rather than leaving it to expire."
          },
          "limits": {
            "concurrent_sessions": 1,
            "note": "One open session per API key on the hosted API. A second open is refused by name and charged nothing — close the first, or wait for it to expire. Sessions share the backend browser pool with every other tool, which is what the cap protects."
          },
          "availability": {
            "hosted_api": true,
            "mcp_server": true,
            "note": "Runs on the CrawlForge execution backend, where the page itself lives. A session id is usable only from the backend instance that opened it and only while that instance is running, so a deploy or a restart ends every open session — treat a \"session not found\" as a signal to open a new one rather than an error to retry. Screenshots come back as crawlforge://screenshot/{id} resource URIs, which this endpoint passes through without resolving, so for now the screenshot operation is usable only from the CrawlForge MCP server. executeJavaScript is refused inside a hosted session."
          }
        },
        "x-docs-url": "https://www.crawlforge.dev/docs/api-reference/tools/browser-session",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BrowserSessionRequest"
              },
              "example": {
                "operation": "open",
                "url": "https://app.example.com/login",
                "ttl": 600
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's result; 3 credits charged (see x-credits-note and x-extra for add-ons).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolSuccess"
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR: the body does not match the request schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "MISSING_API_KEY or INVALID_API_KEY.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "INSUFFICIENT_CREDITS: the balance does not cover the tool's base price. X-Auto-Recharge-Triggered says whether an automatic recharge was started; retry once it lands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMIT_EXCEEDED: your plan's rate limit. Retry-After: 60.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "TOOL_ERROR, INTERNAL_ERROR or CREDIT_DEDUCTION_FAILED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "default": {
            "description": "Any other error. Which of 403, 404, 413, 422, 501, 502, 503 and 504 a tool can return depends on what it does (fetching a page, calling the execution backend, reading stored results); the code vocabulary is listed on the ErrorResponse schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "describe_browser_session",
        "summary": "Describe browser_session",
        "description": "The tool's self-description: its price, its request schema and an example body. No API key is needed.",
        "tags": [
          "tools"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The ToolInfo this document was generated from.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolInfo"
                }
              }
            }
          }
        }
      }
    },
    "/tools/crawl_deep": {
      "post": {
        "operationId": "crawl_deep",
        "summary": "Crawl websites deeply using breadth-first search to discover and extract content",
        "tags": [
          "tools"
        ],
        "x-credits": 4,
        "x-extra": {
          "response_notes": {
            "pages_attempted": "URLs the crawl requested: pages_crawled + error_count",
            "pages_crawled": "Pages fetched and parsed, one entry each in pages",
            "error_count": "URLs that were requested and did not produce a page, one entry each in errors",
            "errors": "One { url, depth, error, code? } per failed URL: a timeout, an unreachable host or an HTTP error status. code is present when known (FETCH_TIMEOUT, FETCH_FAILED, VALIDATION_ERROR for a private address). URLs skipped for robots.txt or Crawl-delay are counted in warnings instead"
          }
        },
        "x-docs-url": "https://www.crawlforge.dev/docs/api-reference/tools/crawl-deep",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CrawlDeepRequest"
              },
              "example": {
                "start_url": "https://example.com",
                "max_pages": 20,
                "max_depth": 3,
                "same_domain_only": true,
                "crawl_delay": 1000
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's result; 4 credits charged (see x-credits-note and x-extra for add-ons).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolSuccess"
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR: the body does not match the request schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "MISSING_API_KEY or INVALID_API_KEY.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "INSUFFICIENT_CREDITS: the balance does not cover the tool's base price. X-Auto-Recharge-Triggered says whether an automatic recharge was started; retry once it lands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMIT_EXCEEDED: your plan's rate limit. Retry-After: 60.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "TOOL_ERROR, INTERNAL_ERROR or CREDIT_DEDUCTION_FAILED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "default": {
            "description": "Any other error. Which of 403, 404, 413, 422, 501, 502, 503 and 504 a tool can return depends on what it does (fetching a page, calling the execution backend, reading stored results); the code vocabulary is listed on the ErrorResponse schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "describe_crawl_deep",
        "summary": "Describe crawl_deep",
        "description": "The tool's self-description: its price, its request schema and an example body. No API key is needed.",
        "tags": [
          "tools"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The ToolInfo this document was generated from.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolInfo"
                }
              }
            }
          }
        }
      }
    },
    "/tools/deep_research": {
      "post": {
        "operationId": "deep_research",
        "summary": "Multi-query web research: runs web searches, fetches the top sources, and returns the most relevant passages verbatim with citations. Synthesis is extractive (no LLM on the hosted API) — for LLM-synthesized research use the CrawlForge MCP server.",
        "tags": [
          "tools"
        ],
        "x-credits": 10,
        "x-extra": {
          "response_shape": {
            "methodology": "Queries run, backend used, source counts, synthesis: extractive, llm_used: false",
            "key_findings": "Up to 10 query-relevant passages, each with text, source_url, source_title, relevance_score. Author/affiliation and DOI front-matter is excluded, passages that barely mention the topic are dropped rather than used as padding (so this list can be short), and no single source domain fills more than 40% of it while another source has a passage to offer",
            "sources": "Every source considered: url, title, snippet, fetched (false = search snippet only), domain",
            "summary": "Extractive summary — the highest-scoring sentences from the findings, verbatim"
          }
        },
        "x-docs-url": "https://www.crawlforge.dev/docs/api-reference/tools/deep-research",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DeepResearchRequest"
              },
              "example": {
                "research_query": "impact of remote work on software team productivity",
                "research_scope": {
                  "depth_level": "moderate",
                  "time_range": "year"
                },
                "max_sources": 5
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's result; 10 credits charged (see x-credits-note and x-extra for add-ons).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolSuccess"
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR: the body does not match the request schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "MISSING_API_KEY or INVALID_API_KEY.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "INSUFFICIENT_CREDITS: the balance does not cover the tool's base price. X-Auto-Recharge-Triggered says whether an automatic recharge was started; retry once it lands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMIT_EXCEEDED: your plan's rate limit. Retry-After: 60.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "TOOL_ERROR, INTERNAL_ERROR or CREDIT_DEDUCTION_FAILED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "default": {
            "description": "Any other error. Which of 403, 404, 413, 422, 501, 502, 503 and 504 a tool can return depends on what it does (fetching a page, calling the execution backend, reading stored results); the code vocabulary is listed on the ErrorResponse schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "describe_deep_research",
        "summary": "Describe deep_research",
        "description": "The tool's self-description: its price, its request schema and an example body. No API key is needed.",
        "tags": [
          "tools"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The ToolInfo this document was generated from.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolInfo"
                }
              }
            }
          }
        }
      }
    },
    "/tools/extract_content": {
      "post": {
        "operationId": "extract_content",
        "summary": "Extract main article content from web pages using readability detection",
        "tags": [
          "tools"
        ],
        "x-credits": 2,
        "x-docs-url": "https://www.crawlforge.dev/docs/api-reference/tools/extract-content",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ExtractContentRequest"
              },
              "example": {
                "url": "https://example.com/article",
                "include_images": true,
                "include_links": false,
                "clean_html": true,
                "extract_main_content": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's result; 2 credits charged (see x-credits-note and x-extra for add-ons).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolSuccess"
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR: the body does not match the request schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "MISSING_API_KEY or INVALID_API_KEY.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "INSUFFICIENT_CREDITS: the balance does not cover the tool's base price. X-Auto-Recharge-Triggered says whether an automatic recharge was started; retry once it lands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMIT_EXCEEDED: your plan's rate limit. Retry-After: 60.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "TOOL_ERROR, INTERNAL_ERROR or CREDIT_DEDUCTION_FAILED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "default": {
            "description": "Any other error. Which of 403, 404, 413, 422, 501, 502, 503 and 504 a tool can return depends on what it does (fetching a page, calling the execution backend, reading stored results); the code vocabulary is listed on the ErrorResponse schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "describe_extract_content",
        "summary": "Describe extract_content",
        "description": "The tool's self-description: its price, its request schema and an example body. No API key is needed.",
        "tags": [
          "tools"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The ToolInfo this document was generated from.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolInfo"
                }
              }
            }
          }
        }
      }
    },
    "/tools/extract_embedded_state": {
      "post": {
        "operationId": "extract_embedded_state",
        "summary": "Return the JSON state a page already ships in its own HTML: __NEXT_DATA__, React Server Component flight chunks (self.__next_f, \"$<id>\" references between rows resolved, plus a data_rows index of the rows carrying data), __NUXT__ and Nuxt 3's __NUXT_DATA__ (decoded), SvelteKit kit.start data, __APOLLO_STATE__, __INITIAL_STATE__, __PRELOADED_STATE__, ytInitialData, ytInitialPlayerResponse, __remixContext and other window globals (JSON, or a JavaScript literal evaluated statically — no page code runs), Inertia data-page, Shopify meta and product JSON, JSON data-* attributes and <script type=\"application/json\"> blocks. One fetch, exact values, and no LLM in the extraction path — the numbers come from the site's own serialized state, so they cannot be fabricated. Sources are reported in \"found\" with their sizes; use \"path\" to return one subtree instead of a multi-megabyte blob, keys_only to see its shape first, or find to list the paths of one key. A bot-defence wall or an empty shell carrying no state returns success: false, unbilled; with escalate: true the same call renders the page once in a stealth browser and reads the state from that instead.",
        "tags": [
          "tools"
        ],
        "x-credits": 2,
        "x-extra": {
          "credits_addon": {
            "escalation": 5,
            "description": "escalation is added only when escalate: true AND the stealth render actually ran — a call that asked to escalate and was served the page by the plain fetch pays the base price, so the projection base + escalation is a ceiling, never a floor. Every failure, a wall the render did not pass included, is charged nothing."
          }
        },
        "x-docs-url": "https://www.crawlforge.dev/docs/api-reference/tools/extract-embedded-state",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ExtractEmbeddedStateRequest"
              },
              "example": {
                "url": "https://www.ticketmaster.com/discover/concerts",
                "path": "next_data.props.pageProps"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's result; 2 credits charged (see x-credits-note and x-extra for add-ons).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolSuccess"
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR: the body does not match the request schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "MISSING_API_KEY or INVALID_API_KEY.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "INSUFFICIENT_CREDITS: the balance does not cover the tool's base price. X-Auto-Recharge-Triggered says whether an automatic recharge was started; retry once it lands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMIT_EXCEEDED: your plan's rate limit. Retry-After: 60.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "TOOL_ERROR, INTERNAL_ERROR or CREDIT_DEDUCTION_FAILED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "default": {
            "description": "Any other error. Which of 403, 404, 413, 422, 501, 502, 503 and 504 a tool can return depends on what it does (fetching a page, calling the execution backend, reading stored results); the code vocabulary is listed on the ErrorResponse schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "describe_extract_embedded_state",
        "summary": "Describe extract_embedded_state",
        "description": "The tool's self-description: its price, its request schema and an example body. No API key is needed.",
        "tags": [
          "tools"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The ToolInfo this document was generated from.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolInfo"
                }
              }
            }
          }
        }
      }
    },
    "/tools/extract_links": {
      "post": {
        "operationId": "extract_links",
        "summary": "Extract and analyze all links from HTML content. A url is fetched once (15 s timeout; a 429 or 503 whose Retry-After is 10 s or less is retried once after that wait). A PDF or other binary returns 415 UNSUPPORTED_CONTENT_TYPE with next_step process_document; a JSON body returns no links and a warning. A bot wall is a BLOCKED error, unbilled; with escalate: true the same call renders the page once in a stealth browser and reads the links from that instead. An empty client-side shell succeeds with rendered: false and the warning \"page renders client-side; use scrape\".",
        "tags": [
          "tools"
        ],
        "x-credits": 1,
        "x-extra": {
          "credits_addon": {
            "escalation": 5,
            "description": "escalation is added only when escalate: true AND the stealth render actually ran — a call that asked to escalate and was served the page by the plain fetch pays the base price, so the projection base + escalation is a ceiling, never a floor. Every failure, a wall the render did not pass included, is charged nothing."
          }
        },
        "x-docs-url": "https://www.crawlforge.dev/docs/api-reference/tools/extract-links",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ExtractLinksRequest"
              },
              "example": {
                "url": "https://example.com",
                "include_external": true,
                "include_internal": true,
                "filter_domains": [
                  "example.com",
                  "subdomain.example.com"
                ],
                "deduplicate": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's result; 1 credits charged (see x-credits-note and x-extra for add-ons).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolSuccess"
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR: the body does not match the request schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "MISSING_API_KEY or INVALID_API_KEY.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "INSUFFICIENT_CREDITS: the balance does not cover the tool's base price. X-Auto-Recharge-Triggered says whether an automatic recharge was started; retry once it lands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMIT_EXCEEDED: your plan's rate limit. Retry-After: 60.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "TOOL_ERROR, INTERNAL_ERROR or CREDIT_DEDUCTION_FAILED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "default": {
            "description": "Any other error. Which of 403, 404, 413, 422, 501, 502, 503 and 504 a tool can return depends on what it does (fetching a page, calling the execution backend, reading stored results); the code vocabulary is listed on the ErrorResponse schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "describe_extract_links",
        "summary": "Describe extract_links",
        "description": "The tool's self-description: its price, its request schema and an example body. No API key is needed.",
        "tags": [
          "tools"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The ToolInfo this document was generated from.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolInfo"
                }
              }
            }
          }
        }
      }
    },
    "/tools/extract_metadata": {
      "post": {
        "operationId": "extract_metadata",
        "summary": "Extract comprehensive metadata from HTML including SEO, social, and technical information",
        "tags": [
          "tools"
        ],
        "x-credits": 1,
        "x-docs-url": "https://www.crawlforge.dev/docs/api-reference/tools/extract-metadata",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ExtractMetadataRequest"
              },
              "example": {
                "url": "https://example.com",
                "include_social": true,
                "include_seo": true,
                "include_technical": true,
                "include_structured_data": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's result; 1 credits charged (see x-credits-note and x-extra for add-ons).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolSuccess"
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR: the body does not match the request schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "MISSING_API_KEY or INVALID_API_KEY.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "INSUFFICIENT_CREDITS: the balance does not cover the tool's base price. X-Auto-Recharge-Triggered says whether an automatic recharge was started; retry once it lands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMIT_EXCEEDED: your plan's rate limit. Retry-After: 60.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "TOOL_ERROR, INTERNAL_ERROR or CREDIT_DEDUCTION_FAILED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "default": {
            "description": "Any other error. Which of 403, 404, 413, 422, 501, 502, 503 and 504 a tool can return depends on what it does (fetching a page, calling the execution backend, reading stored results); the code vocabulary is listed on the ErrorResponse schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "describe_extract_metadata",
        "summary": "Describe extract_metadata",
        "description": "The tool's self-description: its price, its request schema and an example body. No API key is needed.",
        "tags": [
          "tools"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The ToolInfo this document was generated from.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolInfo"
                }
              }
            }
          }
        }
      }
    },
    "/tools/extract_structured": {
      "post": {
        "operationId": "extract_structured",
        "summary": "Extract structured data matching a JSON Schema from CSS selector hints, JSON-LD structured data, and OpenGraph/meta tags. LLM-guided extraction is available via the CrawlForge MCP server, not this endpoint.",
        "tags": [
          "tools"
        ],
        "x-credits": 3,
        "x-docs-url": "https://www.crawlforge.dev/docs/api-reference/tools/extract-structured",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ExtractStructuredRequest"
              },
              "example": {
                "url": "https://example.com/product",
                "schema": {
                  "type": "object",
                  "properties": {
                    "title": {
                      "type": "string"
                    },
                    "price": {
                      "type": "number"
                    },
                    "description": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "title",
                    "price"
                  ]
                },
                "selectorHints": {
                  "title": "h1.product-title",
                  "price": ".price"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's result; 3 credits charged (see x-credits-note and x-extra for add-ons).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolSuccess"
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR: the body does not match the request schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "MISSING_API_KEY or INVALID_API_KEY.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "INSUFFICIENT_CREDITS: the balance does not cover the tool's base price. X-Auto-Recharge-Triggered says whether an automatic recharge was started; retry once it lands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMIT_EXCEEDED: your plan's rate limit. Retry-After: 60.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "TOOL_ERROR, INTERNAL_ERROR or CREDIT_DEDUCTION_FAILED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "default": {
            "description": "Any other error. Which of 403, 404, 413, 422, 501, 502, 503 and 504 a tool can return depends on what it does (fetching a page, calling the execution backend, reading stored results); the code vocabulary is listed on the ErrorResponse schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "describe_extract_structured",
        "summary": "Describe extract_structured",
        "description": "The tool's self-description: its price, its request schema and an example body. No API key is needed.",
        "tags": [
          "tools"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The ToolInfo this document was generated from.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolInfo"
                }
              }
            }
          }
        }
      }
    },
    "/tools/extract_text": {
      "post": {
        "operationId": "extract_text",
        "summary": "Extract clean text content from HTML with various formatting options. A url is fetched once (15 s timeout; a 429 or 503 whose Retry-After is 10 s or less is retried once after that wait). A PDF or other binary returns 415 UNSUPPORTED_CONTENT_TYPE with next_step process_document; a JSON body is returned as text with a warning. A bot wall is a BLOCKED error, unbilled; with escalate: true the same call renders the page once in a stealth browser and extracts the text from that instead. An empty client-side shell succeeds with rendered: false and the warning \"page renders client-side; use scrape\".",
        "tags": [
          "tools"
        ],
        "x-credits": 1,
        "x-extra": {
          "credits_addon": {
            "escalation": 5,
            "description": "escalation is added only when escalate: true AND the stealth render actually ran — a call that asked to escalate and was served the page by the plain fetch pays the base price, so the projection base + escalation is a ceiling, never a floor. Every failure, a wall the render did not pass included, is charged nothing."
          }
        },
        "x-docs-url": "https://www.crawlforge.dev/docs/api-reference/tools/extract-text",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ExtractTextRequest"
              },
              "example": {
                "url": "https://example.com",
                "selector": "article",
                "clean": true,
                "preserve_links": true,
                "max_length": 5000
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's result; 1 credits charged (see x-credits-note and x-extra for add-ons).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolSuccess"
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR: the body does not match the request schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "MISSING_API_KEY or INVALID_API_KEY.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "INSUFFICIENT_CREDITS: the balance does not cover the tool's base price. X-Auto-Recharge-Triggered says whether an automatic recharge was started; retry once it lands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMIT_EXCEEDED: your plan's rate limit. Retry-After: 60.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "TOOL_ERROR, INTERNAL_ERROR or CREDIT_DEDUCTION_FAILED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "default": {
            "description": "Any other error. Which of 403, 404, 413, 422, 501, 502, 503 and 504 a tool can return depends on what it does (fetching a page, calling the execution backend, reading stored results); the code vocabulary is listed on the ErrorResponse schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "describe_extract_text",
        "summary": "Describe extract_text",
        "description": "The tool's self-description: its price, its request schema and an example body. No API key is needed.",
        "tags": [
          "tools"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The ToolInfo this document was generated from.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolInfo"
                }
              }
            }
          }
        }
      }
    },
    "/tools/extract_with_llm": {
      "post": {
        "operationId": "extract_with_llm",
        "summary": "Extract structured data from a URL or text using a natural-language prompt. On a locally-run MCP server this defaults to local Ollama; provider \"openai\" or \"anthropic\" use cloud models.",
        "tags": [
          "tools"
        ],
        "x-credits": 3,
        "x-extra": {
          "availability": {
            "hosted_api": true,
            "mcp_server": true,
            "note": "Runs on the CrawlForge execution backend. Which providers actually work depends on that backend's configuration: the default \"auto\"/\"ollama\" path uses the Ollama endpoint configured there (e.g. Ollama Cloud via OLLAMA_BASE_URL + OLLAMA_API_KEY), and provider \"openai\"/\"anthropic\" run only with API keys configured on the backend. For extraction against your own local Ollama, run the CrawlForge MCP server yourself."
          }
        },
        "x-docs-url": "https://www.crawlforge.dev/docs/api-reference/tools/extract-with-llm",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ExtractWithLlmRequest"
              },
              "example": {
                "url": "https://example.com/article",
                "prompt": "Extract the article title, author, and key takeaways",
                "schema": {
                  "type": "object",
                  "properties": {
                    "title": {
                      "type": "string"
                    },
                    "author": {
                      "type": "string"
                    },
                    "takeaways": {
                      "type": "array"
                    }
                  },
                  "required": [
                    "title"
                  ]
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's result; 3 credits charged (see x-credits-note and x-extra for add-ons).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolSuccess"
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR: the body does not match the request schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "MISSING_API_KEY or INVALID_API_KEY.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "INSUFFICIENT_CREDITS: the balance does not cover the tool's base price. X-Auto-Recharge-Triggered says whether an automatic recharge was started; retry once it lands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMIT_EXCEEDED: your plan's rate limit. Retry-After: 60.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "TOOL_ERROR, INTERNAL_ERROR or CREDIT_DEDUCTION_FAILED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "default": {
            "description": "Any other error. Which of 403, 404, 413, 422, 501, 502, 503 and 504 a tool can return depends on what it does (fetching a page, calling the execution backend, reading stored results); the code vocabulary is listed on the ErrorResponse schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "describe_extract_with_llm",
        "summary": "Describe extract_with_llm",
        "description": "The tool's self-description: its price, its request schema and an example body. No API key is needed.",
        "tags": [
          "tools"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The ToolInfo this document was generated from.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolInfo"
                }
              }
            }
          }
        }
      }
    },
    "/tools/fetch_url": {
      "post": {
        "operationId": "fetch_url",
        "summary": "Fetch content from a URL with optional headers and configuration",
        "tags": [
          "tools"
        ],
        "x-credits": 1,
        "x-extra": {
          "notes": [
            "Responses include response_time_ms — how long the target took to respond and deliver its body.",
            "Bodies are decoded using the response's own charset (Content-Type, then a <meta charset> sniff) rather than assumed UTF-8.",
            "A body larger than 25MB is refused with RESPONSE_TOO_LARGE (413) and is not charged.",
            "A reddit.com URL is refused with USE_REDDIT_SEARCH (422) and is not charged: reddit.com blocks every non-browser client, and the message names the reddit_search call that reads the same posts and comments."
          ]
        },
        "x-docs-url": "https://www.crawlforge.dev/docs/api-reference/tools/fetch-url",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FetchUrlRequest"
              },
              "example": {
                "url": "https://example.com",
                "headers": {
                  "Accept": "text/html"
                },
                "timeout": 15000,
                "follow_redirects": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's result; 1 credits charged (see x-credits-note and x-extra for add-ons).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolSuccess"
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR: the body does not match the request schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "MISSING_API_KEY or INVALID_API_KEY.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "INSUFFICIENT_CREDITS: the balance does not cover the tool's base price. X-Auto-Recharge-Triggered says whether an automatic recharge was started; retry once it lands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMIT_EXCEEDED: your plan's rate limit. Retry-After: 60.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "TOOL_ERROR, INTERNAL_ERROR or CREDIT_DEDUCTION_FAILED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "default": {
            "description": "Any other error. Which of 403, 404, 413, 422, 501, 502, 503 and 504 a tool can return depends on what it does (fetching a page, calling the execution backend, reading stored results); the code vocabulary is listed on the ErrorResponse schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "describe_fetch_url",
        "summary": "Describe fetch_url",
        "description": "The tool's self-description: its price, its request schema and an example body. No API key is needed.",
        "tags": [
          "tools"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The ToolInfo this document was generated from.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolInfo"
                }
              }
            }
          }
        }
      }
    },
    "/tools/generate_llms_txt": {
      "post": {
        "operationId": "generate_llms_txt",
        "summary": "Generate llms.txt (and optionally llms-full.txt) from a real analysis of the site: fetches the target page, robots.txt, and sitemap.xml, then reads up to 8 key same-domain pages (about/docs/pricing/blog/api/contact/faq and similar) for their titles, descriptions, and main text.",
        "tags": [
          "tools"
        ],
        "x-credits": 5,
        "x-docs-url": "https://www.crawlforge.dev/docs/api-reference/tools/generate-llms-txt",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GenerateLlmsTxtRequest"
              },
              "example": {
                "url": "https://example.com",
                "format": "both",
                "outputOptions": {
                  "organizationName": "Example Inc.",
                  "contactEmail": "ai@example.com"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's result; 5 credits charged (see x-credits-note and x-extra for add-ons).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolSuccess"
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR: the body does not match the request schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "MISSING_API_KEY or INVALID_API_KEY.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "INSUFFICIENT_CREDITS: the balance does not cover the tool's base price. X-Auto-Recharge-Triggered says whether an automatic recharge was started; retry once it lands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMIT_EXCEEDED: your plan's rate limit. Retry-After: 60.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "TOOL_ERROR, INTERNAL_ERROR or CREDIT_DEDUCTION_FAILED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "default": {
            "description": "Any other error. Which of 403, 404, 413, 422, 501, 502, 503 and 504 a tool can return depends on what it does (fetching a page, calling the execution backend, reading stored results); the code vocabulary is listed on the ErrorResponse schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "describe_generate_llms_txt",
        "summary": "Describe generate_llms_txt",
        "description": "The tool's self-description: its price, its request schema and an example body. No API key is needed.",
        "tags": [
          "tools"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The ToolInfo this document was generated from.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolInfo"
                }
              }
            }
          }
        }
      }
    },
    "/tools/get_batch_results": {
      "post": {
        "operationId": "get_batch_results",
        "summary": "Retrieve paginated results for a batch_scrape job by batchId. Results are stored for 24h after the batch completes and are only readable by the account that submitted the batch. batch_scrape runs synchronously, so a stored result set is always complete and the response reports status \"completed\".",
        "tags": [
          "tools"
        ],
        "x-credits": 1,
        "x-docs-url": "https://www.crawlforge.dev/docs/api-reference/tools/get-batch-results",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GetBatchResultsRequest"
              },
              "example": {
                "batchId": "3f6f2b8e-8c1a-4a52-9f65-2d0f4a9be1c7",
                "page": 1,
                "limit": 25
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's result; 1 credits charged (see x-credits-note and x-extra for add-ons).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolSuccess"
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR: the body does not match the request schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "MISSING_API_KEY or INVALID_API_KEY.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "INSUFFICIENT_CREDITS: the balance does not cover the tool's base price. X-Auto-Recharge-Triggered says whether an automatic recharge was started; retry once it lands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMIT_EXCEEDED: your plan's rate limit. Retry-After: 60.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "TOOL_ERROR, INTERNAL_ERROR or CREDIT_DEDUCTION_FAILED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "default": {
            "description": "Any other error. Which of 403, 404, 413, 422, 501, 502, 503 and 504 a tool can return depends on what it does (fetching a page, calling the execution backend, reading stored results); the code vocabulary is listed on the ErrorResponse schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "describe_get_batch_results",
        "summary": "Describe get_batch_results",
        "description": "The tool's self-description: its price, its request schema and an example body. No API key is needed.",
        "tags": [
          "tools"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The ToolInfo this document was generated from.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolInfo"
                }
              }
            }
          }
        }
      }
    },
    "/tools/list_ollama_models": {
      "post": {
        "operationId": "list_ollama_models",
        "summary": "Lists the Ollama models installed on the machine running the MCP server. Use this to discover which model values you can pass to extract_with_llm.",
        "tags": [
          "tools"
        ],
        "x-credits": 1,
        "x-extra": {
          "availability": {
            "hosted_api": true,
            "mcp_server": true,
            "note": "Runs on the CrawlForge execution backend and lists models on that backend's Ollama instance, which may not exist — in that case the call fails without charging. The tool is mainly useful on a locally-run MCP server, where it lists your own models."
          }
        },
        "x-docs-url": "https://www.crawlforge.dev/docs/api-reference/tools/list-ollama-models",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ListOllamaModelsRequest"
              },
              "example": {}
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's result; 1 credits charged (see x-credits-note and x-extra for add-ons).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolSuccess"
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR: the body does not match the request schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "MISSING_API_KEY or INVALID_API_KEY.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "INSUFFICIENT_CREDITS: the balance does not cover the tool's base price. X-Auto-Recharge-Triggered says whether an automatic recharge was started; retry once it lands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMIT_EXCEEDED: your plan's rate limit. Retry-After: 60.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "TOOL_ERROR, INTERNAL_ERROR or CREDIT_DEDUCTION_FAILED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "default": {
            "description": "Any other error. Which of 403, 404, 413, 422, 501, 502, 503 and 504 a tool can return depends on what it does (fetching a page, calling the execution backend, reading stored results); the code vocabulary is listed on the ErrorResponse schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "describe_list_ollama_models",
        "summary": "Describe list_ollama_models",
        "description": "The tool's self-description: its price, its request schema and an example body. No API key is needed.",
        "tags": [
          "tools"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The ToolInfo this document was generated from.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolInfo"
                }
              }
            }
          }
        }
      }
    },
    "/tools/localization": {
      "post": {
        "operationId": "localization",
        "summary": "Audit the language and region signals of one page: html lang, the Content-Language header, hreflang alternates, the language detected in the visible text, and geo meta tags. It reads that page and applies nothing to any other call: target_language and target_country shape this one request only. No proxy is used, so the request leaves from the same address as every other tool and a geo-block is not lifted (a regional block follows the exit IP). To request a page in a language elsewhere, pass it on that call: fetch_url headers {\"Accept-Language\": ...} or stealth_mode stealthConfig {locale, timezone}; scrape has no locale parameter. The CrawlForge MCP server's localization tool is a different surface (configure_country and other operations) that returns a country's locale values and likewise applies none.",
        "tags": [
          "tools"
        ],
        "x-credits": 2,
        "x-docs-url": "https://www.crawlforge.dev/docs/api-reference/tools/localization",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LocalizationRequest"
              },
              "example": {
                "url": "https://example.com",
                "target_language": "en",
                "target_country": "US",
                "detect_language": true,
                "extract_hreflang": true,
                "check_geo_targeting": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's result; 2 credits charged (see x-credits-note and x-extra for add-ons).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolSuccess"
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR: the body does not match the request schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "MISSING_API_KEY or INVALID_API_KEY.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "INSUFFICIENT_CREDITS: the balance does not cover the tool's base price. X-Auto-Recharge-Triggered says whether an automatic recharge was started; retry once it lands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMIT_EXCEEDED: your plan's rate limit. Retry-After: 60.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "TOOL_ERROR, INTERNAL_ERROR or CREDIT_DEDUCTION_FAILED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "default": {
            "description": "Any other error. Which of 403, 404, 413, 422, 501, 502, 503 and 504 a tool can return depends on what it does (fetching a page, calling the execution backend, reading stored results); the code vocabulary is listed on the ErrorResponse schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "describe_localization",
        "summary": "Describe localization",
        "description": "The tool's self-description: its price, its request schema and an example body. No API key is needed.",
        "tags": [
          "tools"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The ToolInfo this document was generated from.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolInfo"
                }
              }
            }
          }
        }
      }
    },
    "/tools/map_site": {
      "post": {
        "operationId": "map_site",
        "summary": "Enumerate a site's pages: reads {origin}/sitemap.xml when available (up to 500 URLs, following up to 3 child sitemaps), otherwise falls back to a bounded breadth-first crawl (up to 30 pages, ~18s budget)",
        "tags": [
          "tools"
        ],
        "x-credits": 2,
        "x-extra": {
          "response_notes": {
            "source": "'sitemap' when the URL list came from sitemap.xml, 'crawl' when discovered by crawling",
            "max_depth_reached": "Actual deepest page crawled; null in sitemap mode (no crawl performed)",
            "sitemap": "Map of crawled page URL to the links found on it; null in sitemap mode"
          }
        },
        "x-docs-url": "https://www.crawlforge.dev/docs/api-reference/tools/map-site",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MapSiteRequest"
              },
              "example": {
                "url": "https://example.com",
                "max_depth": 2,
                "include_external": false,
                "timeout": 15000
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's result; 2 credits charged (see x-credits-note and x-extra for add-ons).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolSuccess"
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR: the body does not match the request schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "MISSING_API_KEY or INVALID_API_KEY.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "INSUFFICIENT_CREDITS: the balance does not cover the tool's base price. X-Auto-Recharge-Triggered says whether an automatic recharge was started; retry once it lands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMIT_EXCEEDED: your plan's rate limit. Retry-After: 60.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "TOOL_ERROR, INTERNAL_ERROR or CREDIT_DEDUCTION_FAILED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "default": {
            "description": "Any other error. Which of 403, 404, 413, 422, 501, 502, 503 and 504 a tool can return depends on what it does (fetching a page, calling the execution backend, reading stored results); the code vocabulary is listed on the ErrorResponse schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "describe_map_site",
        "summary": "Describe map_site",
        "description": "The tool's self-description: its price, its request schema and an example body. No API key is needed.",
        "tags": [
          "tools"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The ToolInfo this document was generated from.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolInfo"
                }
              }
            }
          }
        }
      }
    },
    "/tools/process_document": {
      "post": {
        "operationId": "process_document",
        "summary": "Fetch a document by URL and extract its real text, metadata, and tables. Supported on this endpoint: PDF (text, metadata, tables via pdf-parse), CSV (text + parsed rows), TXT, and HTML (text + metadata). DOCX/XLSX return 501 and are supported by the CrawlForge MCP server.",
        "tags": [
          "tools"
        ],
        "x-credits": 2,
        "x-docs-url": "https://www.crawlforge.dev/docs/api-reference/tools/process-document",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProcessDocumentRequest"
              },
              "example": {
                "url": "https://example.com/document.pdf",
                "document_type": "auto",
                "extract_text": true,
                "extract_metadata": true,
                "extract_tables": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's result; 2 credits charged (see x-credits-note and x-extra for add-ons).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolSuccess"
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR: the body does not match the request schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "MISSING_API_KEY or INVALID_API_KEY.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "INSUFFICIENT_CREDITS: the balance does not cover the tool's base price. X-Auto-Recharge-Triggered says whether an automatic recharge was started; retry once it lands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMIT_EXCEEDED: your plan's rate limit. Retry-After: 60.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "TOOL_ERROR, INTERNAL_ERROR or CREDIT_DEDUCTION_FAILED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "default": {
            "description": "Any other error. Which of 403, 404, 413, 422, 501, 502, 503 and 504 a tool can return depends on what it does (fetching a page, calling the execution backend, reading stored results); the code vocabulary is listed on the ErrorResponse schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "describe_process_document",
        "summary": "Describe process_document",
        "description": "The tool's self-description: its price, its request schema and an example body. No API key is needed.",
        "tags": [
          "tools"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The ToolInfo this document was generated from.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolInfo"
                }
              }
            }
          }
        }
      }
    },
    "/tools/read_result": {
      "post": {
        "operationId": "read_result",
        "summary": "Read part of a stored result. A tool whose result exceeded max_inline_chars returned a preview and a result_handle; this reads the stored copy by character range (slice), by case-insensitive literal search with context and offsets (search), by line (lines), or by dotted JSON path (json_path). Results are kept for 1 hour and are readable only by the account that produced them. Nothing is fetched.",
        "tags": [
          "tools"
        ],
        "x-credits": 1,
        "x-docs-url": "https://www.crawlforge.dev/docs/api-reference/tools/read-result",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReadResultRequest"
              },
              "example": {
                "handle": "res_3f6f2b8e-8c1a-4a52-9f65-2d0f4a9be1c7",
                "operation": "search",
                "query": "Pricing",
                "max_matches": 5
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's result; 1 credits charged (see x-credits-note and x-extra for add-ons).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolSuccess"
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR: the body does not match the request schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "MISSING_API_KEY or INVALID_API_KEY.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "INSUFFICIENT_CREDITS: the balance does not cover the tool's base price. X-Auto-Recharge-Triggered says whether an automatic recharge was started; retry once it lands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMIT_EXCEEDED: your plan's rate limit. Retry-After: 60.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "TOOL_ERROR, INTERNAL_ERROR or CREDIT_DEDUCTION_FAILED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "default": {
            "description": "Any other error. Which of 403, 404, 413, 422, 501, 502, 503 and 504 a tool can return depends on what it does (fetching a page, calling the execution backend, reading stored results); the code vocabulary is listed on the ErrorResponse schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "describe_read_result",
        "summary": "Describe read_result",
        "description": "The tool's self-description: its price, its request schema and an example body. No API key is needed.",
        "tags": [
          "tools"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The ToolInfo this document was generated from.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolInfo"
                }
              }
            }
          }
        }
      }
    },
    "/tools/reddit_search": {
      "post": {
        "operationId": "reddit_search",
        "summary": "Search Reddit posts/comments or read a full comment thread — reads the Arctic Shift community archive (reddit.com blocks direct scraping). A scoped search (subreddit or author) queries the archive directly. A Reddit-wide keyword search finds posts with a site-restricted web search and then reads those posts from the archive — or, in comments mode, searches each of the first five posts' comments for the keywords — because Arctic Shift cannot keyword-search across all of Reddit. A scoped comment search Arctic Shift times out on is retried over the last 7d and 3d and reports window_applied. Arctic Shift is tried first and PullPush (api.pullpush.io) second: when Arctic Shift or the web-discovery path fails a posts or comments search, PullPush is queried and the response reports fallback_used. PullPush has refused automated clients since August 2026, so that fallback usually reports its refusal too.",
        "tags": [
          "tools"
        ],
        "x-credits": 5,
        "x-docs-url": "https://www.crawlforge.dev/docs/api-reference/tools/reddit-search",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RedditSearchRequest"
              },
              "example": {
                "query": "best mechanical keyboard",
                "subreddit": "MechanicalKeyboards",
                "limit": 10
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's result; 5 credits charged (see x-credits-note and x-extra for add-ons).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolSuccess"
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR: the body does not match the request schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "MISSING_API_KEY or INVALID_API_KEY.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "INSUFFICIENT_CREDITS: the balance does not cover the tool's base price. X-Auto-Recharge-Triggered says whether an automatic recharge was started; retry once it lands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMIT_EXCEEDED: your plan's rate limit. Retry-After: 60.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "TOOL_ERROR, INTERNAL_ERROR or CREDIT_DEDUCTION_FAILED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "default": {
            "description": "Any other error. Which of 403, 404, 413, 422, 501, 502, 503 and 504 a tool can return depends on what it does (fetching a page, calling the execution backend, reading stored results); the code vocabulary is listed on the ErrorResponse schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "describe_reddit_search",
        "summary": "Describe reddit_search",
        "description": "The tool's self-description: its price, its request schema and an example body. No API key is needed.",
        "tags": [
          "tools"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The ToolInfo this document was generated from.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolInfo"
                }
              }
            }
          }
        }
      }
    },
    "/tools/scrape": {
      "post": {
        "operationId": "scrape",
        "summary": "Unified single-fetch, multi-format extraction. One fetch serves every requested format: markdown, html, rawHtml, text, links, metadata. A bot-defence challenge page returns success: false with blocked.vendor whatever its HTTP status; an empty shell or an error placeholder served as HTTP 200 is a failure too. Neither is charged. A PDF or other binary returns 415 UNSUPPORTED_CONTENT_TYPE, unbilled; use process_document. A success whose markdown is empty carries a warning saying so. With escalate: true that same call renders the page once in a stealth browser and returns the formats from it, instead of returning the block.",
        "tags": [
          "tools"
        ],
        "x-credits": 2,
        "x-extra": {
          "credits_addon": {
            "query_format": 1,
            "escalation": 5,
            "description": "query_format is added once per call when a highlights or question format is present; mode 'model' is rejected here and charged nothing. escalation is added only when escalate: true AND the stealth render actually ran — a call that asked to escalate and was served the page by the plain fetch pays the base price, so the projection base + escalation is a ceiling, never a floor."
          }
        },
        "x-docs-url": "https://www.crawlforge.dev/docs/api-reference/tools/scrape",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ScrapeRequest"
              },
              "example": {
                "url": "https://example.com",
                "formats": [
                  "markdown",
                  "links",
                  "metadata"
                ],
                "onlyMainContent": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's result; 2 credits charged (see x-credits-note and x-extra for add-ons).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolSuccess"
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR: the body does not match the request schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "MISSING_API_KEY or INVALID_API_KEY.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "INSUFFICIENT_CREDITS: the balance does not cover the tool's base price. X-Auto-Recharge-Triggered says whether an automatic recharge was started; retry once it lands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMIT_EXCEEDED: your plan's rate limit. Retry-After: 60.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "TOOL_ERROR, INTERNAL_ERROR or CREDIT_DEDUCTION_FAILED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "default": {
            "description": "Any other error. Which of 403, 404, 413, 422, 501, 502, 503 and 504 a tool can return depends on what it does (fetching a page, calling the execution backend, reading stored results); the code vocabulary is listed on the ErrorResponse schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "describe_scrape",
        "summary": "Describe scrape",
        "description": "The tool's self-description: its price, its request schema and an example body. No API key is needed.",
        "tags": [
          "tools"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The ToolInfo this document was generated from.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolInfo"
                }
              }
            }
          }
        }
      }
    },
    "/tools/scrape_structured": {
      "post": {
        "operationId": "scrape_structured",
        "summary": "Extract structured data from HTML using CSS selectors",
        "tags": [
          "tools"
        ],
        "x-credits": 2,
        "x-extra": {
          "examples": {
            "single_item": {
              "url": "https://example.com/product",
              "selectors": {
                "title": "h1.product-title",
                "price": ".price",
                "description": ".product-description"
              },
              "include_attributes": [
                "href",
                "src"
              ]
            },
            "multiple_items": {
              "url": "https://example.com/products",
              "selectors": {
                "title": ".product-item h3",
                "price": ".product-item .price",
                "link": ".product-item a"
              },
              "multiple": true,
              "max_items": 50,
              "include_attributes": [
                "href"
              ]
            }
          }
        },
        "x-docs-url": "https://www.crawlforge.dev/docs/api-reference/tools/scrape-structured",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ScrapeStructuredRequest"
              },
              "example": {
                "url": "https://example.com/product",
                "selectors": {
                  "title": "h1.product-title",
                  "price": ".price",
                  "description": ".product-description"
                },
                "include_attributes": [
                  "href",
                  "src"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's result; 2 credits charged (see x-credits-note and x-extra for add-ons).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolSuccess"
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR: the body does not match the request schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "MISSING_API_KEY or INVALID_API_KEY.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "INSUFFICIENT_CREDITS: the balance does not cover the tool's base price. X-Auto-Recharge-Triggered says whether an automatic recharge was started; retry once it lands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMIT_EXCEEDED: your plan's rate limit. Retry-After: 60.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "TOOL_ERROR, INTERNAL_ERROR or CREDIT_DEDUCTION_FAILED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "default": {
            "description": "Any other error. Which of 403, 404, 413, 422, 501, 502, 503 and 504 a tool can return depends on what it does (fetching a page, calling the execution backend, reading stored results); the code vocabulary is listed on the ErrorResponse schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "describe_scrape_structured",
        "summary": "Describe scrape_structured",
        "description": "The tool's self-description: its price, its request schema and an example body. No API key is needed.",
        "tags": [
          "tools"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The ToolInfo this document was generated from.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolInfo"
                }
              }
            }
          }
        }
      }
    },
    "/tools/scrape_template": {
      "post": {
        "operationId": "scrape_template",
        "summary": "Pre-built scrapers for popular sites and public APIs — no schema or selectors required. Three modes: name a template and pass a URL; pass template \"auto\" with a URL to have the matching template picked (the response reports which one it chose); or pass template \"list\" to enumerate them. Templates come in two kinds, reported as \"mode\" by the list. An entity template returns one record from one page. A list connector returns N records from one call, as data.items with a data.count — that covers the job-board group (Greenhouse, Lever, Ashby, Workable, Recruitee, Teamtailor), which reads a company's whole board from its ATS API, the government group (NHTSA vPIC VIN decoding, NPPES NPI provider registry), which reads free keyless federal registries, and shopify-collection, which lists a whole storefront collection. A list connector is driven either by a URL or by \"params\" — e.g. { \"template\": \"greenhouse-jobs\", \"params\": { \"company\": \"stripe\" } }. Sites that block plain HTTP fetches (e.g. Amazon) may return sparse data here; the CrawlForge MCP server version uses stealth browsing for those. reddit-thread reads the post from the Arctic Shift archive, because reddit.com blocks direct scraping; the reddit_search tool reads the comment tree. linkedin-profile and tweet are retired — those sites' robots.txt disallow every keyless path — and naming one, or passing one of their URLs to \"auto\", returns 400 TEMPLATE_UNAVAILABLE with the reason and no charge. Several templates read a machine-readable endpoint rather than the rendered page — shopify-product reads the store's own /products/<handle>.json, npm-package reads the npm registry API, and every list connector reads its platform's API — and report the URL they actually read as \"fetched_url\". When a Shopify store refuses its JSON endpoint (401, 403, 404 or 410), shopify-product reads the product page's own schema.org JSON-LD instead: the record carries \"source\": \"json-ld\" and a warning says so, with per-variant stock, compare-at prices and option names null because JSON-LD does not carry them.",
        "tags": [
          "tools"
        ],
        "x-credits": 1,
        "x-extra": {
          "supported_templates": [
            "shopify-product",
            "shopify-collection",
            "amazon-product",
            "github-repo",
            "youtube-video",
            "reddit-thread",
            "hacker-news-front-page",
            "producthunt-launch",
            "stackoverflow-question",
            "npm-package",
            "greenhouse-jobs",
            "lever-postings",
            "ashby-jobs",
            "workable-jobs",
            "recruitee-offers",
            "teamtailor-jobs",
            "nhtsa-vin",
            "npi-provider"
          ]
        },
        "x-docs-url": "https://www.crawlforge.dev/docs/api-reference/tools/scrape-template",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ScrapeTemplateRequest"
              },
              "example": {
                "template": "github-repo",
                "url": "https://github.com/mysleekdesigns/crawlforge-mcp"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's result; 1 credits charged (see x-credits-note and x-extra for add-ons).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolSuccess"
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR: the body does not match the request schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "MISSING_API_KEY or INVALID_API_KEY.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "INSUFFICIENT_CREDITS: the balance does not cover the tool's base price. X-Auto-Recharge-Triggered says whether an automatic recharge was started; retry once it lands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMIT_EXCEEDED: your plan's rate limit. Retry-After: 60.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "TOOL_ERROR, INTERNAL_ERROR or CREDIT_DEDUCTION_FAILED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "default": {
            "description": "Any other error. Which of 403, 404, 413, 422, 501, 502, 503 and 504 a tool can return depends on what it does (fetching a page, calling the execution backend, reading stored results); the code vocabulary is listed on the ErrorResponse schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "describe_scrape_template",
        "summary": "Describe scrape_template",
        "description": "The tool's self-description: its price, its request schema and an example body. No API key is needed.",
        "tags": [
          "tools"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The ToolInfo this document was generated from.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolInfo"
                }
              }
            }
          }
        }
      }
    },
    "/tools/scrape_with_actions": {
      "post": {
        "operationId": "scrape_with_actions",
        "summary": "Interact with a page before scraping — click, type, press keys, hover, choose a select option, scroll, navigate on, run JavaScript, or wait for dynamic content. Use for SPAs, login-gated content, or multi-step flows. Set browserOptions.stealth to run the chain in a stealth browser context. A click or form submit that loads another page is a navigation too: it gets its own navigations entry (trigger names the action type), and finalUrl is where the chain ended. Screenshots are taken by {type:\"screenshot\"} actions (and on error) and stored as crawlforge://screenshot/{actionId} resources.",
        "tags": [
          "tools"
        ],
        "x-credits": 5,
        "x-extra": {
          "availability": {
            "hosted_api": true,
            "mcp_server": true,
            "note": "Runs on the CrawlForge execution backend. The REST call waits up to ~25s — keep action sequences and browserOptions.timeout within that window; longer runs suit the CrawlForge MCP server."
          }
        },
        "x-docs-url": "https://www.crawlforge.dev/docs/api-reference/tools/scrape-with-actions",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ScrapeWithActionsRequest"
              },
              "example": {
                "url": "https://app.example.com/dashboard",
                "actions": [
                  {
                    "type": "click",
                    "selector": "#login"
                  },
                  {
                    "type": "type",
                    "selector": "#email",
                    "text": "user@example.com"
                  },
                  {
                    "type": "wait",
                    "selector": ".dashboard",
                    "condition": "visible"
                  }
                ],
                "formats": [
                  "markdown"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's result; 5 credits charged (see x-credits-note and x-extra for add-ons).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolSuccess"
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR: the body does not match the request schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "MISSING_API_KEY or INVALID_API_KEY.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "INSUFFICIENT_CREDITS: the balance does not cover the tool's base price. X-Auto-Recharge-Triggered says whether an automatic recharge was started; retry once it lands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMIT_EXCEEDED: your plan's rate limit. Retry-After: 60.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "TOOL_ERROR, INTERNAL_ERROR or CREDIT_DEDUCTION_FAILED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "default": {
            "description": "Any other error. Which of 403, 404, 413, 422, 501, 502, 503 and 504 a tool can return depends on what it does (fetching a page, calling the execution backend, reading stored results); the code vocabulary is listed on the ErrorResponse schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "describe_scrape_with_actions",
        "summary": "Describe scrape_with_actions",
        "description": "The tool's self-description: its price, its request schema and an example body. No API key is needed.",
        "tags": [
          "tools"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The ToolInfo this document was generated from.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolInfo"
                }
              }
            }
          }
        }
      }
    },
    "/tools/search_web": {
      "post": {
        "operationId": "search_web",
        "summary": "Search the live web (Brave Search API) and get ranked results as JSON. One query per call, or up to 10 in a single call with queries.",
        "tags": [
          "tools"
        ],
        "x-credits": 5,
        "x-extra": {
          "credits_by_query_count": {
            "note": "Charged per query that returned results, because a batch runs one upstream search per query. The figures below are the projection — the published price times every query sent — which is the ceiling; a query that fails is not charged, so the actual cost can be anything from 0 up to it. A single query costs the published price.",
            "one": 5,
            "ten": 50
          }
        },
        "x-docs-url": "https://www.crawlforge.dev/docs/api-reference/tools/search-web",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SearchWebRequest"
              },
              "example": {
                "query": "web scraping",
                "limit": 10,
                "lang": "en"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's result; 5 credits charged (see x-credits-note and x-extra for add-ons).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolSuccess"
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR: the body does not match the request schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "MISSING_API_KEY or INVALID_API_KEY.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "INSUFFICIENT_CREDITS: the balance does not cover the tool's base price. X-Auto-Recharge-Triggered says whether an automatic recharge was started; retry once it lands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMIT_EXCEEDED: your plan's rate limit. Retry-After: 60.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "TOOL_ERROR, INTERNAL_ERROR or CREDIT_DEDUCTION_FAILED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "default": {
            "description": "Any other error. Which of 403, 404, 413, 422, 501, 502, 503 and 504 a tool can return depends on what it does (fetching a page, calling the execution backend, reading stored results); the code vocabulary is listed on the ErrorResponse schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "describe_search_web",
        "summary": "Describe search_web",
        "description": "The tool's self-description: its price, its request schema and an example body. No API key is needed.",
        "tags": [
          "tools"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The ToolInfo this document was generated from.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolInfo"
                }
              }
            }
          }
        }
      }
    },
    "/tools/serp_rank": {
      "post": {
        "operationId": "serp_rank",
        "summary": "Check a domain's organic position in Google search results for a keyword (powered by DataForSEO). One lookup is one sample: Google can return a different result set for the same query minutes apart, so compare several lookups before reading a change in rank. A result set that barely mentions the keyword, a likely decoy page, comes back with suspect: true and a warning",
        "tags": [
          "tools"
        ],
        "x-credits": 5,
        "x-docs-url": "https://www.crawlforge.dev/docs/api-reference/tools/serp-rank",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SerpRankRequest"
              },
              "example": {
                "keyword": "managed wordpress hosting",
                "target": "example.com",
                "location_name": "United States"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's result; 5 credits charged (see x-credits-note and x-extra for add-ons).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolSuccess"
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR: the body does not match the request schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "MISSING_API_KEY or INVALID_API_KEY.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "INSUFFICIENT_CREDITS: the balance does not cover the tool's base price. X-Auto-Recharge-Triggered says whether an automatic recharge was started; retry once it lands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMIT_EXCEEDED: your plan's rate limit. Retry-After: 60.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "TOOL_ERROR, INTERNAL_ERROR or CREDIT_DEDUCTION_FAILED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "default": {
            "description": "Any other error. Which of 403, 404, 413, 422, 501, 502, 503 and 504 a tool can return depends on what it does (fetching a page, calling the execution backend, reading stored results); the code vocabulary is listed on the ErrorResponse schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "describe_serp_rank",
        "summary": "Describe serp_rank",
        "description": "The tool's self-description: its price, its request schema and an example body. No API key is needed.",
        "tags": [
          "tools"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The ToolInfo this document was generated from.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolInfo"
                }
              }
            }
          }
        }
      }
    },
    "/tools/stealth_mode": {
      "post": {
        "operationId": "stealth_mode",
        "summary": "Stealth browser scraping for sites that block normal scrapers (Cloudflare, DataDome, bot detection). Use operation \"scrape\" for a one-call render of a single URL. create_context then create_page is a navigation and challenge check: create_page loads urlToTest, reports its status, title and any challenge page, and closes the page before the call returns, so nothing on it carries over to a later call. For work over several calls on one page (log in, then read; click, then read), use browser_session with stealth: true. Operation \"configure\" only validates a stealthConfig and returns it with the defaults filled in — it stores nothing, so pass stealthConfig on each scrape or create_context call that should use it.",
        "tags": [
          "tools"
        ],
        "x-credits": 5,
        "x-extra": {
          "credits_cost_by_operation": {
            "note": "Browser work costs the published 5 credits; bookkeeping operations cost 1. An absent or unrecognised operation is charged the flat 5.",
            "scrape": 5,
            "create_context": 5,
            "create_page": 5,
            "configure": 1,
            "get_stats": 1,
            "cleanup": 1
          },
          "availability": {
            "hosted_api": true,
            "mcp_server": true,
            "note": "Runs on the CrawlForge execution backend. Browser contexts live on the backend: a contextId stays valid between calls only until the backend recycles it."
          }
        },
        "x-docs-url": "https://www.crawlforge.dev/docs/api-reference/tools/stealth-mode",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/StealthModeRequest"
              },
              "example": {
                "operation": "scrape",
                "url": "https://protected-site.com/product/42",
                "formats": [
                  "markdown",
                  "links"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's result; 5 credits charged (see x-credits-note and x-extra for add-ons).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolSuccess"
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR: the body does not match the request schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "MISSING_API_KEY or INVALID_API_KEY.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "INSUFFICIENT_CREDITS: the balance does not cover the tool's base price. X-Auto-Recharge-Triggered says whether an automatic recharge was started; retry once it lands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMIT_EXCEEDED: your plan's rate limit. Retry-After: 60.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "TOOL_ERROR, INTERNAL_ERROR or CREDIT_DEDUCTION_FAILED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "default": {
            "description": "Any other error. Which of 403, 404, 413, 422, 501, 502, 503 and 504 a tool can return depends on what it does (fetching a page, calling the execution backend, reading stored results); the code vocabulary is listed on the ErrorResponse schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "describe_stealth_mode",
        "summary": "Describe stealth_mode",
        "description": "The tool's self-description: its price, its request schema and an example body. No API key is needed.",
        "tags": [
          "tools"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The ToolInfo this document was generated from.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolInfo"
                }
              }
            }
          }
        }
      }
    },
    "/tools/summarize_content": {
      "post": {
        "operationId": "summarize_content",
        "summary": "Generate intelligent summaries of web content using extractive summarization",
        "tags": [
          "tools"
        ],
        "x-credits": 4,
        "x-docs-url": "https://www.crawlforge.dev/docs/api-reference/tools/summarize-content",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SummarizeContentRequest"
              },
              "example": {
                "url": "https://example.com/article",
                "max_sentences": 5,
                "summary_type": "extractive",
                "include_metadata": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's result; 4 credits charged (see x-credits-note and x-extra for add-ons).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolSuccess"
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR: the body does not match the request schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "MISSING_API_KEY or INVALID_API_KEY.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "INSUFFICIENT_CREDITS: the balance does not cover the tool's base price. X-Auto-Recharge-Triggered says whether an automatic recharge was started; retry once it lands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMIT_EXCEEDED: your plan's rate limit. Retry-After: 60.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "TOOL_ERROR, INTERNAL_ERROR or CREDIT_DEDUCTION_FAILED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "default": {
            "description": "Any other error. Which of 403, 404, 413, 422, 501, 502, 503 and 504 a tool can return depends on what it does (fetching a page, calling the execution backend, reading stored results); the code vocabulary is listed on the ErrorResponse schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "describe_summarize_content",
        "summary": "Describe summarize_content",
        "description": "The tool's self-description: its price, its request schema and an example body. No API key is needed.",
        "tags": [
          "tools"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The ToolInfo this document was generated from.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolInfo"
                }
              }
            }
          }
        }
      }
    },
    "/tools/track_changes": {
      "post": {
        "operationId": "track_changes",
        "summary": "Detect content changes on a page by comparing it against a stored baseline (create_baseline, then compare), or create a hosted monitor that checks it on a schedule (monitor)",
        "tags": [
          "tools"
        ],
        "x-credits": 3,
        "x-extra": {
          "notes": [
            "Baselines are stored per API account for 90 days; compare against a missing baseline returns 404 BASELINE_NOT_FOUND.",
            "Change detection is text-based (line diff of visible text, whitespace-normalized). JavaScript-rendered content is not executed — use the CrawlForge MCP server for browser-rendered tracking.",
            "compare also returns structural_similarity (0-1): how far the tracked region still matches the baseline in tag vocabulary and nesting depth. A high change_percent with a high structural_similarity is the same layout with new copy; a low structural_similarity means the markup itself was rebuilt, which is usually what breaks a scraper.",
            "structural_similarity is null when the stored baseline was captured before this field existed. Re-run create_baseline, or pass update_baseline once, to start scoring it.",
            "A bot-defence challenge wall or an unusable document is returned as 502 BLOCKED or UNUSABLE_DOCUMENT with no charge, and is never reported as a change. Hosted monitors record such a page as blocked.",
            "monitor returns the created monitor (including its webhook_secret, shown only here) and estimated_credits_per_month; the response bills 0 credits. Scheduled checks appear in the request log as track_changes calls.",
            "Documented option objects (trackingOptions, storageOptions, monitoringOptions) are accepted but not applied by the hosted REST API."
          ]
        },
        "x-docs-url": "https://www.crawlforge.dev/docs/api-reference/tools/track-changes",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TrackChangesRequest"
              },
              "example": {
                "url": "https://competitor.com/pricing",
                "operation": "compare",
                "selector": ".pricing-table",
                "update_baseline": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's result; 3 credits charged (see x-credits-note and x-extra for add-ons).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolSuccess"
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR: the body does not match the request schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "MISSING_API_KEY or INVALID_API_KEY.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "INSUFFICIENT_CREDITS: the balance does not cover the tool's base price. X-Auto-Recharge-Triggered says whether an automatic recharge was started; retry once it lands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMIT_EXCEEDED: your plan's rate limit. Retry-After: 60.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "TOOL_ERROR, INTERNAL_ERROR or CREDIT_DEDUCTION_FAILED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "default": {
            "description": "Any other error. Which of 403, 404, 413, 422, 501, 502, 503 and 504 a tool can return depends on what it does (fetching a page, calling the execution backend, reading stored results); the code vocabulary is listed on the ErrorResponse schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "describe_track_changes",
        "summary": "Describe track_changes",
        "description": "The tool's self-description: its price, its request schema and an example body. No API key is needed.",
        "tags": [
          "tools"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The ToolInfo this document was generated from.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolInfo"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "An API key from the dashboard. Every POST requires one."
      }
    },
    "schemas": {
      "ToolSuccess": {
        "type": "object",
        "description": "The envelope every tool returns on success. Credits are deducted only after the tool succeeded, so credits_used is the price of this call and an error response never charges.",
        "required": [
          "success",
          "data",
          "credits_used",
          "credits_remaining",
          "processing_time"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "additionalProperties": true,
            "description": "The tool's result. Its shape is tool-specific and documented on the tool's docs page (x-docs-url on the operation)."
          },
          "credits_used": {
            "type": "integer",
            "description": "Credits charged for this call: the operation's x-credits, plus any add-on x-credits-note or x-extra describes."
          },
          "credits_remaining": {
            "type": "integer",
            "description": "The account's credit balance after this call."
          },
          "processing_time": {
            "type": "integer",
            "description": "Server-side processing time in milliseconds."
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Non-fatal notes: a robots.txt override that was recorded, a format that could not be produced, a redaction that changed the payload."
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "description": "Every error carries error.code and error.message; no error response is charged. Codes by status:\n400 VALIDATION_ERROR (details holds the validation issues; also a URL that resolves to a private or local address, or a body that is not a JSON object). scrape_template adds UNKNOWN_TEMPLATE, NO_TEMPLATE_MATCH, TEMPLATE_UNAVAILABLE, TEMPLATE_MISMATCH, MISSING_URL, INVALID_TEMPLATE_PARAMS and CREDENTIAL_MISSING; scrape adds UNSUPPORTED_FORMAT; read_result adds PATH_NOT_FOUND.\n401 MISSING_API_KEY, INVALID_API_KEY.\n402 INSUFFICIENT_CREDITS (details: required, tool, autoRechargeTriggered, upgradeUrl; the X-Auto-Recharge-Triggered header says whether an automatic recharge was started).\n403 ROBOTS_DISALLOWED (robots.txt disallows the path for CrawlForge; respect_robots: false overrides it and the override is recorded), HOST_BLOCKED (a host on the operator blocklist).\n404 RESULT_NOT_FOUND, BATCH_NOT_FOUND, BASELINE_NOT_FOUND (an unknown or expired handle, batch id or baseline).\n413 RESPONSE_TOO_LARGE, DOCUMENT_TOO_LARGE.\n422 EXTRACTION_FAILED, SELECTOR_NOT_FOUND, NO_PAGES_MAPPED, DOCUMENT_PARSE_FAILED, USE_REDDIT_SEARCH (a reddit.com URL: reddit.com refuses every non-browser client, so it is never fetched; the message names the reddit_search call that reads the same posts and comments from the community archive).\n429 RATE_LIMIT_EXCEEDED (your plan's rate limit; Retry-After: 60), HOST_BACKOFF (the target site asked us to wait via Retry-After; not overridable), CRAWL_DELAY (a redirect that would have to wait out the target site's robots.txt Crawl-delay for longer than the request has; the message names the redirect target to request directly).\n500 TOOL_ERROR, INTERNAL_ERROR, CREDIT_DEDUCTION_FAILED.\n501 OPERATION_NOT_AVAILABLE, UNSUPPORTED_DOCUMENT_TYPE (supported by the CrawlForge MCP server with the same key).\n502 FETCH_FAILED, TARGET_HTTP_ERROR, DOCUMENT_FETCH_ERROR (the target could not be reached or answered with an error), BLOCKED (a bot-defence challenge page; scrape and extract_embedded_state name the vendor in `blocked`), UNUSABLE_DOCUMENT (an empty shell or error placeholder served as a page), MCP_UPSTREAM_ERROR, MCP_UPSTREAM_UNAVAILABLE, MCP_PROXY_MISCONFIGURED, RESEARCH_SEARCH_FAILED, RESEARCH_SEARCH_UNAVAILABLE, TOOL_ERROR (the execution backend reported a tool failure).\n503 TOOL_NOT_AVAILABLE (needs a browser, an LLM or local infrastructure the hosted API does not run), STORAGE_UNAVAILABLE, MCP_PROXY_MISCONFIGURED, RESEARCH_NOT_CONFIGURED, RESEARCH_RATE_LIMITED.\n504 FETCH_TIMEOUT, MCP_UPSTREAM_TIMEOUT, RESEARCH_SEARCH_TIMEOUT.",
        "required": [
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false,
            "description": "Present, and false, on a tool failure; absent on validation, authentication, credit and rate-limit errors."
          },
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "A stable, upper-case code from the vocabulary above."
              },
              "message": {
                "type": "string",
                "description": "What went wrong, for a person."
              },
              "details": {
                "description": "Extra structure for some codes: the validation issues for VALIDATION_ERROR, the credit shortfall for INSUFFICIENT_CREDITS, the plan and limit for RATE_LIMIT_EXCEEDED."
              }
            }
          },
          "blocked": {
            "type": "object",
            "description": "scrape and extract_embedded_state, with code BLOCKED: the bot-defence vendor recognised on the page and the evidence for it.",
            "required": [
              "vendor",
              "evidence"
            ],
            "properties": {
              "vendor": {
                "type": "string"
              },
              "evidence": {
                "type": "string"
              }
            }
          },
          "escalated": {
            "type": "boolean",
            "const": true,
            "description": "scrape and extract_embedded_state: the call asked to escalate, the stealth render ran, and it did not get past the wall either. Nothing was charged."
          }
        }
      },
      "ToolInfo": {
        "type": "object",
        "description": "A tool's self-description, as its GET returns it and as this document was generated from it.",
        "required": [
          "tool",
          "description",
          "credits_cost",
          "parameters",
          "example"
        ],
        "properties": {
          "tool": {
            "type": "string",
            "description": "The tool name, as in the path."
          },
          "description": {
            "type": "string"
          },
          "credits_cost": {
            "type": "number",
            "description": "The base price in credits. A per-unit or conditional price is explained in credits_note."
          },
          "credits_note": {
            "type": "string"
          },
          "parameters": {
            "type": "object",
            "additionalProperties": true,
            "description": "JSON Schema (draft 2020-12) for the POST request body."
          },
          "example": {
            "type": "object",
            "additionalProperties": true,
            "description": "A request body that runs."
          },
          "extra": {
            "type": "object",
            "additionalProperties": true,
            "description": "Tool-specific notes, such as a price table by operation or an execution model."
          }
        }
      },
      "AgentRequest": {
        "type": "object",
        "properties": {
          "prompt": {
            "type": "string",
            "description": "Natural-language task or question (1-2000 chars)"
          },
          "urls": {
            "type": "array",
            "description": "Optional seed URLs to include (max 20)"
          },
          "schema": {
            "type": "object",
            "additionalProperties": true,
            "description": "Optional JSON schema for structured output"
          },
          "model": {
            "type": "string",
            "enum": [
              "default",
              "pro"
            ],
            "default": "default",
            "description": "\"default\" = built-in planning loop. \"pro\" requires interactive confirmation and is rejected by the REST API — use the CrawlForge MCP server for pro runs."
          },
          "maxSteps": {
            "type": "number",
            "default": 5,
            "description": "Max fetch iterations (hard cap: 10)"
          },
          "maxUrls": {
            "type": "number",
            "default": 10,
            "description": "Max URLs to fetch (hard cap: 20)"
          },
          "max_inline_chars": {
            "type": "number",
            "default": 40000,
            "description": "Largest result returned inline, in characters of its JSON (1000-10000000; env CRAWLFORGE_MAX_INLINE_CHARS sets the default). Over it, the result is stored for 1 hour and the response carries a preview, a result_handle, total_chars and truncated: true; read the rest with read_result (1 credit)."
          }
        },
        "required": [
          "prompt"
        ]
      },
      "AnalyzeContentRequest": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "URL to fetch and analyze content from"
          },
          "content": {
            "type": "string",
            "description": "Direct content to analyze (if URL not provided)"
          },
          "analyze_sentiment": {
            "default": false,
            "type": "boolean",
            "description": "Perform sentiment analysis"
          },
          "extract_keywords": {
            "default": true,
            "type": "boolean",
            "description": "Extract top keywords"
          },
          "detect_language": {
            "default": true,
            "type": "boolean",
            "description": "Detect content language. Returns an ISO 639-1 code, or \"und\" when the text is too short or the language is not recognised."
          },
          "analyze_readability": {
            "default": false,
            "type": "boolean",
            "description": "Calculate readability scores"
          },
          "timeout": {
            "default": 10000,
            "type": "number",
            "minimum": 1000,
            "maximum": 30000,
            "description": "Request timeout in milliseconds (1000-30000)"
          },
          "respect_robots": {
            "default": true,
            "type": "boolean"
          }
        },
        "anyOf": [
          {
            "required": [
              "url"
            ]
          },
          {
            "required": [
              "content"
            ]
          }
        ]
      },
      "BatchScrapeRequest": {
        "type": "object",
        "properties": {
          "urls": {
            "minItems": 1,
            "maxItems": 50,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "url": {
                  "type": "string",
                  "format": "uri",
                  "description": "Target URL"
                },
                "id": {
                  "type": "string",
                  "description": "Optional identifier (defaults to the array index)"
                },
                "priority": {
                  "type": "string",
                  "enum": [
                    "high",
                    "medium",
                    "low"
                  ]
                },
                "custom_config": {
                  "type": "object",
                  "properties": {
                    "headers": {
                      "type": "object",
                      "propertyNames": {
                        "type": "string"
                      },
                      "additionalProperties": {
                        "type": "string"
                      }
                    },
                    "timeout": {
                      "type": "number",
                      "minimum": 1000,
                      "maximum": 60000
                    },
                    "selectors": {
                      "type": "object",
                      "propertyNames": {
                        "type": "string"
                      },
                      "additionalProperties": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "required": [
                "url"
              ]
            },
            "description": "Array of URLs to scrape (max 50 per synchronous batch on the hosted REST API)"
          },
          "respect_robots": {
            "default": true,
            "type": "boolean",
            "description": "Fetch each origin's robots.txt and skip URLs it disallows for CrawlForge. A missing or unreachable robots.txt is treated as no restrictions. A disallowed URL returns status \"skipped\" and is not charged; the rest of the batch runs."
          },
          "batch_config": {
            "type": "object",
            "properties": {
              "concurrency": {
                "default": 5,
                "type": "number",
                "minimum": 1,
                "maximum": 10,
                "description": "Max concurrent requests (capped at 8)"
              },
              "delay_between_batches": {
                "type": "number",
                "minimum": 0,
                "maximum": 10000
              },
              "retry_failed": {
                "type": "boolean"
              },
              "max_retries": {
                "type": "number",
                "minimum": 0,
                "maximum": 5
              },
              "fail_fast": {
                "type": "boolean"
              },
              "preserve_order": {
                "type": "boolean"
              }
            },
            "description": "Batch processing configuration"
          },
          "extraction_template": {
            "type": "object",
            "properties": {
              "fields": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "name": {
                      "type": "string"
                    },
                    "selector": {
                      "type": "string"
                    },
                    "xpath": {
                      "type": "string"
                    },
                    "attribute": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "name"
                  ]
                },
                "description": "Fields to extract from each page: { name, selector (CSS), attribute? }. Without attribute the element text is returned, one line per block element; xpath is not supported."
              },
              "capture_screenshots": {
                "type": "boolean"
              },
              "extract_links": {
                "type": "boolean"
              },
              "extract_images": {
                "type": "boolean"
              }
            },
            "description": "Optional per-page field extraction"
          },
          "output_config": {
            "type": "object",
            "properties": {
              "format": {
                "type": "string",
                "enum": [
                  "json",
                  "csv",
                  "xml"
                ]
              },
              "include_metadata": {
                "type": "boolean"
              },
              "include_errors": {
                "type": "boolean"
              },
              "flatten_results": {
                "type": "boolean"
              }
            }
          },
          "options": {
            "type": "object",
            "properties": {
              "user_agent": {
                "type": "string"
              },
              "follow_redirects": {
                "type": "boolean"
              },
              "javascript_enabled": {
                "type": "boolean"
              },
              "respect_robots_txt": {
                "type": "boolean"
              },
              "rate_limit_per_domain": {
                "type": "number",
                "minimum": 100,
                "maximum": 5000
              }
            }
          },
          "max_inline_chars": {
            "type": "integer",
            "minimum": 1000,
            "maximum": 10000000,
            "description": "Largest result returned inline, in characters of its JSON (1000-10000000; env CRAWLFORGE_MAX_INLINE_CHARS sets the default). Over it, the result is stored for 1 hour and the response carries a preview, a result_handle, total_chars and truncated: true; read the rest with read_result (1 credit)."
          },
          "redact_pii": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "object",
                "properties": {
                  "entities": {
                    "maxItems": 16,
                    "type": "array",
                    "items": {
                      "type": "string",
                      "minLength": 1
                    }
                  },
                  "replace_style": {
                    "type": "string",
                    "enum": [
                      "tag",
                      "mask",
                      "remove"
                    ]
                  },
                  "mode": {
                    "type": "string",
                    "enum": [
                      "fast",
                      "model"
                    ]
                  }
                }
              }
            ],
            "description": "Remove personal data from the text this call returns, before it is stored or sent back. true is shorthand for { mode: \"fast\" }: every entity, tagged. As an object: entities (any of EMAIL, PHONE, FINANCIAL, SECRET; omitted or empty means all four, and any other name is a 400 rather than a silent no-op), replace_style (\"tag\" → <EMAIL>, \"mask\" → [REDACTED], \"remove\" → nothing; default \"tag\") and mode (\"fast\", the default, is regex-only and costs no extra credits; \"model\" covers PERSON and LOCATION, needs an LLM and is rejected here — use the CrawlForge MCP server). The response carries redaction: { entities, count, mode } inside data, saying what was removed. Detection is deliberately conservative: a card number must pass Luhn and an IBAN mod-97, so a false positive cannot silently destroy real page content. URLs, queries and identifiers the response uses to name what was fetched are left intact, and counters derived from the text (content_length, word_count, character_count) describe the text as it was extracted, before redaction."
          }
        },
        "required": [
          "urls"
        ]
      },
      "BrowserSessionRequest": {
        "type": "object",
        "properties": {
          "operation": {
            "type": "string",
            "enum": [
              "open",
              "snapshot",
              "act",
              "read",
              "screenshot",
              "close",
              "list"
            ],
            "description": "What this call does to the session. Every operation but open and list needs session_id. Priced per operation, not per call: open 3, read 2, snapshot / act / screenshot / close / list 1 each."
          },
          "session_id": {
            "type": "string",
            "description": "The id operation \"open\" returned. Required by snapshot, act, read, screenshot and close."
          },
          "url": {
            "type": "string",
            "description": "open: the URL to load the session on"
          },
          "stealth": {
            "type": "boolean",
            "default": false,
            "description": "open: run the session in the stealth browser instead of the standard pool"
          },
          "ttl": {
            "type": "number",
            "description": "open: seconds the session may live from the moment it opens (30-3600, default 600)"
          },
          "activity_ttl": {
            "type": "number",
            "description": "open: seconds the session may sit idle between calls (10-3600, default 300). The session dies at whichever of the two clocks runs out first."
          },
          "viewport": {
            "type": "object",
            "description": "open: viewport size",
            "properties": {
              "width": {
                "type": "number",
                "description": "800-1920"
              },
              "height": {
                "type": "number",
                "description": "600-1080"
              }
            }
          },
          "timeout": {
            "type": "number",
            "default": 30000,
            "description": "Per-action timeout in ms (10000-120000). The REST call itself waits up to ~50s, so a timeout above that cannot be reached here."
          },
          "respect_robots": {
            "type": "boolean",
            "default": true,
            "description": "Respect the target site's robots.txt. Omitted, the compliant default (true) applies: a URL disallowed for CrawlForge is refused before the browser opens and no credits are charged, and every navigate action inside the session is checked the same way. It applies to the call it is sent on and is never remembered by the session, so an override has to be repeated on each call that navigates. Setting this to false is honoured, returns a warning in the response, and is recorded against your API key."
          },
          "interactive_only": {
            "type": "boolean",
            "default": true,
            "description": "snapshot: give refs to interactive elements only. Set false to list headings and landmarks too; only interactive elements are ever given a ref."
          },
          "max_nodes": {
            "type": "number",
            "default": 200,
            "description": "snapshot: cap on how many nodes the tree lists (1-1000). The result reports truncated: true when the cap stopped the walk."
          },
          "actions": {
            "type": "array",
            "description": "act: the actions to run against the open page, 1-20 of them, in the same shape scrape_with_actions takes — see that tool for every per-action field. Target an element by a @e ref from the most recent snapshot (for example \"@e2\") in selector, or by a CSS selector. Navigation invalidates refs, so snapshot again after one.",
            "items": {
              "type": "object",
              "additionalProperties": true,
              "properties": {
                "type": {
                  "type": "string",
                  "enum": [
                    "wait",
                    "click",
                    "type",
                    "press",
                    "scroll",
                    "screenshot",
                    "executeJavaScript",
                    "select",
                    "hover",
                    "navigate",
                    "snapshot"
                  ],
                  "description": "Action to perform. executeJavaScript is refused in a session on the hosted API — the script would run in a browser on our servers, not on your machine."
                },
                "selector": {
                  "type": "string",
                  "description": "What the action targets: a CSS selector, or a @e ref from the last snapshot"
                }
              }
            }
          },
          "continue_on_error": {
            "type": "boolean",
            "default": false,
            "description": "act: keep going past a failed action instead of stopping the call"
          },
          "formats": {
            "type": "array",
            "default": [
              "markdown"
            ],
            "description": "read: formats to return from the live DOM — markdown, html, text, json. The page is read as it stands after everything the session has done, cookies and all; nothing is re-fetched."
          },
          "onlyMainContent": {
            "type": "boolean",
            "default": false,
            "description": "read: false (default) returns the whole page as markdown/text (markdown leaves out nav, footer and aside elements, as scrape's does; text leaves out nothing); true keeps only the main article block via Readability, which drops navigation and footers but can also drop list items on listing and app pages. The result's extractionMethod says which ran (\"full_page\" for the whole page)."
          },
          "full_page": {
            "type": "boolean",
            "default": false,
            "description": "screenshot: capture the full scrollable page"
          },
          "format": {
            "type": "string",
            "enum": [
              "png",
              "jpeg"
            ],
            "default": "png",
            "description": "screenshot: image format"
          },
          "quality": {
            "type": "number",
            "default": 80,
            "description": "screenshot: JPEG quality (0-100)"
          },
          "selector": {
            "type": "string",
            "description": "screenshot: capture just this element (a @e ref works)"
          },
          "max_inline_chars": {
            "type": "number",
            "default": 40000,
            "description": "Largest result returned inline, in characters of its JSON (1000-10000000; env CRAWLFORGE_MAX_INLINE_CHARS sets the default). Over it, the result is stored for 1 hour and the response carries a preview, a result_handle, total_chars and truncated: true; read the rest with read_result (1 credit)."
          },
          "redact_pii": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "object",
                "properties": {
                  "entities": {
                    "maxItems": 16,
                    "type": "array",
                    "items": {
                      "type": "string",
                      "minLength": 1
                    }
                  },
                  "replace_style": {
                    "type": "string",
                    "enum": [
                      "tag",
                      "mask",
                      "remove"
                    ]
                  },
                  "mode": {
                    "type": "string",
                    "enum": [
                      "fast",
                      "model"
                    ]
                  }
                }
              }
            ],
            "default": false,
            "description": "Remove personal data from the text this call returns, before it is stored or sent back. true is shorthand for { mode: \"fast\" }: every entity, tagged. As an object: entities (any of EMAIL, PHONE, FINANCIAL, SECRET; omitted or empty means all four, and any other name is a 400 rather than a silent no-op), replace_style (\"tag\" → <EMAIL>, \"mask\" → [REDACTED], \"remove\" → nothing; default \"tag\") and mode (\"fast\", the default, is regex-only and costs no extra credits; \"model\" covers PERSON and LOCATION, needs an LLM and is rejected here — use the CrawlForge MCP server). The response carries redaction: { entities, count, mode } inside data, saying what was removed. Detection is deliberately conservative: a card number must pass Luhn and an IBAN mod-97, so a false positive cannot silently destroy real page content. URLs, queries and identifiers the response uses to name what was fetched are left intact, and counters derived from the text (content_length, word_count, character_count) describe the text as it was extracted, before redaction."
          }
        },
        "required": [
          "operation"
        ]
      },
      "CrawlDeepRequest": {
        "type": "object",
        "properties": {
          "start_url": {
            "type": "string",
            "format": "uri",
            "description": "The starting URL for the crawl"
          },
          "max_pages": {
            "default": 10,
            "type": "number",
            "minimum": 1,
            "maximum": 100,
            "description": "Maximum number of pages to crawl (1-100)"
          },
          "max_depth": {
            "default": 3,
            "type": "number",
            "minimum": 1,
            "maximum": 5,
            "description": "Maximum depth to crawl (1-5)"
          },
          "same_domain_only": {
            "default": true,
            "type": "boolean",
            "description": "Only crawl pages on the same domain"
          },
          "respect_robots": {
            "default": true,
            "type": "boolean",
            "description": "Fetch each origin's robots.txt and skip URLs it disallows for CrawlForge. A missing or unreachable robots.txt is treated as no restrictions. Returns 403 ROBOTS_DISALLOWED when start_url itself is disallowed, with no credits charged; URLs disallowed mid-crawl are skipped and counted in warnings. Setting this to false is recorded against your API key."
          },
          "respect_robots_txt": {
            "default": true,
            "type": "boolean",
            "description": "Alias of respect_robots, kept for callers that already send it. Either parameter set to false disables the gate."
          },
          "crawl_delay": {
            "default": 1000,
            "type": "number",
            "minimum": 0,
            "maximum": 5000,
            "description": "Delay between requests to the same host, in milliseconds (0-5000). Resolved against the site's own robots.txt Crawl-delay as the longer of the two, so the site wins when it asks for more and the two never stack. A delay that will not fit in the crawl timeout skips the page rather than overrunning the budget."
          },
          "timeout": {
            "default": 30000,
            "type": "number",
            "minimum": 1000,
            "maximum": 60000,
            "description": "Total crawl timeout in milliseconds (1000-60000)"
          },
          "max_inline_chars": {
            "type": "integer",
            "minimum": 1000,
            "maximum": 10000000,
            "description": "Largest result returned inline, in characters of its JSON (1000-10000000; env CRAWLFORGE_MAX_INLINE_CHARS sets the default). Over it, the result is stored for 1 hour and the response carries a preview, a result_handle, total_chars and truncated: true; read the rest with read_result (1 credit)."
          },
          "redact_pii": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "object",
                "properties": {
                  "entities": {
                    "maxItems": 16,
                    "type": "array",
                    "items": {
                      "type": "string",
                      "minLength": 1
                    }
                  },
                  "replace_style": {
                    "type": "string",
                    "enum": [
                      "tag",
                      "mask",
                      "remove"
                    ]
                  },
                  "mode": {
                    "type": "string",
                    "enum": [
                      "fast",
                      "model"
                    ]
                  }
                }
              }
            ],
            "description": "Remove personal data from the text this call returns, before it is stored or sent back. true is shorthand for { mode: \"fast\" }: every entity, tagged. As an object: entities (any of EMAIL, PHONE, FINANCIAL, SECRET; omitted or empty means all four, and any other name is a 400 rather than a silent no-op), replace_style (\"tag\" → <EMAIL>, \"mask\" → [REDACTED], \"remove\" → nothing; default \"tag\") and mode (\"fast\", the default, is regex-only and costs no extra credits; \"model\" covers PERSON and LOCATION, needs an LLM and is rejected here — use the CrawlForge MCP server). The response carries redaction: { entities, count, mode } inside data, saying what was removed. Detection is deliberately conservative: a card number must pass Luhn and an IBAN mod-97, so a false positive cannot silently destroy real page content. URLs, queries and identifiers the response uses to name what was fetched are left intact, and counters derived from the text (content_length, word_count, character_count) describe the text as it was extracted, before redaction."
          }
        },
        "required": [
          "start_url"
        ]
      },
      "DeepResearchRequest": {
        "type": "object",
        "properties": {
          "research_query": {
            "type": "string",
            "minLength": 10,
            "description": "The research question or topic (minimum 10 characters)"
          },
          "research_scope": {
            "type": "object",
            "properties": {
              "depth_level": {
                "default": "moderate",
                "type": "string",
                "enum": [
                  "surface",
                  "moderate",
                  "deep",
                  "comprehensive"
                ],
                "description": "How many sources to gather: surface (3), moderate (5), deep (8), comprehensive (10)"
              },
              "time_range": {
                "default": "any",
                "type": "string",
                "enum": [
                  "any",
                  "day",
                  "week",
                  "month",
                  "year"
                ],
                "description": "Restrict results by recency: any, day, week, month, year"
              },
              "language": {
                "default": "en",
                "type": "string",
                "minLength": 2,
                "maxLength": 10,
                "description": "Search language code (e.g. 'en')"
              },
              "domains": {
                "maxItems": 10,
                "type": "array",
                "items": {
                  "type": "string",
                  "minLength": 3
                },
                "description": "Restrict results to these domains (e.g. ['nature.com', 'arxiv.org']; max 10)"
              }
            },
            "description": "Optional scoping controls"
          },
          "max_sources": {
            "type": "integer",
            "minimum": 1,
            "maximum": 10,
            "description": "Override the source count from depth_level (1-10)"
          },
          "respect_robots": {
            "default": true,
            "type": "boolean",
            "description": "Fetch each origin's robots.txt and skip source pages it disallows for CrawlForge. A missing or unreachable robots.txt is treated as no restrictions. A disallowed source is reported with fetched: false and contributes only its search snippet; the reason is named in warnings. The search itself is a documented Google API call and is never gated."
          },
          "max_inline_chars": {
            "type": "integer",
            "minimum": 1000,
            "maximum": 10000000,
            "description": "Largest result returned inline, in characters of its JSON (1000-10000000; env CRAWLFORGE_MAX_INLINE_CHARS sets the default). Over it, the result is stored for 1 hour and the response carries a preview, a result_handle, total_chars and truncated: true; read the rest with read_result (1 credit)."
          }
        },
        "required": [
          "research_query"
        ]
      },
      "ExtractContentRequest": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "The URL to extract content from"
          },
          "include_images": {
            "default": true,
            "type": "boolean",
            "description": "Include images found in the content"
          },
          "include_links": {
            "default": false,
            "type": "boolean",
            "description": "Include links found in the content"
          },
          "clean_html": {
            "default": true,
            "type": "boolean",
            "description": "Return clean text instead of HTML: one line per block element (heading, paragraph, list item, table row)"
          },
          "extract_main_content": {
            "default": true,
            "type": "boolean",
            "description": "Use readability algorithms to find main content"
          },
          "timeout": {
            "default": 10000,
            "type": "number",
            "minimum": 1000,
            "maximum": 30000,
            "description": "Request timeout in milliseconds (1000-30000)"
          },
          "respect_robots": {
            "default": true,
            "type": "boolean"
          },
          "max_inline_chars": {
            "type": "integer",
            "minimum": 1000,
            "maximum": 10000000,
            "description": "Largest result returned inline, in characters of its JSON (1000-10000000; env CRAWLFORGE_MAX_INLINE_CHARS sets the default). Over it, the result is stored for 1 hour and the response carries a preview, a result_handle, total_chars and truncated: true; read the rest with read_result (1 credit)."
          },
          "redact_pii": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "object",
                "properties": {
                  "entities": {
                    "maxItems": 16,
                    "type": "array",
                    "items": {
                      "type": "string",
                      "minLength": 1
                    }
                  },
                  "replace_style": {
                    "type": "string",
                    "enum": [
                      "tag",
                      "mask",
                      "remove"
                    ]
                  },
                  "mode": {
                    "type": "string",
                    "enum": [
                      "fast",
                      "model"
                    ]
                  }
                }
              }
            ],
            "description": "Remove personal data from the text this call returns, before it is stored or sent back. true is shorthand for { mode: \"fast\" }: every entity, tagged. As an object: entities (any of EMAIL, PHONE, FINANCIAL, SECRET; omitted or empty means all four, and any other name is a 400 rather than a silent no-op), replace_style (\"tag\" → <EMAIL>, \"mask\" → [REDACTED], \"remove\" → nothing; default \"tag\") and mode (\"fast\", the default, is regex-only and costs no extra credits; \"model\" covers PERSON and LOCATION, needs an LLM and is rejected here — use the CrawlForge MCP server). The response carries redaction: { entities, count, mode } inside data, saying what was removed. Detection is deliberately conservative: a card number must pass Luhn and an IBAN mod-97, so a false positive cannot silently destroy real page content. URLs, queries and identifiers the response uses to name what was fetched are left intact, and counters derived from the text (content_length, word_count, character_count) describe the text as it was extracted, before redaction."
          }
        },
        "required": [
          "url"
        ]
      },
      "ExtractEmbeddedStateRequest": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "URL to read embedded state from"
          },
          "path": {
            "type": "string",
            "description": "Return one subtree instead of everything, e.g. \"next_data.props.pageProps\" or \"json_scripts[0].data\". Dotted keys and array indexes only — not JSONPath: no wildcards, filters, slices or recursive descent. A path that does not resolve is a 400 naming the keys that were available at the point it stopped, and costs no credits."
          },
          "user_agent": {
            "type": "string",
            "description": "Override the User-Agent sent to the target"
          },
          "respect_robots": {
            "default": true,
            "type": "boolean",
            "description": "Honour the target's robots.txt. Setting this to false is recorded against your API key."
          },
          "timeout": {
            "default": 20000,
            "type": "integer",
            "minimum": 1000,
            "maximum": 60000,
            "description": "Fetch timeout in ms (1000-60000)"
          },
          "keys_only": {
            "default": false,
            "type": "boolean",
            "description": "Return keys instead of data: the selected data (after path, if given) described two levels deep, each leaf labelled \"object\", \"array(<n>)\", \"string\", \"number\", \"boolean\" or \"null\", and an array as { length, first } with the shape of its first element. found and bytes are unchanged, so one cheap call shows which path to ask for."
          },
          "find": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100,
            "description": "Return `matches` instead of `data`: every property with this key name (case-insensitive) anywhere in the selected data (after `path`), in document order, each as {path, preview} with the first 200 characters of its value - at most 50, with `matches_total` and `matches_truncated`. Each match path already starts with `path`, so it can be passed straight back as `path`. Discover where a field lives without downloading the payload. Cannot be combined with keys_only"
          },
          "raw": {
            "default": false,
            "type": "boolean",
            "description": "Also keep the undecoded __NUXT_DATA__ devalue array under json_scripts, beside the decoded nuxt_data. Default: false"
          },
          "escalate": {
            "default": false,
            "type": "boolean",
            "description": "Retry a blocked page in a stealth browser, in this same call. The plain fetch always runs first; only a bot wall, an empty shell with no state, or a 403, 429 or 444 escalates — never a 404 or a 5xx. The rendered HTML goes through the same parser. The response carries escalated: true|false whenever this is set, plus stealth: { engine, vendor_detected } when the render ran. It costs the escalation add-on only when it ran. The stage needs the CrawlForge execution backend: where that is not configured it returns 503 TOOL_NOT_AVAILABLE and charges nothing. robots.txt is respected on the escalated path too. Reading window globals (window_state) is MCP-server only: the render here returns HTML."
          },
          "escalate_engine": {
            "default": "playwright",
            "type": "string",
            "enum": [
              "playwright",
              "camoufox"
            ],
            "description": "Browser engine for the escalated render (camoufox only where installed on the backend). Ignored when no escalation runs."
          },
          "wait_for": {
            "type": "integer",
            "minimum": 0,
            "maximum": 30000,
            "description": "Extra wait after page load for the escalated render, in ms (0-30000), passed to stealth_mode for state that is written after DOMContentLoaded. Ignored when no escalation runs."
          },
          "max_inline_chars": {
            "type": "integer",
            "minimum": 1000,
            "maximum": 10000000,
            "description": "Largest result returned inline, in characters of its JSON (1000-10000000; env CRAWLFORGE_MAX_INLINE_CHARS sets the default). Over it, the result is stored for 1 hour and the response carries a preview, a result_handle, total_chars and truncated: true; read the rest with read_result (1 credit). Here the response keeps url, path, bytes, found, data_keys (the top-level keys of data) and escalated/stealth, and preview is the head of the pretty-printed JSON cut at a line boundary, so no value is ever split and the response stays within max_inline_chars; read_result json_path reads any field of the stored result, e.g. \"data.next_data.props\"."
          }
        },
        "required": [
          "url"
        ],
        "anyOf": [
          {
            "properties": {
              "keys_only": {
                "const": false
              }
            }
          },
          {
            "properties": {
              "find": false
            }
          }
        ]
      },
      "ExtractLinksRequest": {
        "type": "object",
        "properties": {
          "html": {
            "type": "string",
            "minLength": 1,
            "description": "HTML content to extract links from"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "URL to fetch and extract links from"
          },
          "base_url": {
            "type": "string",
            "format": "uri",
            "description": "Base URL for resolving relative links"
          },
          "filter_domains": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Only include links from specified domains"
          },
          "include_external": {
            "default": true,
            "type": "boolean",
            "description": "Include external links"
          },
          "include_internal": {
            "default": true,
            "type": "boolean",
            "description": "Include internal links (same hostname as the page)"
          },
          "filter_external": {
            "type": "boolean",
            "description": "The MCP tool's name for outbound-only: true is the same as include_internal: false. An alias; when true it overrides include_internal."
          },
          "include_anchors": {
            "default": false,
            "type": "boolean",
            "description": "Include anchor links (#fragment)"
          },
          "deduplicate": {
            "default": true,
            "type": "boolean",
            "description": "Remove duplicate links, compared without fragment or trailing slash; the first occurrence is kept"
          },
          "include_metadata": {
            "default": true,
            "type": "boolean",
            "description": "Include each link's title and the domains count map. Every record always carries href, text, type, domain, rel and original_href — the shape the MCP tool returns. type is internal, external, anchor, other (mailto:, tel:, javascript: and other non-web schemes; href is null except for mailto: and tel:) or relative (html input with no base_url). A <base href> in the page is honoured unless base_url is given."
          },
          "respect_robots": {
            "default": true,
            "type": "boolean"
          },
          "escalate": {
            "default": false,
            "type": "boolean",
            "description": "Retry a walled page in a stealth browser, in this same call (url input only). The plain fetch always runs first; only a named bot wall, a 403, 429 or 444, or an empty shell or error placeholder served as 2xx escalates — never a 404, a 5xx, a binary or a host whose Retry-After window is still open. The response carries escalated: true|false whenever this is set, plus stealth: { engine, vendor_detected } when the render ran. It costs the escalation add-on only when it ran. The stage needs the CrawlForge execution backend: where that is not configured it returns 503 TOOL_NOT_AVAILABLE and charges nothing. robots.txt is respected on the escalated path too."
          },
          "escalate_engine": {
            "default": "playwright",
            "type": "string",
            "enum": [
              "playwright",
              "camoufox"
            ],
            "description": "Browser engine for the escalated render (camoufox only where installed on the backend). Ignored when no escalation runs."
          },
          "max_inline_chars": {
            "type": "integer",
            "minimum": 1000,
            "maximum": 10000000,
            "description": "Largest result returned inline, in characters of its JSON (1000-10000000; env CRAWLFORGE_MAX_INLINE_CHARS sets the default). Over it, the result is stored for 1 hour and the response carries a preview, a result_handle, total_chars and truncated: true; read the rest with read_result (1 credit)."
          }
        },
        "anyOf": [
          {
            "required": [
              "html"
            ]
          },
          {
            "required": [
              "url"
            ]
          }
        ]
      },
      "ExtractMetadataRequest": {
        "type": "object",
        "properties": {
          "html": {
            "type": "string",
            "minLength": 1,
            "description": "HTML content to extract metadata from"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "URL to fetch and extract metadata from. The response always carries redirected (true when the fetch ended on a different URL, compared after parsing, so https://example.com and https://example.com/ are the same page), and requested_url when it did; metadata.basic.url and extracted_from are the final URL"
          },
          "include_social": {
            "default": true,
            "type": "boolean",
            "description": "Include Open Graph and Twitter Card metadata"
          },
          "include_seo": {
            "default": true,
            "type": "boolean",
            "description": "Include SEO-related metadata"
          },
          "include_technical": {
            "default": true,
            "type": "boolean",
            "description": "Include technical metadata (scripts, stylesheets, etc.)"
          },
          "include_structured_data": {
            "default": true,
            "type": "boolean",
            "description": "Include structured data (JSON-LD, microdata)"
          },
          "respect_robots": {
            "default": true,
            "type": "boolean"
          },
          "max_inline_chars": {
            "type": "integer",
            "minimum": 1000,
            "maximum": 10000000,
            "description": "Largest result returned inline, in characters of its JSON (1000-10000000; env CRAWLFORGE_MAX_INLINE_CHARS sets the default). Over it, the result is stored for 1 hour and the response carries a preview, a result_handle, total_chars and truncated: true; read the rest with read_result (1 credit)."
          }
        },
        "anyOf": [
          {
            "required": [
              "html"
            ]
          },
          {
            "required": [
              "url"
            ]
          }
        ]
      },
      "ExtractStructuredRequest": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "The URL to extract structured data from"
          },
          "schema": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string"
              },
              "properties": {
                "type": "object",
                "propertyNames": {
                  "type": "string"
                },
                "additionalProperties": {}
              },
              "required": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            },
            "required": [
              "properties"
            ],
            "description": "JSON Schema defining the data structure to extract. A number or integer property takes the first number in the text found (\"4.5 out of 5\" is 4.5; an integer only when whole); text with no number is returned as it is"
          },
          "prompt": {
            "type": "string",
            "description": "Accepted for compatibility; only used by the MCP server LLM extraction"
          },
          "llmConfig": {
            "type": "object",
            "properties": {
              "provider": {
                "type": "string"
              },
              "apiKey": {
                "type": "string"
              }
            },
            "description": "Accepted for compatibility; this endpoint never calls an LLM provider"
          },
          "selectorHints": {
            "type": "object",
            "propertyNames": {
              "type": "string"
            },
            "additionalProperties": {
              "type": "string"
            },
            "description": "CSS selectors per schema property, checked before structured data"
          },
          "fallbackToSelectors": {
            "default": true,
            "type": "boolean",
            "description": "When false and required fields are set, extracting nothing returns EXTRACTION_FAILED instead of nulls"
          },
          "respect_robots": {
            "default": true,
            "type": "boolean"
          }
        },
        "required": [
          "url",
          "schema"
        ]
      },
      "ExtractTextRequest": {
        "type": "object",
        "properties": {
          "html": {
            "type": "string",
            "minLength": 1,
            "description": "HTML content to extract text from"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "URL to fetch and extract text from"
          },
          "selector": {
            "type": "string",
            "description": "CSS selector to target specific elements"
          },
          "clean": {
            "default": true,
            "type": "boolean",
            "description": "Collapse runs of spaces within a line and drop blank lines; line breaks between blocks are kept. Text mode only."
          },
          "preserve_links": {
            "default": false,
            "type": "boolean",
            "description": "Append the links as \"text (href)\" lines after the text. REST only (the MCP tool has no such option); text mode only."
          },
          "preserve_formatting": {
            "default": false,
            "type": "boolean",
            "description": "Keep paragraph and heading breaks from the HTML source. REST only (the MCP tool has no such option); text mode only."
          },
          "max_length": {
            "type": "number",
            "minimum": 1,
            "maximum": 1000000,
            "description": "Maximum length of the returned text or markdown; a cut text ends in \"...\" and data.truncated is true (data.truncated is always present, false when nothing was cut)"
          },
          "output_format": {
            "default": "text",
            "type": "string",
            "enum": [
              "text",
              "markdown"
            ],
            "description": "text (default): data.text, one line per block element. markdown: data.markdown, the selected elements (or the body) converted with Turndown, data tables as GFM pipe tables and image and link URLs absolute. Either way data carries output_format, word_count and char_count, named as the MCP tool names them; markdown is counted without its # * ` _ [ ] syntax."
          },
          "respect_robots": {
            "default": true,
            "type": "boolean"
          },
          "escalate": {
            "default": false,
            "type": "boolean",
            "description": "Retry a walled page in a stealth browser, in this same call (url input only). The plain fetch always runs first; only a named bot wall, a 403, 429 or 444, or an empty shell or error placeholder served as 2xx escalates — never a 404, a 5xx, a binary or a host whose Retry-After window is still open. The response carries escalated: true|false whenever this is set, plus stealth: { engine, vendor_detected } when the render ran. It costs the escalation add-on only when it ran. The stage needs the CrawlForge execution backend: where that is not configured it returns 503 TOOL_NOT_AVAILABLE and charges nothing. robots.txt is respected on the escalated path too."
          },
          "escalate_engine": {
            "default": "playwright",
            "type": "string",
            "enum": [
              "playwright",
              "camoufox"
            ],
            "description": "Browser engine for the escalated render (camoufox only where installed on the backend). Ignored when no escalation runs."
          },
          "redact_pii": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "object",
                "properties": {
                  "entities": {
                    "maxItems": 16,
                    "type": "array",
                    "items": {
                      "type": "string",
                      "minLength": 1
                    }
                  },
                  "replace_style": {
                    "type": "string",
                    "enum": [
                      "tag",
                      "mask",
                      "remove"
                    ]
                  },
                  "mode": {
                    "type": "string",
                    "enum": [
                      "fast",
                      "model"
                    ]
                  }
                }
              }
            ],
            "description": "Remove personal data from the text this call returns, before it is stored or sent back. true is shorthand for { mode: \"fast\" }: every entity, tagged. As an object: entities (any of EMAIL, PHONE, FINANCIAL, SECRET; omitted or empty means all four, and any other name is a 400 rather than a silent no-op), replace_style (\"tag\" → <EMAIL>, \"mask\" → [REDACTED], \"remove\" → nothing; default \"tag\") and mode (\"fast\", the default, is regex-only and costs no extra credits; \"model\" covers PERSON and LOCATION, needs an LLM and is rejected here — use the CrawlForge MCP server). The response carries redaction: { entities, count, mode } inside data, saying what was removed. Detection is deliberately conservative: a card number must pass Luhn and an IBAN mod-97, so a false positive cannot silently destroy real page content. URLs, queries and identifiers the response uses to name what was fetched are left intact, and counters derived from the text (content_length, word_count, character_count) describe the text as it was extracted, before redaction."
          }
        },
        "anyOf": [
          {
            "required": [
              "html"
            ]
          },
          {
            "required": [
              "url"
            ]
          }
        ]
      },
      "ExtractWithLlmRequest": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "description": "URL to fetch and extract from (one of url/content required)"
          },
          "content": {
            "type": "string",
            "description": "Pre-fetched text to extract from (one of url/content required)"
          },
          "prompt": {
            "type": "string",
            "description": "Natural-language extraction instruction"
          },
          "schema": {
            "type": "object",
            "additionalProperties": true,
            "description": "Optional JSON-schema for output shape (used as Ollama structured-outputs format when provider is \"ollama\")"
          },
          "provider": {
            "type": "string",
            "enum": [
              "openai",
              "anthropic",
              "ollama",
              "auto"
            ],
            "default": "auto",
            "description": "LLM provider. \"auto\" prefers a local Ollama instance where one exists; \"openai\"/\"anthropic\" use cloud models (require the matching API key on the execution side)"
          },
          "model": {
            "type": "string",
            "description": "Override the model. For ollama, pass a name returned by list_ollama_models. Defaults: openai=\"gpt-4o-mini\", anthropic=\"claude-haiku-4-5-20251001\", ollama=\"llama3.2\""
          },
          "maxTokens": {
            "type": "number",
            "default": 4096,
            "description": "Maximum output tokens"
          }
        },
        "required": [
          "prompt"
        ],
        "anyOf": [
          {
            "required": [
              "url"
            ]
          },
          {
            "required": [
              "content"
            ]
          }
        ]
      },
      "FetchUrlRequest": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "The URL to fetch"
          },
          "headers": {
            "type": "object",
            "propertyNames": {
              "type": "string"
            },
            "additionalProperties": {
              "type": "string"
            },
            "description": "Optional HTTP headers to include"
          },
          "timeout": {
            "default": 10000,
            "type": "number",
            "minimum": 1000,
            "maximum": 30000,
            "description": "Request timeout in milliseconds (1000-30000)"
          },
          "follow_redirects": {
            "default": true,
            "type": "boolean",
            "description": "Whether to follow HTTP redirects"
          },
          "user_agent": {
            "type": "string",
            "description": "Custom User-Agent header"
          },
          "respect_robots": {
            "default": true,
            "type": "boolean"
          },
          "max_inline_chars": {
            "type": "integer",
            "minimum": 1000,
            "maximum": 10000000,
            "description": "Largest result returned inline, in characters of its JSON (1000-10000000; env CRAWLFORGE_MAX_INLINE_CHARS sets the default). Over it, the result is stored for 1 hour and the response carries a preview, a result_handle, total_chars and truncated: true; read the rest with read_result (1 credit)."
          }
        },
        "required": [
          "url"
        ]
      },
      "GenerateLlmsTxtRequest": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "The website URL to generate llms.txt for"
          },
          "respect_robots": {
            "default": true,
            "type": "boolean",
            "description": "Fetch each origin's robots.txt and skip URLs it disallows for CrawlForge. A missing or unreachable robots.txt is treated as no restrictions. Returns 403 ROBOTS_DISALLOWED when the target URL itself is disallowed, with no credits charged; disallowed pages are left out of the analysis and counted in warnings. analysisOptions.respectRobots is an alias — either one set to false disables the gate."
          },
          "format": {
            "default": "both",
            "type": "string",
            "enum": [
              "both",
              "llms-txt",
              "llms-full-txt"
            ],
            "description": "Output format; llms-full.txt inlines up to 2000 characters of text per analyzed page"
          },
          "complianceLevel": {
            "default": "standard",
            "type": "string",
            "enum": [
              "basic",
              "standard",
              "strict"
            ],
            "description": "Accepted for compatibility; content is derived from real site data, not canned policy text"
          },
          "analysisOptions": {
            "type": "object",
            "properties": {
              "maxDepth": {
                "default": 3,
                "type": "number",
                "minimum": 1,
                "maximum": 5
              },
              "maxPages": {
                "default": 100,
                "type": "number",
                "minimum": 10,
                "maximum": 500
              },
              "respectRobots": {
                "default": true,
                "type": "boolean"
              },
              "detectAPIs": {
                "default": true,
                "type": "boolean"
              },
              "analyzeContent": {
                "default": true,
                "type": "boolean"
              },
              "checkSecurity": {
                "default": true,
                "type": "boolean"
              }
            },
            "description": "Analysis options. maxPages is clamped to 30 and maxDepth to 2 on the hosted REST API (clamps are reported in clamps_applied). respectRobots skips pages disallowed by robots.txt; detectAPIs flags /api, /docs/api, openapi, and swagger links found in fetched HTML."
          },
          "outputOptions": {
            "type": "object",
            "properties": {
              "organizationName": {
                "type": "string"
              },
              "contactEmail": {
                "type": "string",
                "format": "email",
                "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
              },
              "customGuidelines": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "customRestrictions": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "includeDetailed": {
                "default": true,
                "type": "boolean"
              },
              "includeAnalysis": {
                "default": false,
                "type": "boolean"
              }
            },
            "description": "Output customization: organizationName (overrides the site title), contactEmail, customGuidelines, customRestrictions"
          },
          "max_inline_chars": {
            "type": "integer",
            "minimum": 1000,
            "maximum": 10000000,
            "description": "Largest result returned inline, in characters of its JSON (1000-10000000; env CRAWLFORGE_MAX_INLINE_CHARS sets the default). Over it, the result is stored for 1 hour and the response carries a preview, a result_handle, total_chars and truncated: true; read the rest with read_result (1 credit)."
          }
        },
        "required": [
          "url"
        ]
      },
      "GetBatchResultsRequest": {
        "type": "object",
        "properties": {
          "batchId": {
            "type": "string",
            "minLength": 1,
            "description": "The batch_id returned by batch_scrape"
          },
          "page": {
            "default": 1,
            "type": "integer",
            "minimum": 1,
            "maximum": 9007199254740991,
            "description": "Page number to retrieve (1-based)"
          },
          "limit": {
            "default": 25,
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "description": "Results per page (1-100)"
          }
        },
        "required": [
          "batchId"
        ]
      },
      "ListOllamaModelsRequest": {
        "type": "object",
        "properties": {}
      },
      "LocalizationRequest": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "The URL to analyze for localization"
          },
          "target_language": {
            "type": "string",
            "description": "Language you expect (e.g., \"en\", \"fr\", \"es\"). Sent as this request's Accept-Language and compared with the detected language and html lang"
          },
          "target_country": {
            "type": "string",
            "description": "Country you expect (e.g., \"US\", \"FR\", \"JP\"). Echoed back and sent as an X-Country-Code header on this request; it does not change where the request comes from"
          },
          "detect_language": {
            "default": true,
            "type": "boolean",
            "description": "Detect the page language from its visible text. Returns an ISO 639-1 code, or \"und\" when the language cannot be determined."
          },
          "extract_hreflang": {
            "default": true,
            "type": "boolean",
            "description": "Extract hreflang alternate language tags"
          },
          "check_geo_targeting": {
            "default": false,
            "type": "boolean",
            "description": "Check for geo-targeting metadata"
          },
          "timeout": {
            "default": 10000,
            "type": "number",
            "minimum": 1000,
            "maximum": 30000,
            "description": "Request timeout in milliseconds (1000-30000)"
          },
          "respect_robots": {
            "default": true,
            "type": "boolean"
          }
        },
        "required": [
          "url"
        ]
      },
      "MapSiteRequest": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "The starting URL to map"
          },
          "respect_robots": {
            "default": true,
            "type": "boolean",
            "description": "Fetch each origin's robots.txt and skip URLs it disallows for CrawlForge. A missing or unreachable robots.txt is treated as no restrictions. Returns 403 ROBOTS_DISALLOWED when the start URL itself is disallowed, with no credits charged; a disallowed sitemap.xml falls back to the crawl, and pages disallowed mid-crawl are skipped and counted in warnings."
          },
          "max_depth": {
            "default": 2,
            "type": "number",
            "minimum": 1,
            "maximum": 5,
            "description": "Maximum crawl depth for the fallback crawl (1-5); unused when a sitemap is found"
          },
          "include_external": {
            "default": false,
            "type": "boolean",
            "description": "Include external links in per-page link lists (crawl mode only; external pages are never crawled)"
          },
          "timeout": {
            "default": 15000,
            "type": "number",
            "minimum": 1000,
            "maximum": 30000,
            "description": "Overall crawl budget in milliseconds (1000-30000; capped at ~18000 to fit the serverless limit)"
          },
          "max_inline_chars": {
            "type": "integer",
            "minimum": 1000,
            "maximum": 10000000,
            "description": "Largest result returned inline, in characters of its JSON (1000-10000000; env CRAWLFORGE_MAX_INLINE_CHARS sets the default). Over it, the result is stored for 1 hour and the response carries a preview, a result_handle, total_chars and truncated: true; read the rest with read_result (1 credit)."
          }
        },
        "required": [
          "url"
        ]
      },
      "ProcessDocumentRequest": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "URL of the document to process (max 25MB)"
          },
          "respect_robots": {
            "default": true,
            "type": "boolean",
            "description": "Fetch the origin's robots.txt and refuse the document if it disallows CrawlForge. A missing or unreachable robots.txt is treated as no restrictions. Returns 403 ROBOTS_DISALLOWED, and no credits are charged."
          },
          "document_type": {
            "default": "auto",
            "type": "string",
            "enum": [
              "pdf",
              "docx",
              "xlsx",
              "csv",
              "txt",
              "auto"
            ],
            "description": "Document type; auto-detected from Content-Type, URL extension, then content sniffing"
          },
          "extract_text": {
            "default": true,
            "type": "boolean",
            "description": "Extract text content (capped at 200000 characters; PDFs read up to 200 pages, and pages_read { start, end, count, total_pages } says which; HTML is read one line per block element)"
          },
          "extract_metadata": {
            "default": true,
            "type": "boolean",
            "description": "Extract document metadata (PDF info dictionary; HTML title/description/author)"
          },
          "extract_tables": {
            "default": false,
            "type": "boolean",
            "description": "Extract tables (PDF via ruled-grid detection, CSV via parsing; not available for HTML). Up to 20 tables of 1000 rows; a table ruled only horizontally keeps its rows but reports one column"
          },
          "extract_images": {
            "default": false,
            "type": "boolean",
            "description": "Not available on the hosted REST API — returns images: null with a note; use the CrawlForge MCP server for image extraction"
          },
          "timeout": {
            "default": 30000,
            "type": "number",
            "minimum": 1000,
            "maximum": 60000,
            "description": "Fetch timeout in milliseconds (1000-60000; capped at 20000 to fit the serverless limit)"
          },
          "max_inline_chars": {
            "type": "integer",
            "minimum": 1000,
            "maximum": 10000000,
            "description": "Largest result returned inline, in characters of its JSON (1000-10000000; env CRAWLFORGE_MAX_INLINE_CHARS sets the default). Over it, the result is stored for 1 hour and the response carries a preview, a result_handle, total_chars and truncated: true; read the rest with read_result (1 credit)."
          },
          "redact_pii": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "object",
                "properties": {
                  "entities": {
                    "maxItems": 16,
                    "type": "array",
                    "items": {
                      "type": "string",
                      "minLength": 1
                    }
                  },
                  "replace_style": {
                    "type": "string",
                    "enum": [
                      "tag",
                      "mask",
                      "remove"
                    ]
                  },
                  "mode": {
                    "type": "string",
                    "enum": [
                      "fast",
                      "model"
                    ]
                  }
                }
              }
            ],
            "description": "Remove personal data from the text this call returns, before it is stored or sent back. true is shorthand for { mode: \"fast\" }: every entity, tagged. As an object: entities (any of EMAIL, PHONE, FINANCIAL, SECRET; omitted or empty means all four, and any other name is a 400 rather than a silent no-op), replace_style (\"tag\" → <EMAIL>, \"mask\" → [REDACTED], \"remove\" → nothing; default \"tag\") and mode (\"fast\", the default, is regex-only and costs no extra credits; \"model\" covers PERSON and LOCATION, needs an LLM and is rejected here — use the CrawlForge MCP server). The response carries redaction: { entities, count, mode } inside data, saying what was removed. Detection is deliberately conservative: a card number must pass Luhn and an IBAN mod-97, so a false positive cannot silently destroy real page content. URLs, queries and identifiers the response uses to name what was fetched are left intact, and counters derived from the text (content_length, word_count, character_count) describe the text as it was extracted, before redaction."
          }
        },
        "required": [
          "url"
        ]
      },
      "ReadResultRequest": {
        "type": "object",
        "properties": {
          "handle": {
            "type": "string",
            "minLength": 1,
            "description": "The result_handle returned by a tool (res_ followed by a UUID)"
          },
          "operation": {
            "type": "string",
            "enum": [
              "slice",
              "search",
              "lines",
              "json_path"
            ],
            "description": "slice: a character range. search: every case-insensitive literal occurrence of query, each with its offset and 200 chars of context on each side. lines: a page of lines. json_path: one field of the stored JSON."
          },
          "offset": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991,
            "description": "slice: first character index. lines: first line index."
          },
          "length": {
            "type": "integer",
            "minimum": 1,
            "maximum": 200000,
            "description": "slice: characters to return (default 10000, max 200000). lines: lines to return (default 200, max 5000). Both are also capped at max_inline_chars."
          },
          "query": {
            "type": "string",
            "minLength": 1,
            "maxLength": 500,
            "description": "search: the literal text to find (1-500 chars); never interpreted as a pattern"
          },
          "max_matches": {
            "default": 20,
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "description": "search: matches to return (1-100); total_matches still counts every occurrence"
          },
          "path": {
            "type": "string",
            "minLength": 1,
            "maxLength": 500,
            "description": "json_path: dotted path into the stored result, e.g. \"formats.links[0].href\" or \"formats.links.0.href\". Keys and indexes only — no wildcards. The subject is the stored payload, or the parsed body when the stored text is itself JSON. A path that does not resolve is a 400 naming the keys available where it stopped."
          },
          "max_inline_chars": {
            "type": "integer",
            "minimum": 1000,
            "maximum": 10000000,
            "description": "Cap on the characters this call returns (1000-10000000)"
          }
        },
        "required": [
          "handle",
          "operation"
        ]
      },
      "RedditSearchRequest": {
        "type": "object",
        "properties": {
          "query": {
            "type": "string",
            "minLength": 1,
            "description": "Keyword search. Posts: matches title+selftext; comments: matches body. Supports \"quoted phrases\", OR, -exclusion"
          },
          "subreddit": {
            "type": "string",
            "minLength": 1,
            "description": "Limit to one subreddit (with or without the r/ prefix)"
          },
          "author": {
            "type": "string",
            "minLength": 1,
            "description": "Limit to one author (with or without the u/ prefix)"
          },
          "mode": {
            "default": "posts",
            "type": "string",
            "enum": [
              "posts",
              "comments",
              "thread"
            ],
            "description": "What to search: posts (default), comments, or thread (full comment tree — requires link_id)"
          },
          "link_id": {
            "type": "string",
            "minLength": 1,
            "description": "Post ID (e.g. '1twm1zh' or 't3_1twm1zh') — required for thread mode, optional filter for comments mode"
          },
          "after": {
            "type": "string",
            "minLength": 1,
            "description": "Only content posted after this date — ISO 8601, epoch seconds, or an offset like '7d'"
          },
          "before": {
            "type": "string",
            "minLength": 1,
            "description": "Only content posted before this date — same formats as after"
          },
          "limit": {
            "default": 25,
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "description": "Max results (1-100; thread mode: max comments returned)"
          },
          "sort": {
            "default": "desc",
            "type": "string",
            "enum": [
              "asc",
              "desc"
            ],
            "description": "Sort by post date (desc = newest first)"
          },
          "source": {
            "default": "auto",
            "type": "string",
            "enum": [
              "auto",
              "arctic_shift",
              "pullpush",
              "web_discovery"
            ],
            "description": "Force a specific backend: auto (Arctic Shift first, PullPush second), arctic_shift, web_discovery (unscoped keyword searches only — posts or comments), or pullpush (the automatic second source; has refused automated clients since August 2026)"
          }
        }
      },
      "ScrapeRequest": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "The URL to scrape"
          },
          "formats": {
            "default": [
              "markdown"
            ],
            "minItems": 1,
            "type": "array",
            "items": {
              "anyOf": [
                {
                  "type": "string",
                  "enum": [
                    "markdown",
                    "html",
                    "rawHtml",
                    "text",
                    "links",
                    "metadata",
                    "screenshot",
                    "json-schema"
                  ]
                },
                {
                  "type": "object",
                  "properties": {
                    "type": {
                      "type": "string",
                      "const": "highlights"
                    },
                    "query": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 500
                    },
                    "max_highlights": {
                      "default": 10,
                      "type": "integer",
                      "minimum": 1,
                      "maximum": 50
                    },
                    "mode": {
                      "default": "extractive",
                      "type": "string",
                      "enum": [
                        "extractive",
                        "model"
                      ]
                    }
                  },
                  "required": [
                    "type",
                    "query"
                  ]
                },
                {
                  "type": "object",
                  "properties": {
                    "type": {
                      "type": "string",
                      "const": "question"
                    },
                    "question": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 500
                    },
                    "mode": {
                      "default": "extractive",
                      "type": "string",
                      "enum": [
                        "extractive",
                        "model"
                      ]
                    }
                  },
                  "required": [
                    "type",
                    "question"
                  ]
                }
              ]
            },
            "description": "Output formats: markdown, html, rawHtml, text, links, metadata, plus two query-scoped object formats. { type: 'highlights', query, max_highlights (1-50, default 10), mode } returns the sentences and code blocks matching the query, verbatim, each with an offset and length into the markdown format of the same call (table rows too: markdown keeps data tables as GFM pipe tables). { type: 'question', question, mode } returns an answer assembled from the best-matching passages, with its evidence. At most one of each per request. mode defaults to 'extractive'; mode 'model' needs an LLM and is rejected here, as are the 'screenshot' and 'json-schema' formats, which require the CrawlForge MCP server (browser/LLM).",
            "allOf": [
              {
                "contains": {
                  "type": "object",
                  "properties": {
                    "type": {
                      "const": "highlights"
                    }
                  }
                },
                "minContains": 0,
                "maxContains": 1
              },
              {
                "contains": {
                  "type": "object",
                  "properties": {
                    "type": {
                      "const": "question"
                    }
                  }
                },
                "minContains": 0,
                "maxContains": 1
              }
            ]
          },
          "onlyMainContent": {
            "default": true,
            "type": "boolean",
            "description": "Strip navigation, headers, and footers, returning only the main content"
          },
          "respect_robots": {
            "default": true,
            "type": "boolean"
          },
          "escalate": {
            "default": false,
            "type": "boolean",
            "description": "Retry a blocked page in a stealth browser, in this same call. The plain fetch always runs first; only if it meets a bot wall, an empty shell or an error placeholder does the stealth render run, and the requested formats are then derived from the rendered HTML. The response carries escalated: true|false whenever this is set, plus stealth: { engine, vendor_detected } when the render ran. It costs the escalation add-on only when it ran. The stage needs the CrawlForge execution backend: where that is not configured it returns 503 TOOL_NOT_AVAILABLE and charges nothing. robots.txt is respected on the escalated path too, matched against the same CrawlForge product token."
          },
          "escalate_engine": {
            "default": "playwright",
            "type": "string",
            "enum": [
              "playwright",
              "camoufox"
            ],
            "description": "Browser engine for the escalated render (camoufox only where installed on the backend). Ignored when no escalation runs."
          },
          "max_inline_chars": {
            "type": "integer",
            "minimum": 1000,
            "maximum": 10000000,
            "description": "Largest result returned inline, in characters of its JSON (1000-10000000; env CRAWLFORGE_MAX_INLINE_CHARS sets the default). Over it, the result is stored for 1 hour and the response carries a preview, a result_handle, total_chars and truncated: true; read the rest with read_result (1 credit)."
          },
          "redact_pii": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "object",
                "properties": {
                  "entities": {
                    "maxItems": 16,
                    "type": "array",
                    "items": {
                      "type": "string",
                      "minLength": 1
                    }
                  },
                  "replace_style": {
                    "type": "string",
                    "enum": [
                      "tag",
                      "mask",
                      "remove"
                    ]
                  },
                  "mode": {
                    "type": "string",
                    "enum": [
                      "fast",
                      "model"
                    ]
                  }
                }
              }
            ],
            "description": "Remove personal data from the text this call returns, before it is stored or sent back. true is shorthand for { mode: \"fast\" }: every entity, tagged. As an object: entities (any of EMAIL, PHONE, FINANCIAL, SECRET; omitted or empty means all four, and any other name is a 400 rather than a silent no-op), replace_style (\"tag\" → <EMAIL>, \"mask\" → [REDACTED], \"remove\" → nothing; default \"tag\") and mode (\"fast\", the default, is regex-only and costs no extra credits; \"model\" covers PERSON and LOCATION, needs an LLM and is rejected here — use the CrawlForge MCP server). The response carries redaction: { entities, count, mode } inside data, saying what was removed. Detection is deliberately conservative: a card number must pass Luhn and an IBAN mod-97, so a false positive cannot silently destroy real page content. URLs, queries and identifiers the response uses to name what was fetched are left intact, and counters derived from the text (content_length, word_count, character_count) describe the text as it was extracted, before redaction."
          }
        },
        "required": [
          "url"
        ]
      },
      "ScrapeStructuredRequest": {
        "type": "object",
        "properties": {
          "html": {
            "type": "string",
            "minLength": 1,
            "description": "HTML content to extract data from"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "URL to fetch and extract data from"
          },
          "selectors": {
            "type": "object",
            "propertyNames": {
              "type": "string"
            },
            "additionalProperties": {
              "type": "string"
            },
            "description": "Key-value pairs of field names and CSS selectors",
            "minProperties": 1
          },
          "base_url": {
            "type": "string",
            "format": "uri",
            "description": "Base URL for resolving relative URLs"
          },
          "multiple": {
            "default": false,
            "type": "boolean",
            "description": "Extract multiple items vs single item"
          },
          "clean_text": {
            "default": true,
            "type": "boolean",
            "description": "Clean extracted text by trimming whitespace"
          },
          "include_attributes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "HTML attributes to extract (e.g., href, src, alt)"
          },
          "max_items": {
            "default": 100,
            "type": "number",
            "minimum": 1,
            "maximum": 1000,
            "description": "Maximum number of items to extract (for multiple mode)"
          },
          "respect_robots": {
            "default": true,
            "type": "boolean"
          }
        },
        "required": [
          "selectors"
        ],
        "anyOf": [
          {
            "required": [
              "html"
            ]
          },
          {
            "required": [
              "url"
            ]
          }
        ]
      },
      "ScrapeTemplateRequest": {
        "type": "object",
        "properties": {
          "template": {
            "type": "string",
            "description": "A supported template id, \"auto\" to pick one from the url, or \"list\" to discover them."
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "URL to scrape. Required unless template === \"list\", or params drive a list connector. Always required for template \"auto\"."
          },
          "params": {
            "type": "object",
            "propertyNames": {
              "type": "string"
            },
            "additionalProperties": {},
            "description": "Drives a list connector in place of a url — { \"company\": \"stripe\" } for the job boards, { \"store\": \"www.allbirds.com\", \"collection\": \"mens\" } for shopify-collection, { \"vin\": \"5UXWX7C5*BA\" } for nhtsa-vin. Each connector names its own parameters in the description that template \"list\" returns. A missing or invalid parameter is a 400 naming it, and no credits are charged."
          },
          "respect_robots": {
            "default": true,
            "type": "boolean",
            "description": "Fetch the origin's robots.txt and refuse the URL if it disallows CrawlForge. Checked against the URL actually fetched, which is the API endpoint rather than the URL or params you pass wherever a template resolves one. A missing or unreachable robots.txt is treated as no restrictions. Returns 403 ROBOTS_DISALLOWED, and no credits are charged."
          },
          "timeout": {
            "default": 15000,
            "type": "integer",
            "minimum": 5000,
            "maximum": 60000,
            "description": "Request timeout in milliseconds (5000–60000)."
          },
          "max_inline_chars": {
            "type": "integer",
            "minimum": 1000,
            "maximum": 10000000,
            "description": "Largest result returned inline, in characters of its JSON (1000-10000000; env CRAWLFORGE_MAX_INLINE_CHARS sets the default). Over it, the result is stored for 1 hour and the response carries a preview, a result_handle, total_chars and truncated: true; read the rest with read_result (1 credit)."
          }
        },
        "required": [
          "template"
        ],
        "allOf": [
          {
            "anyOf": [
              {
                "properties": {
                  "template": {
                    "const": "list"
                  }
                }
              },
              {
                "required": [
                  "url"
                ]
              },
              {
                "required": [
                  "params"
                ]
              }
            ]
          },
          {
            "anyOf": [
              {
                "properties": {
                  "template": {
                    "not": {
                      "const": "auto"
                    }
                  }
                }
              },
              {
                "required": [
                  "url"
                ]
              }
            ]
          }
        ]
      },
      "ScrapeWithActionsRequest": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "description": "The URL to scrape"
          },
          "actions": {
            "type": "array",
            "description": "Browser actions to perform before scraping (1-20). A snapshot action first lists the interactive elements on the page, including those inside open shadow roots and iframes, as @e refs (@e1, @e2, … in document order), which later actions can target in place of CSS selectors.",
            "items": {
              "type": "object",
              "properties": {
                "type": {
                  "type": "string",
                  "enum": [
                    "wait",
                    "click",
                    "type",
                    "press",
                    "scroll",
                    "screenshot",
                    "executeJavaScript",
                    "select",
                    "hover",
                    "navigate",
                    "snapshot"
                  ],
                  "description": "Action to perform. executeJavaScript is disabled unless the server runs with ALLOW_JAVASCRIPT_EXECUTION=true; it is refused on the hosted API."
                },
                "selector": {
                  "type": "string",
                  "description": "What the action targets: either a CSS selector or an @e ref (for example @e1) taken from the most recent snapshot action in the same chain. Refs are invalidated by navigation — snapshot again after the page changes."
                },
                "text": {
                  "type": "string",
                  "description": "type: text to enter"
                },
                "key": {
                  "type": "string",
                  "description": "press: key to press"
                },
                "value": {
                  "type": "string",
                  "description": "select: one option to choose in the <select> named by selector. Matches an option by its value or by its visible label."
                },
                "values": {
                  "type": "array",
                  "description": "select: several options to choose, same matching rule as value. Supply value or values, not neither."
                },
                "url": {
                  "type": "string",
                  "description": "navigate: URL to load mid-chain, in the same browser session. Passes the same robots.txt and SSRF checks as the top-level url. Every navigation is checked for a bot wall, HTTP error page or challenge interstitial (an HTTP 202 interstitial included): this action's result carries httpStatus and, when a wall was named, blocked: { vendor, evidence } — reported, not failed. The result lists every navigation in order in navigations[] (index 0 the initial load), each { url, finalUrl, httpStatus, blocked? }, httpStatus null when unknown. A click or form submit that loads another page is a navigation too: it gets its own navigations entry with trigger naming the action type, and the top-level finalUrl is where the chain ended (url stays the one requested). A chain that ends on a wall is never reported as success: it fails (over REST, a 502 TOOL_ERROR with nothing charged)."
                },
                "waitUntil": {
                  "type": "string",
                  "default": "domcontentloaded",
                  "enum": [
                    "load",
                    "domcontentloaded",
                    "networkidle",
                    "commit"
                  ],
                  "description": "navigate: how long to wait for the new page before continuing"
                },
                "script": {
                  "type": "string",
                  "description": "executeJavaScript: script to run"
                },
                "timeout": {
                  "type": "number",
                  "default": 10000,
                  "description": "Per-action timeout in ms, distinct from browserOptions.timeout which budgets the whole chain"
                },
                "description": {
                  "type": "string",
                  "description": "Human-readable label for this action"
                },
                "continueOnError": {
                  "type": "boolean",
                  "description": "Keep going if this action fails"
                },
                "retries": {
                  "type": "number",
                  "description": "Retry attempts for this action (0-5)"
                },
                "captureAfter": {
                  "type": "boolean",
                  "description": "Capture page content after this action"
                },
                "duration": {
                  "type": "number",
                  "description": "wait: milliseconds to wait (0-30000)"
                },
                "condition": {
                  "type": "string",
                  "enum": [
                    "visible",
                    "hidden",
                    "enabled",
                    "disabled",
                    "stable"
                  ],
                  "description": "wait: condition on selector"
                },
                "button": {
                  "type": "string",
                  "enum": [
                    "left",
                    "right",
                    "middle"
                  ],
                  "description": "click: mouse button"
                },
                "clickCount": {
                  "type": "number",
                  "description": "click: number of clicks (1-3)"
                },
                "delay": {
                  "type": "number",
                  "description": "click/type: delay in ms (0-1000)"
                },
                "force": {
                  "type": "boolean",
                  "description": "click/hover: bypass actionability checks"
                },
                "position": {
                  "type": "object",
                  "properties": {
                    "x": {
                      "type": "number"
                    },
                    "y": {
                      "type": "number"
                    }
                  },
                  "description": "click/hover: relative position { x, y }"
                },
                "clear": {
                  "type": "boolean",
                  "description": "type: clear field before typing"
                },
                "modifiers": {
                  "type": "array",
                  "description": "press: modifier keys (Alt, Control, Meta, Shift)"
                },
                "direction": {
                  "type": "string",
                  "enum": [
                    "up",
                    "down",
                    "left",
                    "right"
                  ],
                  "description": "scroll: direction"
                },
                "distance": {
                  "type": "number",
                  "description": "scroll: pixels to scroll"
                },
                "smooth": {
                  "type": "boolean",
                  "description": "scroll: smooth scrolling"
                },
                "toElement": {
                  "type": "string",
                  "description": "scroll: selector to scroll to"
                },
                "x": {
                  "type": "number",
                  "description": "scroll: absolute X coordinate (window.scrollTo; with y, takes precedence over direction/distance)"
                },
                "y": {
                  "type": "number",
                  "description": "scroll: absolute Y coordinate (window.scrollTo; with x, takes precedence over direction/distance)"
                },
                "fullPage": {
                  "type": "boolean",
                  "description": "screenshot: capture full page"
                },
                "quality": {
                  "type": "number",
                  "description": "screenshot: jpeg quality (0-100)"
                },
                "format": {
                  "type": "string",
                  "enum": [
                    "png",
                    "jpeg"
                  ],
                  "description": "screenshot: image format"
                },
                "args": {
                  "type": "array",
                  "description": "executeJavaScript: arguments passed to the script"
                },
                "returnResult": {
                  "type": "boolean",
                  "description": "executeJavaScript: return the script result"
                },
                "interactiveOnly": {
                  "type": "boolean",
                  "default": true,
                  "description": "snapshot: list only interactive elements. Set false to include headings and landmarks too; only interactive elements are ever given a ref."
                },
                "maxNodes": {
                  "type": "number",
                  "default": 200,
                  "description": "snapshot: cap on how many nodes the tree lists (1-1000); under the default interactiveOnly every one of them carries a ref. The result reports truncated: true when the cap stopped the walk."
                }
              }
            }
          },
          "formats": {
            "type": "array",
            "default": [
              "json"
            ],
            "description": "Output formats for scraped content: markdown, html, json, text. For an image, add a {type:\"screenshot\"} action where you want it taken."
          },
          "captureIntermediateStates": {
            "type": "boolean",
            "default": false,
            "description": "Capture page state after each action: intermediateStates[] gets one entry per action (capturePoint is the action's 1-based position) with the url, title and the requested formats at that point"
          },
          "formAutoFill": {
            "type": "object",
            "description": "Form auto-fill configuration",
            "properties": {
              "fields": {
                "type": "array",
                "description": "Fields to fill: { selector, value, type: text|select|checkbox|radio|file, waitAfter }. type picks how the field is filled: text (default) types the value, select picks the option by value or label, checkbox/radio check the input (a selector matching a group checks the member whose value attribute equals value; a checkbox with value \"false\" is unchecked). file is not supported and fails that action"
              },
              "submitSelector": {
                "type": "string",
                "description": "Selector of the submit control"
              },
              "waitAfterSubmit": {
                "type": "number",
                "default": 2000,
                "description": "Wait after submit in ms (0-30000)"
              }
            }
          },
          "browserOptions": {
            "type": "object",
            "description": "Browser configuration options",
            "properties": {
              "headless": {
                "type": "boolean",
                "default": true
              },
              "userAgent": {
                "type": "string"
              },
              "viewportWidth": {
                "type": "number",
                "default": 1280,
                "description": "800-1920"
              },
              "viewportHeight": {
                "type": "number",
                "default": 720,
                "description": "600-1080"
              },
              "timeout": {
                "type": "number",
                "default": 30000,
                "description": "Browser timeout in ms (10000-120000)"
              },
              "stealth": {
                "type": "boolean",
                "default": false,
                "description": "Run the action chain in the stealth Chromium engine at its medium profile instead of the standard browser pool. The boolean is the only knob — level, fingerprint randomization and engine are not settable from here. Slower to start; it renders JavaScript, it does not solve challenges."
              },
              "consent": {
                "type": "string",
                "enum": [
                  "off",
                  "reject",
                  "accept"
                ],
                "default": "off",
                "description": "Cookie/consent banner handling. \"reject\" runs an opt-out on a recognised consent banner (DuckDuckGo autoconsent rules) once after the initial navigation and after each navigate action, capped at 2 s; \"accept\" opts in instead. When not \"off\" the result carries consent: { cmp, action, ms } for the initial navigation, and each navigate action's result carries its own result.consent. cmp is the consent platform matched, or null when none was; action is optOut, optIn, hide (a cosmetic rule hid the banner), timeout (a banner was detected but not answered within 2 s), error, or none (nothing matched). No match never fails the chain. Default \"off\", so a chain that clicks the banner itself keeps working."
              },
              "proxyRotation": {
                "type": "object",
                "description": "Route the stealth browser through proxies you supply. Requires stealth: true (refused otherwise). Chromium engine only: refused on Camoufox, which applies its proxy at browser launch and shares that browser across calls. CrawlForge supplies no proxies; a residential proxy is what gets past an IP-reputation block that no fingerprint fixes. Proxies on loopback, private, link-local or metadata addresses are refused. Credentials are never echoed back: metadata.browserOptions shows them redacted.",
                "properties": {
                  "enabled": {
                    "type": "boolean",
                    "default": false,
                    "description": "Route the chain through proxies"
                  },
                  "proxies": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Proxy URLs: \"http://user:pass@host:port\" (percent-encode a password containing @ : or /), or bare \"host:port\" for an unauthenticated HTTP proxy. http, https, socks4 and socks5 are accepted."
                  },
                  "rotationInterval": {
                    "type": "number",
                    "default": 300000,
                    "description": "Minimum ms on one proxy before the list advances"
                  }
                }
              }
            }
          },
          "respect_robots": {
            "type": "boolean",
            "default": true,
            "description": "Respect the target site's robots.txt. Omitted, the compliant default (true) applies: a URL disallowed for CrawlForge is refused before the browser opens and no credits are charged, and every navigate action is checked the same way. Setting this to false is honoured, returns a warning in the response, and is recorded against your API key."
          },
          "extractionOptions": {
            "type": "object",
            "description": "Content extraction options",
            "properties": {
              "selectors": {
                "type": "object",
                "additionalProperties": {
                  "type": "string"
                },
                "description": "Key-value pairs of data to extract using CSS selectors"
              },
              "includeMetadata": {
                "type": "boolean",
                "default": true
              },
              "includeLinks": {
                "type": "boolean",
                "default": true
              },
              "includeImages": {
                "type": "boolean",
                "default": true
              }
            }
          },
          "continueOnActionError": {
            "type": "boolean",
            "default": false,
            "description": "Continue executing actions if one fails"
          },
          "maxRetries": {
            "type": "number",
            "default": 0,
            "description": "Whole-chain retries on failure (0-3). A retry re-navigates to the starting URL and replays every action; each attempt is reported under attempts[]"
          },
          "screenshotOnError": {
            "type": "boolean",
            "default": true,
            "description": "Capture screenshot when an error occurs"
          },
          "max_inline_chars": {
            "type": "number",
            "default": 40000,
            "description": "Largest result returned inline, in characters of its JSON (1000-10000000; env CRAWLFORGE_MAX_INLINE_CHARS sets the default). Over it, the result is stored for 1 hour and the response carries a preview, a result_handle, total_chars and truncated: true; read the rest with read_result (1 credit)."
          },
          "redact_pii": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "object",
                "properties": {
                  "entities": {
                    "maxItems": 16,
                    "type": "array",
                    "items": {
                      "type": "string",
                      "minLength": 1
                    }
                  },
                  "replace_style": {
                    "type": "string",
                    "enum": [
                      "tag",
                      "mask",
                      "remove"
                    ]
                  },
                  "mode": {
                    "type": "string",
                    "enum": [
                      "fast",
                      "model"
                    ]
                  }
                }
              }
            ],
            "default": false,
            "description": "Remove personal data from the text this call returns, before it is stored or sent back. true is shorthand for { mode: \"fast\" }: every entity, tagged. As an object: entities (any of EMAIL, PHONE, FINANCIAL, SECRET; omitted or empty means all four, and any other name is a 400 rather than a silent no-op), replace_style (\"tag\" → <EMAIL>, \"mask\" → [REDACTED], \"remove\" → nothing; default \"tag\") and mode (\"fast\", the default, is regex-only and costs no extra credits; \"model\" covers PERSON and LOCATION, needs an LLM and is rejected here — use the CrawlForge MCP server). The response carries redaction: { entities, count, mode } inside data, saying what was removed. Detection is deliberately conservative: a card number must pass Luhn and an IBAN mod-97, so a false positive cannot silently destroy real page content. URLs, queries and identifiers the response uses to name what was fetched are left intact, and counters derived from the text (content_length, word_count, character_count) describe the text as it was extracted, before redaction."
          }
        },
        "required": [
          "url",
          "actions"
        ]
      },
      "SearchWebRequest": {
        "type": "object",
        "properties": {
          "query": {
            "type": "string",
            "minLength": 1,
            "description": "Search query. Provide exactly one of query or queries."
          },
          "queries": {
            "minItems": 1,
            "maxItems": 10,
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1
            },
            "description": "Between 1 and 10 search queries to run in one call, as an alternative to query. Each runs the same search query does, and the payloads come back as results_by_query: a list in the order the queries were sent, each entry carrying its own query plus that search — or query and error when that one search failed. queries echoes the input and count is its length. Charged per query that returned results: the projection is the published price times every query sent, and the actual charge falls below it by every query that failed. A batch in which none succeeded still returns 200, with an error entry for each, and costs nothing. Provide exactly one of query or queries."
          },
          "limit": {
            "default": 10,
            "type": "number",
            "minimum": 1,
            "maximum": 100,
            "description": "Number of results (1-100)"
          },
          "offset": {
            "default": 0,
            "type": "number",
            "minimum": 0,
            "description": "Results offset for pagination"
          },
          "lang": {
            "type": "string",
            "description": "Language code (e.g., \"en\")"
          },
          "site": {
            "type": "string",
            "description": "Limit search to specific site"
          },
          "safe_search": {
            "type": "boolean",
            "description": "Enable safe search"
          },
          "time_range": {
            "type": "string",
            "enum": [
              "day",
              "week",
              "month",
              "year",
              "all"
            ],
            "description": "Time range: day, week, month, year, all"
          },
          "file_type": {
            "type": "string",
            "description": "Filter by file type (e.g., \"pdf\")"
          },
          "redact_pii": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "object",
                "properties": {
                  "entities": {
                    "maxItems": 16,
                    "type": "array",
                    "items": {
                      "type": "string",
                      "minLength": 1
                    }
                  },
                  "replace_style": {
                    "type": "string",
                    "enum": [
                      "tag",
                      "mask",
                      "remove"
                    ]
                  },
                  "mode": {
                    "type": "string",
                    "enum": [
                      "fast",
                      "model"
                    ]
                  }
                }
              }
            ],
            "description": "Remove personal data from the text this call returns, before it is stored or sent back. true is shorthand for { mode: \"fast\" }: every entity, tagged. As an object: entities (any of EMAIL, PHONE, FINANCIAL, SECRET; omitted or empty means all four, and any other name is a 400 rather than a silent no-op), replace_style (\"tag\" → <EMAIL>, \"mask\" → [REDACTED], \"remove\" → nothing; default \"tag\") and mode (\"fast\", the default, is regex-only and costs no extra credits; \"model\" covers PERSON and LOCATION, needs an LLM and is rejected here — use the CrawlForge MCP server). The response carries redaction: { entities, count, mode } inside data, saying what was removed. Detection is deliberately conservative: a card number must pass Luhn and an IBAN mod-97, so a false positive cannot silently destroy real page content. URLs, queries and identifiers the response uses to name what was fetched are left intact, and counters derived from the text (content_length, word_count, character_count) describe the text as it was extracted, before redaction."
          },
          "max_inline_chars": {
            "type": "integer",
            "minimum": 1000,
            "maximum": 10000000,
            "description": "Largest result returned inline, in characters of its JSON (1000-10000000; env CRAWLFORGE_MAX_INLINE_CHARS sets the default). Over it, the result is stored for 1 hour and the response carries a preview, a result_handle, total_chars and truncated: true; read the rest with read_result (1 credit)."
          }
        },
        "oneOf": [
          {
            "required": [
              "query"
            ]
          },
          {
            "required": [
              "queries"
            ]
          }
        ]
      },
      "SerpRankRequest": {
        "type": "object",
        "properties": {
          "keyword": {
            "type": "string",
            "minLength": 1,
            "description": "The search query to check ranking for"
          },
          "target": {
            "type": "string",
            "minLength": 1,
            "description": "Domain or URL to locate in the results (e.g. 'example.com'). Matched by host, not exact URL: a URL is reduced to its host, and any page on that host or its subdomains counts"
          },
          "depth": {
            "default": 30,
            "type": "number",
            "minimum": 10,
            "maximum": 200,
            "description": "How many results to scan (10-200, default 30); deeper scans take longer. Credits stay at 5 at any depth"
          },
          "device": {
            "default": "desktop",
            "type": "string",
            "enum": [
              "desktop",
              "mobile"
            ],
            "description": "Device to emulate: desktop or mobile"
          },
          "language_code": {
            "default": "en",
            "type": "string",
            "description": "Language code (e.g. 'en')"
          },
          "location_name": {
            "default": "United States",
            "type": "string",
            "description": "Location, e.g. 'United States' or 'London,England,United Kingdom'"
          },
          "location_code": {
            "type": "number",
            "description": "Numeric DataForSEO location code (overrides location_name)"
          }
        },
        "required": [
          "keyword",
          "target"
        ]
      },
      "StealthModeRequest": {
        "type": "object",
        "properties": {
          "operation": {
            "type": "string",
            "enum": [
              "scrape",
              "configure",
              "create_context",
              "create_page",
              "get_stats",
              "cleanup"
            ],
            "default": "configure",
            "description": "Stealth operation to perform. \"scrape\" creates a context, navigates to url, returns the requested formats and tears the context down, all in one billed call. \"configure\" validates stealthConfig and returns it with defaults filled in; nothing is stored. \"create_context\": a stealth context to run create_page in. \"create_page\": navigate urlToTest in that context and report status, title and any challenge page; the page is closed before the call returns. \"get_stats\": the backend's open contexts, browser and proxy state. \"cleanup\": close the browser and every context."
          },
          "stealthConfig": {
            "type": "object",
            "description": "Stealth browser configuration. Applies to the call it is passed on and is not remembered. Chromium applies customUserAgent, customViewport, locale and timezone per call. On camoufox customUserAgent and customViewport are not applied, locale is fixed when the browser launches, and behind a proxy locale and timezone come from the exit IP; each of these is reported in `warnings`.",
            "properties": {
              "level": {
                "type": "string",
                "enum": [
                  "basic",
                  "medium",
                  "advanced"
                ],
                "default": "medium"
              },
              "randomizeFingerprint": {
                "type": "boolean",
                "default": true
              },
              "hideWebDriver": {
                "type": "boolean",
                "default": true
              },
              "blockWebRTC": {
                "type": "boolean",
                "default": true
              },
              "simulateHumanBehavior": {
                "type": "boolean",
                "default": true
              },
              "customUserAgent": {
                "type": "string"
              },
              "locale": {
                "type": "string",
                "default": "en-US"
              },
              "timezone": {
                "type": "string"
              }
            }
          },
          "engine": {
            "type": "string",
            "enum": [
              "playwright",
              "camoufox"
            ],
            "default": "playwright",
            "description": "Browser engine (camoufox only where installed on the backend)"
          },
          "contextId": {
            "type": "string",
            "description": "Browser context ID for page operations (from a create_context call)"
          },
          "urlToTest": {
            "type": "string",
            "description": "create_page: the URL to navigate to and check"
          },
          "url": {
            "type": "string",
            "description": "URL to scrape. Required for operation \"scrape\"; the other operations use urlToTest instead."
          },
          "formats": {
            "type": "array",
            "default": [
              "markdown"
            ],
            "description": "Formats operation \"scrape\" returns: markdown, html, text, links, metadata, screenshot. A screenshot comes back as a crawlforge://screenshot/{id} resource URI, not inline base64 — this endpoint passes that URI through without resolving it, so for now screenshot is usable only from the CrawlForge MCP server."
          },
          "wait_for": {
            "type": "number",
            "description": "Extra wait after page load, in ms (0-30000), for content that renders after DOMContentLoaded"
          },
          "verbose": {
            "type": "boolean",
            "default": false,
            "description": "Return the full generated fingerprint from create_context instead of a summary"
          },
          "respect_robots": {
            "type": "boolean",
            "default": true,
            "description": "Respect the target site's robots.txt. Omitted, the compliant default (true) applies: a URL disallowed for CrawlForge is refused before any browser is launched and no credits are charged, and every navigation is checked the same way. The rule is matched against the CrawlForge product token even though the stealth browser presents a randomized User-Agent, so a site owner's \"User-agent: CrawlForge\" directive binds stealth traffic too. Setting this to false is honoured, returns a warning in the response, and is recorded against your API key."
          },
          "max_inline_chars": {
            "type": "number",
            "default": 40000,
            "description": "Largest result returned inline, in characters of its JSON (1000-10000000; env CRAWLFORGE_MAX_INLINE_CHARS sets the default). Over it, the result is stored for 1 hour and the response carries a preview, a result_handle, total_chars and truncated: true; read the rest with read_result (1 credit)."
          },
          "redact_pii": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "object",
                "properties": {
                  "entities": {
                    "maxItems": 16,
                    "type": "array",
                    "items": {
                      "type": "string",
                      "minLength": 1
                    }
                  },
                  "replace_style": {
                    "type": "string",
                    "enum": [
                      "tag",
                      "mask",
                      "remove"
                    ]
                  },
                  "mode": {
                    "type": "string",
                    "enum": [
                      "fast",
                      "model"
                    ]
                  }
                }
              }
            ],
            "default": false,
            "description": "Remove personal data from the text this call returns, before it is stored or sent back. true is shorthand for { mode: \"fast\" }: every entity, tagged. As an object: entities (any of EMAIL, PHONE, FINANCIAL, SECRET; omitted or empty means all four, and any other name is a 400 rather than a silent no-op), replace_style (\"tag\" → <EMAIL>, \"mask\" → [REDACTED], \"remove\" → nothing; default \"tag\") and mode (\"fast\", the default, is regex-only and costs no extra credits; \"model\" covers PERSON and LOCATION, needs an LLM and is rejected here — use the CrawlForge MCP server). The response carries redaction: { entities, count, mode } inside data, saying what was removed. Detection is deliberately conservative: a card number must pass Luhn and an IBAN mod-97, so a false positive cannot silently destroy real page content. URLs, queries and identifiers the response uses to name what was fetched are left intact, and counters derived from the text (content_length, word_count, character_count) describe the text as it was extracted, before redaction."
          }
        }
      },
      "SummarizeContentRequest": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "URL to fetch and summarize content from"
          },
          "content": {
            "type": "string",
            "description": "Direct content to summarize (if URL not provided)"
          },
          "respect_robots": {
            "default": true,
            "type": "boolean",
            "description": "Fetch the origin's robots.txt and refuse a url it disallows for CrawlForge. A missing or unreachable robots.txt is treated as no restrictions. Returns 403 ROBOTS_DISALLOWED, and no credits are charged. Ignored when summarizing supplied content."
          },
          "max_sentences": {
            "default": 5,
            "type": "number",
            "minimum": 1,
            "maximum": 20,
            "description": "Maximum sentences in summary (1-20)"
          },
          "summary_type": {
            "default": "extractive",
            "type": "string",
            "enum": [
              "extractive",
              "key_points",
              "brief"
            ],
            "description": "Type of summary to generate"
          },
          "include_metadata": {
            "default": false,
            "type": "boolean",
            "description": "Include page metadata in response"
          },
          "timeout": {
            "default": 10000,
            "type": "number",
            "minimum": 1000,
            "maximum": 30000,
            "description": "Request timeout in milliseconds (1000-30000)"
          }
        },
        "anyOf": [
          {
            "required": [
              "url"
            ]
          },
          {
            "required": [
              "content"
            ]
          }
        ]
      },
      "TrackChangesRequest": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "URL of the webpage to track"
          },
          "operation": {
            "default": "compare",
            "type": "string",
            "enum": [
              "create_baseline",
              "compare",
              "monitor"
            ],
            "description": "create_baseline captures and stores the current page text (kept 90 days). compare fetches the page again and diffs it against the stored baseline. monitor creates a hosted monitor that fetches and compares the page on a schedule; creating it costs nothing, and each scheduled check bills 3 credits per target that was fetched and compared. Manage monitors at /api/v1/monitors or in the dashboard."
          },
          "selector": {
            "type": "string",
            "description": "CSS selector to scope tracking to part of the page (e.g. \".pricing-table\"). Baselines are stored per (url, selector) pair; 422 if the selector matches nothing."
          },
          "update_baseline": {
            "default": false,
            "type": "boolean",
            "description": "compare only: overwrite the stored baseline with the freshly fetched content after diffing"
          },
          "respect_robots": {
            "default": true,
            "type": "boolean",
            "description": "create_baseline and compare: fetch the origin's robots.txt and refuse the URL if it disallows CrawlForge. A missing or unreachable robots.txt is treated as no restrictions. Returns 403 ROBOTS_DISALLOWED, and no credits are charged. Hosted monitors always respect robots.txt; a disallowed target is reported as a page error."
          },
          "schedule": {
            "type": "string",
            "description": "monitor only: five-field cron expression, evaluated in UTC, for how often the monitor checks the page (default \"0 * * * *\", hourly). Consecutive runs must be at least 5 minutes apart; 400 VALIDATION_ERROR otherwise."
          },
          "notify_emails": {
            "maxItems": 5,
            "type": "array",
            "items": {
              "type": "string",
              "format": "email",
              "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
            },
            "description": "monitor only: up to 5 addresses that receive a summary email when a check finds new, changed, blocked or errored pages"
          },
          "webhook_url": {
            "type": "string",
            "format": "uri",
            "description": "monitor only: an https endpoint that receives a signed POST (monitor.page per changed page, then monitor.check.completed) after every check. The signing secret is returned once, as webhook_secret on the created monitor."
          }
        },
        "required": [
          "url"
        ]
      }
    }
  }
}
