{
  "openapi": "3.1.0",
  "info": {
    "title": "Vermin API",
    "version": "1.0.0",
    "description": "Web data for AI: scrape, crawl, map, search, extract and brand intelligence.\nEvery response reports `credits_used`. Failed requests cost 0 credits.\n\n**Authentication.** Send an API key as `Authorization: Bearer vmn_live_…`. Keys are created self-serve\nin the dashboard (free plan, no card) and are scoped to one organization; playground keys also carry\ntheir own credit budget.\n\n**Errors.** Every 4xx/5xx response uses the `Error` schema: `success: false`, a machine-readable\n`error.code` (stable enum), a human-readable `error.message` and, when retrying makes sense,\n`error.retry_after_ms`. Branch on `error.code`, not on the message.\n\n**Rate limits.** Authenticated `/v1` responses carry IETF `RateLimit-Policy` (`\"default\";q=<requests>;w=60`)\nand, when the request used the per-minute bucket, `RateLimit` (`\"default\";r=<remaining>;t=<seconds>`).\nA `429 rate_limited` also sends `Retry-After` (seconds). See https://vermin.dev/docs/rate-limits.\n\n**Versioning.** The major version is in the URL path (`/v1`). Within a major version changes are\nadditive only: new endpoints, optional request fields, response fields and error codes. Breaking\nchanges ship as a new path version (`/v2`). A deprecated operation is marked `deprecated: true` here and\nits responses carry `Deprecation` (RFC 9745) and `Sunset` (RFC 8594) headers at least 90 days before\nremoval, with a `Link: <…>; rel=\"deprecation\"` to the migration notes.\n",
    "contact": {
      "name": "Vermin",
      "url": "https://vermin.dev/contact",
      "email": "hello@vermin.dev"
    },
    "termsOfService": "https://vermin.dev/terms",
    "x-api-versioning": {
      "strategy": "url-path",
      "current": "v1",
      "deprecationHeaders": [
        "Deprecation",
        "Sunset"
      ],
      "minimumNoticeDays": 90
    }
  },
  "externalDocs": {
    "description": "Vermin docs",
    "url": "https://vermin.dev/docs"
  },
  "servers": [
    {
      "url": "https://api.vermin.dev"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "paths": {
    "/v1/scrape": {
      "post": {
        "operationId": "scrape",
        "summary": "Scrape a single URL",
        "description": "Fetch one URL and return clean, LLM-ready content (markdown by default; also html, text, links, screenshot, token-counted chunks, metadata or brand). Starts with plain HTTP and escalates to a headless browser only when the page needs it. 1 credit per page, browser included; failed pages cost 0.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ScrapeRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScrapeResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/v1/crawl": {
      "post": {
        "operationId": "startCrawl",
        "summary": "Start an asynchronous crawl",
        "description": "Start crawling a whole site. Sitemap-first discovery with include/exclude globs, robots.txt respected. Returns a job immediately; follow it with getCrawl, streamCrawl (SSE) or a signed webhook. 1 credit per page scraped successfully.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CrawlRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CrawlJob"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/v1/crawl/{id}": {
      "get": {
        "operationId": "getCrawl",
        "summary": "Get crawl status and a page of results",
        "description": "Return a crawl job's status and progress plus one page of scraped documents. Pass `next` from the previous response as `cursor` to page through results.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CrawlStatus"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "operationId": "cancelCrawl",
        "summary": "Cancel a crawl",
        "description": "Stop a running crawl. Pages already scraped stay available and are charged; queued pages are dropped at no cost.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CrawlJob"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/v1/crawl/{id}/events": {
      "get": {
        "operationId": "streamCrawl",
        "summary": "Server-sent events stream of crawl pages and status",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Last-Event-ID",
            "in": "header",
            "schema": {
              "type": "string"
            },
            "description": "Resume after this event id (sent automatically by EventSource on reconnect)."
          }
        ],
        "description": "Events: `page` (data = Document JSON), `status` (data = CrawlStatus JSON without `data`),\n`done` (data = final CrawlStatus JSON without `data`). Each event has an `id` usable as Last-Event-ID.\nThe stream replays every event after Last-Event-ID (or from the start), then follows the crawl live\nand closes after `done`. Comment lines (`: keep-alive`) are sent every 15s.\n",
        "responses": {
          "200": {
            "description": "SSE stream (page, status, done events)",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/v1/map": {
      "post": {
        "operationId": "map",
        "summary": "Discover URLs on a site",
        "description": "Costs 3 credits per 10 HTTP fetches the map made (robots.txt, sitemap files, pages fetched for links,\nand the rendered-links fallback each count as one), rounded up, with a minimum of 3, however many URLs\ncome back. The worst case (42 credits with `sitemap: include`) is reserved up front and the actual\ncount is settled. With fewer credits left than the worst case, the map runs on the largest multiple of 3\nyou can afford (at least 3; otherwise `insufficient_credits`), stops fetching once that is used up,\nand returns what it found with `budgetLimited: true`. A map whose start page fails to load costs 0.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MapRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MapResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/v1/brand": {
      "get": {
        "operationId": "brand",
        "summary": "Brand identity for a domain or URL",
        "description": "Return a company's logo, icon, colours, fonts, name and social links for a domain. 5 credits for a fresh lookup, 1 when served from cache.",
        "parameters": [
          {
            "name": "domain",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "stripe.com",
            "description": "A domain or URL (IDNs accepted). Brands are per host: the path is ignored and a leading www. is dropped."
          },
          {
            "name": "maxAge",
            "in": "query",
            "schema": {
              "type": "integer",
              "description": "Accept a cached result up to this age (ms). Default 30 days. 0 forces a fresh lookup. Cached results older than 7 days are refreshed in the background."
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BrandResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    }
  },
  "webhooks": {
    "crawlEvent": {
      "post": {
        "operationId": "crawlEvent",
        "summary": "Crawl lifecycle and page events",
        "description": "Sent for `crawl.started`, `crawl.page`, `crawl.completed`, `crawl.failed` and `crawl.cancelled`.\n\n**Signature.** Every delivery carries `Vermin-Signature: t=<unix seconds>,v1=<hex>` where `<hex>` is\nthe lowercase hex HMAC-SHA256 of `\"<t>.<raw request body>\"` keyed with the signing secret. Verify it\nwith a constant-time comparison over the raw bytes (before parsing JSON) and reject timestamps more\nthan 300 seconds from your clock. During secret rotation a header may carry several `v1=` values;\naccept if any matches. Every SDK ships a `verifyWebhook` helper; shared test vectors live in\n`packages/spec/webhook-test-vectors.json`.\n\n**Delivery.** Respond with any 2xx within 10 seconds. Other responses and timeouts are retried with\nexponential backoff (10s, 20s, 40s … up to 1h between attempts, 8 attempts in total); a `410 Gone`\nstops retries. Undeliverable events are recorded as dead letters in the dashboard. Deliveries may\narrive out of order or more than once: de-duplicate on `id` and order on `createdAt`.\n",
        "security": [],
        "parameters": [
          {
            "name": "Vermin-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "t=1790000000,v1=e19cbcc55e778366b144a95f57151266a21ffbf9da41a16e8654d511fe1afbec"
          },
          {
            "name": "Vermin-Event",
            "in": "header",
            "schema": {
              "type": "string"
            },
            "description": "The event type, e.g. `crawl.page`."
          },
          {
            "name": "Vermin-Delivery-Attempt",
            "in": "header",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Any 2xx acknowledges the delivery."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "vmn_live_<key>",
        "description": "API key, e.g. vmn_live_.... Each key is bound to one organization and can call every /v1 operation that organization's plan allows."
      }
    },
    "headers": {
      "RateLimit-Policy": {
        "description": "IETF RateLimit-Policy: requests per 60s window, e.g. `\"default\";q=60;w=60`.",
        "schema": {
          "type": "string"
        }
      },
      "RateLimit": {
        "description": "IETF RateLimit: remaining requests and seconds until the bucket refills, e.g. `\"default\";r=59;t=1`.",
        "schema": {
          "type": "string"
        }
      },
      "Retry-After": {
        "description": "Seconds to wait before retrying.",
        "schema": {
          "type": "integer"
        }
      },
      "X-Request-Id": {
        "description": "Request id; quote it to support.",
        "schema": {
          "type": "string"
        }
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "schema": {
          "type": "string",
          "maxLength": 255
        }
      }
    },
    "responses": {
      "Error": {
        "description": "Error (any other 4xx/5xx, e.g. 409/422 idempotency conflicts, 502 blocked_by_site, 504 timeout)",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "BadRequest": {
        "description": "invalid_request or blocked_url: the request body, query or URL is invalid.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "unauthorized: missing, malformed or revoked API key.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "PaymentRequired": {
        "description": "insufficient_credits or spend_cap_reached: top up, raise the cap or upgrade.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "not_found: unknown route, crawl id or disabled feature.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "rate_limited: per-minute request or concurrency limit reached. Wait Retry-After seconds.",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/Retry-After"
          },
          "RateLimit": {
            "$ref": "#/components/headers/RateLimit"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimit-Policy"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "ServerError": {
        "description": "internal: unexpected server error. Safe to retry with the same Idempotency-Key.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "success",
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "invalid_request",
                  "unauthorized",
                  "forbidden",
                  "not_found",
                  "not_implemented",
                  "rate_limited",
                  "insufficient_credits",
                  "spend_cap_reached",
                  "blocked_url",
                  "fetch_failed",
                  "blocked_by_site",
                  "timeout",
                  "unsupported_content",
                  "capacity_exceeded",
                  "not_configured",
                  "extract_failed",
                  "internal"
                ],
                "description": "insufficient_credits: the org lacks credits for the request's worst-case cost, or a playground key has spent its own credit budget. capacity_exceeded: our browser capacity is exhausted (retry after retry_after_ms; the site did not block you). not_configured: the requested tier or feature (e.g. premium, search, extract) is unavailable on this deployment. extract_failed: the model could not produce JSON matching your schema (after one retry)."
              },
              "message": {
                "type": "string"
              },
              "retry_after_ms": {
                "type": "integer"
              }
            }
          },
          "credits_used": {
            "type": "integer",
            "const": 0
          },
          "request_id": {
            "type": "string"
          }
        }
      },
      "Format": {
        "type": "string",
        "enum": [
          "markdown",
          "html",
          "rawHtml",
          "text",
          "links",
          "screenshot",
          "screenshotFull",
          "chunks",
          "metadata",
          "brand"
        ]
      },
      "Action": {
        "type": "object",
        "required": [
          "type"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "wait",
              "click",
              "scroll",
              "type",
              "press"
            ]
          },
          "selector": {
            "type": "string"
          },
          "text": {
            "type": "string"
          },
          "ms": {
            "type": "integer",
            "maximum": 30000
          },
          "key": {
            "type": "string"
          }
        }
      },
      "ScrapeOptions": {
        "type": "object",
        "properties": {
          "formats": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Format"
            },
            "default": [
              "markdown",
              "metadata"
            ],
            "description": "Basic metadata (title, description, language, canonical, status) is always returned; `metadata` adds parsed JSON-LD."
          },
          "onlyMainContent": {
            "type": "boolean",
            "default": true
          },
          "siteMarkdown": {
            "type": "boolean",
            "default": false,
            "description": "Use the site's own markdown when it serves one (`Accept: text/markdown`, e.g. many docs sites) and it passes the quality check; otherwise the HTML is extracted as usual. About 2x faster where available, but usually with less metadata (no language or canonical URL). Ignored with the `html`, `rawHtml` or `links` formats."
          },
          "includeTags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "excludeTags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "tier": {
            "type": "string",
            "enum": [
              "auto",
              "fetch",
              "render",
              "premium"
            ],
            "default": "auto",
            "description": "auto escalates fetch → render when the page needs it. `premium` is not available yet."
          },
          "allowPremium": {
            "type": "boolean",
            "default": false,
            "description": "Not available yet; requests with it set are refused."
          },
          "actions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Action"
            },
            "maxItems": 20
          },
          "waitFor": {
            "type": "integer",
            "maximum": 30000
          },
          "timeout": {
            "type": "integer",
            "minimum": 1000,
            "maximum": 60000,
            "default": 30000
          },
          "maxAge": {
            "type": "integer",
            "default": 172800000,
            "description": "Serve a cached copy younger than this (ms). Default 2 days; 0 forces a fresh scrape. Cache hits are served from the nearest data center."
          },
          "headers": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "location": {
            "type": "object",
            "properties": {
              "country": {
                "type": "string"
              },
              "languages": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          },
          "chunkSize": {
            "type": "integer",
            "minimum": 200,
            "maximum": 8000,
            "default": 1500
          },
          "ignoreRobots": {
            "type": "boolean",
            "default": false,
            "description": "Skip the robots.txt check. Only set this when you have permission to fetch the page."
          }
        }
      },
      "ScrapeRequest": {
        "allOf": [
          {
            "type": "object",
            "required": [
              "url"
            ],
            "properties": {
              "url": {
                "type": "string",
                "format": "uri"
              }
            }
          },
          {
            "$ref": "#/components/schemas/ScrapeOptions"
          }
        ]
      },
      "Chunk": {
        "type": "object",
        "required": [
          "text",
          "index"
        ],
        "properties": {
          "index": {
            "type": "integer"
          },
          "text": {
            "type": "string"
          },
          "headingPath": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "tokens": {
            "type": "integer"
          }
        }
      },
      "Metadata": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "language": {
            "type": "string"
          },
          "canonicalUrl": {
            "type": "string"
          },
          "statusCode": {
            "type": "integer"
          },
          "contentType": {
            "type": "string"
          },
          "publishedAt": {
            "type": "string"
          },
          "author": {
            "type": "string"
          },
          "ogImage": {
            "type": "string"
          }
        }
      },
      "Quality": {
        "type": "object",
        "description": "Why content may be thin, and what the pipeline did about it.",
        "properties": {
          "score": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          },
          "issues": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "thin_content",
                "js_required",
                "bot_wall",
                "cookie_wall",
                "login_wall",
                "soft_404",
                "truncated",
                "non_html"
              ]
            }
          },
          "escalations": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "reason": {
            "type": "string",
            "description": "Why the returned tier was used or why escalation stopped, in words."
          }
        }
      },
      "Document": {
        "type": "object",
        "required": [
          "url"
        ],
        "properties": {
          "url": {
            "type": "string"
          },
          "finalUrl": {
            "type": "string"
          },
          "markdown": {
            "type": "string"
          },
          "html": {
            "type": "string"
          },
          "rawHtml": {
            "type": "string"
          },
          "text": {
            "type": "string"
          },
          "links": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "screenshot": {
            "type": "string",
            "description": "Signed URL (expires in 24h)"
          },
          "chunks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Chunk"
            }
          },
          "metadata": {
            "$ref": "#/components/schemas/Metadata"
          },
          "brand": {
            "$ref": "#/components/schemas/Brand"
          },
          "tier": {
            "type": "string",
            "enum": [
              "fetch",
              "render",
              "premium"
            ]
          },
          "cached": {
            "type": "boolean"
          },
          "quality": {
            "$ref": "#/components/schemas/Quality"
          }
        }
      },
      "ScrapeResponse": {
        "type": "object",
        "required": [
          "success",
          "data",
          "credits_used",
          "request_id"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/Document"
          },
          "credits_used": {
            "type": "integer"
          },
          "latency_ms": {
            "type": "integer"
          },
          "request_id": {
            "type": "string"
          }
        }
      },
      "CrawlRequest": {
        "type": "object",
        "required": [
          "url"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri"
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100000,
            "default": 100
          },
          "maxDepth": {
            "type": "integer",
            "minimum": 0,
            "maximum": 20,
            "description": "Link hops from the start URL (sitemap URLs count as depth 1). 0 = only the start URL."
          },
          "includePaths": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Path globs matched against path + query: `*` within a segment, `**` across segments, a trailing `$` anchors the end; patterns starting with `/` match from the start of the path."
          },
          "excludePaths": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Path globs (same syntax as includePaths) never crawled."
          },
          "allowSubdomains": {
            "type": "boolean",
            "default": false
          },
          "sitemap": {
            "type": "string",
            "enum": [
              "include",
              "skip",
              "only"
            ],
            "default": "include"
          },
          "removeBoilerplate": {
            "type": "boolean",
            "default": true,
            "description": "Strip content repeated across pages (nav, footers)"
          },
          "ignoreRobots": {
            "type": "boolean",
            "default": false
          },
          "scrapeOptions": {
            "$ref": "#/components/schemas/ScrapeOptions"
          },
          "webhook": {
            "type": "object",
            "description": "Deliver crawl events to this URL as signed POSTs (see the `crawlEvent` webhook). Deliveries are signed\nwith your organisation's webhook signing secret (Dashboard → Webhooks); endpoints registered in the\ndashboard also receive `crawl.*` events, signed with their own secret.\n",
            "properties": {
              "url": {
                "type": "string",
                "format": "uri"
              },
              "events": {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "started",
                    "page",
                    "completed",
                    "failed",
                    "cancelled"
                  ]
                },
                "description": "Default: all events."
              },
              "metadata": {
                "type": "object",
                "additionalProperties": true,
                "description": "Echoed back in every delivery."
              }
            }
          }
        }
      },
      "CrawlJob": {
        "type": "object",
        "required": [
          "id",
          "status"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "running",
              "completed",
              "failed",
              "cancelled"
            ]
          },
          "url": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CrawlStatus": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CrawlJob"
          },
          {
            "type": "object",
            "properties": {
              "total": {
                "type": "integer"
              },
              "completed": {
                "type": "integer"
              },
              "failed": {
                "type": "integer"
              },
              "credits_used": {
                "type": "integer"
              },
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Document"
                }
              },
              "next": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Cursor for the next page of results; null once the crawl is finished and every page has been returned."
              },
              "error": {
                "type": "object",
                "description": "Why the crawl failed or stopped early (e.g. spend_cap_reached, insufficient_credits, blocked_by_site). Pages crawled before the stop are still returned.",
                "required": [
                  "code",
                  "message"
                ],
                "properties": {
                  "code": {
                    "type": "string"
                  },
                  "message": {
                    "type": "string"
                  }
                }
              }
            }
          }
        ]
      },
      "WebhookEvent": {
        "type": "object",
        "required": [
          "id",
          "type",
          "createdAt",
          "data"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Unique event id; identical across retries of the same event."
          },
          "type": {
            "type": "string",
            "enum": [
              "crawl.started",
              "crawl.page",
              "crawl.completed",
              "crawl.failed",
              "crawl.cancelled"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "data": {
            "type": "object",
            "description": "crawl.page: `{ id, url, document }` (document = Document, boilerplate removed as in GET /v1/crawl/{id}); other events: CrawlStatus without `data`.",
            "required": [
              "id"
            ],
            "properties": {
              "id": {
                "type": "string",
                "description": "Crawl id"
              },
              "url": {
                "type": "string"
              },
              "document": {
                "$ref": "#/components/schemas/Document"
              },
              "status": {
                "type": "string",
                "enum": [
                  "queued",
                  "running",
                  "completed",
                  "failed",
                  "cancelled"
                ]
              },
              "total": {
                "type": "integer"
              },
              "completed": {
                "type": "integer"
              },
              "failed": {
                "type": "integer"
              },
              "credits_used": {
                "type": "integer"
              },
              "error": {
                "type": "object",
                "properties": {
                  "code": {
                    "type": "string"
                  },
                  "message": {
                    "type": "string"
                  }
                }
              }
            },
            "additionalProperties": true
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true,
            "description": "`webhook.metadata` from the crawl request."
          }
        }
      },
      "MapRequest": {
        "type": "object",
        "required": [
          "url"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri"
          },
          "search": {
            "type": "string",
            "description": "Rank URLs by relevance to this query"
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100000,
            "default": 5000
          },
          "includeSubdomains": {
            "type": "boolean",
            "default": false
          },
          "sitemap": {
            "type": "string",
            "enum": [
              "include",
              "skip",
              "only"
            ],
            "default": "include"
          },
          "includePaths": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Only return URLs whose path matches one of these globs (CrawlRequest.includePaths syntax)."
          },
          "excludePaths": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Never return URLs whose path matches one of these globs."
          }
        }
      },
      "MapResponse": {
        "type": "object",
        "required": [
          "success",
          "links",
          "credits_used"
        ],
        "properties": {
          "success": {
            "type": "boolean"
          },
          "links": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "url"
              ],
              "properties": {
                "url": {
                  "type": "string"
                },
                "title": {
                  "type": "string"
                },
                "source": {
                  "type": "string",
                  "enum": [
                    "sitemap",
                    "link",
                    "render"
                  ]
                }
              }
            }
          },
          "budgetLimited": {
            "type": "boolean",
            "description": "Set (true) when discovery stopped early because the credits available couldn't cover more fetches; `links` holds what was found, and `credits_used` never exceeds what could be reserved."
          },
          "credits_used": {
            "type": "integer"
          },
          "request_id": {
            "type": "string"
          }
        }
      },
      "SearchRequest": {
        "type": "object",
        "required": [
          "query"
        ],
        "properties": {
          "query": {
            "type": "string",
            "maxLength": 500
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 50,
            "default": 10
          },
          "country": {
            "type": "string"
          },
          "language": {
            "type": "string"
          },
          "timeRange": {
            "type": "string",
            "enum": [
              "day",
              "week",
              "month",
              "year"
            ]
          },
          "scrapeOptions": {
            "$ref": "#/components/schemas/ScrapeOptions",
            "description": "If set, each result is scraped (billed as scrapes)"
          }
        }
      },
      "SearchResponse": {
        "type": "object",
        "required": [
          "success",
          "results",
          "credits_used"
        ],
        "properties": {
          "success": {
            "type": "boolean"
          },
          "results": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "url",
                "title"
              ],
              "properties": {
                "url": {
                  "type": "string"
                },
                "title": {
                  "type": "string"
                },
                "snippet": {
                  "type": "string"
                },
                "position": {
                  "type": "integer"
                },
                "date": {
                  "type": "string",
                  "description": "Publication date as reported by the search provider (ISO 8601 when available, otherwise its text such as 3 days ago)"
                },
                "source": {
                  "type": "string",
                  "description": "Site or publisher name when the provider reports one"
                },
                "document": {
                  "$ref": "#/components/schemas/Document"
                }
              }
            }
          },
          "credits_used": {
            "type": "integer"
          },
          "request_id": {
            "type": "string"
          }
        }
      },
      "ExtractRequest": {
        "type": "object",
        "required": [
          "urls"
        ],
        "properties": {
          "urls": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uri"
            },
            "minItems": 1,
            "maxItems": 25
          },
          "schema": {
            "type": "object",
            "additionalProperties": true,
            "description": "JSON Schema for the output"
          },
          "prompt": {
            "type": "string",
            "maxLength": 4000
          },
          "scrapeOptions": {
            "$ref": "#/components/schemas/ScrapeOptions"
          }
        }
      },
      "ExtractResponse": {
        "type": "object",
        "required": [
          "success",
          "data",
          "credits_used"
        ],
        "properties": {
          "success": {
            "type": "boolean"
          },
          "data": {
            "description": "JSON matching the requested schema"
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "url": {
                  "type": "string"
                },
                "ok": {
                  "type": "boolean"
                }
              }
            }
          },
          "credits_used": {
            "type": "integer"
          },
          "request_id": {
            "type": "string"
          }
        }
      },
      "Brand": {
        "type": "object",
        "properties": {
          "domain": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "logo": {
            "$ref": "#/components/schemas/BrandImage"
          },
          "icon": {
            "$ref": "#/components/schemas/BrandImage"
          },
          "colors": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "hex"
              ],
              "properties": {
                "hex": {
                  "type": "string"
                },
                "role": {
                  "type": "string",
                  "enum": [
                    "primary",
                    "accent",
                    "background",
                    "text"
                  ]
                },
                "textColor": {
                  "type": "string",
                  "description": "Readable text colour on this background"
                },
                "contrast": {
                  "type": "number",
                  "description": "WCAG contrast ratio of textColor on hex (1-21)."
                }
              }
            }
          },
          "logos": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BrandImage"
            },
            "description": "Every image found (icon and logo), each tagged with its type."
          },
          "fonts": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "family": {
                  "type": "string"
                },
                "role": {
                  "type": "string",
                  "enum": [
                    "heading",
                    "body",
                    "mono"
                  ]
                }
              }
            }
          },
          "socials": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "network": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                }
              }
            }
          },
          "confidence": {
            "type": "number"
          }
        }
      },
      "BrandImage": {
        "type": "object",
        "required": [
          "url"
        ],
        "properties": {
          "url": {
            "type": "string",
            "description": "Vermin-hosted copy when available (stable, sanitised), else the site's own URL."
          },
          "sourceUrl": {
            "type": "string",
            "description": "The image's original URL on the site (absent for inline SVGs)."
          },
          "type": {
            "type": "string",
            "enum": [
              "logo",
              "icon",
              "wordmark"
            ],
            "description": "icon: square mark for avatars. logo: the full lockup. wordmark: a text-only logo."
          },
          "format": {
            "type": "string",
            "enum": [
              "png",
              "jpeg",
              "gif",
              "webp",
              "ico",
              "svg",
              "avif"
            ]
          },
          "width": {
            "type": "integer"
          },
          "height": {
            "type": "integer"
          },
          "background": {
            "type": "string",
            "enum": [
              "light",
              "dark",
              "any"
            ],
            "description": "Backgrounds the image reads well on: light (dark artwork on transparency), dark (light artwork), any (opaque or mixed)."
          },
          "source": {
            "type": "string"
          }
        }
      },
      "BrandResponse": {
        "type": "object",
        "required": [
          "success",
          "data",
          "credits_used"
        ],
        "properties": {
          "success": {
            "type": "boolean"
          },
          "data": {
            "$ref": "#/components/schemas/Brand"
          },
          "cached": {
            "type": "boolean"
          },
          "stale": {
            "type": "boolean",
            "description": "A cached result older than maxAge, served because a fresh lookup failed transiently."
          },
          "credits_used": {
            "type": "integer"
          },
          "request_id": {
            "type": "string"
          }
        }
      }
    }
  }
}