{
  "openapi": "3.1.0",
  "info": {
    "title": "Zenrows API",
    "version": "1.0.0",
    "summary": "Reach any page, including the protected web, and return it as HTML, Markdown, plaintext, PDF or structured JSON.",
    "description": "Zenrows is web data infrastructure for agents and applications. Three HTTP surfaces are described here:\n\n- **Fetch and Extract**: one synchronous request per page, at `https://api.zenrows.com/v1/`. Authentication is the `apikey` query parameter.\n- **Crawl**: collect the URLs behind one start page, at `https://api.zenrows.com/v1/crawls`. Authentication is the `X-API-Key` header or the `apikey` query parameter.\n- **Batch**: asynchronous jobs of up to 100,000 URLs, at `https://async.api.zenrows.com/v1`. Authentication is the `X-API-Key` header.\n\n**Browser** is not described here because it is not HTTP: you connect Puppeteer or Playwright to a WebSocket endpoint. See https://docs.zenrows.com/browser-sessions/introduction.\n\n**Cost.** A request costs 1 credit, 5 with JavaScript rendering, 10 with premium proxies, 25 with both, and 1 to 25 under `mode=auto`. Failed requests are not charged, except target `404` and `410` answers, which are billed like a success. The free tier is 5,000 credits a month, no card.\n\n**Versioning and deprecation.** The version is in the URL path (`/v1`, the only version so far). Inside a version Zenrows only adds: new endpoints, optional parameters, response fields and headers. Nothing is removed or changed. Retiring a version is announced at least twelve months ahead, and every response from a deprecated version carries the `Deprecation` (RFC 9745) and `Sunset` (RFC 8594) headers. Policy: https://docs.zenrows.com/api-versioning.\n\n**Getting a key without a human.** `POST https://app.zenrows.com/api/agent/signup` returns a usable key; the spec is at https://www.zenrows.com/auth.md and a setup playbook at https://www.zenrows.com/SKILL.md.",
    "termsOfService": "https://www.zenrows.com/legal/terms-of-use",
    "contact": {
      "name": "Zenrows support",
      "email": "support@zenrows.com",
      "url": "https://www.zenrows.com/contact"
    },
    "license": {
      "name": "Proprietary",
      "url": "https://www.zenrows.com/legal/terms-of-use"
    }
  },
  "externalDocs": {
    "description": "Zenrows documentation",
    "url": "https://docs.zenrows.com"
  },
  "servers": [
    {
      "url": "https://api.zenrows.com/v1",
      "description": "Fetch and Extract (synchronous)"
    }
  ],
  "tags": [
    {
      "name": "Fetch",
      "description": "Retrieve one page, optionally rendered, proxied, converted or extracted."
    },
    {
      "name": "Extract",
      "description": "Which domains Extract serves, and getting a new one prepared."
    },
    {
      "name": "Crawl",
      "description": "Collect the URLs behind one start page, and each page's HTML when asked."
    },
    {
      "name": "Batch",
      "description": "Submit many URLs as one job and collect the results."
    },
    {
      "name": "Account",
      "description": "Plan, credits and concurrency for the calling key."
    },
    {
      "name": "Onboarding",
      "description": "Get a working key with no human in the loop."
    }
  ],
  "security": [
    {
      "apiKeyQuery": []
    }
  ],
  "paths": {
    "/": {
      "parameters": [
        {
          "name": "url",
          "in": "query",
          "description": "The URL of the page to retrieve.",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uri",
            "examples": [
              "https://www.scrapingcourse.com/ecommerce/"
            ]
          },
          "example": "https://www.scrapingcourse.com/ecommerce/"
        },
        {
          "name": "mode",
          "in": "query",
          "description": "Adaptive Stealth Mode. `auto` starts from the cheapest viable configuration and escalates to JavaScript rendering or premium proxies only when the target requires it. You are billed for the configuration that succeeds.",
          "required": false,
          "schema": {
            "type": "string",
            "enum": [
              "auto"
            ]
          }
        },
        {
          "name": "js_render",
          "in": "query",
          "description": "Render the page in a headless browser before returning it. Required for content that only exists after JavaScript runs.",
          "required": false,
          "schema": {
            "type": "boolean",
            "default": false
          }
        },
        {
          "name": "js_instructions",
          "in": "query",
          "description": "JSON array of browser instructions (click, fill, scroll, wait) executed on the page before the content is returned. Requires js_render.",
          "required": false,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "custom_headers",
          "in": "query",
          "description": "Forward your own request headers (cookies, referer) to the target.",
          "required": false,
          "schema": {
            "type": "boolean",
            "default": false
          }
        },
        {
          "name": "premium_proxy",
          "in": "query",
          "description": "Route the request through residential IPs. Required by most protected sites.",
          "required": false,
          "schema": {
            "type": "boolean",
            "default": false
          }
        },
        {
          "name": "proxy_country",
          "in": "query",
          "description": "Two-letter country code for the exit IP. Requires premium_proxy.",
          "required": false,
          "schema": {
            "type": "string",
            "pattern": "^[a-z]{2}$",
            "examples": [
              "us"
            ]
          },
          "example": "us"
        },
        {
          "name": "session_id",
          "in": "query",
          "description": "Reuse the same IP across requests for up to 10 minutes. Any integer you choose identifies the session.",
          "required": false,
          "schema": {
            "type": "integer"
          }
        },
        {
          "name": "original_status",
          "in": "query",
          "description": "Return the target's own HTTP status code instead of the Zenrows one.",
          "required": false,
          "schema": {
            "type": "boolean",
            "default": false
          }
        },
        {
          "name": "allowed_status_codes",
          "in": "query",
          "description": "Comma-separated status codes whose body should be returned instead of raising an error, for example `404,500`.",
          "required": false,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "wait_for",
          "in": "query",
          "description": "CSS selector to wait for before returning. Requires js_render.",
          "required": false,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "wait",
          "in": "query",
          "description": "Milliseconds to wait after load before returning. Requires js_render.",
          "required": false,
          "schema": {
            "type": "integer",
            "default": 0,
            "minimum": 0
          }
        },
        {
          "name": "block_resources",
          "in": "query",
          "description": "Comma-separated resource types to block (for example `image,media,font`). Blocking is on by default.",
          "required": false,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "json_response",
          "in": "query",
          "description": "Return a JSON envelope that also carries the network requests (XHR/fetch) the page made.",
          "required": false,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "css_extractor",
          "in": "query",
          "description": "JSON object mapping output keys to CSS selectors, applied to the retrieved page.",
          "required": false,
          "schema": {
            "type": "string",
            "examples": [
              "{\"title\":\"h1\",\"price\":\".price\"}"
            ]
          },
          "example": "{\"title\":\"h1\",\"price\":\".price\"}"
        },
        {
          "name": "extract",
          "in": "query",
          "description": "Return structured JSON extracted from the page with no selectors to write (Extract). Omit it and Extract is off. `auto` and `native` return the website's own field names; `standard` returns a canonical schema shared by every website in a vertical (real estate only for now). Extract works on any prepared domain, with any active API key (prepare a new one with `POST /extract/prepared-domains`), and is currently free on top of the Fetch cost. A domain, or a kind of page on it, that is not prepared yet answers 403 `REQS007`: see `requestExtractDomain`.",
          "required": false,
          "schema": {
            "type": "string",
            "enum": [
              "auto",
              "native",
              "standard"
            ]
          }
        },
        {
          "name": "extract_fields",
          "in": "query",
          "description": "Comma-separated list of the fields `extract` should return, dot-notated for fields inside a group (for example `products.name,products.price`). Omit it to receive every field.",
          "required": false,
          "schema": {
            "type": "string",
            "examples": [
              "products.name,products.price"
            ]
          },
          "example": "products.name,products.price"
        },
        {
          "name": "autoparse",
          "in": "query",
          "description": "Deprecated in favour of Extract (`extract=auto`). Returns general-purpose JSON parsed from the page, not tailored to the website.",
          "required": false,
          "schema": {
            "type": "boolean",
            "default": false
          },
          "deprecated": true
        },
        {
          "name": "response_type",
          "in": "query",
          "description": "Convert the page before returning it: Markdown, plaintext or PDF.",
          "required": false,
          "schema": {
            "type": "string",
            "enum": [
              "markdown",
              "plaintext",
              "pdf"
            ]
          }
        },
        {
          "name": "screenshot",
          "in": "query",
          "description": "Return an above-the-fold screenshot of the page. Requires js_render.",
          "required": false,
          "schema": {
            "type": "boolean",
            "default": false
          }
        },
        {
          "name": "screenshot_fullpage",
          "in": "query",
          "description": "Return a full-page screenshot. Requires js_render.",
          "required": false,
          "schema": {
            "type": "boolean",
            "default": false
          }
        },
        {
          "name": "screenshot_selector",
          "in": "query",
          "description": "Screenshot only the element matching this CSS selector. Requires js_render.",
          "required": false,
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "screenshot_format",
          "in": "query",
          "description": "Screenshot image format.",
          "required": false,
          "schema": {
            "type": "string",
            "enum": [
              "png",
              "jpeg"
            ],
            "default": "png"
          }
        },
        {
          "name": "screenshot_quality",
          "in": "query",
          "description": "JPEG quality, 1 to 100. Only with screenshot_format=jpeg.",
          "required": false,
          "schema": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          }
        },
        {
          "name": "outputs",
          "in": "query",
          "description": "Comma-separated data types to pull out of the retrieved HTML (for example `emails,phone_numbers`).",
          "required": false,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "operationId": "fetchPage",
        "summary": "Fetch a page",
        "description": "Retrieve a URL through Zenrows and return its content. The same operation covers Extract (`extract=auto` or `css_extractor`), format conversion (`response_type`), screenshots and structured output, because they are configurations of one request rather than separate endpoints.",
        "tags": [
          "Fetch"
        ],
        "responses": {
          "200": {
            "description": "The target's content, in the shape the request asked for: HTML by default, Markdown or plaintext with `response_type`, a PDF or PNG/JPEG binary for `response_type=pdf` and the screenshot parameters, and JSON for `extract`, `css_extractor`, `autoparse` or `json_response`.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string",
                  "description": "The page as returned by the target."
                },
                "example": "<!doctype html>\n<html lang=\"en-US\">\n<head>\n<title>Ecommerce Test Site to Learn Web Scraping</title>\n</head>\n<body>\n<h1>Shop</h1>\n...\n</body>\n</html>"
              },
              "text/markdown": {
                "schema": {
                  "type": "string",
                  "description": "The page converted to Markdown."
                },
                "example": "[Ecommerce Test Site to Learn Web Scraping](https://www.scrapingcourse.com/ecommerce/)\n\n# Shop\n\n- [Abominable Hoodie](https://www.scrapingcourse.com/ecommerce/product/abominable-hoodie/) $69.00\n..."
              },
              "text/plain": {
                "schema": {
                  "type": "string",
                  "description": "The page as plaintext."
                },
                "example": "Ecommerce Test Site to Learn Web Scraping\nShop\nAbominable Hoodie $69.00\n..."
              },
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Structured output: the extracted fields, the autoparsed record, or the JSON envelope with captured network requests.",
                  "additionalProperties": true
                },
                "example": {
                  "html": "<!doctype html>\n<html lang=\"en-US\">...</html>",
                  "parsed": {
                    "breadcrumbs": [
                      {
                        "name": "Home",
                        "position": 1,
                        "url": "https://www.scrapingcourse.com/ecommerce"
                      }
                    ],
                    "product": {
                      "name": "Abominable Hoodie",
                      "availability": "out_of_stock",
                      "category": "Hoodies & Sweatshirts"
                    }
                  }
                }
              },
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary",
                  "description": "The page rendered to PDF."
                }
              },
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary",
                  "description": "Screenshot of the page or of the selected element."
                }
              }
            },
            "headers": {
              "Concurrency-Limit": {
                "$ref": "#/components/headers/ConcurrencyLimit"
              },
              "Concurrency-Remaining": {
                "$ref": "#/components/headers/ConcurrencyRemaining"
              },
              "X-Request-Cost": {
                "$ref": "#/components/headers/XRequestCost"
              },
              "X-Request-Credits": {
                "$ref": "#/components/headers/XRequestCredits"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "The request is malformed, or a parameter combination is not allowed.",
            "headers": {
              "Concurrency-Limit": {
                "$ref": "#/components/headers/ConcurrencyLimit"
              },
              "Concurrency-Remaining": {
                "$ref": "#/components/headers/ConcurrencyRemaining"
              },
              "X-Request-Cost": {
                "$ref": "#/components/headers/XRequestCost"
              },
              "X-Request-Credits": {
                "$ref": "#/components/headers/XRequestCredits"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "REQS004",
                  "instance": "/v1",
                  "status": 400,
                  "title": "Invalid value provided for 'js_render' parameter; invalid boolean value 'maybe' (REQS004)",
                  "type": "https://docs.zenrows.com/api-error-codes#REQS004"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing or invalid.",
            "headers": {
              "Concurrency-Limit": {
                "$ref": "#/components/headers/ConcurrencyLimit"
              },
              "Concurrency-Remaining": {
                "$ref": "#/components/headers/ConcurrencyRemaining"
              },
              "X-Request-Cost": {
                "$ref": "#/components/headers/XRequestCost"
              },
              "X-Request-Credits": {
                "$ref": "#/components/headers/XRequestCredits"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "AUTH002",
                  "detail": "Invalid API key.",
                  "instance": "/v1",
                  "status": 401,
                  "title": "Invalid apikey provided (AUTH002)",
                  "type": "https://docs.zenrows.com/api-error-codes#AUTH002"
                }
              }
            }
          },
          "402": {
            "description": "The account is out of credits, or the plan does not include this capability.",
            "headers": {
              "Concurrency-Limit": {
                "$ref": "#/components/headers/ConcurrencyLimit"
              },
              "Concurrency-Remaining": {
                "$ref": "#/components/headers/ConcurrencyRemaining"
              },
              "X-Request-Cost": {
                "$ref": "#/components/headers/XRequestCost"
              },
              "X-Request-Credits": {
                "$ref": "#/components/headers/XRequestCredits"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "AUTH004",
                  "detail": "This account has used all its credits for the period. Upgrade or add credits to continue.",
                  "instance": "/v1",
                  "status": 402,
                  "title": "Usage exceeded (AUTH004)",
                  "type": "https://docs.zenrows.com/api-error-codes#AUTH004"
                }
              }
            }
          },
          "403": {
            "description": "With `extract=auto`, error `REQS007`: the domain, or this kind of page on it, has not been prepared for Extract yet. The message names the domain. The request is not charged and no HTML is returned. Prepare the domain with `requestExtractDomain`, then retry once it reports `ready`. Documented at https://docs.zenrows.com/api-error-codes#REQS007.",
            "headers": {
              "Concurrency-Limit": {
                "$ref": "#/components/headers/ConcurrencyLimit"
              },
              "Concurrency-Remaining": {
                "$ref": "#/components/headers/ConcurrencyRemaining"
              },
              "X-Request-Cost": {
                "$ref": "#/components/headers/XRequestCost"
              },
              "X-Request-Credits": {
                "$ref": "#/components/headers/XRequestCredits"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "REQS007",
                  "detail": "Extract has not been prepared for example.org yet. Ask for it with POST /v1/extract/prepared-domains, then retry once it reports ready. GET /v1/extract/domains lists what Extract serves today.",
                  "instance": "/v1",
                  "status": 403,
                  "title": "Extract is not prepared for the requested domain (example.org) (REQS007)",
                  "type": "https://docs.zenrows.com/api-error-codes#REQS007"
                }
              }
            }
          },
          "422": {
            "description": "Zenrows reached the target but could not return it under the requested configuration. With `extract`, the lowercase code `no_data_extracted` means something else: Extract knows this kind of page and applied its extraction, but no field came back filled. That answer is not charged, carries no HTML, and comes back the same on a retry, so do not retry it; check that the page rendered fully instead. The lowercase code `blocked_document` means the page Zenrows fetched was a bot challenge, an error page or a page that had not finished loading, so Extract had nothing real to read. It is not charged either; retry with `js_render=true` and/or `premium_proxy=true`, or with Adaptive Stealth Mode (`mode=auto`). Documented at https://docs.zenrows.com/api-error-codes#no_data_extracted.",
            "headers": {
              "Concurrency-Limit": {
                "$ref": "#/components/headers/ConcurrencyLimit"
              },
              "Concurrency-Remaining": {
                "$ref": "#/components/headers/ConcurrencyRemaining"
              },
              "X-Request-Cost": {
                "$ref": "#/components/headers/XRequestCost"
              },
              "X-Request-Credits": {
                "$ref": "#/components/headers/XRequestCredits"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "RESP001",
                  "detail": "The target could not be returned under this configuration. Try js_render=true and premium_proxy=true, or mode=auto.",
                  "instance": "/v1",
                  "status": 422,
                  "title": "Could not get content (RESP001)",
                  "type": "https://docs.zenrows.com/api-error-codes#RESP001"
                }
              }
            }
          },
          "429": {
            "description": "Error `AUTH008`: every concurrency slot on the plan is occupied. There is no fixed requests-per-minute quota to wait out and no Retry-After to read: a slot frees when one of your requests in flight finishes. Read `Concurrency-Remaining` on any response to pace yourself, and back off exponentially before retrying. Documented at https://docs.zenrows.com/api-error-codes#AUTH008 and https://docs.zenrows.com/fetch/features/concurrency.",
            "headers": {
              "Concurrency-Limit": {
                "$ref": "#/components/headers/ConcurrencyLimit"
              },
              "Concurrency-Remaining": {
                "$ref": "#/components/headers/ConcurrencyRemaining"
              },
              "X-Request-Cost": {
                "$ref": "#/components/headers/XRequestCost"
              },
              "X-Request-Credits": {
                "$ref": "#/components/headers/XRequestCredits"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "AUTH008",
                  "detail": "Every concurrency slot on the plan is in use. Retry when a request in flight finishes.",
                  "instance": "/v1",
                  "status": 429,
                  "title": "Concurrency limit reached (AUTH008)",
                  "type": "https://docs.zenrows.com/api-error-codes#AUTH008"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "bash",
            "label": "cURL",
            "source": "curl --max-time 180 \"https://api.zenrows.com/v1/?apikey=YOUR_ZENROWS_API_KEY&url=https%3A%2F%2Fhttpbin.io%2Fget&mode=auto\""
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "# pip install requests\nimport requests\n\nurl = 'https://httpbin.io/get'\napikey = 'YOUR_ZENROWS_API_KEY'\nparams = {\n    'url': url,\n    'apikey': apikey,\n    'mode': 'auto',\n}\n\nresponse = requests.get('https://api.zenrows.com/v1/', params=params)\nprint(response.text)"
          },
          {
            "lang": "javascript",
            "label": "Node.js",
            "source": "// npm install axios\nconst axios = require('axios');\n\nconst url = 'https://httpbin.io/get';\nconst apikey = 'YOUR_ZENROWS_API_KEY';\naxios({\n    url: 'https://api.zenrows.com/v1/',\n    method: 'GET',\n    params: {\n        'url': url,\n        'apikey': apikey,\n        'mode': 'auto',\n    },\n})\n    .then(response => console.log(response.data))\n    .catch(error => console.log(error));"
          },
          {
            "lang": "go",
            "label": "Go",
            "source": "package main\n\nimport (\n    \"io\"\n    \"log\"\n    \"net/http\"\n)\n\nfunc main() {\n    client := &http.Client{}\n    req, err := http.NewRequest(\"GET\", \"https://api.zenrows.com/v1/?apikey=YOUR_ZENROWS_API_KEY&url=https%3A%2F%2Fhttpbin.io%2Fget&mode=auto\", nil)\n    if err != nil {\n        log.Fatalln(err)\n    }\n    resp, err := client.Do(req)\n    if err != nil {\n        log.Fatalln(err)\n    }\n    defer resp.Body.Close()\n\n    body, err := io.ReadAll(resp.Body)\n    if err != nil {\n        log.Fatalln(err)\n    }\n\n    log.Println(string(body))\n}"
          },
          {
            "lang": "php",
            "label": "PHP",
            "source": "<?php\n$ch = curl_init();\ncurl_setopt($ch, CURLOPT_URL, 'https://api.zenrows.com/v1/?apikey=YOUR_ZENROWS_API_KEY&url=https%3A%2F%2Fhttpbin.io%2Fget&mode=auto');\ncurl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');\ncurl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);\ncurl_setopt($ch, CURLOPT_TIMEOUT, 180);\n$response = curl_exec($ch);\necho $response . PHP_EOL;\ncurl_close($ch);\n?>"
          },
          {
            "lang": "ruby",
            "label": "Ruby",
            "source": "# gem install faraday\nrequire 'faraday'\n\nurl = URI.parse('https://api.zenrows.com/v1/?apikey=YOUR_ZENROWS_API_KEY&url=https%3A%2F%2Fhttpbin.io%2Fget&mode=auto')\nconn = Faraday.new()\nconn.options.timeout = 180\nres = conn.get(url, nil, nil)\nprint(res.body)"
          },
          {
            "lang": "java",
            "label": "Java",
            "source": "import org.apache.hc.client5.http.fluent.Request;\n\npublic class APIRequest {\n    public static void main(final String... args) throws Exception {\n        String apiUrl = \"https://api.zenrows.com/v1/?apikey=YOUR_ZENROWS_API_KEY&url=https%3A%2F%2Fhttpbin.io%2Fget&mode=auto\";\n        String response = Request.get(apiUrl)\n                .execute().returnContent().asString();\n\n        System.out.println(response);\n    }\n}"
          }
        ]
      },
      "post": {
        "operationId": "fetchPagePost",
        "summary": "Fetch a page with a POST",
        "description": "Send a POST to the target through Zenrows. Takes the same query parameters as fetchPage; the request body is forwarded to the target as is, and the response carries the target's content.",
        "tags": [
          "Fetch"
        ],
        "requestBody": {
          "description": "Forwarded to the target as is.",
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "additionalProperties": true
              }
            },
            "application/json": {
              "schema": {}
            },
            "*/*": {
              "schema": {
                "type": "string"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The target's content, in the shape the request asked for: HTML by default, Markdown or plaintext with `response_type`, a PDF or PNG/JPEG binary for `response_type=pdf` and the screenshot parameters, and JSON for `extract`, `css_extractor`, `autoparse` or `json_response`.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string",
                  "description": "The page as returned by the target."
                },
                "example": "<!doctype html>\n<html lang=\"en-US\">\n<head>\n<title>Ecommerce Test Site to Learn Web Scraping</title>\n</head>\n<body>\n<h1>Shop</h1>\n...\n</body>\n</html>"
              },
              "text/markdown": {
                "schema": {
                  "type": "string",
                  "description": "The page converted to Markdown."
                },
                "example": "[Ecommerce Test Site to Learn Web Scraping](https://www.scrapingcourse.com/ecommerce/)\n\n# Shop\n\n- [Abominable Hoodie](https://www.scrapingcourse.com/ecommerce/product/abominable-hoodie/) $69.00\n..."
              },
              "text/plain": {
                "schema": {
                  "type": "string",
                  "description": "The page as plaintext."
                },
                "example": "Ecommerce Test Site to Learn Web Scraping\nShop\nAbominable Hoodie $69.00\n..."
              },
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Structured output: the extracted fields, the autoparsed record, or the JSON envelope with captured network requests.",
                  "additionalProperties": true
                },
                "example": {
                  "html": "<!doctype html>\n<html lang=\"en-US\">...</html>",
                  "parsed": {
                    "breadcrumbs": [
                      {
                        "name": "Home",
                        "position": 1,
                        "url": "https://www.scrapingcourse.com/ecommerce"
                      }
                    ],
                    "product": {
                      "name": "Abominable Hoodie",
                      "availability": "out_of_stock",
                      "category": "Hoodies & Sweatshirts"
                    }
                  }
                }
              },
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary",
                  "description": "The page rendered to PDF."
                }
              },
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary",
                  "description": "Screenshot of the page or of the selected element."
                }
              }
            },
            "headers": {
              "Concurrency-Limit": {
                "$ref": "#/components/headers/ConcurrencyLimit"
              },
              "Concurrency-Remaining": {
                "$ref": "#/components/headers/ConcurrencyRemaining"
              },
              "X-Request-Cost": {
                "$ref": "#/components/headers/XRequestCost"
              },
              "X-Request-Credits": {
                "$ref": "#/components/headers/XRequestCredits"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "The request is malformed, or a parameter combination is not allowed.",
            "headers": {
              "Concurrency-Limit": {
                "$ref": "#/components/headers/ConcurrencyLimit"
              },
              "Concurrency-Remaining": {
                "$ref": "#/components/headers/ConcurrencyRemaining"
              },
              "X-Request-Cost": {
                "$ref": "#/components/headers/XRequestCost"
              },
              "X-Request-Credits": {
                "$ref": "#/components/headers/XRequestCredits"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "REQS004",
                  "instance": "/v1",
                  "status": 400,
                  "title": "Invalid value provided for 'js_render' parameter; invalid boolean value 'maybe' (REQS004)",
                  "type": "https://docs.zenrows.com/api-error-codes#REQS004"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing or invalid.",
            "headers": {
              "Concurrency-Limit": {
                "$ref": "#/components/headers/ConcurrencyLimit"
              },
              "Concurrency-Remaining": {
                "$ref": "#/components/headers/ConcurrencyRemaining"
              },
              "X-Request-Cost": {
                "$ref": "#/components/headers/XRequestCost"
              },
              "X-Request-Credits": {
                "$ref": "#/components/headers/XRequestCredits"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "AUTH002",
                  "detail": "Invalid API key.",
                  "instance": "/v1",
                  "status": 401,
                  "title": "Invalid apikey provided (AUTH002)",
                  "type": "https://docs.zenrows.com/api-error-codes#AUTH002"
                }
              }
            }
          },
          "402": {
            "description": "The account is out of credits, or the plan does not include this capability.",
            "headers": {
              "Concurrency-Limit": {
                "$ref": "#/components/headers/ConcurrencyLimit"
              },
              "Concurrency-Remaining": {
                "$ref": "#/components/headers/ConcurrencyRemaining"
              },
              "X-Request-Cost": {
                "$ref": "#/components/headers/XRequestCost"
              },
              "X-Request-Credits": {
                "$ref": "#/components/headers/XRequestCredits"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "AUTH004",
                  "detail": "This account has used all its credits for the period. Upgrade or add credits to continue.",
                  "instance": "/v1",
                  "status": 402,
                  "title": "Usage exceeded (AUTH004)",
                  "type": "https://docs.zenrows.com/api-error-codes#AUTH004"
                }
              }
            }
          },
          "403": {
            "description": "With `extract=auto`, error `REQS007`: the domain, or this kind of page on it, has not been prepared for Extract yet. The message names the domain. The request is not charged and no HTML is returned. Prepare the domain with `requestExtractDomain`, then retry once it reports `ready`. Documented at https://docs.zenrows.com/api-error-codes#REQS007.",
            "headers": {
              "Concurrency-Limit": {
                "$ref": "#/components/headers/ConcurrencyLimit"
              },
              "Concurrency-Remaining": {
                "$ref": "#/components/headers/ConcurrencyRemaining"
              },
              "X-Request-Cost": {
                "$ref": "#/components/headers/XRequestCost"
              },
              "X-Request-Credits": {
                "$ref": "#/components/headers/XRequestCredits"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "REQS007",
                  "detail": "Extract has not been prepared for example.org yet. Ask for it with POST /v1/extract/prepared-domains, then retry once it reports ready. GET /v1/extract/domains lists what Extract serves today.",
                  "instance": "/v1",
                  "status": 403,
                  "title": "Extract is not prepared for the requested domain (example.org) (REQS007)",
                  "type": "https://docs.zenrows.com/api-error-codes#REQS007"
                }
              }
            }
          },
          "422": {
            "description": "Zenrows reached the target but could not return it under the requested configuration. With `extract`, the lowercase code `no_data_extracted` means something else: Extract knows this kind of page and applied its extraction, but no field came back filled. That answer is not charged, carries no HTML, and comes back the same on a retry, so do not retry it; check that the page rendered fully instead. The lowercase code `blocked_document` means the page Zenrows fetched was a bot challenge, an error page or a page that had not finished loading, so Extract had nothing real to read. It is not charged either; retry with `js_render=true` and/or `premium_proxy=true`, or with Adaptive Stealth Mode (`mode=auto`). Documented at https://docs.zenrows.com/api-error-codes#no_data_extracted.",
            "headers": {
              "Concurrency-Limit": {
                "$ref": "#/components/headers/ConcurrencyLimit"
              },
              "Concurrency-Remaining": {
                "$ref": "#/components/headers/ConcurrencyRemaining"
              },
              "X-Request-Cost": {
                "$ref": "#/components/headers/XRequestCost"
              },
              "X-Request-Credits": {
                "$ref": "#/components/headers/XRequestCredits"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "RESP001",
                  "detail": "The target could not be returned under this configuration. Try js_render=true and premium_proxy=true, or mode=auto.",
                  "instance": "/v1",
                  "status": 422,
                  "title": "Could not get content (RESP001)",
                  "type": "https://docs.zenrows.com/api-error-codes#RESP001"
                }
              }
            }
          },
          "429": {
            "description": "Error `AUTH008`: every concurrency slot on the plan is occupied. There is no fixed requests-per-minute quota to wait out and no Retry-After to read: a slot frees when one of your requests in flight finishes. Read `Concurrency-Remaining` on any response to pace yourself, and back off exponentially before retrying. Documented at https://docs.zenrows.com/api-error-codes#AUTH008 and https://docs.zenrows.com/fetch/features/concurrency.",
            "headers": {
              "Concurrency-Limit": {
                "$ref": "#/components/headers/ConcurrencyLimit"
              },
              "Concurrency-Remaining": {
                "$ref": "#/components/headers/ConcurrencyRemaining"
              },
              "X-Request-Cost": {
                "$ref": "#/components/headers/XRequestCost"
              },
              "X-Request-Credits": {
                "$ref": "#/components/headers/XRequestCredits"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "AUTH008",
                  "detail": "Every concurrency slot on the plan is in use. Retry when a request in flight finishes.",
                  "instance": "/v1",
                  "status": 429,
                  "title": "Concurrency limit reached (AUTH008)",
                  "type": "https://docs.zenrows.com/api-error-codes#AUTH008"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "fetchPagePut",
        "summary": "Fetch a page with a PUT",
        "description": "Send a PUT to the target through Zenrows. Takes the same query parameters as fetchPage; the request body is forwarded to the target as is, and the response carries the target's content.",
        "tags": [
          "Fetch"
        ],
        "requestBody": {
          "description": "Forwarded to the target as is.",
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "additionalProperties": true
              }
            },
            "application/json": {
              "schema": {}
            },
            "*/*": {
              "schema": {
                "type": "string"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The target's content, in the shape the request asked for: HTML by default, Markdown or plaintext with `response_type`, a PDF or PNG/JPEG binary for `response_type=pdf` and the screenshot parameters, and JSON for `extract`, `css_extractor`, `autoparse` or `json_response`.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string",
                  "description": "The page as returned by the target."
                },
                "example": "<!doctype html>\n<html lang=\"en-US\">\n<head>\n<title>Ecommerce Test Site to Learn Web Scraping</title>\n</head>\n<body>\n<h1>Shop</h1>\n...\n</body>\n</html>"
              },
              "text/markdown": {
                "schema": {
                  "type": "string",
                  "description": "The page converted to Markdown."
                },
                "example": "[Ecommerce Test Site to Learn Web Scraping](https://www.scrapingcourse.com/ecommerce/)\n\n# Shop\n\n- [Abominable Hoodie](https://www.scrapingcourse.com/ecommerce/product/abominable-hoodie/) $69.00\n..."
              },
              "text/plain": {
                "schema": {
                  "type": "string",
                  "description": "The page as plaintext."
                },
                "example": "Ecommerce Test Site to Learn Web Scraping\nShop\nAbominable Hoodie $69.00\n..."
              },
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Structured output: the extracted fields, the autoparsed record, or the JSON envelope with captured network requests.",
                  "additionalProperties": true
                },
                "example": {
                  "html": "<!doctype html>\n<html lang=\"en-US\">...</html>",
                  "parsed": {
                    "breadcrumbs": [
                      {
                        "name": "Home",
                        "position": 1,
                        "url": "https://www.scrapingcourse.com/ecommerce"
                      }
                    ],
                    "product": {
                      "name": "Abominable Hoodie",
                      "availability": "out_of_stock",
                      "category": "Hoodies & Sweatshirts"
                    }
                  }
                }
              },
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary",
                  "description": "The page rendered to PDF."
                }
              },
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary",
                  "description": "Screenshot of the page or of the selected element."
                }
              }
            },
            "headers": {
              "Concurrency-Limit": {
                "$ref": "#/components/headers/ConcurrencyLimit"
              },
              "Concurrency-Remaining": {
                "$ref": "#/components/headers/ConcurrencyRemaining"
              },
              "X-Request-Cost": {
                "$ref": "#/components/headers/XRequestCost"
              },
              "X-Request-Credits": {
                "$ref": "#/components/headers/XRequestCredits"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "400": {
            "description": "The request is malformed, or a parameter combination is not allowed.",
            "headers": {
              "Concurrency-Limit": {
                "$ref": "#/components/headers/ConcurrencyLimit"
              },
              "Concurrency-Remaining": {
                "$ref": "#/components/headers/ConcurrencyRemaining"
              },
              "X-Request-Cost": {
                "$ref": "#/components/headers/XRequestCost"
              },
              "X-Request-Credits": {
                "$ref": "#/components/headers/XRequestCredits"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "REQS004",
                  "instance": "/v1",
                  "status": 400,
                  "title": "Invalid value provided for 'js_render' parameter; invalid boolean value 'maybe' (REQS004)",
                  "type": "https://docs.zenrows.com/api-error-codes#REQS004"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing or invalid.",
            "headers": {
              "Concurrency-Limit": {
                "$ref": "#/components/headers/ConcurrencyLimit"
              },
              "Concurrency-Remaining": {
                "$ref": "#/components/headers/ConcurrencyRemaining"
              },
              "X-Request-Cost": {
                "$ref": "#/components/headers/XRequestCost"
              },
              "X-Request-Credits": {
                "$ref": "#/components/headers/XRequestCredits"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "AUTH002",
                  "detail": "Invalid API key.",
                  "instance": "/v1",
                  "status": 401,
                  "title": "Invalid apikey provided (AUTH002)",
                  "type": "https://docs.zenrows.com/api-error-codes#AUTH002"
                }
              }
            }
          },
          "402": {
            "description": "The account is out of credits, or the plan does not include this capability.",
            "headers": {
              "Concurrency-Limit": {
                "$ref": "#/components/headers/ConcurrencyLimit"
              },
              "Concurrency-Remaining": {
                "$ref": "#/components/headers/ConcurrencyRemaining"
              },
              "X-Request-Cost": {
                "$ref": "#/components/headers/XRequestCost"
              },
              "X-Request-Credits": {
                "$ref": "#/components/headers/XRequestCredits"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "AUTH004",
                  "detail": "This account has used all its credits for the period. Upgrade or add credits to continue.",
                  "instance": "/v1",
                  "status": 402,
                  "title": "Usage exceeded (AUTH004)",
                  "type": "https://docs.zenrows.com/api-error-codes#AUTH004"
                }
              }
            }
          },
          "403": {
            "description": "With `extract=auto`, error `REQS007`: the domain, or this kind of page on it, has not been prepared for Extract yet. The message names the domain. The request is not charged and no HTML is returned. Prepare the domain with `requestExtractDomain`, then retry once it reports `ready`. Documented at https://docs.zenrows.com/api-error-codes#REQS007.",
            "headers": {
              "Concurrency-Limit": {
                "$ref": "#/components/headers/ConcurrencyLimit"
              },
              "Concurrency-Remaining": {
                "$ref": "#/components/headers/ConcurrencyRemaining"
              },
              "X-Request-Cost": {
                "$ref": "#/components/headers/XRequestCost"
              },
              "X-Request-Credits": {
                "$ref": "#/components/headers/XRequestCredits"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "REQS007",
                  "detail": "Extract has not been prepared for example.org yet. Ask for it with POST /v1/extract/prepared-domains, then retry once it reports ready. GET /v1/extract/domains lists what Extract serves today.",
                  "instance": "/v1",
                  "status": 403,
                  "title": "Extract is not prepared for the requested domain (example.org) (REQS007)",
                  "type": "https://docs.zenrows.com/api-error-codes#REQS007"
                }
              }
            }
          },
          "422": {
            "description": "Zenrows reached the target but could not return it under the requested configuration. With `extract`, the lowercase code `no_data_extracted` means something else: Extract knows this kind of page and applied its extraction, but no field came back filled. That answer is not charged, carries no HTML, and comes back the same on a retry, so do not retry it; check that the page rendered fully instead. The lowercase code `blocked_document` means the page Zenrows fetched was a bot challenge, an error page or a page that had not finished loading, so Extract had nothing real to read. It is not charged either; retry with `js_render=true` and/or `premium_proxy=true`, or with Adaptive Stealth Mode (`mode=auto`). Documented at https://docs.zenrows.com/api-error-codes#no_data_extracted.",
            "headers": {
              "Concurrency-Limit": {
                "$ref": "#/components/headers/ConcurrencyLimit"
              },
              "Concurrency-Remaining": {
                "$ref": "#/components/headers/ConcurrencyRemaining"
              },
              "X-Request-Cost": {
                "$ref": "#/components/headers/XRequestCost"
              },
              "X-Request-Credits": {
                "$ref": "#/components/headers/XRequestCredits"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "RESP001",
                  "detail": "The target could not be returned under this configuration. Try js_render=true and premium_proxy=true, or mode=auto.",
                  "instance": "/v1",
                  "status": 422,
                  "title": "Could not get content (RESP001)",
                  "type": "https://docs.zenrows.com/api-error-codes#RESP001"
                }
              }
            }
          },
          "429": {
            "description": "Error `AUTH008`: every concurrency slot on the plan is occupied. There is no fixed requests-per-minute quota to wait out and no Retry-After to read: a slot frees when one of your requests in flight finishes. Read `Concurrency-Remaining` on any response to pace yourself, and back off exponentially before retrying. Documented at https://docs.zenrows.com/api-error-codes#AUTH008 and https://docs.zenrows.com/fetch/features/concurrency.",
            "headers": {
              "Concurrency-Limit": {
                "$ref": "#/components/headers/ConcurrencyLimit"
              },
              "Concurrency-Remaining": {
                "$ref": "#/components/headers/ConcurrencyRemaining"
              },
              "X-Request-Cost": {
                "$ref": "#/components/headers/XRequestCost"
              },
              "X-Request-Credits": {
                "$ref": "#/components/headers/XRequestCredits"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "AUTH008",
                  "detail": "Every concurrency slot on the plan is in use. Retry when a request in flight finishes.",
                  "instance": "/v1",
                  "status": 429,
                  "title": "Concurrency limit reached (AUTH008)",
                  "type": "https://docs.zenrows.com/api-error-codes#AUTH008"
                }
              }
            }
          }
        }
      }
    },
    "/extract/domains": {
      "get": {
        "operationId": "listExtractDomains",
        "summary": "List the domains Extract serves",
        "description": "Every domain Extract can handle right now. A domain in this list is one you can send `extract=auto` to; a domain missing from it answers `REQS007`. Costs no credits. Documented at https://docs.zenrows.com/extract/endpoints.",
        "tags": [
          "Extract"
        ],
        "security": [
          {
            "apiKeyQuery": []
          },
          {
            "apiKeyHeader": []
          }
        ],
        "responses": {
          "200": {
            "description": "The domains Extract serves.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "domains": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "domain": {
                            "type": "string",
                            "description": "The registrable domain. It covers every subdomain."
                          },
                          "verticals": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            },
                            "description": "The kinds of page Extract has prepared for this domain. May be empty."
                          },
                          "warm": {
                            "type": "boolean",
                            "description": "Always `true` today."
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "domains": [
                    {
                      "domain": "scrapingcourse.com",
                      "verticals": [
                        "Shopping"
                      ],
                      "warm": true
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing or invalid.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "AUTH002",
                  "detail": "Invalid API key.",
                  "instance": "/v1",
                  "status": 401,
                  "title": "Invalid apikey provided (AUTH002)",
                  "type": "https://docs.zenrows.com/api-error-codes#AUTH002"
                }
              }
            }
          }
        }
      }
    },
    "/extract/prepared-domains": {
      "post": {
        "operationId": "requestExtractDomain",
        "summary": "Ask for a domain to be prepared for Extract",
        "description": "Submit a domain for preparation, within a monthly allowance. The submission is accepted immediately and works through discovery, preparation and verification, typically in minutes: poll `getExtractDomainStatus` instead of blocking. Send `example_urls` when you can, one per kind of page; they replace discovery. Submitting a domain that is already prepared returns its current record rather than starting again, so it does not add new kinds of page. Send an `Idempotency-Key` header to make a retry safe. Costs no credits. Documented at https://docs.zenrows.com/extract/endpoints.",
        "tags": [
          "Extract"
        ],
        "security": [
          {
            "apiKeyQuery": []
          },
          {
            "apiKeyHeader": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "The same key returns the original submission instead of spending a second allowance slot.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "domain"
                ],
                "properties": {
                  "domain": {
                    "type": "string",
                    "description": "The domain to prepare, without scheme.",
                    "example": "scrapingcourse.com"
                  },
                  "example_urls": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "uri"
                    },
                    "description": "URLs on that domain, one per kind of page. When present, they replace discovery."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. The body is the submission in the same shape `getExtractDomainStatus` returns.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExtractPreparation"
                },
                "example": {
                  "domain": "scrapingcourse.com",
                  "status": "preparing",
                  "state": "received",
                  "submitted_at": "2026-10-15T10:00:00Z",
                  "updated_at": "2026-10-15T10:00:00Z"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_argument`: the body is missing or malformed. `reason` carries a machine-readable tag such as `invalid_domain`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExtractEndpointError"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing or invalid.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "AUTH002",
                  "detail": "Invalid API key.",
                  "instance": "/v1",
                  "status": 401,
                  "title": "Invalid apikey provided (AUTH002)",
                  "type": "https://docs.zenrows.com/api-error-codes#AUTH002"
                }
              }
            }
          },
          "409": {
            "description": "`in_progress`: the domain already has a preparation running, so poll its status. Or `idempotency_conflict`: the same `Idempotency-Key` was reused with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExtractEndpointError"
                }
              }
            }
          },
          "429": {
            "description": "`out_of_allowance`: no preparation slots left this month. See `getExtractAllowance`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExtractEndpointError"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "listExtractPreparations",
        "summary": "List the domains your account submitted",
        "description": "Every domain this account has submitted for preparation, with the monthly allowance alongside it. This lists what you submitted, not every domain Extract serves (see `listExtractDomains`). Costs no credits. Documented at https://docs.zenrows.com/extract/endpoints.",
        "tags": [
          "Extract"
        ],
        "security": [
          {
            "apiKeyQuery": []
          },
          {
            "apiKeyHeader": []
          }
        ],
        "responses": {
          "200": {
            "description": "The account's submissions and allowance.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "allowance": {
                      "type": "object",
                      "description": "Domains this account can still ask to have prepared this month. A failure that returns its slot is not counted in `used`.",
                      "properties": {
                        "used": {
                          "type": "integer"
                        },
                        "limit": {
                          "type": "integer"
                        },
                        "remaining": {
                          "type": "integer"
                        },
                        "month": {
                          "type": "string",
                          "description": "The UTC calendar month the count applies to, `YYYY-MM`. The allowance resets when it rolls over.",
                          "example": "2026-09"
                        }
                      }
                    },
                    "domains": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ExtractPreparation"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing or invalid.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "AUTH002",
                  "detail": "Invalid API key.",
                  "instance": "/v1",
                  "status": 401,
                  "title": "Invalid apikey provided (AUTH002)",
                  "type": "https://docs.zenrows.com/api-error-codes#AUTH002"
                }
              }
            }
          }
        }
      }
    },
    "/extract/prepared-domains/allowance": {
      "get": {
        "operationId": "getExtractAllowance",
        "summary": "Read the monthly preparation allowance",
        "description": "How many domains this account can still ask to have prepared this month. Costs no credits. Documented at https://docs.zenrows.com/extract/endpoints.",
        "tags": [
          "Extract"
        ],
        "security": [
          {
            "apiKeyQuery": []
          },
          {
            "apiKeyHeader": []
          }
        ],
        "responses": {
          "200": {
            "description": "The allowance for the current month.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Domains this account can still ask to have prepared this month. A failure that returns its slot is not counted in `used`.",
                  "properties": {
                    "used": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "remaining": {
                      "type": "integer"
                    },
                    "month": {
                      "type": "string",
                      "description": "The UTC calendar month the count applies to, `YYYY-MM`. The allowance resets when it rolls over.",
                      "example": "2026-09"
                    }
                  }
                },
                "example": {
                  "used": 3,
                  "limit": 10,
                  "remaining": 7,
                  "month": "2026-10"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing or invalid.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "AUTH002",
                  "detail": "Invalid API key.",
                  "instance": "/v1",
                  "status": 401,
                  "title": "Invalid apikey provided (AUTH002)",
                  "type": "https://docs.zenrows.com/api-error-codes#AUTH002"
                }
              }
            }
          }
        }
      }
    },
    "/extract/prepared-domains/{domain}": {
      "get": {
        "operationId": "getExtractDomainStatus",
        "summary": "Track a domain's preparation",
        "description": "How far a submission has got. Branch on `status` (`preparing`, `ready`, `failed`); `state` is the finer-grained step. Costs no credits. Documented at https://docs.zenrows.com/extract/endpoints.",
        "tags": [
          "Extract"
        ],
        "security": [
          {
            "apiKeyQuery": []
          },
          {
            "apiKeyHeader": []
          }
        ],
        "parameters": [
          {
            "name": "domain",
            "in": "path",
            "required": true,
            "description": "The domain, without scheme.",
            "schema": {
              "type": "string"
            },
            "example": "scrapingcourse.com"
          }
        ],
        "responses": {
          "200": {
            "description": "The domain's most recent submission.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExtractPreparation"
                },
                "example": {
                  "domain": "scrapingcourse.com",
                  "status": "ready",
                  "state": "ready",
                  "roles": [
                    {
                      "role": "detail",
                      "status": "ready"
                    }
                  ],
                  "submitted_at": "2026-09-11T14:33:45Z",
                  "updated_at": "2026-09-11T14:35:03Z"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing or invalid.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "AUTH002",
                  "detail": "Invalid API key.",
                  "instance": "/v1",
                  "status": 401,
                  "title": "Invalid apikey provided (AUTH002)",
                  "type": "https://docs.zenrows.com/api-error-codes#AUTH002"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: this domain has never been submitted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExtractEndpointError"
                }
              }
            }
          }
        }
      }
    },
    "/api/agent/signup": {
      "post": {
        "operationId": "createAgentAccount",
        "summary": "Get an API key for your agent, no signup required",
        "description": "Start using Zenrows from your agent without signing up. This call returns a working API key straight away: no account setup, no login, no payment details. The key works right away on the free plan. The account is on the Free plan from signup: 5,000 credits per month, renewed monthly. A person who wants to own it opens `claimUrl` and claims it; the same key keeps working.\n\nContract published at https://app.zenrows.com/agent-onboarding/auth.md, and advertised as `agent_auth.signup_endpoint` in https://app.zenrows.com/.well-known/oauth-protected-resource.",
        "tags": [
          "Onboarding"
        ],
        "servers": [
          {
            "url": "https://app.zenrows.com",
            "description": "Dashboard host, which serves the agent onboarding surface"
          }
        ],
        "security": [],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "No fields are required. An empty body is the normal call.",
                "additionalProperties": false
              }
            }
          }
        },
        "parameters": [
          {
            "name": "X-ZR-Agent-Id",
            "in": "header",
            "required": false,
            "description": "Optional provenance, anonymous and free of personal data.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-ZR-Client",
            "in": "header",
            "required": false,
            "description": "Optional name of the calling client.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-ZR-Source",
            "in": "header",
            "required": false,
            "description": "Optional origin of the signup.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "The account exists and the key works immediately.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "apiKey",
                    "accountId",
                    "claimUrl",
                    "trialEndsAt"
                  ],
                  "properties": {
                    "apiKey": {
                      "type": "string",
                      "description": "Pass it as the `apikey` query parameter to Fetch, or as `X-API-Key` to Batch."
                    },
                    "accountId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "claimUrl": {
                      "type": "string",
                      "format": "uri",
                      "description": "Where a person claims the account to own it. Show this link to a human; do not sit on it."
                    },
                    "trialEndsAt": {
                      "type": "string",
                      "format": "date-time",
                      "description": "End of the first monthly period (signup + 1 month). The Free plan renews monthly; this value is not refreshed."
                    }
                  }
                },
                "example": {
                  "apiKey": "8d1c2e9f0a7b4c3d5e6f7a8b9c0d1e2f3a4b5c6d",
                  "accountId": "3f2b8c1e-5a4d-4e7f-9b6a-1c2d3e4f5a6b",
                  "claimUrl": "https://app.zenrows.com/claim/q7Rk2LmX9pTz?utm_source=agent",
                  "trialEndsAt": "2026-11-15T10:00:00+00:00"
                }
              }
            }
          },
          "429": {
            "description": "Error `rate_limited`: three signups per client IP per 24 hours. Wait; do not go looking for another signup URL.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SignupError"
                }
              }
            }
          },
          "500": {
            "description": "Error `signup_failed`. Retry once with backoff, then hand it to a human.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SignupError"
                }
              }
            }
          },
          "503": {
            "description": "Error `signup_disabled`: the kill switch is on. Retry later, and do not invent another signup URL.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SignupError"
                }
              }
            }
          }
        }
      }
    },
    "/api/agent/account": {
      "get": {
        "operationId": "getAgentAccount",
        "summary": "Check whether a person has claimed this account",
        "description": "Report the claim state of an account created by createAgentAccount. Claiming does not rotate the key, so an agent keeps working across it; this is how it learns a human took ownership. Never contains an email or any secret.",
        "tags": [
          "Onboarding"
        ],
        "servers": [
          {
            "url": "https://app.zenrows.com",
            "description": "Dashboard host, which serves the agent onboarding surface"
          }
        ],
        "security": [
          {
            "apiKeyHeader": []
          }
        ],
        "responses": {
          "200": {
            "description": "The account and its claim state.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "accountId",
                    "claimed",
                    "isAgent"
                  ],
                  "properties": {
                    "accountId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "claimed": {
                      "type": "boolean",
                      "description": "Whether a person has taken ownership."
                    },
                    "isAgent": {
                      "type": "boolean",
                      "description": "Whether the account was created by the agent signup."
                    },
                    "trialEndsAt": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "End of the first monthly period (signup + 1 month). The Free plan renews monthly; this value is not refreshed."
                    }
                  }
                },
                "example": {
                  "accountId": "3f2b8c1e-5a4d-4e7f-9b6a-1c2d3e4f5a6b",
                  "claimed": false,
                  "isAgent": true,
                  "trialEndsAt": "2026-11-15T10:00:00+00:00"
                }
              }
            }
          },
          "401": {
            "description": "Error `invalid_key`: the key is missing or unknown.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SignupError"
                }
              }
            }
          }
        }
      }
    },
    "/subscriptions/self/details": {
      "get": {
        "operationId": "getSubscriptionDetails",
        "summary": "Read the plan behind the key",
        "description": "Return the calling key's plan: credit allowance, credits used, and the concurrency it is allowed. Use it to budget before a large run.",
        "tags": [
          "Account"
        ],
        "security": [
          {
            "apiKeyHeader": []
          },
          {
            "apiKeyQuery": []
          }
        ],
        "responses": {
          "200": {
            "description": "Plan, credit and concurrency figures for the calling key.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "description": "The subscription's state, for example `ACTIVE`."
                    },
                    "period_starts_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "period_ends_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "When the credit allowance renews."
                    },
                    "usage": {
                      "type": "number",
                      "description": "Spend this period, in US dollars."
                    },
                    "usage_credits": {
                      "type": "integer",
                      "description": "Credits used this period, across products."
                    },
                    "credit_limit": {
                      "type": "integer",
                      "description": "Credits the plan includes for the period."
                    },
                    "usage_percent": {
                      "type": "integer",
                      "description": "`usage_credits` as a whole percentage of `credit_limit`."
                    },
                    "plan": {
                      "type": "object",
                      "properties": {
                        "name": {
                          "type": "string"
                        },
                        "price": {
                          "type": "number"
                        },
                        "unit_cost": {
                          "type": "number",
                          "description": "Price of one credit, in US dollars."
                        },
                        "recurrence": {
                          "type": "string",
                          "examples": [
                            "MONTHLY",
                            "YEARLY"
                          ]
                        },
                        "products": {
                          "type": "object",
                          "description": "Usage per product. `api` also carries the plan's concurrency.",
                          "properties": {
                            "api": {
                              "type": "object",
                              "properties": {
                                "name": {
                                  "type": "string"
                                },
                                "usage": {
                                  "type": "number",
                                  "description": "Spend this period, in US dollars."
                                },
                                "usage_credits": {
                                  "type": "integer",
                                  "description": "Credits used this period."
                                },
                                "concurrency": {
                                  "type": "object",
                                  "properties": {
                                    "limit": {
                                      "type": "integer"
                                    },
                                    "usage": {
                                      "type": "integer"
                                    }
                                  }
                                }
                              }
                            },
                            "extract": {
                              "type": "object",
                              "properties": {
                                "name": {
                                  "type": "string"
                                },
                                "usage": {
                                  "type": "number",
                                  "description": "Spend this period, in US dollars."
                                },
                                "usage_credits": {
                                  "type": "integer",
                                  "description": "Credits used this period."
                                }
                              }
                            },
                            "scraping_browser": {
                              "type": "object",
                              "properties": {
                                "name": {
                                  "type": "string"
                                },
                                "usage": {
                                  "type": "number",
                                  "description": "Spend this period, in US dollars."
                                },
                                "usage_credits": {
                                  "type": "integer",
                                  "description": "Credits used this period."
                                }
                              }
                            },
                            "proxy_residential": {
                              "type": "object",
                              "properties": {
                                "name": {
                                  "type": "string"
                                },
                                "usage": {
                                  "type": "number",
                                  "description": "Spend this period, in US dollars."
                                },
                                "usage_credits": {
                                  "type": "integer",
                                  "description": "Credits used this period."
                                }
                              }
                            },
                            "data_apis": {
                              "type": "object",
                              "properties": {
                                "name": {
                                  "type": "string"
                                },
                                "usage": {
                                  "type": "number",
                                  "description": "Spend this period, in US dollars."
                                },
                                "usage_credits": {
                                  "type": "integer",
                                  "description": "Credits used this period."
                                }
                              }
                            }
                          }
                        }
                      }
                    },
                    "top_ups": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      },
                      "description": "Credit top-ups bought this period."
                    },
                    "api_key": {
                      "type": "object",
                      "properties": {
                        "caps": {
                          "type": "array",
                          "items": {
                            "type": "object"
                          },
                          "description": "Credit caps set on the calling key."
                        }
                      }
                    }
                  }
                },
                "example": {
                  "status": "ACTIVE",
                  "period_starts_at": "2026-10-01T00:00:00Z",
                  "period_ends_at": "2026-11-01T00:00:00Z",
                  "usage": 11.59,
                  "usage_credits": 42000,
                  "credit_limit": 250000,
                  "usage_percent": 16,
                  "plan": {
                    "name": "Launch",
                    "price": 69,
                    "unit_cost": 0.000276,
                    "recurrence": "MONTHLY",
                    "products": {
                      "api": {
                        "name": "WebScraping API",
                        "usage": 8.56,
                        "usage_credits": 31000,
                        "concurrency": {
                          "limit": 20,
                          "usage": 2
                        }
                      },
                      "extract": {
                        "name": "Extract",
                        "usage": 0,
                        "usage_credits": 0
                      },
                      "scraping_browser": {
                        "name": "Scraping Browser",
                        "usage": 3.03,
                        "usage_credits": 11000
                      },
                      "proxy_residential": {
                        "name": "Residential Proxies",
                        "usage": 0,
                        "usage_credits": 0
                      },
                      "data_apis": {
                        "name": "Data APIs",
                        "usage": 0,
                        "usage_credits": 0
                      }
                    }
                  },
                  "top_ups": [],
                  "api_key": {
                    "caps": []
                  }
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing or invalid.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "AUTH002",
                  "detail": "Invalid API key.",
                  "instance": "/v1",
                  "status": 401,
                  "title": "Invalid apikey provided (AUTH002)",
                  "type": "https://docs.zenrows.com/api-error-codes#AUTH002"
                }
              }
            }
          }
        }
      }
    },
    "/jobs": {
      "post": {
        "operationId": "createBatchJob",
        "summary": "Create a batch job (many URLs in one request)",
        "description": "Validates inputs and authenticates the caller, then persists\nthe job, its first run, and (for small submits) all tasks\nbefore returning. Validation failures (4xx) create nothing. A\n`503` from a temporary failure usually creates nothing\neither, but a late-stage failure can leave the job visible\nwith its run already `stopped`: safe to `DELETE`. A `503`\nthat came before the job was created gives the\n`Idempotency-Key` back, so the same key works on retry. A\n`503` after the job was created keeps the key (a retry under\nit is `409 idempotency_key_conflict`): check `GET /v1/jobs`\nbefore you resend with a fresh key.\n\nSubmissions under 10,000 inline `tasks` return `201 Created`\nwith the task rows already in place: results are immediately\nqueryable via `GET /v1/jobs/{id}/results`.\n\nSubmissions at or above **10,000 inline tasks** are processed\noff the request path and return `202 Accepted`: `accepted_tasks`\nis the final validated count, but task rows stream into\nstorage over the following seconds (large submissions can take\nlonger). `GET /v1/jobs/{id}/results` returns only the rows\nwritten so far until ingest completes; the run's `stats.total`\nis correct from the start so completion polling\n(`stats.completed >= stats.total`) is reliable from the first\nresponse. While ingestion is in progress,\n`POST /v1/jobs/{id}/tasks` on an open job returns `409`:\nretry it once results pages fill out. The run's\n`ingest_status` field reports the ingest state: poll\n`GET /v1/jobs/{id}` until `latest_run.ingest_status` is\n`done` to know every task row is visible.\n\nLimits (defaults; an account can have its own): at most\n100,000 tasks per submit, 3 active (non-terminal) regular\njobs and 5 scheduled jobs per account. An open job holds an\nactive slot until it is closed; an idle open job is closed\nautomatically after 48 hours. A `rate` schedule must fire at\nmost once every 60 minutes. Going over a job cap returns\n`429 quota_exceeded`; going over the task limit or the\nschedule interval returns `400 invalid_argument`. A request\nrefused by one of these checks gives its `Idempotency-Key`\nback: once you free a slot (or fix the request), retry with\nthe same key.\n",
        "tags": [
          "Batch"
        ],
        "servers": [
          {
            "url": "https://async.api.zenrows.com/v1",
            "description": "Batch (asynchronous jobs)"
          }
        ],
        "security": [
          {
            "apiKeyHeader": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/BatchIdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BatchSubmitJobRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Job created (sync path). For a regular job, run 1 has started and all its task rows are written. A scheduled job has no run yet (`latest_run` is omitted) until its first fire.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchSubmitJobResponse"
                }
              }
            }
          },
          "202": {
            "description": "Job accepted (large submission). The validated task list\nis durable and `accepted_tasks` is the final count, but\nindividual task rows stream into storage off the request\npath. `GET /v1/jobs/{id}/results` may return partial pages\nuntil ingest completes; the run's\n`stats.total` is correct from this response forward and\n`stats.completed` climbs from 0, so the run reaches\n`completed` only after every task drains: completion\npolling is safe. `latest_run.ingest_status` is `pending`\nin this response and flips to `done` on `GET /v1/jobs/{id}`\nonce every task row is visible. Body shape matches the\n201 response.\n",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchSubmitJobResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BatchInvalidArgument"
          },
          "401": {
            "$ref": "#/components/responses/BatchUnauthenticated"
          },
          "402": {
            "$ref": "#/components/responses/BatchPaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/BatchForbidden"
          },
          "409": {
            "$ref": "#/components/responses/BatchIdempotencyConflict"
          },
          "429": {
            "$ref": "#/components/responses/BatchQuotaExceeded"
          },
          "503": {
            "$ref": "#/components/responses/BatchUnavailable"
          }
        }
      },
      "get": {
        "operationId": "listBatchJobs",
        "summary": "List batch jobs",
        "description": "Newest first. Optional `status` / `type` filters.\n",
        "tags": [
          "Batch"
        ],
        "servers": [
          {
            "url": "https://async.api.zenrows.com/v1",
            "description": "Batch (asynchronous jobs)"
          }
        ],
        "security": [
          {
            "apiKeyHeader": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "status",
            "schema": {
              "$ref": "#/components/schemas/BatchJobStatus"
            }
          },
          {
            "in": "query",
            "name": "type",
            "schema": {
              "$ref": "#/components/schemas/BatchJobType"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "description": "Page size, 1 to 100 (larger values are treated as 100). Not a positive integer → 400.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "in": "query",
            "name": "cursor",
            "description": "Opaque; echo the previous response's `next_cursor` to continue.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of jobs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchListJobsResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BatchInvalidArgument"
          },
          "401": {
            "$ref": "#/components/responses/BatchUnauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/BatchForbidden"
          }
        }
      }
    },
    "/jobs/{job_id}": {
      "get": {
        "operationId": "getBatchJob",
        "summary": "Read a job and its latest run",
        "tags": [
          "Batch"
        ],
        "servers": [
          {
            "url": "https://async.api.zenrows.com/v1",
            "description": "Batch (asynchronous jobs)"
          }
        ],
        "security": [
          {
            "apiKeyHeader": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchJob"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/BatchUnauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/BatchForbidden"
          },
          "404": {
            "$ref": "#/components/responses/BatchNotFound"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/BatchJobID"
          }
        ]
      },
      "delete": {
        "operationId": "deleteBatchJob",
        "summary": "Delete a job and all its artifacts (asynchronous)",
        "description": "Flips the job to `deleted` immediately and **flips the latest\nrun to `deleted`** if it was in-flight (so no new tasks get\npicked up), then returns 202. The job's stored result bodies\nand all of its data are deleted asynchronously in the\nbackground. Idempotent.\n",
        "tags": [
          "Batch"
        ],
        "servers": [
          {
            "url": "https://async.api.zenrows.com/v1",
            "description": "Batch (asynchronous jobs)"
          }
        ],
        "security": [
          {
            "apiKeyHeader": []
          }
        ],
        "responses": {
          "202": {
            "description": "Accepted for deletion."
          },
          "401": {
            "$ref": "#/components/responses/BatchUnauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/BatchForbidden"
          },
          "404": {
            "$ref": "#/components/responses/BatchNotFound"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/BatchJobID"
          }
        ]
      }
    },
    "/jobs/{job_id}/tasks": {
      "post": {
        "operationId": "addBatchTasks",
        "summary": "Add URLs to an open job",
        "description": "Valid only when the job is `open` AND its latest run is\n`run_sequence == 1` AND `last_batch_received == false`.\nTargets the latest (initial) run. Closed jobs and jobs past\ntheir first run reject further adds with 409; a deleted job\nis a 404, like every other read of it.\n\nSetting `last_batch: true` closes the job (status → `closed`)\nand signals the run to flip to `completed` once tasks drain.\n\nHonors `Idempotency-Key`, scoped to the caller and the job: a\nrepeat of a call that answered `200` replays that answer, and a\nrepeat of a call that failed part-way (a `503` after its tasks\nwere stored, or a lost response) finishes it without adding,\nenqueuing or counting any task twice. Without the header every\ncall appends its tasks.\n",
        "tags": [
          "Batch"
        ],
        "servers": [
          {
            "url": "https://async.api.zenrows.com/v1",
            "description": "Batch (asynchronous jobs)"
          }
        ],
        "security": [
          {
            "apiKeyHeader": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/BatchJobID"
          },
          {
            "$ref": "#/components/parameters/BatchIdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BatchAddTasksRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tasks queued.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchAddTasksResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BatchInvalidArgument"
          },
          "401": {
            "$ref": "#/components/responses/BatchUnauthenticated"
          },
          "402": {
            "$ref": "#/components/responses/BatchPaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/BatchForbidden"
          },
          "404": {
            "$ref": "#/components/responses/BatchNotFound"
          },
          "409": {
            "description": "Job is not open, last batch already received, or the\ninitial run's task ingestion is still in progress (large\n`202`-accepted submissions): in the last case, retry\nonce ingest completes.\nAlso returned when an `Idempotency-Key` is reused with a\ndifferent body, or while an earlier call with the same key\nis still running (retry shortly).\n",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchProblem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/BatchQuotaExceeded"
          },
          "503": {
            "$ref": "#/components/responses/BatchUnavailable"
          }
        }
      }
    },
    "/jobs/{job_id}/close": {
      "post": {
        "operationId": "closeBatchJob",
        "summary": "Close an open job (no more tasks coming)",
        "description": "Flips `job.status` from `open` to `closed` and sets the\ninitial run's `last_batch_received = true`, the same as an\n`addBatchTasks` call with `last_batch: true` on its last batch. (An\n`addBatchTasks` call needs at least one task, so with nothing left\nto add, call `/close`.)\n\nDoes NOT stop the job: any pending or processing tasks\ncontinue. If all known tasks are already terminal, the run\nflips to `completed` immediately and the job's webhook fires.\nThat includes an open job that never got a task: closing it\ncompletes its run and frees its active-job slot.\n\nIdempotent: calling on an already-closed job returns the\ncurrent state with 200. A scheduled job has no open/closed\nflow: closing one returns `409 conflict`.\n",
        "tags": [
          "Batch"
        ],
        "servers": [
          {
            "url": "https://async.api.zenrows.com/v1",
            "description": "Batch (asynchronous jobs)"
          }
        ],
        "security": [
          {
            "apiKeyHeader": []
          }
        ],
        "responses": {
          "200": {
            "description": "Close accepted. Body is the refreshed Job object, without `latest_run` (read it with `GET /v1/jobs/{id}`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchJob"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/BatchUnauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/BatchForbidden"
          },
          "404": {
            "$ref": "#/components/responses/BatchNotFound"
          },
          "409": {
            "description": "The job is a scheduled job (`conflict`).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchProblem"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/BatchJobID"
          }
        ]
      }
    },
    "/jobs/{job_id}/stop": {
      "post": {
        "operationId": "stopBatchRun",
        "summary": "Stop the current run",
        "description": "Synchronously transitions `latest_run.status` from `running`\nor `pending` to terminal `stopped`. No new tasks are picked\nup; tasks already in flight may still finish and record their\nresult. Pending tasks are left as-is: not re-queued, not\nsynchronously failed.\n\nResult bodies are kept. To free the storage, follow up with\n`DELETE /v1/jobs/{id}/runs/{run_id}`.\n\nIdempotent on `stopped`. Returns `409 run_not_stoppable` on\n`completed` / `failed` / `deleted` (use `/rerun` to start\nfresh) and 404 on a deleted job. A stop sends no webhook.\n",
        "tags": [
          "Batch"
        ],
        "servers": [
          {
            "url": "https://async.api.zenrows.com/v1",
            "description": "Batch (asynchronous jobs)"
          }
        ],
        "security": [
          {
            "apiKeyHeader": []
          }
        ],
        "responses": {
          "200": {
            "description": "Run is stopped. Body is the refreshed Run object.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchRun"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/BatchUnauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/BatchForbidden"
          },
          "404": {
            "$ref": "#/components/responses/BatchNotFound"
          },
          "409": {
            "description": "Latest run is not in a stoppable state.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchProblem"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/BatchJobID"
          }
        ]
      }
    },
    "/jobs/{job_id}/cancel": {
      "post": {
        "operationId": "cancelBatchJob",
        "summary": "Cancel the current run",
        "description": "User-facing alias for `/stop`. Same semantics, same terminal\n`stopped` state, same response shape.\n",
        "tags": [
          "Batch"
        ],
        "servers": [
          {
            "url": "https://async.api.zenrows.com/v1",
            "description": "Batch (asynchronous jobs)"
          }
        ],
        "security": [
          {
            "apiKeyHeader": []
          }
        ],
        "responses": {
          "200": {
            "description": "Run is stopped. Body is the refreshed Run object.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchRun"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/BatchUnauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/BatchForbidden"
          },
          "404": {
            "$ref": "#/components/responses/BatchNotFound"
          },
          "409": {
            "description": "Latest run is not in a stoppable state.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchProblem"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/BatchJobID"
          }
        ]
      }
    },
    "/jobs/{job_id}/pause": {
      "post": {
        "operationId": "pauseBatchRun",
        "summary": "Pause the current run",
        "description": "Reversible suspend of the latest run. Sets\n`latest_run.pause_state = paused`; no new tasks start. The run's `status`\n(`running` / `pending`) is unchanged: pause is orthogonal.\nAlready-in-flight tasks may still settle.\n\nIdempotent on already-paused runs. Returns 409 when the\nlatest run is terminal (use `/rerun` to start fresh) and\n404 on a deleted job.\n\nBilling note: requests already in flight still finish and\nconsume credits. `/stop` (or `/cancel`) stops new work the same\nway; neither cancels requests already in flight.\n",
        "tags": [
          "Batch"
        ],
        "servers": [
          {
            "url": "https://async.api.zenrows.com/v1",
            "description": "Batch (asynchronous jobs)"
          }
        ],
        "security": [
          {
            "apiKeyHeader": []
          }
        ],
        "responses": {
          "200": {
            "description": "Run is paused. Body is the refreshed Run object.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchRun"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/BatchUnauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/BatchForbidden"
          },
          "404": {
            "$ref": "#/components/responses/BatchNotFound"
          },
          "409": {
            "description": "Latest run is terminal; pause does not apply.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchProblem"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/BatchJobID"
          }
        ]
      }
    },
    "/jobs/{job_id}/resume": {
      "post": {
        "operationId": "resumeBatchRun",
        "summary": "Resume a paused run",
        "description": "Inverse of `/pause`. Sets `latest_run.pause_state = active`;\nthe remaining tasks start again. Idempotent on already-active\nruns.\n",
        "tags": [
          "Batch"
        ],
        "servers": [
          {
            "url": "https://async.api.zenrows.com/v1",
            "description": "Batch (asynchronous jobs)"
          }
        ],
        "security": [
          {
            "apiKeyHeader": []
          }
        ],
        "responses": {
          "200": {
            "description": "Run is active. Body is the refreshed Run object.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchRun"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/BatchUnauthenticated"
          },
          "402": {
            "$ref": "#/components/responses/BatchPaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/BatchForbidden"
          },
          "404": {
            "$ref": "#/components/responses/BatchNotFound"
          },
          "409": {
            "description": "Latest run is terminal; resume does not apply.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchProblem"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/BatchJobID"
          }
        ]
      }
    },
    "/jobs/{job_id}/rerun": {
      "post": {
        "operationId": "rerunBatchJob",
        "summary": "Start a new run (full replay or partial retry of the previous run)",
        "description": "Two modes share this endpoint, picked by the optional\n`?status=` filter:\n\n**Full rerun (no `status`).** Replays every task from the\nprevious run into a fresh new run. Same `task_id` carries\nthrough together with `external_id`, `url`, `metadata`, and\nper-task `zenrows_params`. **No source-child link**: the\nnew run is fully independent. For scheduled jobs with no\nprior run, builds from the job's task template instead: the\nmanual equivalent of an automatic scheduled fire.\n\n**Partial retry (`?status=failed[,pending,...]`).** Copies\nevery prev-run task row, but rows matching the filter are\nreset to `pending` and re-enqueued; everything else is\ncopied verbatim with `source_run_id` (the run that owns the\nstored result) stamped. The new run's stats are pre-seeded\nfrom the inherited terminal rows. On chained retries,\n`source_run_id` chases back to the run that owns the result\nso intermediate retries can be deleted freely;\n`DELETE /v1/jobs/{id}/runs/{run_id}` only 409s when a\nnewer run still has tasks pointing at the targeted run.\n\nBoth modes stamp `trigger: manual` on the new run.\n\nAccepted `status` values: `pending`, `processing`,\n`failed`, `successful`. Unknown values → 400. `?status=`\nwith no value is the same as leaving it out (a full rerun); a\nlist with no values in it (`?status=,`) → 400.\n\nPreconditions:\n- The job is not `deleted`.\n- If a previous run exists, it must be terminal-non-deleted\n  (`completed` / `stopped` / `failed`), and its task ingestion\n  must have finished: a run interrupted mid-ingest is refused\n  with `409 run_not_terminal` (delete the job and resubmit).\n- `?status=` mode additionally requires a prior run, created\n  no more than 10 days ago; an older one is refused with\n  `409 partial_rerun_window_expired` (a full rerun has no\n  such limit).\n- Full mode on a scheduled job with no prior run requires\n  a non-empty task template.\n\nSide effects:\n- Auto-closes an `open` job (existing behavior).\n- Honors `Idempotency-Key`: a repeat call with the same key\n  and filter replays the original response (the same new\n  run), also while that run is still in flight. A rerun\n  refused before it started a run (`no_matching_tasks`,\n  `no_prior_run`, a `503`) gives the key back, so the same\n  key works once the cause is fixed.\n\nReruns are normally accepted asynchronously: the call returns\n`202` and the new run's task rows stream into storage off the\nrequest path. A deployment that processes reruns on the\nrequest path answers `201` instead. Treat both as success.\n",
        "tags": [
          "Batch"
        ],
        "servers": [
          {
            "url": "https://async.api.zenrows.com/v1",
            "description": "Batch (asynchronous jobs)"
          }
        ],
        "security": [
          {
            "apiKeyHeader": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/BatchJobID"
          },
          {
            "in": "query",
            "name": "status",
            "description": "Optional comma-separated list of task statuses to\nretry. When omitted: full replay (no source-child\nlink). When set: only matching statuses are reset to\n`pending`; everything else is inherited with\n`source_run_id`.\n",
            "schema": {
              "type": "string",
              "example": "failed,pending"
            }
          },
          {
            "$ref": "#/components/parameters/BatchIdempotencyKey"
          }
        ],
        "responses": {
          "201": {
            "description": "New run created synchronously; the API can answer this way instead of `202`. All task rows are written.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchRerunJobResponse"
                }
              }
            }
          },
          "202": {
            "description": "New run accepted. `retried_tasks` /\n`inherited_tasks` are the final counts, but individual\ntask rows stream into storage off the request path.\n`latest_run.ingest_status` is `pending` in this response\nand flips to `done` on `GET /v1/jobs/{id}` once every\ntask row is visible; the run's `stats.total` is correct\nfrom this response forward, so completion polling is\nsafe. Body shape matches the 201 response.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchRerunJobResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BatchInvalidArgument"
          },
          "401": {
            "$ref": "#/components/responses/BatchUnauthenticated"
          },
          "402": {
            "$ref": "#/components/responses/BatchPaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/BatchForbidden"
          },
          "404": {
            "$ref": "#/components/responses/BatchNotFound"
          },
          "409": {
            "description": "`run_not_terminal` (the latest run is in flight, or its\ningestion was interrupted), `no_prior_run`,\n`no_matching_tasks`, `partial_rerun_window_expired`, or\n`idempotency_key_conflict` (the key was used with another\nfilter, or that call is still in progress).\n",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchProblem"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/BatchUnavailable"
          }
        }
      }
    },
    "/jobs/{job_id}/schedule": {
      "put": {
        "operationId": "putBatchSchedule",
        "summary": "Replace the schedule of a scheduled job",
        "description": "Wholesale replaces the schedule on a `type: scheduled` job.\nThe body is the same `BatchJobSchedule` shape used at submit.\nOnly valid on scheduled jobs: regular jobs reject with 409.\n\nAn in-flight run keeps running; the new schedule only\ngoverns future fires. The response's `next_scheduled_run`\nreflects the new schedule.\n\nNaturally idempotent: re-applying the same body is a\nno-op success. No `Idempotency-Key` required.\n\nThe change is applied atomically from your perspective; after\na transient failure, re-PUT the same body to recover (the\noperation is idempotent).\n",
        "tags": [
          "Batch"
        ],
        "servers": [
          {
            "url": "https://async.api.zenrows.com/v1",
            "description": "Batch (asynchronous jobs)"
          }
        ],
        "security": [
          {
            "apiKeyHeader": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BatchJobSchedule"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Schedule updated; returns the updated Job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchJob"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BatchInvalidArgument"
          },
          "401": {
            "$ref": "#/components/responses/BatchUnauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/BatchForbidden"
          },
          "404": {
            "$ref": "#/components/responses/BatchNotFound"
          },
          "409": {
            "description": "Job isn't a scheduled job.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchProblem"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/BatchUnavailable"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/BatchJobID"
          }
        ]
      }
    },
    "/jobs/{job_id}/schedule/state": {
      "post": {
        "operationId": "setBatchScheduleState",
        "summary": "Pause or resume a scheduled job",
        "description": "Flips `schedule_state` between `active` and `paused`. Only\nvalid on `type: scheduled` jobs.\n\nWhile `paused`, scheduled fires are skipped; resuming makes\nthe next scheduled fire produce a run normally.\n\n`next_scheduled_run` is omitted while paused: no fire is\ncoming until you resume.\n",
        "tags": [
          "Batch"
        ],
        "servers": [
          {
            "url": "https://async.api.zenrows.com/v1",
            "description": "Batch (asynchronous jobs)"
          }
        ],
        "security": [
          {
            "apiKeyHeader": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BatchUpdateScheduleStateRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Schedule state updated; returns the updated Job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchJob"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BatchInvalidArgument"
          },
          "401": {
            "$ref": "#/components/responses/BatchUnauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/BatchForbidden"
          },
          "404": {
            "$ref": "#/components/responses/BatchNotFound"
          },
          "409": {
            "description": "Job isn't a scheduled job.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchProblem"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/BatchUnavailable"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/BatchJobID"
          }
        ]
      }
    },
    "/jobs/{job_id}/webhook": {
      "get": {
        "operationId": "getBatchWebhook",
        "summary": "Read the current webhook config",
        "description": "Returns the job's webhook config or `404` when none is set.\nThe same shape is also surfaced inline on `GET /v1/jobs/{id}`\nvia the `webhook` field.\n",
        "tags": [
          "Batch"
        ],
        "servers": [
          {
            "url": "https://async.api.zenrows.com/v1",
            "description": "Batch (asynchronous jobs)"
          }
        ],
        "security": [
          {
            "apiKeyHeader": []
          }
        ],
        "responses": {
          "200": {
            "description": "Webhook is configured.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchWebhookConfig"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/BatchUnauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/BatchForbidden"
          },
          "404": {
            "description": "Job not found, or no webhook configured.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchProblem"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/BatchJobID"
          }
        ]
      },
      "put": {
        "operationId": "putBatchWebhook",
        "summary": "Replace the webhook config",
        "description": "Replaces the webhook config wholesale. Both `url` and\n`signature` are required: no defaulting at the mutate\nboundary (preventing a `PUT {\"url\":\"...\"}` from silently\ndisabling signing on a job that previously had it on).\n\nValidation matches submit: URL must be HTTPS with a host\nthat resolves via DNS within 1 second. When `signature:\ntrue`, the account must already have an active HMAC key\n(`POST /v1/hmac/keys/rotate`), or the request fails with\n`400 webhook_signing_requires_active_key`.\n\nA `PUT` during a retry cycle applies from the next delivery\nattempt.\n",
        "tags": [
          "Batch"
        ],
        "servers": [
          {
            "url": "https://async.api.zenrows.com/v1",
            "description": "Batch (asynchronous jobs)"
          }
        ],
        "security": [
          {
            "apiKeyHeader": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BatchWebhookConfig"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "New webhook config persisted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchWebhookConfig"
                }
              }
            }
          },
          "400": {
            "description": "Validation failed (HTTPS / DNS / missing active HMAC key).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchProblem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/BatchUnauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/BatchForbidden"
          },
          "404": {
            "$ref": "#/components/responses/BatchNotFound"
          },
          "409": {
            "description": "Concurrent modification: retry the request.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchProblem"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/BatchJobID"
          }
        ]
      },
      "delete": {
        "operationId": "deleteBatchWebhook",
        "summary": "Clear the webhook config",
        "description": "Idempotent: returns `204` whether or not a webhook was\nconfigured. After this call no further `run.completed` /\n`run.failed` deliveries are sent for this Job: including a\ncurrently-in-flight one that hasn't been delivered yet\n(a delivery whose Job no longer has a webhook configured is\ndropped).\n",
        "tags": [
          "Batch"
        ],
        "servers": [
          {
            "url": "https://async.api.zenrows.com/v1",
            "description": "Batch (asynchronous jobs)"
          }
        ],
        "security": [
          {
            "apiKeyHeader": []
          }
        ],
        "responses": {
          "204": {
            "description": "Webhook cleared."
          },
          "401": {
            "$ref": "#/components/responses/BatchUnauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/BatchForbidden"
          },
          "404": {
            "$ref": "#/components/responses/BatchNotFound"
          },
          "409": {
            "description": "Concurrent modification: retry the request.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchProblem"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/BatchJobID"
          }
        ]
      }
    },
    "/jobs/{job_id}/runs": {
      "get": {
        "operationId": "listBatchRuns",
        "summary": "List runs of a job (newest first)",
        "tags": [
          "Batch"
        ],
        "servers": [
          {
            "url": "https://async.api.zenrows.com/v1",
            "description": "Batch (asynchronous jobs)"
          }
        ],
        "security": [
          {
            "apiKeyHeader": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/BatchJobID"
          },
          {
            "in": "query",
            "name": "limit",
            "description": "Page size, 1 to 100 (larger values are treated as 100). Not a positive integer → 400.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "in": "query",
            "name": "cursor",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchListJobRunsResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BatchInvalidArgument"
          },
          "401": {
            "$ref": "#/components/responses/BatchUnauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/BatchForbidden"
          },
          "404": {
            "$ref": "#/components/responses/BatchNotFound"
          }
        }
      }
    },
    "/jobs/{job_id}/runs/{run_id}": {
      "get": {
        "operationId": "getBatchRun",
        "summary": "Get one run by id",
        "tags": [
          "Batch"
        ],
        "servers": [
          {
            "url": "https://async.api.zenrows.com/v1",
            "description": "Batch (asynchronous jobs)"
          }
        ],
        "security": [
          {
            "apiKeyHeader": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchRun"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/BatchUnauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/BatchForbidden"
          },
          "404": {
            "$ref": "#/components/responses/BatchNotFound"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/BatchJobID"
          },
          {
            "$ref": "#/components/parameters/BatchRunID"
          }
        ]
      },
      "delete": {
        "operationId": "deleteBatchRun",
        "summary": "Delete one run and its artifacts (asynchronous)",
        "description": "Async hard-delete of a single run. Synchronously: flips the\nrun to `deleted` and (if this was the latest run) makes the\nnext-newest non-deleted run the job's `latest_run`, or leaves\nthe job with none. A later `/rerun` replays that run (`409\nno_prior_run` when none is left). The run's stored result bodies and its\nper-task data are then deleted asynchronously.\n\nDistinct from `/stop`: this deletes the result bodies; `/stop`\nkeeps them. To stop a run *and* free its storage, call\n`/stop` then `DELETE /runs/{run_id}` (order matters only\nfor semantics: both are idempotent).\n\nIdempotent: re-DELETE returns 202. A run that a later\npartial rerun still reads results from (through\n`source_run_id`) can't be deleted on its own: that returns\n`409 run_has_descendants`; delete the later runs first, or\nthe whole job.\n",
        "tags": [
          "Batch"
        ],
        "servers": [
          {
            "url": "https://async.api.zenrows.com/v1",
            "description": "Batch (asynchronous jobs)"
          }
        ],
        "security": [
          {
            "apiKeyHeader": []
          }
        ],
        "responses": {
          "202": {
            "description": "Accepted for deletion."
          },
          "401": {
            "$ref": "#/components/responses/BatchUnauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/BatchForbidden"
          },
          "404": {
            "$ref": "#/components/responses/BatchNotFound"
          },
          "409": {
            "description": "Later runs still depend on this run's results (`run_has_descendants`).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchProblem"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/BatchJobID"
          },
          {
            "$ref": "#/components/parameters/BatchRunID"
          }
        ]
      }
    },
    "/jobs/{job_id}/results": {
      "get": {
        "operationId": "getBatchResults",
        "summary": "Page through per-task results for the latest run of a job",
        "description": "Shortcut for the latest run; see `getBatchRunResults` for the explicit form.",
        "tags": [
          "Batch"
        ],
        "servers": [
          {
            "url": "https://async.api.zenrows.com/v1",
            "description": "Batch (asynchronous jobs)"
          }
        ],
        "security": [
          {
            "apiKeyHeader": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/BatchJobID"
          },
          {
            "$ref": "#/components/parameters/BatchResultStatus"
          },
          {
            "$ref": "#/components/parameters/BatchResultCursor"
          }
        ],
        "responses": {
          "200": {
            "description": "Page of results.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchListResultsResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BatchInvalidArgument"
          },
          "401": {
            "$ref": "#/components/responses/BatchUnauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/BatchForbidden"
          },
          "404": {
            "$ref": "#/components/responses/BatchNotFound"
          }
        }
      }
    },
    "/jobs/{job_id}/runs/{run_id}/results": {
      "get": {
        "operationId": "getBatchRunResults",
        "summary": "Page through per-task results for a specific run",
        "tags": [
          "Batch"
        ],
        "servers": [
          {
            "url": "https://async.api.zenrows.com/v1",
            "description": "Batch (asynchronous jobs)"
          }
        ],
        "security": [
          {
            "apiKeyHeader": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/BatchJobID"
          },
          {
            "$ref": "#/components/parameters/BatchRunID"
          },
          {
            "$ref": "#/components/parameters/BatchResultStatus"
          },
          {
            "$ref": "#/components/parameters/BatchResultCursor"
          }
        ],
        "responses": {
          "200": {
            "description": "Page of results.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchListResultsResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BatchInvalidArgument"
          },
          "401": {
            "$ref": "#/components/responses/BatchUnauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/BatchForbidden"
          },
          "404": {
            "$ref": "#/components/responses/BatchNotFound"
          }
        }
      }
    },
    "/jobs/{job_id}/tasks/{task_id}/content": {
      "get": {
        "operationId": "getBatchTaskContent",
        "summary": "Fetch the scraped content for a task in the latest run",
        "description": "Latest-run shortcut. See `getBatchRunTaskContent` for explicit-run form.",
        "tags": [
          "Batch"
        ],
        "servers": [
          {
            "url": "https://async.api.zenrows.com/v1",
            "description": "Batch (asynchronous jobs)"
          }
        ],
        "security": [
          {
            "apiKeyHeader": []
          }
        ],
        "responses": {
          "200": {
            "description": "Scraped content. `Content-Type` follows the task result's\n`type`: `text/html`, `application/json`, `text/markdown`,\n`text/plain` or `application/pdf`.\n",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              },
              "application/json": {
                "schema": {
                  "type": "object"
                }
              },
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              },
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/BatchUnauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/BatchForbidden"
          },
          "404": {
            "$ref": "#/components/responses/BatchNotFound"
          },
          "409": {
            "description": "Task is still pending or processing.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchProblem"
                }
              }
            }
          },
          "410": {
            "description": "The result body is past the job's result-visibility window\n(14 days by default): `result_expired`.\n",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchProblem"
                }
              }
            }
          },
          "422": {
            "description": "Task is failed. Body is the task's stored error as Problem\nJSON, so its `code` and `status` are the error's own, not\n422.\n",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchProblem"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/BatchJobID"
          },
          {
            "$ref": "#/components/parameters/BatchTaskID"
          }
        ]
      }
    },
    "/jobs/{job_id}/runs/{run_id}/tasks/{task_id}/content": {
      "get": {
        "operationId": "getBatchRunTaskContent",
        "summary": "Fetch the scraped content for a task in a specific run",
        "tags": [
          "Batch"
        ],
        "servers": [
          {
            "url": "https://async.api.zenrows.com/v1",
            "description": "Batch (asynchronous jobs)"
          }
        ],
        "security": [
          {
            "apiKeyHeader": []
          }
        ],
        "responses": {
          "200": {
            "description": "Scraped content. `Content-Type` follows the task result's\n`type`: `text/html`, `application/json`, `text/markdown`,\n`text/plain` or `application/pdf`.\n",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              },
              "application/json": {
                "schema": {
                  "type": "object"
                }
              },
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              },
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/BatchUnauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/BatchForbidden"
          },
          "404": {
            "$ref": "#/components/responses/BatchNotFound"
          },
          "409": {
            "description": "Task is still pending or processing.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchProblem"
                }
              }
            }
          },
          "410": {
            "description": "The result body is past the job's result-visibility window\n(14 days by default): `result_expired`.\n",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchProblem"
                }
              }
            }
          },
          "422": {
            "description": "Task is failed. Body is the task's stored error as Problem\nJSON, so its `code` and `status` are the error's own, not\n422.\n",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchProblem"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/BatchJobID"
          },
          {
            "$ref": "#/components/parameters/BatchRunID"
          },
          {
            "$ref": "#/components/parameters/BatchTaskID"
          }
        ]
      }
    },
    "/jobs/{job_id}/tasks/{task_id}/history": {
      "get": {
        "operationId": "getBatchTaskHistory",
        "summary": "Get the attempt history for a task in the latest run",
        "tags": [
          "Batch"
        ],
        "servers": [
          {
            "url": "https://async.api.zenrows.com/v1",
            "description": "Batch (asynchronous jobs)"
          }
        ],
        "security": [
          {
            "apiKeyHeader": []
          }
        ],
        "responses": {
          "200": {
            "description": "List of attempt events.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchTaskHistoryResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/BatchUnauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/BatchForbidden"
          },
          "404": {
            "$ref": "#/components/responses/BatchNotFound"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/BatchJobID"
          },
          {
            "$ref": "#/components/parameters/BatchTaskID"
          }
        ]
      }
    },
    "/jobs/{job_id}/runs/{run_id}/tasks/{task_id}/history": {
      "get": {
        "operationId": "getBatchRunTaskHistory",
        "summary": "Get the attempt history for a task in a specific run",
        "tags": [
          "Batch"
        ],
        "servers": [
          {
            "url": "https://async.api.zenrows.com/v1",
            "description": "Batch (asynchronous jobs)"
          }
        ],
        "security": [
          {
            "apiKeyHeader": []
          }
        ],
        "responses": {
          "200": {
            "description": "List of attempt events.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchTaskHistoryResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/BatchUnauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/BatchForbidden"
          },
          "404": {
            "$ref": "#/components/responses/BatchNotFound"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/BatchJobID"
          },
          {
            "$ref": "#/components/parameters/BatchRunID"
          },
          {
            "$ref": "#/components/parameters/BatchTaskID"
          }
        ]
      }
    },
    "/hmac/keys": {
      "get": {
        "operationId": "listHmacKeys",
        "summary": "List the account's HMAC key metadata",
        "description": "Returns kid + created_at for whichever slots are populated\n(active and/or candidate). NEVER returns secret material:\nthe only chance to capture a secret is the rotate response.\n",
        "tags": [
          "Batch"
        ],
        "servers": [
          {
            "url": "https://async.api.zenrows.com/v1",
            "description": "Batch (asynchronous jobs)"
          }
        ],
        "security": [
          {
            "apiKeyHeader": []
          }
        ],
        "responses": {
          "200": {
            "description": "Key metadata.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchHMACKeyList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/BatchUnauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/BatchForbidden"
          }
        }
      }
    },
    "/hmac/keys/rotate": {
      "post": {
        "operationId": "startHmacKeyRotation",
        "summary": "Generate the initial key, or stage a candidate",
        "description": "Unified rotation endpoint:\n- No active key yet → generates the account's first active key.\n- Active exists, no candidate → generates a candidate for\n  phased rotation.\n- Both slots populated → 409 `candidate_pending`. Finalize\n  or cancel first.\n\nThe response body's `secret` is the **only** time the raw\nkey value leaves the service. Capture it now or call rotate\nagain to issue a new one.\n",
        "tags": [
          "Batch"
        ],
        "servers": [
          {
            "url": "https://async.api.zenrows.com/v1",
            "description": "Batch (asynchronous jobs)"
          }
        ],
        "security": [
          {
            "apiKeyHeader": []
          }
        ],
        "responses": {
          "201": {
            "description": "Key created. `secret` returned once.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchHMACKeyCreated"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/BatchUnauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/BatchForbidden"
          },
          "409": {
            "description": "Candidate already pending.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchProblem"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "cancelHmacKeyRotation",
        "summary": "Discard the pending candidate",
        "description": "Drops the candidate slot. Idempotent: returns 204 even when\nno candidate is present.\n",
        "tags": [
          "Batch"
        ],
        "servers": [
          {
            "url": "https://async.api.zenrows.com/v1",
            "description": "Batch (asynchronous jobs)"
          }
        ],
        "security": [
          {
            "apiKeyHeader": []
          }
        ],
        "responses": {
          "204": {
            "description": "Candidate discarded (or none was pending)."
          },
          "401": {
            "$ref": "#/components/responses/BatchUnauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/BatchForbidden"
          }
        }
      }
    },
    "/hmac/keys/rotate/finalize": {
      "post": {
        "operationId": "finalizeHmacKeyRotation",
        "summary": "Promote the candidate to active",
        "description": "Promotes the pending candidate to active; the prior active\nis discarded. Returns the new active's metadata; no secret.\nReturns 409 `no_candidate` if nothing is pending.\n",
        "tags": [
          "Batch"
        ],
        "servers": [
          {
            "url": "https://async.api.zenrows.com/v1",
            "description": "Batch (asynchronous jobs)"
          }
        ],
        "security": [
          {
            "apiKeyHeader": []
          }
        ],
        "responses": {
          "200": {
            "description": "Candidate promoted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchHMACKeyFinalized"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/BatchUnauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/BatchForbidden"
          },
          "409": {
            "description": "No candidate to finalize, or concurrent modification.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchProblem"
                }
              }
            }
          }
        }
      }
    },
    "/webhook/test": {
      "post": {
        "operationId": "testWebhook",
        "summary": "Send a synthetic test event to a webhook URL",
        "description": "Synchronous one-shot dispatch of a `webhook.test` event\nagainst a caller-supplied URL. Independent of any job: body\ncarries the same `{ url, signature }` shape as the submit-time\nwebhook field. Used by integrators to verify a receiver is\nreachable, terminates TLS, and validates the signature before\nattaching the URL to a real job.\n\nValidation is identical to submit (HTTPS, 1-second DNS lookup,\nactive HMAC key required when `signature: true`). On validation\nfailure, returns the same Problem JSON codes\n(`callback_url_must_be_https`, `callback_url_unresolvable`,\n`webhook_signing_requires_active_key`).\n\nOn validation pass, always returns `200 OK`; the receiver\noutcome rides in the body so callers can distinguish\n\"the API rejected my config\" (4xx) from \"my receiver\nreturned 500\" (200 + `delivered: false`).\n",
        "tags": [
          "Batch"
        ],
        "servers": [
          {
            "url": "https://async.api.zenrows.com/v1",
            "description": "Batch (asynchronous jobs)"
          }
        ],
        "security": [
          {
            "apiKeyHeader": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BatchTestWebhookRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Dispatch attempted. Inspect `delivered` to see whether the\nreceiver returned 2xx.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchTestWebhookResponse"
                }
              }
            }
          },
          "400": {
            "description": "Validation failed: HTTPS / DNS / missing active HMAC key.\nThe receiver was not contacted.\n",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchProblem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/BatchUnauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/BatchForbidden"
          },
          "503": {
            "$ref": "#/components/responses/BatchUnavailable"
          }
        }
      }
    },
    "/job_inputs": {
      "post": {
        "operationId": "uploadBatchInput",
        "summary": "Create a CSV upload slot",
        "description": "Allocates a `file_input_id`, persists the parse spec, and\nreturns a presigned PUT URL the caller uploads the CSV body\nto. The slot lives 24 h; subsequent `POST /v1/jobs` references\nit by `file_input_id`.\n\nThe CSV is parsed when the job is submitted, where a\n50 MB / 100 000-row cap is enforced.\n",
        "tags": [
          "Batch"
        ],
        "servers": [
          {
            "url": "https://async.api.zenrows.com/v1",
            "description": "Batch (asynchronous jobs)"
          }
        ],
        "security": [
          {
            "apiKeyHeader": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BatchCreateJobInputRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Slot created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchCreateJobInputResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BatchInvalidArgument"
          },
          "401": {
            "$ref": "#/components/responses/BatchUnauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/BatchForbidden"
          },
          "503": {
            "$ref": "#/components/responses/BatchUnavailable"
          }
        }
      }
    },
    "/jobs/{job_id}/runs/{run_id}/exports": {
      "post": {
        "operationId": "createBatchExport",
        "summary": "Start an async results export",
        "description": "Kicks off an async zip of every task body in the run for\nshort-term download. Returns an `export_id` immediately; poll\n`getBatchExport` for status and the presigned download URL.\n\nEach export expires 12 h after creation (`expires_at`). After\nthat a poll answers `410 result_expired`, and once the export\nis cleaned up the id is a `404`. There is no delete / update.\n\nThe export fails with\n`error: \"results are larger than N gb\"` if the combined size\nof the results exceeds the export byte cap (100 GiB in\nproduction).\n\nThe run does not have to be finished: an export of a run that\nis still in flight zips the results stored so far.\n",
        "tags": [
          "Batch"
        ],
        "servers": [
          {
            "url": "https://async.api.zenrows.com/v1",
            "description": "Batch (asynchronous jobs)"
          }
        ],
        "security": [
          {
            "apiKeyHeader": []
          }
        ],
        "responses": {
          "202": {
            "description": "Export accepted; the zip is produced asynchronously.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchStartExportResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/BatchUnauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/BatchForbidden"
          },
          "404": {
            "$ref": "#/components/responses/BatchNotFound"
          },
          "503": {
            "$ref": "#/components/responses/BatchUnavailable"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/BatchJobID"
          },
          {
            "$ref": "#/components/parameters/BatchRunID"
          }
        ]
      }
    },
    "/jobs/{job_id}/runs/{run_id}/exports/{export_id}": {
      "get": {
        "operationId": "getBatchExport",
        "summary": "Poll a results export",
        "description": "Returns the current status and (on `completed`) a freshly\npresigned `download_url`, valid for 2 hours. The URL is signed\non every call, so callers may stash the metadata but should\nre-fetch right before downloading.\n\nAn export is valid for 12 hours after `created_at` (its\n`expires_at`). Past that the poll answers\n`410 result_expired` with no download link; start a new\nexport. Once the expired export is cleaned up, the id is a\n`404`.\n",
        "tags": [
          "Batch"
        ],
        "servers": [
          {
            "url": "https://async.api.zenrows.com/v1",
            "description": "Batch (asynchronous jobs)"
          }
        ],
        "security": [
          {
            "apiKeyHeader": []
          }
        ],
        "responses": {
          "200": {
            "description": "Export state (with `download_url` once complete).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchExport"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/BatchUnauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/BatchForbidden"
          },
          "404": {
            "$ref": "#/components/responses/BatchNotFound"
          },
          "410": {
            "description": "`result_expired`: the export is past its `expires_at`\n(12 hours after it was started). Start a new export.\n",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchProblem"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/BatchJobID"
          },
          {
            "$ref": "#/components/parameters/BatchRunID"
          },
          {
            "$ref": "#/components/parameters/BatchExportID"
          }
        ]
      }
    },
    "/crawls": {
      "post": {
        "operationId": "startCrawl",
        "summary": "Start a crawl",
        "description": "Start a crawl from one URL and return at once with `202 Accepted`, the crawl in the body and its path in the `Location` header. Each page it opens is one Fetch request with Adaptive Stealth Mode, billed 1 to 25 credits; failed pages are not billed. Documented at https://docs.zenrows.com/crawl/endpoints#start-a-crawl.",
        "tags": [
          "Crawl"
        ],
        "security": [
          {
            "apiKeyHeader": []
          },
          {
            "apiKeyQuery": []
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 255
            },
            "description": "Makes a retry safe: a retry with the same key and body returns the same crawl and starts no second one. The same key with a different body is `422 idempotency_key_reused`.",
            "example": "catalog-2026-10-09"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "additionalProperties": false,
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "maxLength": 2048,
                    "description": "The start URL: a public `http` or `https` URL. The crawl starts on this exact URL and stays on its domain, subdomains included."
                  },
                  "depth": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 100000,
                    "description": "How many link steps to follow from the start URL. At `1`, the links on the start page. Omit it to follow links until `max_items` or `max_pages` stops the crawl."
                  },
                  "max_items": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 100000,
                    "default": 100,
                    "description": "Stop after keeping this many URLs."
                  },
                  "max_pages": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 100000,
                    "default": 100,
                    "description": "Stop after fetching this many pages. Caps what the crawl costs. If both limits are reached together, `stop_reason` is `max_pages`."
                  },
                  "include_patterns": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Keep, and open, only URLs that contain at least one of these texts, such as `/product/`. The start URL is never returned."
                  },
                  "exclude_patterns": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Drop URLs that contain any of these texts, even when they match an include pattern."
                  },
                  "output_format": {
                    "type": "string",
                    "enum": [
                      "html"
                    ],
                    "description": "`html` also returns the HTML of each kept page. Omit it to get URLs only."
                  }
                }
              },
              "example": {
                "url": "https://www.scrapingcourse.com/ecommerce/",
                "max_pages": 10,
                "include_patterns": [
                  "/product/"
                ]
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "The crawl started.",
            "headers": {
              "Location": {
                "description": "The crawl's path.",
                "schema": {
                  "type": "string",
                  "example": "/v1/crawls/c_01M3XP7RZD4JT88F7PNPGBFRAF"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Crawl"
                },
                "example": {
                  "crawl_id": "c_01M3XP7RZD4JT88F7PNPGBFRAF",
                  "status": "running",
                  "url": "https://www.scrapingcourse.com/ecommerce/",
                  "max_items": 100,
                  "max_pages": 10,
                  "include_patterns": [
                    "/product/"
                  ],
                  "exclude_patterns": [],
                  "discovery": [
                    "links"
                  ],
                  "coverage": {
                    "pages_fetched": 0,
                    "pages_failed": 0,
                    "items_found": 0
                  },
                  "duplicates_removed": 0,
                  "created_at": "2026-10-09T09:00:12Z",
                  "results": [],
                  "next_cursor": "cur_Y18wMU0zWFA3UlpENEpUODhGN1BOUEdCRlJBRjow"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request` (the body is not valid JSON, or the `Idempotency-Key` is over 255 characters), `unknown_parameter` (`detail` names it) or `invalid_parameter`. Documented at https://docs.zenrows.com/crawl/endpoints#errors.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "unknown_parameter",
                  "title": "Unknown parameter",
                  "detail": "The request has a parameter this endpoint does not accept: max_depth.",
                  "status": 400,
                  "instance": "urn:zenrows:request:9ac5b40be6127c8809b91babda784059"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing or invalid.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "AUTH002",
                  "detail": "Invalid API key.",
                  "instance": "/v1",
                  "status": 401,
                  "title": "Invalid apikey provided (AUTH002)",
                  "type": "https://docs.zenrows.com/api-error-codes#AUTH002"
                }
              }
            }
          },
          "402": {
            "description": "The account is out of credits, or the plan does not include this capability.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "AUTH004",
                  "detail": "This account has used all its credits for the period. Upgrade or add credits to continue.",
                  "instance": "/v1",
                  "status": 402,
                  "title": "Usage exceeded (AUTH004)",
                  "type": "https://docs.zenrows.com/api-error-codes#AUTH004"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_request_in_flight`: a request with this `Idempotency-Key` is still in progress. Retry when it ends.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "idempotency_request_in_flight",
                  "title": "Request in flight",
                  "detail": "A request with this Idempotency-Key is still in progress. Retry when it ends.",
                  "status": 409,
                  "instance": "urn:zenrows:request:9ac5b40be6127c8809b91babda784059"
                }
              }
            }
          },
          "422": {
            "description": "`invalid_start_url` (the host is an IP address or a private address, or the URL is over 2,048 characters), `invalid_parameter` or `idempotency_key_reused`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "invalid_start_url",
                  "title": "Invalid start URL",
                  "detail": "The start URL must be a public http or https URL.",
                  "status": 422,
                  "instance": "urn:zenrows:request:9ac5b40be6127c8809b91babda784059"
                }
              }
            }
          },
          "429": {
            "description": "`too_many_crawls`: the account already runs its active jobs (3 by default, shared with Batch). Wait for one to end; the response carries `Retry-After: 30`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "too_many_crawls",
                  "title": "Too many active crawls",
                  "detail": "Your account already runs 3 active jobs. Wait for one to end, then start the crawl again.",
                  "status": 429,
                  "instance": "urn:zenrows:request:9ac5b40be6127c8809b91babda784059"
                }
              }
            }
          },
          "500": {
            "description": "`internal_error`: something failed on our side. Retry later.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "internal_error",
                  "title": "Internal error",
                  "detail": "Something failed on our side. Retry later.",
                  "status": 500,
                  "instance": "urn:zenrows:request:9ac5b40be6127c8809b91babda784059"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "listCrawls",
        "summary": "List your crawls",
        "description": "Your crawls, newest first, without their results. Documented at https://docs.zenrows.com/crawl/endpoints#list-your-crawls.",
        "tags": [
          "Crawl"
        ],
        "security": [
          {
            "apiKeyHeader": []
          },
          {
            "apiKeyQuery": []
          }
        ],
        "parameters": [
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "The `next_cursor` from the previous page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "How many crawls to return.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of crawls. `next_cursor` is absent on the last page.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "crawls": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CrawlSummary"
                      }
                    },
                    "next_cursor": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "crawls": [
                    {
                      "crawl_id": "c_01M3XP7RZD4JT88F7PNPGBFRAF",
                      "status": "completed",
                      "stop_reason": "max_pages",
                      "url": "https://www.scrapingcourse.com/ecommerce/",
                      "max_items": 100,
                      "max_pages": 10,
                      "include_patterns": [
                        "/product/"
                      ],
                      "exclude_patterns": [],
                      "discovery": [
                        "links"
                      ],
                      "coverage": {
                        "pages_fetched": 10,
                        "pages_failed": 0,
                        "items_found": 88
                      },
                      "duplicates_removed": 412,
                      "created_at": "2026-10-09T09:00:12Z",
                      "finished_at": "2026-10-09T09:00:41Z"
                    }
                  ],
                  "next_cursor": "eyJjcmF3bF9pZCI6ImNfMDFNM1hQN1JaRDRKVDg4RjdQTlBHQkZSQUYifQ"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request`: `limit` or `cursor` is not valid. Documented at https://docs.zenrows.com/crawl/endpoints#errors.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "invalid_request",
                  "title": "Invalid request",
                  "detail": "limit must be between 1 and 100.",
                  "status": 400,
                  "instance": "urn:zenrows:request:9ac5b40be6127c8809b91babda784059"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing or invalid.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "AUTH002",
                  "detail": "Invalid API key.",
                  "instance": "/v1",
                  "status": 401,
                  "title": "Invalid apikey provided (AUTH002)",
                  "type": "https://docs.zenrows.com/api-error-codes#AUTH002"
                }
              }
            }
          },
          "402": {
            "description": "The account is out of credits, or the plan does not include this capability.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "AUTH004",
                  "detail": "This account has used all its credits for the period. Upgrade or add credits to continue.",
                  "instance": "/v1",
                  "status": 402,
                  "title": "Usage exceeded (AUTH004)",
                  "type": "https://docs.zenrows.com/api-error-codes#AUTH004"
                }
              }
            }
          },
          "429": {
            "description": "`too_many_crawls`: the account already runs its active jobs (3 by default, shared with Batch). Wait for one to end; the response carries `Retry-After: 30`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "too_many_crawls",
                  "title": "Too many active crawls",
                  "detail": "Your account already runs 3 active jobs. Wait for one to end, then start the crawl again.",
                  "status": 429,
                  "instance": "urn:zenrows:request:9ac5b40be6127c8809b91babda784059"
                }
              }
            }
          },
          "500": {
            "description": "`internal_error`: something failed on our side. Retry later.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "internal_error",
                  "title": "Internal error",
                  "detail": "Something failed on our side. Retry later.",
                  "status": 500,
                  "instance": "urn:zenrows:request:9ac5b40be6127c8809b91babda784059"
                }
              }
            }
          }
        }
      }
    },
    "/crawls/{crawl_id}": {
      "get": {
        "operationId": "getCrawl",
        "summary": "Read a crawl and its results",
        "description": "The crawl, its coverage so far and one page of results. While it runs, `next_cursor` is never `null`: read again with the same cursor to get only the new URLs. Each URL is returned once. Documented at https://docs.zenrows.com/crawl/endpoints#read-a-crawl.",
        "tags": [
          "Crawl"
        ],
        "security": [
          {
            "apiKeyHeader": []
          },
          {
            "apiKeyQuery": []
          }
        ],
        "parameters": [
          {
            "name": "crawl_id",
            "in": "path",
            "required": true,
            "description": "The crawl's ID, as returned when it started.",
            "schema": {
              "type": "string"
            },
            "example": "c_01M3XP7RZD4JT88F7PNPGBFRAF"
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "The `next_cursor` from the previous read. Omit it to start from the first result.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "How many results to return.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10000,
              "default": 1000
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The crawl and a page of its results.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Crawl"
                },
                "example": {
                  "crawl_id": "c_01M3XP7RZD4JT88F7PNPGBFRAF",
                  "status": "completed",
                  "stop_reason": "max_pages",
                  "url": "https://www.scrapingcourse.com/ecommerce/",
                  "max_items": 100,
                  "max_pages": 10,
                  "include_patterns": [
                    "/product/"
                  ],
                  "exclude_patterns": [],
                  "discovery": [
                    "links"
                  ],
                  "coverage": {
                    "pages_fetched": 10,
                    "pages_failed": 0,
                    "items_found": 88
                  },
                  "duplicates_removed": 412,
                  "created_at": "2026-10-09T09:00:12Z",
                  "finished_at": "2026-10-09T09:00:41Z",
                  "results": [
                    {
                      "url": "https://www.scrapingcourse.com/ecommerce/product/abominable-hoodie/"
                    },
                    {
                      "url": "https://www.scrapingcourse.com/ecommerce/product/adrienne-trek-jacket/"
                    }
                  ],
                  "next_cursor": null
                }
              }
            }
          },
          "400": {
            "description": "`invalid_cursor` (the cursor belongs to another crawl) or `invalid_request` (`limit` is not valid). Documented at https://docs.zenrows.com/crawl/endpoints#errors.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "invalid_cursor",
                  "title": "Invalid cursor",
                  "detail": "The cursor does not belong to this crawl.",
                  "status": 400,
                  "instance": "urn:zenrows:request:9ac5b40be6127c8809b91babda784059"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing or invalid.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "AUTH002",
                  "detail": "Invalid API key.",
                  "instance": "/v1",
                  "status": 401,
                  "title": "Invalid apikey provided (AUTH002)",
                  "type": "https://docs.zenrows.com/api-error-codes#AUTH002"
                }
              }
            }
          },
          "402": {
            "description": "The account is out of credits, or the plan does not include this capability.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "AUTH004",
                  "detail": "This account has used all its credits for the period. Upgrade or add credits to continue.",
                  "instance": "/v1",
                  "status": 402,
                  "title": "Usage exceeded (AUTH004)",
                  "type": "https://docs.zenrows.com/api-error-codes#AUTH004"
                }
              }
            }
          },
          "404": {
            "description": "`crawl_not_found`: no crawl with this ID exists on your account.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "crawl_not_found",
                  "title": "Crawl not found",
                  "detail": "No crawl with this identifier exists for this account.",
                  "status": 404,
                  "instance": "urn:zenrows:request:9ac5b40be6127c8809b91babda784059"
                }
              }
            }
          },
          "429": {
            "description": "`too_many_crawls`: the account already runs its active jobs (3 by default, shared with Batch). Wait for one to end; the response carries `Retry-After: 30`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "too_many_crawls",
                  "title": "Too many active crawls",
                  "detail": "Your account already runs 3 active jobs. Wait for one to end, then start the crawl again.",
                  "status": 429,
                  "instance": "urn:zenrows:request:9ac5b40be6127c8809b91babda784059"
                }
              }
            }
          },
          "500": {
            "description": "`internal_error`: something failed on our side. Retry later.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "internal_error",
                  "title": "Internal error",
                  "detail": "Something failed on our side. Retry later.",
                  "status": 500,
                  "instance": "urn:zenrows:request:9ac5b40be6127c8809b91babda784059"
                }
              }
            }
          }
        }
      }
    },
    "/crawls/{crawl_id}/download": {
      "get": {
        "operationId": "downloadCrawl",
        "summary": "Download all results",
        "description": "Every result as one NDJSON file named `{crawl_id}.jsonl`, one JSON object per line. Each line has the `url`; with `output_format`, also `content_status` and, once fetched, the page HTML in `content`. While the crawl runs, a later download may hold more lines. Documented at https://docs.zenrows.com/crawl/endpoints#download-the-results.",
        "tags": [
          "Crawl"
        ],
        "security": [
          {
            "apiKeyHeader": []
          },
          {
            "apiKeyQuery": []
          }
        ],
        "parameters": [
          {
            "name": "crawl_id",
            "in": "path",
            "required": true,
            "description": "The crawl's ID, as returned when it started.",
            "schema": {
              "type": "string"
            },
            "example": "c_01M3XP7RZD4JT88F7PNPGBFRAF"
          }
        ],
        "responses": {
          "200": {
            "description": "The results as NDJSON.",
            "headers": {
              "X-Crawl-Status": {
                "description": "The crawl's status when the file was produced. After a stop it reads `stopped`, yet a later download may hold more lines for up to 10 minutes.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "running",
                    "completed",
                    "stopped",
                    "failed"
                  ]
                }
              }
            },
            "content": {
              "application/x-ndjson": {
                "schema": {
                  "type": "string"
                },
                "example": "{\"url\":\"https://www.scrapingcourse.com/ecommerce/product/abominable-hoodie/\"}\n{\"url\":\"https://www.scrapingcourse.com/ecommerce/product/adrienne-trek-jacket/\"}\n"
              }
            }
          },
          "400": {
            "description": "`unknown_parameter`: the request has a query parameter this endpoint does not accept; `detail` names it. Documented at https://docs.zenrows.com/crawl/endpoints#errors.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "unknown_parameter",
                  "title": "Unknown parameter",
                  "detail": "The request has a parameter this endpoint does not accept: limit.",
                  "status": 400,
                  "instance": "urn:zenrows:request:9ac5b40be6127c8809b91babda784059"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing or invalid.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "AUTH002",
                  "detail": "Invalid API key.",
                  "instance": "/v1",
                  "status": 401,
                  "title": "Invalid apikey provided (AUTH002)",
                  "type": "https://docs.zenrows.com/api-error-codes#AUTH002"
                }
              }
            }
          },
          "402": {
            "description": "The account is out of credits, or the plan does not include this capability.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "AUTH004",
                  "detail": "This account has used all its credits for the period. Upgrade or add credits to continue.",
                  "instance": "/v1",
                  "status": 402,
                  "title": "Usage exceeded (AUTH004)",
                  "type": "https://docs.zenrows.com/api-error-codes#AUTH004"
                }
              }
            }
          },
          "404": {
            "description": "`crawl_not_found`: no crawl with this ID exists on your account.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "crawl_not_found",
                  "title": "Crawl not found",
                  "detail": "No crawl with this identifier exists for this account.",
                  "status": 404,
                  "instance": "urn:zenrows:request:9ac5b40be6127c8809b91babda784059"
                }
              }
            }
          },
          "429": {
            "description": "`too_many_crawls`: the account already runs its active jobs (3 by default, shared with Batch). Wait for one to end; the response carries `Retry-After: 30`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "too_many_crawls",
                  "title": "Too many active crawls",
                  "detail": "Your account already runs 3 active jobs. Wait for one to end, then start the crawl again.",
                  "status": 429,
                  "instance": "urn:zenrows:request:9ac5b40be6127c8809b91babda784059"
                }
              }
            }
          },
          "500": {
            "description": "`internal_error`: something failed on our side. Retry later.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "internal_error",
                  "title": "Internal error",
                  "detail": "Something failed on our side. Retry later.",
                  "status": 500,
                  "instance": "urn:zenrows:request:9ac5b40be6127c8809b91babda784059"
                }
              }
            }
          }
        }
      }
    },
    "/crawls/{crawl_id}/contents/{content_id}": {
      "get": {
        "operationId": "getCrawlContent",
        "summary": "Read one page's HTML",
        "description": "The HTML of one kept page, for a crawl started with `output_format`. Take the path from the result's `content_url`. Documented at https://docs.zenrows.com/crawl/endpoints#read-a-page.",
        "tags": [
          "Crawl"
        ],
        "security": [
          {
            "apiKeyHeader": []
          },
          {
            "apiKeyQuery": []
          }
        ],
        "parameters": [
          {
            "name": "crawl_id",
            "in": "path",
            "required": true,
            "description": "The crawl's ID, as returned when it started.",
            "schema": {
              "type": "string"
            },
            "example": "c_01M3XP7RZD4JT88F7PNPGBFRAF"
          },
          {
            "name": "content_id",
            "in": "path",
            "required": true,
            "description": "From the result's `content_url`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "download",
            "in": "query",
            "required": false,
            "description": "`true` returns the page as a file to save.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The page's HTML.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                },
                "example": "<!doctype html>\n<html lang=\"en-US\">\n<head><title>Abominable Hoodie</title></head>\n<body>...</body>\n</html>"
              }
            }
          },
          "400": {
            "description": "`unknown_parameter`: the request has a query parameter this endpoint does not accept; `detail` names it. Documented at https://docs.zenrows.com/crawl/endpoints#errors.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "unknown_parameter",
                  "title": "Unknown parameter",
                  "detail": "The request has a parameter this endpoint does not accept: limit.",
                  "status": 400,
                  "instance": "urn:zenrows:request:9ac5b40be6127c8809b91babda784059"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing or invalid.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "AUTH002",
                  "detail": "Invalid API key.",
                  "instance": "/v1",
                  "status": 401,
                  "title": "Invalid apikey provided (AUTH002)",
                  "type": "https://docs.zenrows.com/api-error-codes#AUTH002"
                }
              }
            }
          },
          "402": {
            "description": "The account is out of credits, or the plan does not include this capability.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "AUTH004",
                  "detail": "This account has used all its credits for the period. Upgrade or add credits to continue.",
                  "instance": "/v1",
                  "status": 402,
                  "title": "Usage exceeded (AUTH004)",
                  "type": "https://docs.zenrows.com/api-error-codes#AUTH004"
                }
              }
            }
          },
          "404": {
            "description": "`crawl_not_found`, or `content_not_found`: the page is not fetched yet, its fetch failed, or the crawl has no `output_format`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "content_not_found",
                  "title": "Content not found",
                  "detail": "This page has not been fetched yet.",
                  "status": 404,
                  "instance": "urn:zenrows:request:9ac5b40be6127c8809b91babda784059"
                }
              }
            }
          },
          "429": {
            "description": "`too_many_crawls`: the account already runs its active jobs (3 by default, shared with Batch). Wait for one to end; the response carries `Retry-After: 30`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "too_many_crawls",
                  "title": "Too many active crawls",
                  "detail": "Your account already runs 3 active jobs. Wait for one to end, then start the crawl again.",
                  "status": 429,
                  "instance": "urn:zenrows:request:9ac5b40be6127c8809b91babda784059"
                }
              }
            }
          },
          "500": {
            "description": "`internal_error`: something failed on our side. Retry later.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "internal_error",
                  "title": "Internal error",
                  "detail": "Something failed on our side. Retry later.",
                  "status": 500,
                  "instance": "urn:zenrows:request:9ac5b40be6127c8809b91babda784059"
                }
              }
            }
          }
        }
      }
    },
    "/crawls/{crawl_id}/stop": {
      "post": {
        "operationId": "stopCrawl",
        "summary": "Stop a crawl",
        "description": "Stop a running crawl. No new page is fetched; pages already being fetched finish, are billed and are kept for up to 10 minutes. Read the crawl until `next_cursor` is `null` for its final results. A crawl that already ended returns its final state. Send no body. Documented at https://docs.zenrows.com/crawl/endpoints#stop-a-crawl.",
        "tags": [
          "Crawl"
        ],
        "security": [
          {
            "apiKeyHeader": []
          },
          {
            "apiKeyQuery": []
          }
        ],
        "parameters": [
          {
            "name": "crawl_id",
            "in": "path",
            "required": true,
            "description": "The crawl's ID, as returned when it started.",
            "schema": {
              "type": "string"
            },
            "example": "c_01M3XP7RZD4JT88F7PNPGBFRAF"
          }
        ],
        "responses": {
          "200": {
            "description": "The crawl, stopped.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "crawl_id": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string"
                    },
                    "stop_reason": {
                      "type": "string"
                    },
                    "finished_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                },
                "example": {
                  "crawl_id": "c_01M3XP7RZD4JT88F7PNPGBFRAF",
                  "status": "stopped",
                  "stop_reason": "user",
                  "finished_at": "2026-10-09T09:01:30Z"
                }
              }
            }
          },
          "400": {
            "description": "`unknown_parameter`: the request has a query parameter this endpoint does not accept; `detail` names it. Documented at https://docs.zenrows.com/crawl/endpoints#errors.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "unknown_parameter",
                  "title": "Unknown parameter",
                  "detail": "The request has a parameter this endpoint does not accept: limit.",
                  "status": 400,
                  "instance": "urn:zenrows:request:9ac5b40be6127c8809b91babda784059"
                }
              }
            }
          },
          "401": {
            "description": "The API key is missing or invalid.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "AUTH002",
                  "detail": "Invalid API key.",
                  "instance": "/v1",
                  "status": 401,
                  "title": "Invalid apikey provided (AUTH002)",
                  "type": "https://docs.zenrows.com/api-error-codes#AUTH002"
                }
              }
            }
          },
          "402": {
            "description": "The account is out of credits, or the plan does not include this capability.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "AUTH004",
                  "detail": "This account has used all its credits for the period. Upgrade or add credits to continue.",
                  "instance": "/v1",
                  "status": 402,
                  "title": "Usage exceeded (AUTH004)",
                  "type": "https://docs.zenrows.com/api-error-codes#AUTH004"
                }
              }
            }
          },
          "404": {
            "description": "`crawl_not_found`: no crawl with this ID exists on your account.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "crawl_not_found",
                  "title": "Crawl not found",
                  "detail": "No crawl with this identifier exists for this account.",
                  "status": 404,
                  "instance": "urn:zenrows:request:9ac5b40be6127c8809b91babda784059"
                }
              }
            }
          },
          "429": {
            "description": "`too_many_crawls`: the account already runs its active jobs (3 by default, shared with Batch). Wait for one to end; the response carries `Retry-After: 30`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "too_many_crawls",
                  "title": "Too many active crawls",
                  "detail": "Your account already runs 3 active jobs. Wait for one to end, then start the crawl again.",
                  "status": 429,
                  "instance": "urn:zenrows:request:9ac5b40be6127c8809b91babda784059"
                }
              }
            }
          },
          "500": {
            "description": "`internal_error`: something failed on our side. Retry later.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "internal_error",
                  "title": "Internal error",
                  "detail": "Something failed on our side. Retry later.",
                  "status": 500,
                  "instance": "urn:zenrows:request:9ac5b40be6127c8809b91babda784059"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "headers": {
      "ConcurrencyLimit": {
        "description": "Concurrent requests this plan allows. The rate limit, in the unit Zenrows meters: slots in flight, not requests per interval.",
        "schema": {
          "type": "integer",
          "example": 20
        }
      },
      "ConcurrencyRemaining": {
        "description": "Free slots at the moment the request was received. Zero means the next request is refused with 429 until one finishes.",
        "schema": {
          "type": "integer",
          "example": 17
        }
      },
      "XRequestCost": {
        "description": "Cost of this request in US dollars at your plan's price per credit, so it varies with the features the request enabled. `0` when the request is not charged. For the number of credits, read `X-Request-Credits`.",
        "schema": {
          "type": "number",
          "example": 0.001
        }
      },
      "XRequestCredits": {
        "description": "Credits charged for this request. Requests that fail, and Extract refusals such as `REQS007`, `no_data_extracted` and `blocked_document`, are not charged.",
        "schema": {
          "type": "integer",
          "example": 5
        }
      },
      "XRequestId": {
        "description": "Identifier for this request. Quote it when reporting a problem.",
        "schema": {
          "type": "string",
          "example": "67fa4e35647515d8ad61bb3ee041e1bb"
        }
      },
      "RateLimitPolicy": {
        "description": "The standing rule, as a structured field: `\"signup\";q=3;w=86400` is three signups per client IP per 24 hours. Admin-editable, so read it rather than assuming three.",
        "schema": {
          "type": "string",
          "example": "\"signup\";q=3;w=86400"
        }
      },
      "RateLimit": {
        "description": "Where this caller stands: `r` is signups left, `t` is seconds until one frees. `t` counts from the oldest signup still inside the window, so it is a real wait, not the window length restated.",
        "schema": {
          "type": "string",
          "example": "\"signup\";r=1;t=52341"
        }
      },
      "RetryAfter": {
        "description": "Seconds to wait before signing up again. Present only when nothing is left.",
        "schema": {
          "type": "integer",
          "example": 52341
        }
      }
    },
    "parameters": {
      "BatchJobID": {
        "name": "job_id",
        "in": "path",
        "required": true,
        "description": "Assigned by the server on submit.",
        "schema": {
          "type": "string",
          "pattern": "^[0-9A-HJKMNP-TV-Z]{26}$"
        }
      },
      "BatchRunID": {
        "name": "run_id",
        "in": "path",
        "required": true,
        "description": "Assigned by the server when a run is created (submit or rerun).",
        "schema": {
          "type": "string",
          "pattern": "^[0-9A-HJKMNP-TV-Z]{26}$"
        }
      },
      "BatchTaskID": {
        "name": "task_id",
        "in": "path",
        "required": true,
        "description": "Server-assigned task id within a run. Use `external_id` for your own correlation id.",
        "schema": {
          "type": "string",
          "pattern": "^[A-Za-z0-9._-]{1,128}$"
        }
      },
      "BatchExportID": {
        "name": "export_id",
        "in": "path",
        "required": true,
        "description": "Assigned by the server when a results export is started.",
        "schema": {
          "type": "string",
          "pattern": "^[0-9A-HJKMNP-TV-Z]{26}$"
        }
      },
      "BatchIdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "Optional opaque token scoping a single submit / rerun /\nadd-tasks request, kept for 24 hours. A retry with the same\nkey and the same body replays the original status and body.\nThe same key with a different body, or while the original\nrequest is still being processed, returns\n`409 idempotency_key_conflict`. A request that is refused\n(any `4xx`, or a `503` before the job or run was created)\ngives the key back, so a retry with the same key is\nprocessed afresh. A `503` after the job or run was created\nkeeps the key: a retry under it is `409`; check the job\nbefore you resend with a fresh key. On add-tasks a retry\nwith the same body finishes an attempt that stopped partway\nand adds each task once. The credit check runs first, so a\nretry of a call that succeeded returns `402` if the account\nhas run out of credit since. At most 128 characters; a\nlonger key is `400 invalid_argument`.\n",
        "schema": {
          "type": "string",
          "maxLength": 128
        }
      },
      "BatchResultStatus": {
        "name": "status",
        "in": "query",
        "schema": {
          "type": "string",
          "enum": [
            "all",
            "successful",
            "failed",
            "processing"
          ],
          "default": "all"
        },
        "description": "`processing` covers both `pending` and `processing` task\nstates (anything not yet terminal).\n"
      },
      "BatchResultCursor": {
        "name": "cursor",
        "in": "query",
        "schema": {
          "type": "string"
        },
        "description": "Opaque pagination token; echo `next_cursor`."
      }
    },
    "responses": {
      "BatchInvalidArgument": {
        "description": "Request validation failed.",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/BatchProblem"
            }
          }
        }
      },
      "BatchUnauthenticated": {
        "description": "Missing / invalid API key.",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/BatchProblem"
            }
          }
        }
      },
      "BatchPaymentRequired": {
        "description": "The run cannot start because of billing. The problem `code`\ntells the cases apart:\n- `payment_required`: the subscription has expired, has no\n  credit available, or its plan does not include the API\n  (\"Subscription does not allow access to this service.\").\n- `api_key_cap_reached`: this API key reached one of its\n  credit caps. `detail` names the window, the cap and when it\n  resets (UTC). Other keys on the account are unaffected.\n\nClients must accept `code` values not listed here.\n",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/BatchProblem"
            }
          }
        }
      },
      "BatchIdempotencyConflict": {
        "description": "`idempotency_key_conflict`: the Idempotency-Key was already\nused by this caller with a different body, or the first\nrequest with it is still in progress (or answered `503` after\nit created the job).\n",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/BatchProblem"
            }
          }
        }
      },
      "BatchNotFound": {
        "description": "Resource not found (or not owned by caller).",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/BatchProblem"
            }
          }
        }
      },
      "BatchForbidden": {
        "description": "`permission_denied`: the Batch API is not enabled for this\naccount. Any `/v1` route can return it.\n",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/BatchProblem"
            }
          }
        }
      },
      "BatchQuotaExceeded": {
        "description": "`quota_exceeded`: an account cap is reached. On submit: the\naccount already has its maximum of active regular jobs (3 by\ndefault, Crawl jobs included; an open job counts until it is closed) or of\nscheduled jobs (5 by default). On add-tasks: the open job's\nrun would go over 100,000 tasks. For a job cap, finish, close\nor delete a job, then retry; for the task cap, send fewer\ntasks or close the job. A refused submit gives its\n`Idempotency-Key` back, so the retry can reuse it; an\nadd-tasks retry with the same body is checked again. No\n`Retry-After` is sent: the cap clears when a job finishes or\nis deleted, not with time.\n",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/BatchProblem"
            }
          }
        }
      },
      "BatchUnavailable": {
        "description": "Transient upstream failure; safe to retry.",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/BatchProblem"
            }
          }
        }
      }
    },
    "securitySchemes": {
      "apiKeyQuery": {
        "type": "apiKey",
        "in": "query",
        "name": "apikey",
        "description": "Fetch, Extract, Crawl and the account endpoint accept the key as the `apikey` query parameter. Keys are per account and self-serve: create one in the Zenrows dashboard (app.zenrows.com), or, for an agent with no human present, `POST https://app.zenrows.com/api/agent/signup`."
      },
      "apiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "Batch, Crawl, the Extract endpoints and the account endpoint read the same key from the `X-API-Key` header."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "Zenrows error envelope, sent as `application/problem+json` (RFC 9457 problem details). `code` is the stable identifier to branch on; `type` links to its entry in the catalogue at https://docs.zenrows.com/api-error-codes. Read back from live responses.",
        "properties": {
          "code": {
            "type": "string",
            "example": "AUTH002"
          },
          "status": {
            "type": "integer",
            "example": 401
          },
          "title": {
            "type": "string",
            "example": "Invalid apikey provided (AUTH002)"
          },
          "detail": {
            "type": "string",
            "example": "Invalid API key."
          },
          "type": {
            "type": "string",
            "format": "uri",
            "example": "https://docs.zenrows.com/api-error-codes#AUTH002"
          },
          "instance": {
            "type": "string",
            "description": "The request path or a request URN. Quote it when reporting a problem.",
            "example": "/v1"
          }
        }
      },
      "BatchJobType": {
        "type": "string",
        "enum": [
          "regular",
          "scheduled"
        ],
        "default": "regular",
        "description": "- `regular`: a run is created at submit time.\n- `scheduled`: fires on a recurring or one-shot schedule.\n  Submit stores the task list as a template on the job row;\n  each scheduled fire produces a fresh Run. `schedule`\n  field required. Read-only template: `addBatchTasks` and\n  `close` return 409. `rerun` (full or filtered) and `stop`\n  are allowed.\n"
      },
      "BatchJobStatus": {
        "type": "string",
        "x-extensible-enum": true,
        "enum": [
          "open",
          "closed",
          "deleted"
        ],
        "description": "- `open`: initial run still accepting `addBatchTasks`. Only\n  meaningful while `latest_run.run_sequence == 1`.\n- `closed`: no more tasks accepted (created closed, or\n  closed via `/close` / `addTasks{last_batch}` / `/rerun`).\n- `deleted`: never returned. A deleted job disappears from\n  every read and list the moment `DELETE` is accepted (reads\n  answer 404); its data is cleaned up asynchronously after.\n\nClients must accept values not listed here.\n"
      },
      "BatchScheduleState": {
        "type": "string",
        "enum": [
          "active",
          "paused"
        ],
        "description": "Run/pause flag on a scheduled job. While `paused`, scheduled\nfires are skipped. Default at submit: `active`. Flip via\n`POST /v1/jobs/{id}/schedule/state`.\n"
      },
      "BatchUpdateScheduleStateRequest": {
        "type": "object",
        "required": [
          "schedule_state"
        ],
        "properties": {
          "schedule_state": {
            "$ref": "#/components/schemas/BatchScheduleState"
          }
        }
      },
      "BatchRunTrigger": {
        "type": "string",
        "x-extensible-enum": true,
        "enum": [
          "manual",
          "scheduled"
        ],
        "description": "What set this run in motion. Always set.\n- `manual`: caller-initiated: `POST /jobs`, `POST /jobs/{id}/rerun`\n  (any job type, full or `?status=`-filtered, manual fire of\n  a scheduled job).\n- `scheduled`: automatic, fired by the configured\n  schedule (recurring or one-shot).\n\nClients must accept values not listed here.\n"
      },
      "BatchRunStatus": {
        "type": "string",
        "x-extensible-enum": true,
        "enum": [
          "running",
          "pending",
          "completed",
          "stopped",
          "failed",
          "deleted"
        ],
        "description": "In-flight:\n- `running`: work queued / in flight.\n- `pending`: initial run of an open job, idle between batches.\n\nTerminal:\n- `completed`: natural finish.\n- `stopped`: caller called `POST /jobs/{id}/stop`. No new\n  tasks are picked up; in-flight tasks may still finish.\n  Result bodies are kept. `stats.completed < stats.total`\n  signals \"stopped early\".\n- `failed`: the run was auto-failed on an account-level error\n  (insufficient credits / inactive subscription / the API key's\n  credit cap reached). No new tasks are picked up;\n  `failure_reason` and `failure_detail` carry the cause.\n  Re-runnable once the account or cap is resolved. Result bodies already produced\n  are kept.\n- `deleted`: caller called\n  `DELETE /v1/jobs/{id}/runs/{run_id}`. The run's result\n  bodies and data are being deleted; once complete the run\n  disappears.\n\nClients must accept values not listed here.\n"
      },
      "BatchTaskStatus": {
        "type": "string",
        "x-extensible-enum": true,
        "enum": [
          "pending",
          "processing",
          "successful",
          "failed",
          "expired"
        ],
        "description": "- `pending`, `processing`: not finished yet.\n- `successful`, `failed`: terminal.\n- `expired`: a successful task whose result body is past the\n  job's result-visibility window (14 days by default). It has\n  no `result_url`, and its `/content` returns\n  `410 result_expired`. Result filters match the stored\n  status, so `?status=successful` still returns these rows.\n\nClients must accept values not listed here.\n"
      },
      "BatchResultType": {
        "type": "string",
        "x-extensible-enum": true,
        "enum": [
          "html",
          "json",
          "markdown",
          "plaintext",
          "pdf"
        ],
        "description": "Body format of a successful task result: the task's effective\nformat, derived (the same way as `BatchFormat`) from the job's\n`zenrows_params` merged with the task's own. A task that\noverrides `response_type` can differ from the job's `format`.\n\nClients must accept values not listed here.\n"
      },
      "BatchFormat": {
        "type": "string",
        "x-extensible-enum": true,
        "enum": [
          "html",
          "json",
          "markdown",
          "plaintext",
          "pdf"
        ],
        "description": "Derived server-side from `zenrows_params` at submit time.\nPrecedence:\n  - `response_type: markdown|plaintext|pdf` → matching format.\n  - a non-empty `include` → `json`, ahead of `response_type`.\n  - `autoparse: true`, `json_response: true`, or non-empty\n    `css_extractor` or `extract` → `json`. (`outputs` returns\n    JSON from the scraper but is not part of this derivation, so\n    such a result is typed `html`; kept as is for existing\n    clients.)\n  - otherwise → `html`.\nThe job's `format` is the default from its job-level params;\neach task result's `type` is derived from that task's merged\nparams and sets the `Content-Type` of its content.\n\nClients must accept values not listed here.\n"
      },
      "BatchMetadata": {
        "type": "object",
        "description": "Opaque user-supplied key/value pairs. Stored on the task; not forwarded.",
        "additionalProperties": {
          "type": "string",
          "maxLength": 1024
        },
        "maxProperties": 20
      },
      "BatchScraperParams": {
        "type": "object",
        "description": "Subset of the Fetch parameters\n(https://docs.zenrows.com/fetch/api-reference).\nThe API pins the surface to a known set: unknown keys are\nrejected with `400 invalid_argument` (`unknown_param`). The\n`screenshot*` family is deliberately excluded; `mode` accepts\n`\"auto\"` only for now.\n\nEach value can be any JSON scalar (string, boolean, integral\nnumber); the API stores it as a string.\n\nPer-task `zenrows_params` override job-level `zenrows_params`\non key collision.\n\nSupported keys: `mode`, `js_render`, `js_instructions`,\n`custom_headers`, `premium_proxy`, `proxy_country`,\n`session_id`, `original_status`, `allowed_status_codes`,\n`wait_for`, `wait`, `block_resources`, `json_response`,\n`css_extractor`, `autoparse`, `extract`, `extract_fields`,\n`response_type`, `outputs`.\n\n`extract` selects the structured-extraction contract family\n(`auto`, `native`, or `standard`); `extract_fields` optionally\nnarrows the returned fields. Extract works on any domain\nZenrows has prepared, with any active API key; a task against a\ndomain that isn't prepared yet fails with the scraper API's\n403 `REQS007`.\n\n`custom_headers` additionally accepts an object of header\nname → value pairs (or that object serialised as a JSON\nstring), e.g. `{\"custom_headers\": {\"Referer\":\n\"https://www.google.com/\"}}`. The headers are forwarded to\nthe scraping target, subject to the scraper API's usual\nheader sanitisation. At most 32 headers / 8 KB serialised;\nnames must be valid HTTP tokens. Header filtering and\nbrowser-header sanitisation follow the same rules as a\nsingle Fetch request.\nA task-level\n`custom_headers` replaces the job-level object wholesale:\nno per-name merging.\n",
        "additionalProperties": {
          "oneOf": [
            {
              "type": "string"
            },
            {
              "type": "boolean"
            },
            {
              "type": "integer"
            },
            {
              "type": "object",
              "additionalProperties": {
                "type": "string"
              }
            }
          ]
        }
      },
      "BatchTaskInput": {
        "type": "object",
        "required": [
          "url"
        ],
        "properties": {
          "external_id": {
            "type": "string",
            "maxLength": 128,
            "pattern": "^[A-Za-z0-9._-]+$",
            "description": "Optional caller-supplied correlation id: typically an\nidentifier from the caller's own system. Surfaced verbatim\nin result/content responses so callers can match results\nback to their records. **Not required to be unique**:\ncallers may reuse the same value across tasks (e.g. when\nmultiple scrapes correlate to the same upstream record).\nIndependent of the server-\nassigned `task_id`.\n"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "maxLength": 2048,
            "description": "Must be http(s). Other schemes rejected at submit."
          },
          "metadata": {
            "$ref": "#/components/schemas/BatchMetadata"
          },
          "method": {
            "type": "string",
            "enum": [
              "GET",
              "POST"
            ],
            "default": "GET",
            "description": "HTTP method used against `url`. Case-insensitive. POST is\nfor **safe/idempotent** requests only (GraphQL queries,\nsearch endpoints): tasks are retried on transient failures\nand reruns, so the target may see the same POST more than\nonce. Don't send a request that must not run twice. POST\nrides the standard (non-headless)\nscraping path: combining it with `js_render`,\n`js_instructions`, or `json_response` is rejected with\n`400 invalid_argument`, the task's `invalid_tasks[].reason`\nbeing `method_param_conflict`.\n"
          },
          "body": {
            "description": "Request body, only with `method: POST`. Any JSON value,\n16 KiB max. An object/array/number/boolean is sent as its\nJSON encoding with `Content-Type: application/json`; a\nstring is sent verbatim with\n`Content-Type: application/x-www-form-urlencoded`. Set a\ndifferent target Content-Type via the `custom_headers`\nzenrows param. Never echoed in results listings.\n"
          },
          "zenrows_params": {
            "$ref": "#/components/schemas/BatchScraperParams",
            "description": "Per-task scraper params. Override the job-level\n`zenrows_params` on key collision (task wins).\n"
          }
        }
      },
      "BatchSubmitJobRequest": {
        "type": "object",
        "properties": {
          "type": {
            "$ref": "#/components/schemas/BatchJobType"
          },
          "status": {
            "type": "string",
            "enum": [
              "open",
              "closed"
            ],
            "default": "closed",
            "description": "Initial state. `open` is only allowed for `regular` jobs;\nafter the initial run, `open` has no meaning so the job\nis auto-closed.\n"
          },
          "zenrows_params": {
            "$ref": "#/components/schemas/BatchScraperParams",
            "description": "Job-level scraper params, applied to every task of every\nrun of the job. Each task can override individual keys\nvia its own `zenrows_params` (task wins on collision).\n"
          },
          "schedule": {
            "$ref": "#/components/schemas/BatchJobSchedule",
            "description": "Schedule block for `type: scheduled`. Required there,\nignored otherwise.\n"
          },
          "tasks": {
            "type": "array",
            "description": "Required for closed jobs (1 to 100,000 tasks by default;\nan account can have its own limit) unless `file_input_id`\nis provided. Optional for open jobs: may be empty if the\ncaller will follow up with `addBatchTasks`. Mutually exclusive\nwith `file_input_id`.\n",
            "minItems": 0,
            "items": {
              "$ref": "#/components/schemas/BatchTaskInput"
            }
          },
          "file_input_id": {
            "type": "string",
            "description": "Reference to a previously-uploaded CSV input (see\n`POST /v1/job_inputs`). Mutually exclusive with `tasks`.\nEligible only for regular-closed and scheduled job types.\nThe uploaded CSV is parsed under the saved spec; its rows\nbecome the tasks (regular-closed) or the task template\n(scheduled).\n"
          },
          "external_id": {
            "type": "string",
            "maxLength": 128,
            "pattern": "^[A-Za-z0-9._-]+$",
            "description": "Optional caller-supplied correlation id for the job:\nsame semantics as `task.external_id`. Shape-checked,\n**not** required to be unique. Surfaced verbatim in\n`getBatchJob` / `listBatchJobs` responses.\n"
          },
          "name": {
            "type": "string",
            "maxLength": 100,
            "description": "Optional human-readable label for the job. Free-form:\nno shape rules. Up to 100 characters. Surfaced verbatim\nin `getBatchJob` / `listBatchJobs` responses. No uniqueness, no\nindexing; cannot be changed after submit.\n"
          },
          "metadata": {
            "$ref": "#/components/schemas/BatchMetadata"
          },
          "webhook": {
            "$ref": "#/components/schemas/BatchSubmitWebhookConfig",
            "description": "Optional `run.completed` / `run.failed` delivery config.\nA terminal run fires `run.completed` on a natural finish, or\n`run.failed` (with `failure_reason` + partial stats) when the\nrun auto-fails on an account-level error. `signature`\ndefaults to `false` here so first-time integrations\ndon't need an HMAC key. Config is mutable post-submit\nvia `PUT /v1/jobs/{id}/webhook` and `DELETE\n/v1/jobs/{id}/webhook`; the current config is surfaced\non `GET /v1/jobs/{id}`.\n"
          }
        }
      },
      "BatchSubmitJobResponse": {
        "type": "object",
        "required": [
          "job_id",
          "status",
          "accepted_tasks"
        ],
        "properties": {
          "job_id": {
            "type": "string"
          },
          "status": {
            "$ref": "#/components/schemas/BatchJobStatus"
          },
          "latest_run": {
            "$ref": "#/components/schemas/BatchRun",
            "description": "Absent for `scheduled` jobs that haven't fired yet."
          },
          "accepted_tasks": {
            "type": "integer"
          },
          "webhook": {
            "$ref": "#/components/schemas/BatchWebhookConfig",
            "description": "Echo of the webhook config persisted on the job (when\none was supplied). Omitted when no webhook was set.\n"
          }
        }
      },
      "BatchAddTasksRequest": {
        "type": "object",
        "required": [
          "tasks"
        ],
        "properties": {
          "tasks": {
            "type": "array",
            "description": "1 to 10,000 tasks per call by default (an account can have\nits own limit). An open job's run holds at most 100,000\ntasks in total; going over returns `429 quota_exceeded`.\n",
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/BatchTaskInput"
            }
          },
          "last_batch": {
            "type": "boolean",
            "default": false,
            "description": "Set true on the final batch. Closes the job (status →\n`closed`) and marks the run's `last_batch_received`.\n"
          }
        }
      },
      "BatchAddTasksResponse": {
        "type": "object",
        "required": [
          "accepted_tasks",
          "job_status",
          "latest_run"
        ],
        "properties": {
          "accepted_tasks": {
            "type": "integer"
          },
          "job_status": {
            "$ref": "#/components/schemas/BatchJobStatus"
          },
          "latest_run": {
            "$ref": "#/components/schemas/BatchRun"
          }
        }
      },
      "BatchRerunJobResponse": {
        "type": "object",
        "required": [
          "job_id",
          "status",
          "latest_run",
          "retried_tasks",
          "inherited_tasks"
        ],
        "properties": {
          "job_id": {
            "type": "string"
          },
          "status": {
            "$ref": "#/components/schemas/BatchJobStatus"
          },
          "latest_run": {
            "$ref": "#/components/schemas/BatchRun"
          },
          "rerun_of": {
            "type": "string",
            "description": "`run_id` of the previous run that was replayed. Empty on\nthe first manual fire of a scheduled job (no prior run).\n"
          },
          "retried_tasks": {
            "type": "integer",
            "description": "Number of rows reset to `pending` and re-enqueued. Equals\n`latest_run.stats.total` for a full rerun; equals the\nfilter-matched count for a `?status=` partial retry.\n"
          },
          "inherited_tasks": {
            "type": "integer",
            "description": "Number of rows copied verbatim from the previous run with\n`source_run_id` stamped. Zero for a full rerun; non-zero\nonly when `?status=` is set.\n"
          }
        }
      },
      "BatchSpend": {
        "type": "object",
        "description": "Indicative `{credits, cost}` pair: what was charged for\nthe scoped work. **Not billing-grade**; your account\nstatement is authoritative. Use as a \"how much did this\ncost?\" indicator, not for reconciliation.\n",
        "required": [
          "credits",
          "cost"
        ],
        "properties": {
          "credits": {
            "type": "integer",
            "minimum": 0
          },
          "cost": {
            "type": "number",
            "format": "double",
            "minimum": 0
          }
        }
      },
      "BatchTaskSpend": {
        "type": "object",
        "deprecated": true,
        "description": "Deprecated, kept so existing clients keep compiling: no response\ncarries it. `total` summed every attempt and `last_attempt` the\nmost recent one.\n",
        "required": [
          "total",
          "last_attempt"
        ],
        "properties": {
          "total": {
            "$ref": "#/components/schemas/BatchSpend"
          },
          "last_attempt": {
            "$ref": "#/components/schemas/BatchSpend"
          }
        }
      },
      "BatchRunStats": {
        "type": "object",
        "required": [
          "total",
          "completed",
          "successful",
          "failed"
        ],
        "properties": {
          "total": {
            "type": "integer",
            "description": "Number of tasks in this run."
          },
          "completed": {
            "type": "integer",
            "description": "successful + failed."
          },
          "successful": {
            "type": "integer"
          },
          "failed": {
            "type": "integer"
          },
          "failure_reasons": {
            "type": "object",
            "description": "Coarse rollup of terminal failures keyed by a small public\ntaxonomy. Lets you answer \"what kinds of failures did I\nget?\" without paging every error blob. Best-effort,\nindicative: omitted on runs with no failures yet and on\nruns that predate the feature.\n\nVocabulary (the only keys that appear):\n- `auth_failed`: credentials or billing\n- `blocked`: anti-bot / policy denials, including a bot wall\n  Extract met on the fetched page\n- `bad_target`: target URL is the problem (bad host, 404, 410,\n  too large, or a page Extract does not cover or found no data on)\n- `rate_limited`: target throttled the request\n- `timeout`: the scrape didn't complete in time\n- `gateway_error`: Zenrows-side transport / 5xx\n- `other`: anything else\n",
            "additionalProperties": {
              "type": "integer",
              "minimum": 0
            },
            "example": {
              "blocked": 9,
              "gateway_error": 4
            }
          },
          "spend": {
            "allOf": [
              {
                "$ref": "#/components/schemas/BatchSpend"
              }
            ],
            "description": "Indicative spend summed across every task attempt in\nthis run. Absent on runs whose tasks predate this\nfield (treat as zero).\n"
          }
        }
      },
      "BatchRun": {
        "type": "object",
        "required": [
          "run_id",
          "job_id",
          "run_sequence",
          "status",
          "stats",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "run_id": {
            "type": "string"
          },
          "job_id": {
            "type": "string"
          },
          "run_sequence": {
            "type": "integer",
            "minimum": 1
          },
          "status": {
            "$ref": "#/components/schemas/BatchRunStatus"
          },
          "trigger": {
            "$ref": "#/components/schemas/BatchRunTrigger"
          },
          "stats": {
            "$ref": "#/components/schemas/BatchRunStats"
          },
          "last_batch_received": {
            "type": "boolean",
            "description": "Meaningful only for the initial run of an open job.\nOnce true, `addBatchTasks` is rejected and the run drains\ninto `completed`.\n"
          },
          "pause_state": {
            "type": "string",
            "enum": [
              "active",
              "paused"
            ],
            "description": "Reversible-suspend flag, orthogonal to `status`. A run\nnever paused omits it; a resumed run returns `active`\nexplicitly. Treat a missing field as `active`. Flip via\n`POST /jobs/{id}/pause` and `/resume`.\n"
          },
          "ingest_status": {
            "type": "string",
            "x-extensible-enum": true,
            "enum": [
              "pending",
              "done"
            ],
            "description": "Present only on runs created by a large (202) submission\nor a large (202) rerun. `pending`: task rows are still\nstreaming into storage; reads may return partial pages\nand `addBatchTasks` returns `409`. `done`: every accepted\ntask row is visible. Omitted on runs whose tasks were\nwritten on the request path (201 submissions and\nreruns, `addBatchTasks` batches).\n\nClients must accept values not listed here.\n"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "failure_reason": {
            "type": "string",
            "x-extensible-enum": true,
            "enum": [
              "insufficient_credits",
              "subscription_inactive",
              "api_key_cap_reached"
            ],
            "description": "Present only when `status == failed`: the account-level\ncause of the auto-fail. Omitted otherwise. Distinct from\n`stats.failure_reasons` (the per-task rollup).\n- `insufficient_credits`: the account is out of credits, or\n  the plan doesn't include a feature the task needs.\n- `subscription_inactive`: the subscription is not active\n  or has expired, the plan doesn't include the API, or the\n  job's API key is no longer valid.\n- `api_key_cap_reached`: the job's API key reached one of\n  its credit caps (day, week, month or billing period).\n  Resolves on its own when that cap's window resets, or\n  when the cap is raised.\n\nClients must accept values not listed here.\n"
          },
          "failure_detail": {
            "type": "string",
            "description": "Human-readable explanation of `failure_reason`, e.g. which\ncap was reached and when it resets. Present only when\n`failure_reason` is. At most 500 characters (Unicode code\npoints); longer upstream text is cut. Free text for display:\nbranch on `failure_reason`, never on this string.\n",
            "example": "Subscription has no credit available."
          }
        }
      },
      "BatchJobSchedule": {
        "type": "object",
        "description": "Structured scheduling block attached to `type: scheduled`\njobs. Exactly one of `at`, `rate`, or `calendar` must be\nset.\n",
        "properties": {
          "at": {
            "type": "string",
            "description": "One-shot fire at a specific wall-clock timestamp.\nMutually exclusive with `rate` and `calendar`.\n\n**Must be tz-naive**: no trailing `Z`, no offset. The\nsibling `timezone` field (mandatory) is the single\nauthoritative interpreter. This keeps DST transitions\ndeterministic.\n",
            "example": "2026-09-01T09:00:00"
          },
          "rate": {
            "$ref": "#/components/schemas/BatchScheduleRate"
          },
          "calendar": {
            "$ref": "#/components/schemas/BatchScheduleCalendar"
          },
          "timezone": {
            "type": "string",
            "description": "IANA timezone name (e.g. `Europe/Berlin`, `UTC`).\n**Required** when `at` or `calendar` is set;\nignored by `rate` (interval-based, no wall-clock\nmeaning). Anchoring wall-clock times to a named zone\n(rather than a UTC offset baked into the string) keeps\nDST transitions deterministic.\n",
            "example": "Europe/Berlin"
          }
        }
      },
      "BatchScheduleRate": {
        "type": "object",
        "required": [
          "every",
          "unit"
        ],
        "description": "Interval-based fire policy: every N units. The interval must\nbe at least 60 minutes by default (an account can have its own\nminimum); a shorter one returns `400 invalid_argument`.\n",
        "properties": {
          "every": {
            "type": "integer",
            "minimum": 1,
            "example": 6
          },
          "unit": {
            "type": "string",
            "enum": [
              "minute",
              "hour",
              "day"
            ],
            "example": "hour"
          }
        }
      },
      "BatchScheduleCalendar": {
        "type": "object",
        "required": [
          "times_of_day",
          "cadence"
        ],
        "description": "Calendar-style fire policy. Fires at every `times_of_day`\nentry on every day matching the cadence.\n",
        "properties": {
          "times_of_day": {
            "type": "array",
            "minItems": 1,
            "description": "Wall-clock times on a 24-hour clock, full hours only\n(`\"09:00\"`, `\"18:00\"`). Minute granularity is rejected\nwith 400.\n",
            "items": {
              "type": "string",
              "pattern": "^([01][0-9]|2[0-3]):00$"
            },
            "example": [
              "09:00",
              "18:00"
            ]
          },
          "cadence": {
            "$ref": "#/components/schemas/BatchScheduleCadence"
          }
        }
      },
      "BatchScheduleCadence": {
        "type": "object",
        "description": "Picks which days the schedule fires on. Exactly one of\n`daily`, `weekly`, `monthly` must be set.\n",
        "properties": {
          "daily": {
            "type": "object",
            "description": "Fire every day. No knobs."
          },
          "weekly": {
            "type": "object",
            "required": [
              "days"
            ],
            "properties": {
              "days": {
                "type": "array",
                "minItems": 1,
                "items": {
                  "type": "string",
                  "enum": [
                    "mon",
                    "tue",
                    "wed",
                    "thu",
                    "fri",
                    "sat",
                    "sun"
                  ]
                },
                "example": [
                  "mon",
                  "wed",
                  "fri"
                ]
              }
            }
          },
          "monthly": {
            "type": "object",
            "required": [
              "days"
            ],
            "properties": {
              "days": {
                "type": "array",
                "minItems": 1,
                "items": {
                  "type": "integer",
                  "minimum": 1,
                  "maximum": 31
                },
                "example": [
                  1,
                  15
                ]
              }
            }
          }
        }
      },
      "BatchJob": {
        "type": "object",
        "required": [
          "job_id",
          "type",
          "status",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "job_id": {
            "type": "string"
          },
          "type": {
            "$ref": "#/components/schemas/BatchJobType"
          },
          "status": {
            "$ref": "#/components/schemas/BatchJobStatus"
          },
          "format": {
            "$ref": "#/components/schemas/BatchFormat"
          },
          "template_tasks": {
            "type": "array",
            "description": "A scheduled job's task template, when it is small enough to\nbe stored inline on the job; omitted otherwise (a large\ntemplate is stored separately and not returned here).\n",
            "items": {
              "$ref": "#/components/schemas/BatchTaskInput"
            }
          },
          "zenrows_params": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Stored canonical form: values are always strings even\nthough submit accepts any JSON scalar (see ScraperParams).\n"
          },
          "external_id": {
            "type": "string",
            "description": "Caller-supplied correlation id passed at submit (omitted\nwhen the caller did not supply one). Not server-enforced\nunique.\n"
          },
          "name": {
            "type": "string",
            "description": "Optional human label passed at submit (omitted when the\ncaller did not supply one). Free-form, up to 100 chars.\n"
          },
          "metadata": {
            "$ref": "#/components/schemas/BatchMetadata"
          },
          "schedule": {
            "$ref": "#/components/schemas/BatchJobSchedule",
            "description": "Schedule block: present only for `type: scheduled`\njobs.\n"
          },
          "next_scheduled_run": {
            "type": "string",
            "format": "date-time",
            "description": "The next expected fire, computed from the schedule when\nthe response is built. Omitted for non-scheduled jobs,\nwhile `schedule_state == paused`, and for a one-shot `at`\nschedule that has already fired.\n"
          },
          "schedule_state": {
            "$ref": "#/components/schemas/BatchScheduleState",
            "description": "Set only for `type: scheduled`. Default `active` at\nsubmit; flip via `POST /v1/jobs/{id}/schedule/state`.\n"
          },
          "webhook": {
            "$ref": "#/components/schemas/BatchWebhookConfig",
            "description": "Webhook delivery config. Present iff a\nwebhook is configured. Mutable via `PUT/DELETE\n/v1/jobs/{id}/webhook`; `signature` never appears\nalone: the whole `webhook` key is omitted when no\nURL is set.\n"
          },
          "latest_run": {
            "$ref": "#/components/schemas/BatchRun",
            "description": "The job's most recent run, read when the response is\nbuilt. Absent for `scheduled` jobs that haven't fired yet,\nand once every run was deleted.\n"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "BatchListJobsResponse": {
        "type": "object",
        "required": [
          "jobs"
        ],
        "properties": {
          "jobs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BatchJob"
            }
          },
          "next_cursor": {
            "type": "string",
            "description": "Echo as `?cursor=` for the next page. Omitted on the last page."
          }
        }
      },
      "BatchListJobRunsResponse": {
        "type": "object",
        "required": [
          "runs"
        ],
        "properties": {
          "runs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BatchRun"
            }
          },
          "next_cursor": {
            "type": "string",
            "description": "Echo as `?cursor=` for the next page. Omitted on the last page."
          }
        }
      },
      "BatchTaskResult": {
        "type": "object",
        "required": [
          "task_id",
          "run_id",
          "url",
          "status",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "task_id": {
            "type": "string"
          },
          "external_id": {
            "type": "string",
            "description": "Caller-supplied correlation id from submit/AddTasks.\nOmitted when the caller did not supply one.\n"
          },
          "run_id": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "metadata": {
            "$ref": "#/components/schemas/BatchMetadata"
          },
          "method": {
            "type": "string",
            "enum": [
              "GET",
              "POST"
            ],
            "description": "The task's HTTP method. Omitted for GET (the default).\nThe request `body` is intentionally not part of listing\nresponses.\n"
          },
          "status": {
            "$ref": "#/components/schemas/BatchTaskStatus"
          },
          "type": {
            "$ref": "#/components/schemas/BatchResultType"
          },
          "result_url": {
            "type": "string",
            "description": "Presigned download URL for the result body, valid for 2\nhours: fetch it soon after listing, and list again for a\nfresh one rather than storing it. Omitted for tasks that\nare not `successful`, including `expired` ones.\n"
          },
          "error": {
            "$ref": "#/components/schemas/BatchProblem",
            "description": "Present on failed tasks: the scraping engine's error as\nProblem JSON, or one of `gateway_unreachable` (503),\n`gateway_timeout` (504), `gateway_bad_response` or\n`internal` (500) when the attempt itself failed.\n"
          },
          "source_run_id": {
            "type": "string",
            "description": "Set when this row was copied from another run by\n`/rerun?status=`. The row keeps the status it had in the\nsource run (usually terminal, but a `pending` row of a\nstopped run stays `pending`), is never re-executed, and its\n`result_url` resolves to the source run's stored result. On chained retries,\n`source_run_id` chases back to the run that actually\nowns the result. Empty for normally-executed rows.\n"
          },
          "spend": {
            "$ref": "#/components/schemas/BatchTaskSpend",
            "deprecated": true,
            "description": "Deprecated: the API does not send this field on result rows.\nRead per-task spend from the task's `/history` events (one\n`spend` per attempt) and the run total from `stats.spend`.\n"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the task was created."
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "The task's last update. Once a task is terminal\n(`successful` / `failed`) this is when it finished: use\nit to verify how fresh a result is on recurring jobs.\nOn a row inherited by a partial rerun (`source_run_id`\nset) it is the time of the copy, not of the original\nscrape.\n"
          }
        }
      },
      "BatchListResultsResponse": {
        "type": "object",
        "required": [
          "results"
        ],
        "properties": {
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BatchTaskResult"
            }
          },
          "next_cursor": {
            "type": "string",
            "description": "Echo as `?cursor=` for the next page. Omitted on the last page."
          }
        }
      },
      "BatchTaskHistoryEvent": {
        "type": "object",
        "required": [
          "started_at",
          "ended_at",
          "attempt"
        ],
        "properties": {
          "started_at": {
            "type": "string",
            "format": "date-time"
          },
          "ended_at": {
            "type": "string",
            "format": "date-time"
          },
          "attempt": {
            "type": "integer",
            "minimum": 1,
            "description": "1-indexed attempt within the run."
          },
          "error": {
            "$ref": "#/components/schemas/BatchProblem"
          },
          "spend": {
            "allOf": [
              {
                "$ref": "#/components/schemas/BatchSpend"
              }
            ],
            "description": "Indicative spend charged for this single attempt. Zero\non attempts that didn't reach the scraping engine or\nwere not charged.\n"
          }
        }
      },
      "BatchTaskHistoryResponse": {
        "type": "object",
        "required": [
          "events"
        ],
        "properties": {
          "events": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BatchTaskHistoryEvent"
            }
          }
        }
      },
      "BatchWebhookConfig": {
        "type": "object",
        "required": [
          "url",
          "signature"
        ],
        "description": "Webhook delivery config for `run.completed` / `run.failed`. Returned on\n`GET /v1/jobs/{id}` (inline under `webhook`) and on\n`GET /v1/jobs/{id}/webhook`. `PUT` requires both fields:\nno defaulting at the mutate boundary (otherwise toggling\n`url` would silently disable signing). Submit body accepts\nthe same shape with `signature` optional (defaults `false`).\n\nDelivery: one event per run that completes or fails on its own (a\nrun stopped with `/stop` sends none), retried with backoff for up\nto 3 days, so the same event can arrive more than once; dedupe on\n`X-ZenRows-Event-Id`. Read `GET /v1/jobs/{id}` when you need to be\nsure a run finished.\n",
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "HTTPS only. Host must resolve via DNS within 1 second\n(1+ A/AAAA record).\n"
          },
          "signature": {
            "type": "boolean",
            "description": "Opt-in HMAC signing. When `true`, each delivery is signed\nwith the account's active HMAC key\n(`POST /v1/hmac/keys/rotate`) and carries\n`X-Signature: t=<unix>,v1=<hex>,kid=<active>`. The signed\ninput is `t + \".\" + raw_body`, HMAC-SHA256, keyed with the\nsecret's raw bytes: base64-decode the `secret` that\n`/rotate` returned before using it. When `false`,\ndeliveries carry **no** `X-Signature` header: header\nabsence is the signal.\n"
          }
        }
      },
      "BatchSubmitWebhookConfig": {
        "type": "object",
        "required": [
          "url"
        ],
        "description": "The webhook block accepted at submit: `url` is required and\n`signature` defaults to `false`. `PUT /v1/jobs/{id}/webhook`\ntakes `BatchWebhookConfig`, which requires both.\n",
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "HTTPS URL that receives the run events."
          },
          "signature": {
            "type": "boolean",
            "default": false,
            "description": "Sign each delivery with the account's active HMAC key (see `BatchWebhookConfig.signature`)."
          }
        }
      },
      "BatchWebhookEventType": {
        "type": "string",
        "x-extensible-enum": true,
        "enum": [
          "run.completed",
          "run.failed",
          "webhook.test"
        ],
        "description": "`event_type` of a webhook delivery (see `BatchWebhookEvent`).\n- `run.completed`: the run finished naturally.\n- `run.failed`: the run auto-failed on an account-level\n  error; `failure_reason` and `failure_detail` carry the cause.\n- `webhook.test`: synthetic delivery from `POST /v1/webhook/test`.\n\nClients must accept values not listed here.\n"
      },
      "BatchWebhookEvent": {
        "type": "object",
        "required": [
          "schema",
          "event_id",
          "event_type",
          "occurred_at",
          "job_id",
          "run_id",
          "run_sequence"
        ],
        "description": "Body POSTed to the configured webhook URL (`BatchWebhookConfig`).\n",
        "properties": {
          "schema": {
            "type": "string"
          },
          "event_id": {
            "type": "string"
          },
          "event_type": {
            "$ref": "#/components/schemas/BatchWebhookEventType"
          },
          "occurred_at": {
            "type": "string",
            "format": "date-time"
          },
          "job_id": {
            "type": "string"
          },
          "external_id": {
            "type": "string"
          },
          "metadata": {
            "$ref": "#/components/schemas/BatchMetadata"
          },
          "run_id": {
            "type": "string"
          },
          "run_sequence": {
            "type": "integer",
            "minimum": 0,
            "description": "The run's `run_sequence` (1 and up). `0` only on the\nsynthetic event `POST /webhook/test` sends.\n"
          },
          "stats": {
            "$ref": "#/components/schemas/BatchRunStats",
            "description": "Omitted on the synthetic test event."
          },
          "failure_reason": {
            "type": "string",
            "x-extensible-enum": true,
            "enum": [
              "insufficient_credits",
              "subscription_inactive",
              "api_key_cap_reached"
            ],
            "description": "`run.failed` only. Same vocabulary as `BatchRun.failure_reason`.\nClients must accept values not listed here.\n"
          },
          "failure_detail": {
            "type": "string",
            "description": "`run.failed` only, present when `failure_reason` is. Same as\n`BatchRun.failure_detail`: at most 500 characters (Unicode code\npoints).\n"
          }
        }
      },
      "BatchTestWebhookRequest": {
        "type": "object",
        "required": [
          "url"
        ],
        "description": "Body for `POST /v1/webhook/test`. Same shape as the submit-time\n`webhook` field; `signature` is optional and defaults `false`.\n",
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "HTTPS only. Host must resolve via DNS within 1 second.\nIdentical validation to the submit/PUT webhook URL.\n"
          },
          "signature": {
            "type": "boolean",
            "default": false,
            "description": "When `true`, the test event is signed with the account's\nactive HMAC key: same `X-Signature: t,v1,kid` header\nreal deliveries use. Returns `400\nwebhook_signing_requires_active_key` when no active key\nexists.\n"
          }
        }
      },
      "BatchTestWebhookResponse": {
        "type": "object",
        "required": [
          "delivered",
          "event_id",
          "elapsed_ms"
        ],
        "description": "Outcome of the synthetic test dispatch. HTTP status is always\n`200` when the request was validated: the receiver outcome\nis in the body. The synthetic envelope uses `event_type:\n\"webhook.test\"` and stable sentinel IDs (`job_id`/`run_id` =\n`\"test\"`) so receivers can recognise and discard test traffic.\n",
        "properties": {
          "delivered": {
            "type": "boolean",
            "description": "`true` iff the receiver responded with a 2xx status. `false`\non non-2xx, timeout, or transport error.\n"
          },
          "event_id": {
            "type": "string",
            "description": "ID of the synthetic event. Different on every call so\nreceivers' dedup tables don't suppress repeated tests.\n"
          },
          "status_code": {
            "type": "integer",
            "description": "HTTP status returned by the receiver. Absent on timeout or\ntransport error (no response was received).\n"
          },
          "error": {
            "type": "string",
            "description": "Human-readable reason when `delivered: false`. Same\nvocabulary as real webhook deliveries\n(`http_4xx:<code>`, `http_5xx:<code>`, `timeout`,\n`transport_error:<detail>`). Absent on success.\n"
          },
          "elapsed_ms": {
            "type": "integer",
            "description": "Wall-clock duration of the receiver POST in milliseconds."
          }
        }
      },
      "BatchFileInputColumnRef": {
        "description": "Either a column name (string): requires `csv.header: true`:\nor a 0-based column index (integer). Other shapes are\nrejected at create-time.\n",
        "oneOf": [
          {
            "type": "string",
            "minLength": 1
          },
          {
            "type": "integer",
            "minimum": 0
          }
        ]
      },
      "BatchCreateJobInputRequest": {
        "type": "object",
        "required": [
          "type"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "csv"
            ],
            "description": "Only `csv` is supported in v1."
          },
          "csv": {
            "type": "object",
            "required": [
              "fields"
            ],
            "properties": {
              "delimiter": {
                "type": "string",
                "minLength": 1,
                "maxLength": 1,
                "default": ",",
                "description": "Single-character field delimiter."
              },
              "quote": {
                "type": "string",
                "minLength": 1,
                "maxLength": 1,
                "default": "\"",
                "description": "Accepted for forward compatibility but not used yet:\nfields are always quoted with `\"` (standard CSV).\n"
              },
              "header": {
                "type": "boolean",
                "default": false,
                "description": "When true, the first CSV row is consumed as a header\nrow and `csv.fields.*` values may be column names.\n"
              },
              "fields": {
                "type": "object",
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "$ref": "#/components/schemas/BatchFileInputColumnRef"
                  },
                  "external_id": {
                    "$ref": "#/components/schemas/BatchFileInputColumnRef"
                  }
                },
                "additionalProperties": false,
                "description": "Map from canonical task field → CSV column. Only\n`url` (required) and `external_id` (optional) are\naccepted. Each value is a column index (int) or a\ncolumn name (string, requires `header: true`).\n"
              }
            }
          }
        }
      },
      "BatchFileInputUploadTarget": {
        "type": "object",
        "required": [
          "method",
          "url",
          "expires_at"
        ],
        "properties": {
          "method": {
            "type": "string",
            "enum": [
              "PUT"
            ]
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Presigned PUT URL. Caller MUST send the body with the\nexact `Content-Type` shown in `headers`: the signature\nbinds the content-type.\n"
          },
          "headers": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "example": {
              "Content-Type": "text/csv"
            }
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "PUT URL TTL (~30 min)."
          }
        }
      },
      "BatchCreateJobInputResponse": {
        "type": "object",
        "required": [
          "file_input_id",
          "upload",
          "expires_at"
        ],
        "properties": {
          "file_input_id": {
            "type": "string"
          },
          "upload": {
            "$ref": "#/components/schemas/BatchFileInputUploadTarget"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "24 h slot lifetime: beyond this the slot and its uploaded\nbody are removed and the `file_input_id` returns 404.\n"
          }
        }
      },
      "BatchHMACKeyMeta": {
        "type": "object",
        "required": [
          "kid",
          "created_at"
        ],
        "description": "Public view of one HMAC key: id + creation time. Never\nincludes secret material; that's only returned at /rotate.\n",
        "properties": {
          "kid": {
            "type": "string",
            "description": "Identifies this key. Stable for the life of the slot; a new candidate gets a new kid.",
            "pattern": "^[0-9A-HJKMNP-TV-Z]{26}$"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "BatchHMACKeyList": {
        "type": "object",
        "description": "Slots populated at the time of the call.",
        "properties": {
          "active": {
            "$ref": "#/components/schemas/BatchHMACKeyMeta"
          },
          "candidate": {
            "$ref": "#/components/schemas/BatchHMACKeyMeta"
          }
        }
      },
      "BatchHMACKeyCreated": {
        "type": "object",
        "required": [
          "kid",
          "secret",
          "created_at"
        ],
        "description": "Response to `/rotate`. `secret` is base64-encoded raw key\nmaterial. **This is the ONLY response that ever contains\nthe secret value**: capture it now or generate a new one\nvia another /rotate call.\n",
        "properties": {
          "kid": {
            "type": "string",
            "pattern": "^[0-9A-HJKMNP-TV-Z]{26}$"
          },
          "secret": {
            "type": "string",
            "description": "Base64-encoded 32-byte HMAC key."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "BatchHMACKeyFinalized": {
        "type": "object",
        "required": [
          "active_kid",
          "created_at"
        ],
        "description": "Response to `/rotate/finalize`. No secret.",
        "properties": {
          "active_kid": {
            "type": "string",
            "pattern": "^[0-9A-HJKMNP-TV-Z]{26}$"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "BatchProblem": {
        "type": "object",
        "description": "RFC 7807 Problem Details.\n",
        "required": [
          "type",
          "title",
          "status",
          "code"
        ],
        "properties": {
          "type": {
            "type": "string",
            "format": "uri"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "integer"
          },
          "detail": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "description": "Stable machine-readable error code. Branch on this, not on\n`detail` or `title`. `type` links to the code's entry in the\npublic error reference. Codes and their usual status:\n\n| Status | Codes |\n|---|---|\n| 400 | `invalid_argument` (with `invalid_tasks` for per-task problems), `callback_url_must_be_https`, `callback_url_unresolvable`, `webhook_signing_requires_active_key` |\n| 401 | `unauthenticated`, `subscription_inactive` (a verified, enabled account whose subscription is past due, unpaid, incomplete or paused; `detail` stays \"Subscription is not active.\") |\n| 402 | `payment_required`, `api_key_cap_reached` |\n| 403 | `permission_denied` (the Batch API is not enabled for this account) |\n| 404 | `not_found` (job, run, task or export), `file_input_not_found` |\n| 409 | `conflict`, `job_not_open`, `last_batch_received`, `run_not_terminal`, `run_not_stoppable`, `no_prior_run`, `no_matching_tasks`, `run_has_descendants`, `idempotency_key_conflict`, `task_not_ready`, `partial_rerun_window_expired`, `candidate_pending`, `no_candidate`, `file_input_not_uploaded`, `file_input_unsupported_for_job_type` |\n| 410 | `result_expired` (a task's content past the result window, or an export past `expires_at`) |\n| 429 | `quota_exceeded` |\n| 500 / 503 | `internal` |\n\nA failed task's stored `error` uses the scraping engine's\nown codes, plus `gateway_unreachable` (503),\n`gateway_timeout` (504), `gateway_bad_response` and\n`internal` (500) for attempts that failed before the engine\nanswered with Problem JSON.\n\nA path that matches no API route answers a plain `404`\nwithout a Problem body.\n\nClients must accept values not listed here.\n"
          },
          "instance": {
            "type": "string",
            "description": "Always `/v1`; it does not name the request path."
          },
          "invalid_tasks": {
            "type": "array",
            "description": "Present on validation errors.",
            "items": {
              "type": "object",
              "required": [
                "index",
                "reason"
              ],
              "properties": {
                "index": {
                  "type": "integer"
                },
                "reason": {
                  "type": "string",
                  "x-extensible-enum": true,
                  "description": "Clients must accept values not listed here.",
                  "enum": [
                    "malformed_url",
                    "unsupported_scheme",
                    "missing_host",
                    "url_too_long",
                    "metadata_too_large",
                    "metadata_key_invalid",
                    "invalid_external_id",
                    "unknown_param",
                    "invalid_param_value",
                    "unsupported_method",
                    "body_not_allowed",
                    "body_too_large",
                    "body_malformed",
                    "method_param_conflict"
                  ]
                },
                "value": {
                  "type": "string",
                  "description": "Offending input. For URL / metadata reasons this is\na redacted/truncated form of the bad value. For\n`unknown_param` / `invalid_param_value` this is the\nparam key.\n"
                }
              }
            }
          }
        },
        "additionalProperties": true
      },
      "BatchExportStatus": {
        "type": "string",
        "x-extensible-enum": true,
        "enum": [
          "pending",
          "running",
          "completed",
          "failed"
        ],
        "description": "Lifecycle state of a results export.\n* `pending`: export accepted, not started yet.\n* `running`: the zip is being produced.\n* `completed`: `download_url` will be present.\n* `failed`  : `error` carries the reason.\n\nClients must accept values not listed here.\n"
      },
      "BatchStartExportResponse": {
        "type": "object",
        "required": [
          "export_id",
          "status",
          "created_at",
          "expires_at"
        ],
        "description": "Returned by `createBatchExport`.",
        "properties": {
          "export_id": {
            "type": "string",
            "description": "Identifies this export. Use it for `getBatchExport`.",
            "pattern": "^[0-9A-HJKMNP-TV-Z]{26}$"
          },
          "status": {
            "$ref": "#/components/schemas/BatchExportStatus"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "12 h after `created_at`. After it, polling the export\nanswers `410 result_expired`; once it is cleaned up, `404`.\n"
          }
        }
      },
      "BatchExport": {
        "type": "object",
        "required": [
          "export_id",
          "status",
          "created_at",
          "expires_at"
        ],
        "description": "Polled view of a results export. `download_url` is presigned\nfresh on every successful poll: stash the metadata, but\nre-fetch the URL right before you download.\n",
        "properties": {
          "export_id": {
            "type": "string",
            "pattern": "^[0-9A-HJKMNP-TV-Z]{26}$"
          },
          "status": {
            "$ref": "#/components/schemas/BatchExportStatus"
          },
          "error": {
            "type": "string",
            "description": "Non-empty only when `status = failed`. Stable strings:\ne.g. `\"results are larger than N gb\"` when the combined\nresults exceed the export byte cap (100 GiB in\nproduction). Omitted otherwise.\n"
          },
          "download_url": {
            "type": "string",
            "format": "uri",
            "description": "Presigned download URL for the zipped run results.\nPresent only when `status = completed`. Valid for 2 hours:\nthe server mints a new one on every poll.\n"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "12 h after `created_at`. After it, the poll answers\n`410 result_expired` with no `download_url`.\n"
          }
        }
      },
      "ExtractEndpointError": {
        "type": "object",
        "description": "Error shape of the Extract endpoints: lowercase `code` values, and `type` links to the code's entry in https://docs.zenrows.com/api-error-codes.",
        "properties": {
          "code": {
            "type": "string",
            "examples": [
              "invalid_argument",
              "not_found",
              "in_progress",
              "idempotency_conflict",
              "out_of_allowance"
            ]
          },
          "detail": {
            "type": "string"
          },
          "reason": {
            "type": "string",
            "description": "Machine-readable tag on `invalid_argument`, for example `invalid_domain`."
          },
          "status": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "ExtractPreparation": {
        "type": "object",
        "description": "A domain's preparation for Extract.",
        "properties": {
          "domain": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "preparing",
              "ready",
              "failed"
            ],
            "description": "Whether the domain can be used yet. Branch on this."
          },
          "state": {
            "type": "string",
            "description": "The current step. Gains steps over time.",
            "examples": [
              "received",
              "discovering",
              "preparing",
              "verifying",
              "ready",
              "failed"
            ]
          },
          "roles": {
            "type": "array",
            "description": "Progress per kind of page. Only present for the account that submitted the domain.",
            "items": {
              "type": "object",
              "properties": {
                "role": {
                  "type": "string"
                },
                "status": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "detail": {
                  "type": "string",
                  "description": "Why this kind of page stopped, when it did."
                }
              }
            }
          },
          "failure": {
            "type": "object",
            "description": "Present when `status` is `failed`.",
            "properties": {
              "code": {
                "type": "string",
                "examples": [
                  "interrupted",
                  "preparation_exhausted",
                  "discovery_failed",
                  "unpreparable",
                  "preparation_failed",
                  "verification_failed",
                  "unsupported"
                ]
              },
              "detail": {
                "type": "string"
              },
              "slot_consumed": {
                "type": "boolean",
                "description": "Whether the attempt used an allowance slot."
              }
            }
          },
          "submitted_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SignupError": {
        "type": "object",
        "description": "The onboarding surface has its own error shape, flatter than the API's envelope. Branch on `error`.",
        "properties": {
          "error": {
            "type": "string",
            "examples": [
              "rate_limited",
              "signup_disabled",
              "signup_failed",
              "invalid_key"
            ]
          },
          "message": {
            "type": "string"
          }
        }
      },
      "Crawl": {
        "type": "object",
        "description": "A crawl, its coverage so far and one page of results.",
        "properties": {
          "crawl_id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "running",
              "completed",
              "stopped",
              "failed"
            ],
            "description": "`completed`: nothing was left to open, or a limit was reached (see `stop_reason`). `stopped`: you stopped it. `failed`: see `error`."
          },
          "stop_reason": {
            "type": "string",
            "enum": [
              "max_items",
              "max_pages",
              "user"
            ],
            "description": "Why the crawl ended early. Absent when nothing was left to open, and when the crawl failed."
          },
          "error": {
            "type": "object",
            "description": "Present only when `status` is `failed`. Older crawls can carry `domain_not_allowed` (the domain was not prepared for Extract).",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "seed_unreachable",
                  "no_items_found",
                  "insufficient_credits",
                  "internal_error",
                  "domain_not_allowed"
                ]
              },
              "detail": {
                "type": "string"
              }
            }
          },
          "url": {
            "type": "string",
            "format": "uri",
            "maxLength": 2048,
            "description": "The start URL: a public `http` or `https` URL. The crawl starts on this exact URL and stays on its domain, subdomains included."
          },
          "depth": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100000,
            "description": "How many link steps to follow from the start URL. At `1`, the links on the start page. Omit it to follow links until `max_items` or `max_pages` stops the crawl."
          },
          "max_items": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100000,
            "default": 100,
            "description": "Stop after keeping this many URLs."
          },
          "max_pages": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100000,
            "default": 100,
            "description": "Stop after fetching this many pages. Caps what the crawl costs. If both limits are reached together, `stop_reason` is `max_pages`."
          },
          "include_patterns": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Keep, and open, only URLs that contain at least one of these texts, such as `/product/`. The start URL is never returned."
          },
          "exclude_patterns": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Drop URLs that contain any of these texts, even when they match an include pattern."
          },
          "output_format": {
            "type": "string",
            "enum": [
              "html"
            ],
            "description": "`html` also returns the HTML of each kept page. Omit it to get URLs only."
          },
          "discovery": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "links",
                "pagination"
              ]
            },
            "description": "`[\"links\"]`: the crawl follows the links in each page's HTML. Older crawls can also list `pagination`."
          },
          "coverage": {
            "type": "object",
            "properties": {
              "pages_fetched": {
                "type": "integer",
                "description": "Pages fetched so far, successful or not, at most `max_pages`."
              },
              "pages_failed": {
                "type": "integer",
                "description": "Pages that failed to load."
              },
              "items_found": {
                "type": "integer",
                "description": "URLs returned so far, after duplicates and patterns. At most `max_items`."
              }
            }
          },
          "duplicates_removed": {
            "type": "integer",
            "description": "Links met again and dropped. Results never hold a duplicate."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "finished_at": {
            "type": "string",
            "format": "date-time",
            "description": "Absent while the crawl runs."
          },
          "results": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "url": {
                  "type": "string",
                  "format": "uri",
                  "description": "A URL the crawl found, normalized: scheme, `www.` and a trailing slash do not make two URLs, and tracking parameters such as `utm_*` are removed."
                },
                "content_status": {
                  "type": "string",
                  "enum": [
                    "pending",
                    "fetched",
                    "failed"
                  ],
                  "description": "With `output_format` only."
                },
                "content_url": {
                  "type": "string",
                  "description": "With `output_format` only, once fetched: the path to read the page."
                }
              }
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Send it as `cursor` on the next read. Never `null` while the crawl runs; `null` once it has ended and its last results were read."
          }
        }
      },
      "CrawlSummary": {
        "type": "object",
        "description": "A crawl and its coverage so far.",
        "properties": {
          "crawl_id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "running",
              "completed",
              "stopped",
              "failed"
            ],
            "description": "`completed`: nothing was left to open, or a limit was reached (see `stop_reason`). `stopped`: you stopped it. `failed`: see `error`."
          },
          "stop_reason": {
            "type": "string",
            "enum": [
              "max_items",
              "max_pages",
              "user"
            ],
            "description": "Why the crawl ended early. Absent when nothing was left to open, and when the crawl failed."
          },
          "error": {
            "type": "object",
            "description": "Present only when `status` is `failed`. Older crawls can carry `domain_not_allowed` (the domain was not prepared for Extract).",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "seed_unreachable",
                  "no_items_found",
                  "insufficient_credits",
                  "internal_error",
                  "domain_not_allowed"
                ]
              },
              "detail": {
                "type": "string"
              }
            }
          },
          "url": {
            "type": "string",
            "format": "uri",
            "maxLength": 2048,
            "description": "The start URL: a public `http` or `https` URL. The crawl starts on this exact URL and stays on its domain, subdomains included."
          },
          "depth": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100000,
            "description": "How many link steps to follow from the start URL. At `1`, the links on the start page. Omit it to follow links until `max_items` or `max_pages` stops the crawl."
          },
          "max_items": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100000,
            "default": 100,
            "description": "Stop after keeping this many URLs."
          },
          "max_pages": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100000,
            "default": 100,
            "description": "Stop after fetching this many pages. Caps what the crawl costs. If both limits are reached together, `stop_reason` is `max_pages`."
          },
          "include_patterns": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Keep, and open, only URLs that contain at least one of these texts, such as `/product/`. The start URL is never returned."
          },
          "exclude_patterns": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Drop URLs that contain any of these texts, even when they match an include pattern."
          },
          "output_format": {
            "type": "string",
            "enum": [
              "html"
            ],
            "description": "`html` also returns the HTML of each kept page. Omit it to get URLs only."
          },
          "discovery": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "links",
                "pagination"
              ]
            },
            "description": "`[\"links\"]`: the crawl follows the links in each page's HTML. Older crawls can also list `pagination`."
          },
          "coverage": {
            "type": "object",
            "properties": {
              "pages_fetched": {
                "type": "integer",
                "description": "Pages fetched so far, successful or not, at most `max_pages`."
              },
              "pages_failed": {
                "type": "integer",
                "description": "Pages that failed to load."
              },
              "items_found": {
                "type": "integer",
                "description": "URLs returned so far, after duplicates and patterns. At most `max_items`."
              }
            }
          },
          "duplicates_removed": {
            "type": "integer",
            "description": "Links met again and dropped. Results never hold a duplicate."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "finished_at": {
            "type": "string",
            "format": "date-time",
            "description": "Absent while the crawl runs."
          }
        }
      }
    }
  }
}
