{"openapi":"3.1.0","info":{"title":"SuperScraper API","version":"0.1.0","description":"The web data layer agents can trust. Web scraping, structured extraction and company data. Page scrapes always report how the page was fetched (`metadata.fetchMethod`). Structured data, when found, carries `_completeness`: how complete the result is, one score per result, not a verified-accuracy score. `/v1/resolve` carries `_match_score` (how the company was matched) instead. Search and parse results carry no score. Authenticate with an `ss_live_` API key via the `x-api-key` header or `Authorization: Bearer <key>`.","contact":{"url":"https://superscraper.dev"},"license":{"name":"Proprietary"}},"servers":[{"url":"https://api.superscraper.dev","description":"Production"},{"url":"http://localhost:3001","description":"Local development"}],"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"ss_live_<key>","description":"Pass your SuperScraper API key as a Bearer token: `Authorization: Bearer ss_live_<key>`."},"apiKeyHeader":{"type":"apiKey","in":"header","name":"x-api-key","description":"Alternatively pass your SuperScraper API key in the `x-api-key` header."}},"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"Human-readable error message"}}},"ListingData":{"type":"object","description":"Structured listing data extracted from a page (JSON-LD / OG / LLM)","properties":{"name":{"type":"string"},"description":{"type":"string"},"address":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"url":{"type":"string","format":"uri"},"rating":{"type":"number"},"reviewCount":{"type":"integer"},"_completeness":{"type":"number","minimum":0,"maximum":1,"description":"0–1: weighted share of core fields (name, phone, address, city, state) and bonus fields (rating, price_range, description, photo_urls) found. Not a verified-accuracy score."},"_extraction_method":{"type":"string","description":"`json-ld` or `regex-cascade`"}},"additionalProperties":true},"ProviderErrorClass":{"type":["string","null"],"enum":["timeout","http_error","network","blocked","robots","provider_error","parse_error","js_shell","empty","cost_ceiling","unknown",null],"description":"Why an attempt produced no usable answer; null when the attempt succeeded."},"ProviderAttempt":{"type":"object","required":["provider","ok","latency_ms","error_class"],"properties":{"provider":{"type":"string","description":"Backend that was tried"},"ok":{"type":"boolean"},"latency_ms":{"type":"integer","minimum":0},"error_class":{"$ref":"#/components/schemas/ProviderErrorClass"}}},"CascadeRung":{"type":"string","enum":["unblock","plain","playwright","spider","browserbase"],"description":"Cascade rung, listed in escalation order (level 0 to 4)."},"CascadeAttempt":{"type":"object","required":["rung","level","ok","latency_ms","error_class"],"properties":{"rung":{"$ref":"#/components/schemas/CascadeRung"},"level":{"type":"integer","minimum":0,"maximum":4},"ok":{"type":"boolean"},"latency_ms":{"type":"integer","minimum":0},"error_class":{"$ref":"#/components/schemas/ProviderErrorClass"}}},"UnblockAttempt":{"type":"object","required":["provider","ok","latency_ms","error_class","outcome","cost_class"],"properties":{"provider":{"type":"string"},"ok":{"type":"boolean"},"latency_ms":{"type":"integer","minimum":0},"error_class":{"$ref":"#/components/schemas/ProviderErrorClass"},"outcome":{"type":"string","enum":["ok","blocked","failed"]},"cost_class":{"type":"string","enum":["free","paid"],"description":"Which fetch tier ran: `paid` means a third-party fetch provider received the URL, `free` means it did not. A tier label for data-handling transparency; it carries no amount."}}},"ScrapeCascade":{"type":"object","required":["level","attempts"],"properties":{"level":{"type":["integer","null"],"minimum":0,"maximum":4,"description":"Level of the rung that served the content; null when nothing was served or no rung ran (cache hit)."},"attempts":{"type":"array","items":{"$ref":"#/components/schemas/CascadeAttempt"}}}},"ScrapeFallback":{"type":"object","required":["used","from","to"],"description":"Present only when a later rung served COMPLETE content after an earlier rung failed. Never present with `partial`.","properties":{"used":{"type":"boolean","const":true},"from":{"$ref":"#/components/schemas/CascadeRung"},"to":{"$ref":"#/components/schemas/CascadeRung"}}},"ScrapeResult":{"type":"object","required":["url","ok"],"properties":{"url":{"type":"string","format":"uri"},"ok":{"type":"boolean"},"markdown":{"type":"string","description":"Page content as Markdown"},"html":{"type":"string","description":"Raw HTML (only present when `includeHtml: true`)"},"listing":{"$ref":"#/components/schemas/ListingData"},"statusCode":{"type":"integer"},"error":{"type":"string"},"metadata":{"type":"object","properties":{"fetchMethod":{"type":"string","enum":["fetch","playwright","spider","browserbase"]},"latencyMs":{"type":"integer"}}}}},"TechEntry":{"type":"object","description":"One detected technology. `tech_stack` lists every technology found across the crawled pages, sorted by confidence (highest first), then category, then name.","required":["name","category","confidence"],"properties":{"name":{"type":"string"},"category":{"type":"string"},"confidence":{"type":"number","minimum":0,"maximum":1},"version":{"type":"string","description":"Version, when the page exposes one (for example a generator meta tag)."}}},"TechMeta":{"type":"object","description":"How complete the tech scan was. `partial` is true when any reason is present: a cap or budget cut the scan of at least one page short, so `tech_stack` may be missing technologies.","required":["partial","reasons","scanned_bytes","duration_ms"],"properties":{"partial":{"type":"boolean"},"reasons":{"type":"array","items":{"type":"string","enum":["time_budget","html_cap","tag_scan_cap","value_cap","item_cap","inline_script_cap","worker_terminated","queue_timeout","worker_error","request_budget","deep_budget","request_cap","render_failed","tenant_cooldown"]},"description":"time_budget: the time limit ran out before every fingerprint was tried. html_cap: the page is larger than the HTML sample (first 384 KB + last 128 KB). tag_scan_cap: the page is larger than 4 MB; script and meta tags past that were not read. value_cap: a header, meta, script URL, cookie or URL value was cut at 2000 characters. item_cap: more than 500 script URLs, meta tags, cookies, variables or headers, or more than 2000 tags of one kind. inline_script_cap: inline script text past 64 KB per block or 512 KB in total was not read. worker_terminated: the scan was stopped at the hard time limit. queue_timeout: no scanner was free before the time limit. worker_error: the scanner failed. request_budget: the request-wide time budget was spent, so later pages were not scanned. deep_budget (deep scan): the total time budget (default 15000 ms) ran out before every page was rendered or matched. request_cap (deep scan): a page made more than 300 requests; the rest were not loaded. render_failed (deep scan): the page did not render, so only the plain fetch was matched. tenant_cooldown: scans for your account kept hitting the hard time limit, so fingerprints are skipped for a cool-down (default 10 minutes after 5 such scans in 10 minutes, counted per API server)."},"scanned_bytes":{"type":"integer","description":"HTML bytes the HTML fingerprints read, summed over the scanned pages."},"duration_ms":{"type":"integer","description":"Time spent fingerprinting, summed over pages."},"sources":{"type":"array","items":{"type":"string","enum":["html","runtime","network"]},"description":"Deep scan only: the evidence it read. html: the page HTML; runtime: globals and cookies after the page ran; network: the requests the page made."}}},"BrandMeta":{"type":"object","description":"Which caps cut the brand analysis short. `partial` is true when any reason is present; colors and fonts may then be missing.","required":["partial","reasons"],"properties":{"partial":{"type":"boolean"},"reasons":{"type":"array","items":{"type":"string","enum":["stylesheet_budget","stylesheet_count_cap","stylesheet_byte_cap","stylesheet_total_byte_cap","stylesheet_timeout","css_cap","var_budget","var_depth_cap","css_value_cap","button_scan_cap","jsonld_node_cap"]},"description":"stylesheet_budget: the 6 second budget for linked stylesheets ran out. stylesheet_count_cap: the page links more than 40 stylesheets. stylesheet_byte_cap: a stylesheet was cut at 512 KB. stylesheet_total_byte_cap: the 2 MB total for stylesheets was reached. stylesheet_timeout: a stylesheet did not arrive within its 4 second timeout. css_cap: CSS past 3 million characters was not analysed. var_budget: the limit of 5000 CSS var() substitutions per page was reached. var_depth_cap: a var() chain was deeper than 7 levels. css_value_cap: a CSS value, or its var() expansion, was cut at 2048 characters, or a custom property longer than 500 characters was dropped. button_scan_cap: more than 500 inline-styled buttons or links; the rest were not read. jsonld_node_cap: a JSON-LD block had more than 500 nodes; the logo search stopped there."}}},"TeamMember":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"role":{"type":"string"},"email":{"type":"string","format":"email"}}},"SocialLinks":{"type":"object","properties":{"linkedin":{"type":"string","format":"uri"},"twitter":{"type":"string","format":"uri"},"facebook":{"type":"string","format":"uri"},"instagram":{"type":"string","format":"uri"},"youtube":{"type":"string","format":"uri"}}},"EnrichWebsiteResponse":{"type":"object","required":["domain"],"properties":{"domain":{"type":"string"},"emails":{"type":"array","items":{"type":"string","format":"email"}},"phones":{"type":"array","items":{"type":"string"}},"social":{"$ref":"#/components/schemas/SocialLinks"},"tech_stack":{"type":"array","items":{"$ref":"#/components/schemas/TechEntry"}},"team":{"type":"array","items":{"$ref":"#/components/schemas/TeamMember"}},"pages_crawled":{"type":"integer"},"homepage":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"hero_image":{"type":"string","format":"uri"}}},"brand":{"type":"object","description":"Brand kit (colors, fonts, logo). Only present when `includeBrand: true`.","additionalProperties":true},"brand_meta":{"$ref":"#/components/schemas/BrandMeta"},"tech_meta":{"$ref":"#/components/schemas/TechMeta"},"_completeness":{"type":"number","minimum":0,"maximum":1,"description":"Share of 8 expected fields found. One score per result; not a verified-accuracy score."},"_extraction_method":{"type":"string","enum":["deterministic","deterministic+llm"]},"_latency_ms":{"type":"integer"}}},"CrawlJobStatus":{"type":"object","required":["id","status"],"properties":{"id":{"type":"string"},"url":{"type":"string","format":"uri"},"status":{"type":"string","enum":["queued","running","processing","completed","failed","cancelled"],"description":"Terminal states: completed | failed | cancelled."},"result_url":{"type":"string","format":"uri"},"pages_scraped":{"type":"integer"},"error":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"completed_at":{"type":"string","format":"date-time"}}},"UsageResponse":{"type":"object","properties":{"plan":{"type":"string","enum":["free","starter","pro","business","enterprise"]},"pages_scraped":{"type":"integer"},"pages_quota":{"type":"integer"},"llm_extractions":{"type":"integer"},"extract_quota":{"type":"integer"},"year_month":{"type":"string","example":"2026-06"},"credits":{"type":"object","description":"Whether this period's credit grant is applied in the ledger.","properties":{"state":{"type":"string","enum":["ok","grant_pending","grant_failed","reconciling","unavailable"]},"period":{"type":"string","example":"2026-06"},"prior_periods_unresolved":{"type":"boolean"}}}}}}},"paths":{"/v1/scrape":{"post":{"operationId":"scrape","summary":"Scrape a URL to Markdown","description":"Fetches a URL through the SmartScrape cascade (plain fetch → headless browser → hosted rendering) and returns Markdown plus optional structured listing data. PDF URLs are auto-detected and text-extracted. Non-PDF responses carry `cascade` (served level + every rung tried), `unblock.attempts[]`, and either `fallback` (a later rung served complete content) or `partial` + `partial_reason` (content incomplete), never both.","tags":["Scraping"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri","description":"The URL to scrape"},"formats":{"type":"array","items":{"type":"string","enum":["markdown","rawHtml","links","screenshot","json","branding","summary"]},"description":"Output formats. Default: [\"markdown\"]."},"actions":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","enum":["click","scroll","wait","write","press","screenshot","executeJavascript","pdf"],"description":"Action type. Presence of any action forces the Playwright tier. `executeJavascript` is coming soon and not callable yet: a request that includes it is refused with 400 action_not_available before anything runs or is charged."},"selector":{"type":"string","description":"CSS selector target (click/write/screenshot)"},"text":{"type":"string","description":"Text to type (write)"},"key":{"type":"string","description":"Key to press (press)"},"ms":{"type":"integer","description":"Milliseconds to wait (wait) or scroll amount (scroll)"},"script":{"type":"string","maxLength":20000,"description":"Reserved for executeJavascript, which is coming soon and not callable yet (400 action_not_available)."}},"required":["type"],"additionalProperties":false},"maxItems":20,"description":"Scripted browser actions (forces Playwright), at most 20. All actions together, plus waitFor, run at most 30 s. A request with screenshot actions always runs fresh: it never reads or writes the cache, and cannot be combined with minAge."},"schema":{"type":"object","additionalProperties":{},"description":"JSON schema for the `json` format (triggers LLM extraction)"},"jsonSchema":{"type":"object","additionalProperties":{},"description":"Alias of `schema`"},"includeHtml":{"type":"boolean","default":false,"description":"Include raw HTML in the response"},"extractListing":{"type":"boolean","default":true,"description":"Run JSON-LD/OG extraction to populate `listing`"},"cookies":{"type":"string","description":"Cookie string to inject (Netscape format or JSON array)"},"headers":{"type":"object","additionalProperties":{"type":"string"},"description":"Custom request headers"},"forcePlaywright":{"type":"boolean","default":false,"description":"Skip plain-fetch tier, go straight to Playwright"},"waitFor":{"type":"integer","description":"Extra ms to wait after page load (Playwright only). Capped at 30000; shares the 30 s budget with any actions."},"acceptPartial":{"type":"boolean","default":false,"description":"Return an auth-walled page instead of failing; the response then carries partial:true, partial_reason:\"auth_wall\""},"region":{"type":"string","description":"Unblock Router region hint (e.g. \"br\", \"mx\")"},"maxCostUsd":{"type":"number","description":"Unblock Router per-fetch cost ceiling"},"onlyMainContent":{"type":"boolean","description":"Strip nav/footer/boilerplate before returning markdown/html"},"includeTags":{"type":"array","items":{"type":"string"},"description":"Keep only these HTML tags when shaping content"},"excludeTags":{"type":"array","items":{"type":"string"},"description":"Drop these HTML tags when shaping content"},"removeBase64Images":{"type":"boolean","description":"Strip inline base64 images from HTML/markdown"},"ignoreRobotsTxt":{"type":"boolean","default":false,"description":"Opt out of robots.txt compliance"},"maxAge":{"type":"integer","minimum":0,"default":0,"description":"Serve cached response if fresher than this many ms. 0 = always fresh."},"minAge":{"type":"integer","exclusiveMinimum":true,"minimum":0,"description":"Cache-ONLY mode: require a cached copy at least this fresh, else 404"},"storeInCache":{"type":"boolean","default":true,"description":"Persist this response to the cache"},"zeroDataRetention":{"type":"boolean","default":false,"description":"Never store, and purge any existing cache entry for this URL+options"}},"required":["url"],"additionalProperties":true,"description":"POST /v1/scrape request body"},"example":{"url":"https://example.com"}}}},"responses":{"200":{"description":"Scrape succeeded","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"markdown":{"type":"string"},"html":{"type":"string"},"listing":{"$ref":"#/components/schemas/ListingData"},"formats":{"type":"object","description":"Requested output formats (present only when `formats[]` was supplied)","additionalProperties":true},"metadata":{"type":"object","properties":{"fetchMethod":{"type":"string"},"latencyMs":{"type":"integer"},"completeness":{"type":"number","description":"The listing `_completeness`, when a listing was found"},"extractionMethod":{"type":"string"},"cached":{"type":"boolean"},"cacheAgeMs":{"type":"integer"}}},"cascade":{"$ref":"#/components/schemas/ScrapeCascade"},"unblock":{"type":"object","required":["attempts"],"properties":{"attempts":{"type":"array","items":{"$ref":"#/components/schemas/UnblockAttempt"},"description":"One entry per unblock-router provider tried; empty when the router was not engaged."}}},"fallback":{"$ref":"#/components/schemas/ScrapeFallback"},"partial":{"type":"boolean","const":true,"description":"Present only when the served content is incomplete. Never present with `fallback`."},"partial_reason":{"type":"string","enum":["timeout","auth_wall","js_not_rendered"],"description":"Present with `partial`."},"ok":{"type":"boolean"},"error":{"type":"string"}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Scrape failed (upstream error)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/extract":{"post":{"operationId":"extract","summary":"Extract structured data from a URL or HTML","description":"Without `schema`: runs the free JSON-LD/OG/regex cascade. With `schema`: routes to the LLM tier (DeepSeek/Claude) for structured extraction.","tags":["Extraction"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri","description":"URL to fetch and extract (single-URL mode)"},"urls":{"type":"array","items":{"type":"string","format":"uri"},"maxItems":1000,"description":"Multiple URLs to extract in one call (bounded concurrency 5). Over your plan's cap is a 400 too_many_urls. Plan cap: Free 10, Hobby 50, Pro 200, Scale 500."},"html":{"type":"string","description":"Raw HTML to extract from (alternative to url)"},"markdown":{"type":"string","description":"Pre-converted markdown (skips HTML→MD step)"},"schema":{"type":"object","additionalProperties":{},"description":"JSON schema describing fields to extract. Triggers LLM extraction."},"complexity":{"type":"string","enum":["low","high"],"default":"low","description":"`high` routes to Claude; `low` routes to DeepSeek/Llama"}},"additionalProperties":true,"description":"POST /v1/extract request body — provide url, urls[], or html"},"examples":{"freeExtract":{"summary":"Free JSON-LD extraction","value":{"url":"https://example.com/product"}},"llmExtract":{"summary":"LLM schema extraction","value":{"url":"https://example.com/product","schema":{"name":"string","price":"number","inStock":"boolean"}}}}}}},"responses":{"200":{"description":"Extraction result","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","additionalProperties":true,"description":"Extracted fields"},"url":{"type":"string","format":"uri"},"schema":{"type":"object","additionalProperties":true},"metadata":{"type":"object","properties":{"model":{"type":"string"},"promptTokens":{"type":"integer"},"completionTokens":{"type":"integer"},"latencyMs":{"type":"integer"},"provider":{"type":"string"}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"URL fetch failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/parse":{"post":{"operationId":"parse","summary":"Parse a document (PDF/DOCX) to Markdown","description":"Accepts EITHER a multipart/form-data upload (field `file`) OR a JSON body `{ url }` pointing at a document. PDFs are parsed with pdf.js in an isolated, time/memory/CPU-limited child process; there is no OCR yet, so pages without a text layer are reported, never guessed. Every response carries `status` (ok|partial|failed), `method`, `source`, `pages_total`, `pages_parsed` and `truncated`. Documents above PARSE_MAX_PAGES are rejected with 413 `page_limit`. DOCX is read by a built-in extractor (paragraph text and heading levels) in the same isolated child. Results are content-hash cached (in-memory + optional Redis, 24h TTL); a `format: \"rag\"` output mode returns heading-aware, embeddings-ready chunks.","tags":["Extraction"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri","description":"URL of a PDF or DOCX document to fetch and parse"},"format":{"type":"string","enum":["markdown","text","rag"],"default":"markdown","description":"`markdown` (default): full document as Markdown. `text`: plain text. `rag`: clean heading-aware chunks with url/position metadata, ready to embed."},"chunkSize":{"type":"integer","exclusiveMinimum":true,"minimum":0,"default":1000,"description":"Approx. max characters per chunk (format:\"rag\" only)"},"chunkOverlap":{"type":"integer","minimum":0,"default":100,"description":"Characters of overlap between adjacent chunks (format:\"rag\" only)"}},"required":["url"],"additionalProperties":true,"description":"POST /v1/parse JSON body (fetch-by-URL variant). A multipart/form-data `file` upload is also accepted — see the multipart content schema."},"example":{"url":"https://example.com/whitepaper.pdf","format":"rag","chunkSize":1000}},"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary","description":"PDF or DOCX file to parse"},"format":{"type":"string","enum":["markdown","text","rag"],"default":"markdown"},"chunkSize":{"type":"integer","default":1000},"chunkOverlap":{"type":"integer","default":100}}}}}},"responses":{"200":{"description":"Parsed document","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"markdown":{"type":"string","description":"Document content as Markdown"},"text":{"type":"string","description":"Plain-text content (when available)"},"format":{"type":"string","enum":["pdf","docx"],"description":"Detected source document type"},"status":{"type":"string","enum":["ok","partial"],"description":"`partial` = some pages had no text layer, or the parser stopped early (see `truncated`)"},"method":{"type":"string","enum":["pdfjs","docx"],"description":"Parser that produced the content (kept on cache hits)"},"parser":{"type":"string","enum":["pdfjs","docx"],"description":"Alias of `method`"},"source":{"type":"string","enum":["upload","url"]},"pages":{"type":"integer","description":"Page count (PDF only; alias of `pages_total`)"},"pages_total":{"type":"integer","nullable":true,"description":"Pages in the document (null for DOCX)"},"pages_parsed":{"type":"integer","nullable":true,"description":"Pages whose text was extracted (null for DOCX)"},"truncated":{"type":"boolean","description":"True when the parser stopped before the end of the document"},"truncated_reason":{"type":"string","enum":["deadline","memory","cpu","cancelled","crash","text_budget"]},"partial_reason":{"type":"string","enum":["no_text_layer"]},"pages_without_text":{"type":"array","items":{"type":"integer"},"description":"1-based pages with no extractable text"},"outputFormat":{"type":"string","enum":["markdown","text","rag"],"description":"Echoes the requested `format`"},"chunks":{"type":"array","description":"Present only when `format: \"rag\"` was requested. Embeddings-ready.","items":{"type":"object","properties":{"position":{"type":"integer","description":"0-based index in the chunk stream"},"text":{"type":"string"},"heading":{"type":"string","nullable":true,"description":"Nearest enclosing heading, or null"},"headingLevel":{"type":"integer","nullable":true},"headingPath":{"type":"array","items":{"type":"string"},"description":"Heading breadcrumb, e.g. [\"Getting Started\",\"Installation\"]"},"charStart":{"type":"integer","description":"Absolute char offset into the source document"},"charEnd":{"type":"integer"},"url":{"type":"string","format":"uri","description":"Source URL, when parsed from a URL rather than a file upload"}}}},"totalChunks":{"type":"integer","description":"Present only when `format: \"rag\"` was requested"},"cached":{"type":"boolean","description":"True when served from the content-hash cache"},"cacheAgeMs":{"type":"integer","description":"Present only when `cached: true`"}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"page_limit, file_too_large, download_too_large or decompression_limit","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"Unsupported document type (e.g. DOCX parsing not available)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"no_text_layer, invalid_document, encrypted_document, fetch_failed, parse_resource_limit or parser_crashed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"parser_busy (capacity full; retry after `retry_after` seconds) or parser_unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"504":{"description":"parse_timeout or download_timeout","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/crawl":{"post":{"operationId":"crawlStart","summary":"Start an async crawl job","description":"Queues a BullMQ crawl job. Returns a `jobId` immediately. Poll `GET /v1/jobs/{id}` for status. Requires `REDIS_URL` to be configured. With `tech` the crawl is a tag audit: every crawled page lists the technologies found on it and `GET /v1/crawl/{id}/results` adds `tech_summary` (each technology and the pages it appears on). `tech: \"profile\"` costs the crawl price (detection adds nothing); `tech: \"deep\"` renders each page in a real browser for 5 credits a page instead (1 for a page that did not render, 0 for one that could not be fetched), at most 200 pages; its credits are held when it is queued and settled to the pages scanned when it ends. Where deep scans are not switched on, `tech: \"deep\"` is refused with 400 `tier_not_available`.","tags":["Crawl"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri","description":"Seed URL to crawl from"},"maxPages":{"type":"integer","exclusiveMinimum":true,"minimum":0,"maximum":100000,"default":10,"description":"Page cap (default 10), clamped to your plan's cap: Free 50, Hobby 500, Pro 10,000, Scale 50,000."},"limit":{"type":"integer","exclusiveMinimum":true,"minimum":0,"maximum":100000,"description":"Alias of maxPages; takes precedence when both are set"},"maxDepth":{"type":"integer","minimum":0,"description":"Max link depth from the seed URL (seed = depth 0)"},"includePaths":{"type":"array","items":{"type":"string"},"description":"Only crawl URLs whose pathname matches one of these globs/prefixes"},"excludePaths":{"type":"array","items":{"type":"string"},"description":"Skip URLs whose pathname matches one of these globs/prefixes"},"allowBackwardLinks":{"type":"boolean","default":false,"description":"Allow following links that climb above the seed path"},"ignoreRobotsTxt":{"type":"boolean","default":false,"description":"Opt out of robots.txt compliance for every crawled page"},"webhook":{"type":"string","format":"uri","description":"POST a signed payload to this URL when the crawl completes"},"webhookUrl":{"type":"string","format":"uri","description":"Legacy alias of `webhook`"}},"required":["url"],"additionalProperties":true,"description":"POST /v1/crawl request body"}}}},"responses":{"202":{"description":"Crawl job queued","content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"string"},"status":{"type":"string","enum":["queued"]},"url":{"type":"string","format":"uri"},"max_credits":{"type":"integer","description":"`tech: \"deep\"` only: the most the audit can cost (held now, settled when it ends)."},"message":{"type":"string"}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"DB error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/crawl/{id}":{"get":{"operationId":"crawlStatus","summary":"Get crawl job status","description":"Tenant-scoped status for a crawl job (mirror of GET /v1/jobs/{id}).","tags":["Crawl"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Job status","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"url":{"type":"string","format":"uri"},"status":{"type":"string","enum":["queued","running","processing","completed","failed","cancelled"]},"result_url":{"type":"string"},"pages_scraped":{"type":"integer"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"crawlCancel","summary":"Cancel a crawl job","description":"Cancels a running crawl (tenant-scoped). Terminal jobs report their state instead of cancelling. Idempotent.","tags":["Crawl"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Cancel result","content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"string"},"status":{"type":"string"},"cancelled":{"type":"boolean"},"message":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/crawl/{id}/results":{"get":{"operationId":"crawlResults","summary":"Get the pages a crawl scraped","description":"Tenant-scoped. When results are stored off-box this answers 302 to the stored object (JSONL); otherwise it returns the pages inline. 404 `results_not_available` says why there is nothing yet (still running, or the job ended without storing results).","tags":["Crawl"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Crawled pages","content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"string"},"status":{"type":"string","enum":["queued","running","processing","completed","failed","cancelled"]},"pageCount":{"type":"integer"},"totalPages":{"type":"integer"},"truncated":{"type":"boolean"},"pages":{"type":"array","description":"With `tech`, each fetched page also has `tech`: `{mode, technologies, rendered?, partial, reasons}`.","items":{"type":"object","additionalProperties":true}},"tech_summary":{"type":"object","description":"Tag audit only: each technology, the number of pages it appears on and those pages (first 100).","properties":{"mode":{"type":"string","enum":["profile","deep"]},"pages_scanned":{"type":"integer"},"technologies":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"category":{"type":"string"},"pages":{"type":"integer"},"urls":{"type":"array","items":{"type":"string"}}}}}}}}}}}},"302":{"description":"Results are stored off-box; follow `Location` to the JSONL object"},"404":{"description":"`job_not_found` or `results_not_available`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`job_store_not_configured`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/jobs":{"get":{"operationId":"listJobs","summary":"List recent crawl jobs","description":"Returns the 20 most recent crawl jobs for the authenticated tenant.","tags":["Crawl"],"responses":{"200":{"description":"List of jobs","content":{"application/json":{"schema":{"type":"object","properties":{"jobs":{"type":"array","items":{"$ref":"#/components/schemas/CrawlJobStatus"}}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/jobs/{id}":{"get":{"operationId":"getJob","summary":"Get crawl job status","tags":["Crawl"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Job ID returned by POST /v1/crawl"}],"responses":{"200":{"description":"Job details","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CrawlJobStatus"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Job not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/map":{"post":{"operationId":"map","summary":"Discover URLs on a site (sitemap or crawl)","description":"Attempts sitemap.xml discovery first; falls back to HTML link extraction. Returns a deduplicated list of URLs on the same domain.","tags":["Scraping"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri","description":"Site URL to discover links on"},"limit":{"type":"integer","exclusiveMinimum":true,"minimum":0,"maximum":10000,"default":100,"description":"Max URLs to return (default 100), clamped to your plan's cap: Free 100, Hobby 1,000, Pro 5,000, Scale 10,000."},"search":{"type":"string","description":"Filter returned URLs to those containing this substring"}},"required":["url"],"additionalProperties":true,"description":"POST /v1/map request body"}}}},"responses":{"200":{"description":"URL list","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"links":{"type":"array","items":{"type":"string","format":"uri"}},"total":{"type":"integer"},"source":{"type":"string","enum":["sitemap","crawl","none"]},"error":{"type":"string"}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/batch":{"post":{"operationId":"batch","summary":"Scrape many URLs in one request","description":"Concurrent multi-URL scrape. Sync mode takes up to 100 URLs (or the plan cap if lower) and charges per URL that succeeded. `async: true`, or more than 100 URLs, enqueues a job up to the plan cap and returns 202 `{ jobId }` (charged per URL at enqueue); poll GET /v1/jobs/{id}. Concurrency is capped by plan.","tags":["Scraping"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"urls":{"type":"array","items":{"type":"string","format":"uri"},"minItems":1,"maxItems":10000,"description":"URLs to scrape. Sync mode: up to 100 (or your plan cap if lower). async:true or more than 100 URLs: up to your plan cap. Plan cap: Free 25, Hobby 200, Pro 2,000, Scale 10,000."},"async":{"type":"boolean","description":"Force async mode (returns jobId immediately, poll GET /v1/jobs/:id)"},"webhook":{"type":"string","format":"uri","description":"POST to this URL when an async batch completes"},"options":{"type":"object","properties":{"extractListing":{"type":"boolean","default":true},"includeHtml":{"type":"boolean","default":false},"cookies":{"type":"string"},"headers":{"type":"object","additionalProperties":{"type":"string"}},"concurrency":{"type":"integer","exclusiveMinimum":true,"minimum":0,"default":5,"description":"URLs fetched in parallel, clamped to your plan's batch concurrency (request_caps.batch_concurrency: Free 2, Hobby 5, Pro 10, Scale 20) and to 20."}},"additionalProperties":false}},"required":["urls"],"additionalProperties":true,"description":"POST /v1/batch request body"}}}},"responses":{"200":{"description":"Batch scrape results","headers":{"X-Batch-Summary":{"description":"ok=N;failed=M;latencyMs=X","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"type":"object","properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/ScrapeResult"}},"summary":{"type":"object","properties":{"total":{"type":"integer"},"ok":{"type":"integer"},"failed":{"type":"integer"},"latencyMs":{"type":"integer"}}}}}}}},"202":{"description":"Async batch queued (`async: true`, or more than 100 URLs). Poll GET /v1/jobs/{id}.","content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"string"},"status":{"type":"string","enum":["queued"]},"total":{"type":"integer"},"message":{"type":"string"}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/screenshot":{"post":{"operationId":"screenshot","summary":"Capture a screenshot of a URL","description":"Uses Playwright (or @ss/engine/media). Uploads to Cloudflare R2 if configured; otherwise returns `base64`.","tags":["Scraping"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri","description":"URL to screenshot"},"fullPage":{"type":"boolean","default":true,"description":"Capture the full scrollable page, not just the viewport"},"width":{"type":"integer","exclusiveMinimum":true,"minimum":0,"default":1280,"description":"Viewport width in pixels"},"mobile":{"type":"boolean","default":false,"description":"Emulate a mobile device viewport + UA"},"waitFor":{"type":"integer","minimum":0,"default":1000,"description":"Milliseconds to wait after page load before capturing"},"cookies":{"type":"string","description":"Cookies to inject (JSON array of {name,value,domain?,path?})"},"element":{"type":"string","description":"CSS selector. Capture only this element."},"format":{"type":"string","enum":["png","jpeg"],"default":"png"}},"required":["url"],"additionalProperties":true,"description":"POST /v1/screenshot request body"}}}},"responses":{"200":{"description":"Screenshot captured","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"imageUrl":{"type":"string","format":"uri","description":"R2 public URL (when R2 is configured)"},"base64":{"type":"string","description":"Base64-encoded image (fallback when R2 is not configured)"},"width":{"type":"integer"},"height":{"type":"integer"},"format":{"type":"string"},"latencyMs":{"type":"integer"},"dailyLimit":{"type":"integer"}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Screenshot engine unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/search":{"post":{"operationId":"search","summary":"Web search with an optional cited answer","description":"Cascades DataForSEO SERP → DuckDuckGo HTML → Bing HTML. Set `scrapeResults: true` to get full Markdown for each result. Every response lists `attempts[]` per backend tried; `zero_results` (nothing matched) is distinct from `provider_failed` (503). Sources other than `web` are reported in `unsupported[]`. With `include_answer`, `answer` is kept only when its citations resolve to returned results.","tags":["Search"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"query":{"type":"string","minLength":1,"description":"Search query. Operator-bearing queries (site:/filetype:/intitle:) pass through verbatim."},"limit":{"type":"integer","exclusiveMinimum":true,"minimum":0,"maximum":100,"default":10,"description":"Max results (default 10), clamped to your plan's cap: Free 20, Hobby 50, Pro 100, Scale 100."},"scrapeResults":{"type":"boolean","default":false,"description":"Fully scrape each result page to markdown (uses scrape credits)"},"country":{"type":"string","default":"us","description":"2-letter country code for SERP localization"},"include_answer":{"type":"boolean","default":false,"description":"Generate an LLM answer from the top results. Kept only when every [n] citation is the position of a returned result; else answer:null + answer_reason"},"categories":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Content categories: \"github\" | \"research\" | \"pdf\""},"sources":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"\"web\" | \"news\" | \"images\". Only \"web\" has a backend; any other value is reported in unsupported[] (never served from web results)"},"tbs":{"type":"string","description":"Time filter, e.g. \"qdr:d\" (past day), \"qdr:w\", \"qdr:m\", \"qdr:y\""},"includeDomains":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Only return results from these domains (bare hostnames; up to 20; invalid values are dropped)"},"excludeDomains":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Never return results from these domains (bare hostnames; up to 20; invalid values are dropped)"}},"required":["query"],"additionalProperties":true,"description":"POST /v1/search request body"},"example":{"query":"plumbers in Austin TX","limit":10}}}},"responses":{"200":{"description":"Search results","content":{"application/json":{"schema":{"type":"object","properties":{"query":{"type":"string"},"results":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"title":{"type":"string"},"snippet":{"type":"string"},"position":{"type":"integer"},"markdown":{"type":"string","description":"Only present when scrapeResults is true"},"partial":{"type":"boolean","const":true,"description":"Present when `markdown` came from an incomplete page scrape"},"partial_reason":{"type":"string","description":"Present with `partial`"}}}},"source":{"type":["string","null"],"description":"Backend that answered (or completed empty). Null when none ran. Never a vendor default."},"total":{"type":"integer"},"attempts":{"type":"array","items":{"$ref":"#/components/schemas/ProviderAttempt"},"description":"One entry per backend tried, in order."},"zero_results":{"type":"boolean","description":"True when a backend completed and nothing matched. Distinct from provider_failed."},"provider_failed":{"type":"boolean","description":"True only when no backend completed (served as 503 search_unavailable)."},"error":{"type":"string","enum":["no_results"],"description":"Legacy alias of zero_results:true"},"unsupported":{"type":"array","description":"One entry per requested source with no backend. Never silently served from web results.","items":{"type":"object","required":["source","reason"],"properties":{"source":{"type":"string"},"reason":{"type":"string","enum":["no_provider","unknown_source"]}}}},"answer":{"type":["string","null"],"description":"Present when include_answer:true. Null unless every [n] citation is the position of a returned result."},"answer_reason":{"type":"string","enum":["no_results","summarizer_unavailable","no_citations","invalid_citation"],"description":"Present when answer is null."},"llm_fallback":{"type":"boolean","const":true,"description":"Present when the include_answer summariser ran on the platform LLM key because the tenant holds no key that serves the model (R1-01b)."},"citations":{"type":"array","description":"Present with a non-null answer: each cited id resolved to its result.","items":{"type":"object","required":["id","url","title"],"properties":{"id":{"type":"integer","description":"The cited result position"},"url":{"type":"string","format":"uri"},"title":{"type":"string"}}}},"search":{"type":"object","additionalProperties":true,"description":"Echo of the operators applied"}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"No search backend completed (provider_failed:true). Not an empty result.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"code":{"type":"string","enum":["search_unavailable"]},"source":{"type":"null"},"attempts":{"type":"array","items":{"$ref":"#/components/schemas/ProviderAttempt"}},"zero_results":{"type":"boolean","const":false},"provider_failed":{"type":"boolean","const":true},"unsupported":{"type":"array","items":{"type":"object","required":["source","reason"],"properties":{"source":{"type":"string"},"reason":{"type":"string","enum":["no_provider","unknown_source"]}}}}}}}}}}}},"/v1/enrich/website":{"post":{"operationId":"enrichWebsite","summary":"Enrich a domain: emails, phones, tech stack, team, social","description":"Crawls up to `maxPages` pages (default 10, max 20). Runs deterministic extraction then optionally escalates to LLM for team pages.","tags":["Enrichment"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"domain":{"type":"string","minLength":1,"description":"Domain or URL to enrich, e.g. \"acme.com\""},"maxPages":{"type":"integer","exclusiveMinimum":true,"minimum":0,"maximum":20,"default":10,"description":"Max pages to crawl (homepage + priority paths), hard cap 20"},"includeEmails":{"type":"boolean","default":true},"includeTech":{"type":"boolean","default":true},"includeTeam":{"type":"boolean","default":true},"includeBrand":{"type":"boolean","default":false,"description":"Include a brand kit (colors/fonts/logo) extracted from the homepage"}},"required":["domain"],"additionalProperties":true,"description":"POST /v1/enrich request body (mounted at /v1/enrich in index.ts — the enrich.ts route itself is `/`)."},"example":{"domain":"stripe.com"}}}},"responses":{"200":{"description":"Enrichment result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnrichWebsiteResponse"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Homepage fetch failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-alias-of":"/v1/enrich"}},"/v1/brand":{"post":{"operationId":"brand","summary":"Resolve a domain into a brand + firmographic profile","description":"Reads the homepage, the stylesheets it links and the logo file, and returns one profile: name, description, logo, favicon, colors, a palette with the primary color, fonts, socials and NAICS industry. Up to 40 linked stylesheets are read (512 KB each, 2 MB and 6 seconds in total); the logo is read at the same time (512 KB, 4 seconds). The result carries one record-level _completeness, a _provenance map from each found field to the page it was read from, and _freshness.","tags":["Context"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["domain"],"properties":{"domain":{"type":"string","description":"Domain or URL, e.g. stripe.com"},"url":{"type":"string","description":"Alias for domain"}}}}}},"responses":{"200":{"description":"Brand profile with completeness + provenance","content":{"application/json":{"schema":{"type":"object","properties":{"domain":{"type":"string"},"name":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"logo":{"type":"string","nullable":true,"description":"The Organization logo in the page JSON-LD, else a header logo image, an SVG icon, the apple-touch-icon or a large PNG icon. The og:image share picture is used only when none of those exist."},"favicon":{"type":"string","nullable":true},"colors":{"type":"array","items":{"type":"string"},"description":"Up to 8 hex strings. The theme-color the site declares comes first, then CSS variables named brand, primary or accent, then the colors its CSS uses most. Near-duplicate shades, common framework defaults and more than two greys are left out."},"palette":{"type":"object","description":"The primary brand color plus the logo and site palettes. Added alongside colors, which keeps its meaning.","properties":{"primary":{"type":"string","nullable":true,"description":"Hex color. A color the site declares (theme-color, msapplication-TileColor, the mask-icon color, or a --brand / --primary CSS variable) wins; else a logo color that also appears in the CSS (CIEDE2000 distance of 10 or less); else the CSS button or link color; else the main logo color; else the most used CSS color. Greys, black and white are never chosen, so a monochrome logo adds nothing. null when nothing chromatic was found."},"confidence":{"type":"string","enum":["high","medium","low"],"description":"high: declared and not contradicted by the logo, or found in both logo and CSS. medium: declared but the logo disagrees, a button color with no colored logo, or the logo alone. low: a button color the logo disagrees with, the most used CSS color, or no primary."},"source":{"type":"string","nullable":true,"enum":["declared","logo+css","css","logo",null],"description":"Where primary came from; null when primary is null."},"logo":{"type":"array","items":{"type":"string"},"description":"Up to 6 hex colors read from the logo file, largest share first. SVG logos give their fill, stroke and stop-color values exactly; PNG, JPEG, WebP and GIF logos are median-cut, skipping transparent, near-white and near-black pixels. The file is read once (at most 512 KB, 4 seconds, private addresses refused). Empty when the logo could not be read or the only candidate is the og:image share picture."},"site":{"type":"array","items":{"type":"string"},"description":"The colors read from the site CSS and markup (the same list as colors)."}}},"fonts":{"type":"array","items":{"type":"string"},"description":"Up to 8 font family names, most used first. Icon fonts are dropped; monospace and system fonts rank last."},"socials":{"type":"object"},"industry":{"type":"object","nullable":true,"description":"NAICS code, title and confidence, matched from the page title and descriptions. null when no rule matches."},"_completeness":{"type":"number","description":"0.35 plus 0.1 per signal found (logo, colors, fonts, socials, industry, name, description), max 1; capped when only a favicon or a thin profile was found."},"_provenance":{"type":"object","description":"Field name to the page URL it was read from. For colors and fonts this is the homepage; the values may come from its own CSS or from the stylesheets it links."},"_freshness":{"type":"string","format":"date-time"},"brand_meta":{"$ref":"#/components/schemas/BrandMeta"}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/resolve":{"post":{"operationId":"resolve","summary":"Resolve a company (domain | email | name) into a company profile","description":"Reads the shared business graph: a HIT returns the stored record (with source_count + freshness). A MISS with a domain is extracted live from the site and returned; it is not written back to the graph. The result carries record-level _match_score and _freshness, not per-field values.","tags":["Context"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"domain":{"type":"string"},"email":{"type":"string","description":"Resolved to its domain"},"name":{"type":"string"},"state":{"type":"string"},"zip":{"type":"string"}}}}}},"responses":{"200":{"description":"Resolved profile (or resolved:false on a live miss)","content":{"application/json":{"schema":{"type":"object","properties":{"resolved":{"type":"boolean"},"domain":{"type":"string","nullable":true},"name":{"type":"string","nullable":true},"industry":{"type":"object","nullable":true},"socials":{"type":"object"},"location":{"type":"object"},"_resolved_by":{"type":"string"},"_match_score":{"type":"number","description":"How the company was matched. Graph hit: 0.9 domain, 0.85 phone, 0.7 name, capped at 0.5 for a thin or stale record. Live: 0.4 plus 0.1 per signal found, capped when thin."},"_freshness":{"type":"string","nullable":true},"_source_count":{"type":"integer","nullable":true}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No graph record and no domain to resolve live","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Graph disabled and no domain supplied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/graph/query":{"post":{"operationId":"graphQuery","summary":"Reverse-index the graph (firms by tech / industry / geo)","description":"Which firms use tech X in state Y. Requires at least one selective filter (tech, techAny, industry, state, or city). Returns clean-title facts only (never owner PII or raw source data). A second request dialect exists for reverse-index lookups: pass `signal`, `bucket` (hot|warm|cold), `geo{state,city,industry}`, or a `filters{state,industry,freshnessWithinDays,limit,offset}` object. That dialect responds with `{ businesses, count, total }` (same clean-title projection) instead of `{ count, results, criteria }`.","tags":["Context"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"tech":{"type":"string"},"techAny":{"type":"array","items":{"type":"string"}},"notTech":{"type":"array","items":{"type":"string"}},"industry":{"type":"string"},"state":{"type":"string"},"city":{"type":"string"},"hasDomain":{"type":"boolean"},"limit":{"type":"integer","default":50,"maximum":200}}}}}},"responses":{"200":{"description":"Matching firms (clean-title facts)","content":{"application/json":{"schema":{"type":"object","properties":{"count":{"type":"integer"},"results":{"type":"array","items":{"type":"object"}}}}}}},"400":{"description":"No selective filter provided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Graph disabled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/graph/business/{idOrDomain}":{"get":{"operationId":"graphBusiness","summary":"Read a fused business record (PII-stripped)","description":"Clean-title business facts + marketing summary + signal COUNT. Never returns owner PII or raw source payloads.","tags":["Context"],"parameters":[{"name":"idOrDomain","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Business record","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Graph disabled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/enrich/phone":{"post":{"operationId":"enrichPhone","summary":"Resolve + verify a public business phone line","description":"Runs the wall-aware number waterfall (graph → website → GBP → license → SoS → Yelp → line-type). Google/GBP and Yelp are SCORE-ONLY voters: they can agree with a number but never author it, and their observed values are redacted from the returned provenance (`value: null`). Billed only when a canonical number is authored.","tags":["Enrichment"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"At least one of `domain`, `name`, or `businessId` is required.","properties":{"domain":{"type":"string"},"name":{"type":"string","description":"Business / legal name"},"state":{"type":"string","description":"Two-letter state, narrows the license + SoS steps"},"businessId":{"oneOf":[{"type":"integer"},{"type":"string"}],"description":"graph_businesses id, when already resolved"},"trade":{"type":"string","description":"Trade hint forwarded to the license lookup, e.g. \"HVAC\""}}},"example":{"domain":"acmeplumbing.com","state":"TX"}}}},"responses":{"200":{"description":"Resolution result (a null `number` means nothing could be authored)","content":{"application/json":{"schema":{"type":"object","properties":{"number":{"type":"string","nullable":true,"description":"Canonical phone, or null"},"lineType":{"type":"string"},"confidence":{"type":"number","minimum":0,"maximum":1},"source":{"type":"string","nullable":true,"description":"Authoring source (never gbp/yelp)"},"agreed_sources":{"type":"array","items":{"type":"string"}},"provenance":{"type":"array","description":"Audit trail of every source consulted. Score-only sources carry `value: null`.","items":{"type":"object","properties":{"source":{"type":"string"},"value":{"type":"string","nullable":true},"weight":{"type":"number"},"detail":{"type":"string"},"authoring":{"type":"boolean"}}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Phone resolution failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/enrich/batch":{"post":{"operationId":"enrichBatch","summary":"Enrich many records across many fields (waterfall)","description":"Up to 100 records × the requested fields, resolved through the graph-first waterfall (owned data before any paid finder). Billing is hit-only: one credit per FILLED cell, empty cells are free. `tier` is a spend dial and is CLAMPED to what the plan allows.","tags":["Enrichment"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["records"],"properties":{"records":{"type":"array","maxItems":100,"items":{"type":"object","properties":{"domain":{"type":"string"},"url":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"fullName":{"type":"string"},"email":{"type":"string"},"phone":{"type":"string"},"companyName":{"type":"string"},"name":{"type":"string","description":"Alias for companyName"},"state":{"type":"string"}}}},"fields":{"type":"array","items":{"type":"string","enum":["email","phone","company","kyb","person"]},"default":["email","phone","company"],"description":"Omit for the default set. An explicit empty/unknown list is a 400, never a silent default."},"tier":{"type":"string","description":"Spend depth dial (plan-clamped)"},"gate":{"type":"string","description":"Waterfall stop condition"},"maxCost":{"type":"number","description":"Max provider spend per field resolution (USD)"}}},"example":{"records":[{"domain":"acme.com"}],"fields":["email","phone"]}}}},"responses":{"200":{"description":"Per-record cells + a run summary","content":{"application/json":{"schema":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object","properties":{"input":{"type":"object","additionalProperties":true,"description":"The record as submitted"},"fields":{"type":"object","description":"field name → resolved cell","additionalProperties":{"type":"object","properties":{"value":{},"source":{"type":"string"},"confidence":{"type":"number"},"verified":{"type":"boolean"}}}},"hits":{"type":"integer"},"skipped":{"type":"string","description":"Present as 'no-identity' when a record had nothing to resolve on"}}}},"summary":{"type":"object","properties":{"records":{"type":"integer"},"fields":{"type":"array","items":{"type":"string"}},"cells":{"type":"integer"},"hits":{"type":"integer"},"tenantCharged":{"type":"integer"},"tier":{"type":"string"},"tier_clamped":{"type":"boolean"},"tier_requested":{"type":"string"},"budget_capped":{"type":"boolean"}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"Daily enrichment spend cap reached, or the spend ledger is unavailable (fail-closed)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/enrich/tech":{"post":{"operationId":"enrichTech","summary":"Tech lookup: check named technologies, profile the full stack, or scan in a real browser","description":"Fetches one URL and fingerprints it. `tech: \"profile\"` (default) runs every fingerprint, the same scan as /v1/enrich. `tech: \"check\"` runs only the technologies named in `technologies` (up to 10) and answers yes or no for each. `tech: \"deep\"` loads the page in a real browser and also matches the network requests, scripts, globals and cookies the page produces after it runs (tags a tag manager injects, chat and consent widgets), then more same-site pages up to `pages` in all (default 4, at most 20: pricing, then contact / demo / signup, then other internal links), within a total time budget (15000 ms for 4 pages, 3500 ms more per further page, at most 90000 ms). With `subdomains: true` it also scans up to 5 of the site's own subdomains that resolve (1-2 pages each, counted in `pages`) and groups the result per host in `hosts`. `technologies` narrows a deep scan the same way as a check. Where the deep scan is not switched on it is refused with 400 `tier_not_available`, before any work or charge. With `maxAge`, a result of the same lookup at most that old is returned from cache without fetching the page. Price per lookup (see /v1/pricing): check 1 credit, profile 1, deep 5 for up to 4 pages plus 1 for each further page that renders; from cache 0, 0 and half the deep price rounded down (2 for up to 4 pages). A deep scan whose page did not render but whose plain fetch worked is charged as a profile and carries `render_failed`. A page that cannot be fetched is not charged. A deep scan whose planned time budget is over 25 seconds, or one sent with `async: true`, runs as a job: the answer is 202 with `jobId`, and GET /v1/jobs/{id} returns the same body (plus `credits` and `credits_charged`) when it is done. Its credits are held when it is queued and settled to what it scanned when it ends; a scan that could not load the page at all costs nothing.","tags":["Enrichment"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","anyOf":[{"required":["domain"]},{"required":["url"]}],"properties":{"domain":{"type":"string","description":"Domain or URL, e.g. `acme.com`. Required unless `url` is given."},"url":{"type":"string","description":"Alias for domain"},"tech":{"type":"string","enum":["check","profile","deep"],"default":"profile"},"technologies":{"type":"array","items":{"type":"string"},"maxItems":10,"description":"Technology names to check (case-insensitive). Required with `tech: \"check\"`; optional with `tech: \"deep\"`, where it limits the answer to these names."},"maxAge":{"type":"integer","minimum":0,"default":0,"description":"Serve a cached result at most this many ms old. 0 = always scan fresh. Clamped to 30 days."},"pages":{"type":"integer","minimum":1,"maximum":20,"default":4,"description":"`tech: \"deep\"` only: pages to render in all, homepage and subdomain pages included. Each page that renders beyond the first 4 costs 1 more credit. More than 6 pages runs as a job (202)."},"subdomains":{"type":"boolean","default":false,"description":"`tech: \"deep\"` only: also scan up to 5 direct subdomains of the site's own host (linked from the scanned pages, or common names such as blog, docs, shop, help) that resolve to a public address, 1-2 pages each, inside `pages`. The answer adds `hosts`."},"async":{"type":"boolean","default":false,"description":"`tech: \"deep\"` only: run the scan as a job and answer 202 with `jobId` at once. A deep scan whose planned budget is over 25 seconds runs as a job anyway. A cached result is answered directly."}}},"example":{"domain":"acme.com","tech":"check","technologies":["ServiceTitan","HubSpot"],"maxAge":86400000}}}},"responses":{"200":{"description":"Lookup result","content":{"application/json":{"schema":{"type":"object","required":["domain","url","tier","tech_stack","tech_meta","cached"],"properties":{"domain":{"type":"string"},"url":{"type":"string"},"tier":{"type":"string","enum":["check","profile","deep"]},"tech_stack":{"type":"array","items":{"$ref":"#/components/schemas/TechEntry"}},"checked":{"type":"object","additionalProperties":{"type":"boolean"},"description":"With `technologies` (check, or deep with names): each named technology (canonical name) and whether it was detected."},"tech_meta":{"$ref":"#/components/schemas/TechMeta"},"pages":{"type":"array","description":"`deep` only: the pages rendered, homepage first, and whether each one loaded.","items":{"type":"object","required":["url","ok"],"properties":{"url":{"type":"string"},"ok":{"type":"boolean"}}}},"hosts":{"type":"array","description":"`deep` with `subdomains: true` only: detections per host, the site first, then each subdomain.","items":{"type":"object","required":["host","pages","tech_stack"],"properties":{"host":{"type":"string"},"pages":{"type":"array","items":{"type":"object","required":["url","ok"],"properties":{"url":{"type":"string"},"ok":{"type":"boolean"}}}},"tech_stack":{"type":"array","items":{"$ref":"#/components/schemas/TechEntry"}}}}},"cached":{"type":"boolean","description":"True when served from cache (see maxAge)."},"_cached_age_ms":{"type":"integer","description":"Age of a cached result. Present when cached."},"_freshness":{"type":"string","format":"date-time","description":"When the scan ran."}}}}}},"202":{"description":"`deep` run as a job (`async: true`, or a planned budget over 25 s). Poll GET /v1/jobs/{id}.","content":{"application/json":{"schema":{"type":"object","required":["jobId","status","tier","domain","url","pages_planned","budget_ms","max_credits"],"properties":{"jobId":{"type":"string"},"status":{"type":"string","enum":["queued"]},"tier":{"type":"string","enum":["deep"]},"subdomains":{"type":"boolean","description":"Present (true) when the scan includes subdomains."},"domain":{"type":"string"},"url":{"type":"string"},"pages_planned":{"type":"integer","description":"Most pages the scan may render, homepage included."},"budget_ms":{"type":"integer","description":"The scan's total time budget."},"max_credits":{"type":"integer","description":"The most the job can cost (held now, settled when it ends)."},"message":{"type":"string"}}}}}},"400":{"description":"Bad request (unknown technology names are listed in `unknown`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Page fetch failed (not charged)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`deep_jobs_busy`: too many deep scan jobs queued or running (not charged; see Retry-After)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"504":{"description":"`tech_deep_timeout`: the deep scan ran out of its time budget before anything was scanned (not charged)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/enrich/estimate":{"post":{"operationId":"enrichEstimate","summary":"Quote a batch enrichment before spending","description":"Check-before-spend. Side-effect free: no billing, no provider calls, no cache writes. Probes the exact cache keys a real run would use and answers, per cell, whether the value is already known, how stale it is, and whether a paid provider call would fire. Same request body as `/v1/enrich/batch`.","tags":["Enrichment"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["records"],"properties":{"records":{"type":"array","maxItems":100,"items":{"type":"object","additionalProperties":true}},"fields":{"type":"array","items":{"type":"string","enum":["email","phone","company","kyb","person"]},"default":["email","phone","company"]},"tier":{"type":"string"},"maxCost":{"type":"number"}}},"example":{"records":[{"domain":"acme.com"}],"fields":["email"]}}}},"responses":{"200":{"description":"Per-cell estimate + a quote summary","content":{"application/json":{"schema":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object","properties":{"input":{"type":"object","additionalProperties":true},"fields":{"type":"object","additionalProperties":{"type":"object","properties":{"known":{"type":"boolean"},"source":{"type":"string","nullable":true},"staleDays":{"type":"integer","nullable":true},"willCallProvider":{"type":"boolean"},"estimatedCredits":{"type":"integer"}}}},"skipped":{"type":"string"}}}},"summary":{"type":"object","properties":{"records":{"type":"integer"},"fields":{"type":"array","items":{"type":"string"}},"cells":{"type":"integer"},"known":{"type":"integer"},"providerCalls":{"type":"integer"},"estimatedCredits":{"type":"integer"},"tier":{"type":"string"}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/directory/catalog":{"get":{"operationId":"directoryCatalog","summary":"Purchasable category × geography datasets","description":"Aggregate counts per industry × state from the business graph. Counts only: no rows are shipped here. `verified` is the license-backed (clean-title) subset.","tags":["Context"],"responses":{"200":{"description":"Dataset cards, densest first","content":{"application/json":{"schema":{"type":"object","properties":{"datasets":{"type":"array","items":{"type":"object","properties":{"industry":{"type":"string"},"state":{"type":"string"},"count":{"type":"integer"},"verified":{"type":"integer"},"updatedAt":{"type":"string","format":"date-time","nullable":true}}}}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/directory/list/{state}/{industry}":{"get":{"operationId":"directoryList","summary":"Facts-only business rows for one state + industry hub","description":"Paginated teaser rows. FACTS ONLY: name, address, city, state, zip, website, general phone, tech stack and the public license filing. Owner PII is never selected or returned.","tags":["Context"],"parameters":[{"name":"state","in":"path","required":true,"schema":{"type":"string"},"description":"Two-letter state, lowercase"},{"name":"industry","in":"path","required":true,"schema":{"type":"string"},"description":"Industry slug, e.g. \"hvac\""},{"name":"page","in":"query","required":false,"schema":{"type":"integer","default":1}}],"responses":{"200":{"description":"One page of rows","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"slug":{"type":"string"},"name":{"type":"string"},"address":{"type":"string"},"city":{"type":"string"},"state":{"type":"string"},"zip":{"type":"string"},"website":{"type":"string","format":"uri"},"phone":{"type":"string"},"phoneType":{"type":"string"},"techStack":{"type":"array","items":{"type":"string"}},"licenseNumber":{"type":"string"},"licenseStatus":{"type":"string"},"licenseExpiration":{"type":"string"}}}},"total":{"type":"integer"},"pageCount":{"type":"integer"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/directory/business/{slug}":{"get":{"operationId":"directoryBusiness","summary":"One directory business + similar businesses","tags":["Context"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The business (same facts-only fields as the hub rows) plus `industry`, `lastVerifiedAt` and `similar`","content":{"application/json":{"schema":{"type":"object","properties":{"slug":{"type":"string"},"name":{"type":"string"},"city":{"type":"string"},"state":{"type":"string"},"industry":{"type":"string"},"website":{"type":"string","format":"uri"},"phone":{"type":"string"},"techStack":{"type":"array","items":{"type":"string"}},"lastVerifiedAt":{"type":"string","format":"date-time"},"similar":{"type":"array","items":{"type":"object","properties":{"slug":{"type":"string"},"name":{"type":"string"},"city":{"type":"string"}}}}},"additionalProperties":true}}}},"404":{"description":"Business not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Failed to fetch business","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Graph unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/directory/sitemap":{"get":{"operationId":"directorySitemap","summary":"Bounded enumeration of directory slugs","tags":["Context"],"responses":{"200":{"description":"Slug list","content":{"application/json":{"schema":{"type":"object","properties":{"slugs":{"type":"array","items":{"type":"string"}}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/credits":{"get":{"operationId":"getCredits","summary":"Credit balance","tags":["Account"],"responses":{"200":{"description":"Balance","content":{"application/json":{"schema":{"type":"object","properties":{"balanceCredits":{"type":"integer"},"balanceUsd":{"type":"number"},"creditUsdValue":{"type":"number","description":"USD value of one credit"}}}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/credits/ledger":{"get":{"operationId":"getCreditLedger","summary":"Recent credit ledger entries","tags":["Account"],"responses":{"200":{"description":"Ledger entries","content":{"application/json":{"schema":{"type":"object","properties":{"entries":{"type":"array","items":{"type":"object","additionalProperties":true}}}}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/byok/keys":{"post":{"operationId":"storeByokKey","summary":"Store a provider credential (encrypted at rest)","description":"The plaintext is encrypted before it is written, never returned by any endpoint, and only a masked 4-character tail is echoed back. Requires server-side encryption to be configured.","tags":["Account"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["provider","key"],"properties":{"provider":{"type":"string","enum":["anthropic","openai","deepseek","gemini","openrouter","smartlead","instantly","stripe","qbo"]},"key":{"type":"string"}}}}}},"responses":{"200":{"description":"Stored (or acknowledged-but-not-persisted when no database is configured)","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"stored":{"type":"boolean"},"provider":{"type":"string"},"kind":{"type":"string","enum":["llm","sender","revenue"],"nullable":true},"masked":{"type":"string"},"note":{"type":"string"}}}}}},"400":{"description":"Unknown provider or missing key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No tenant context","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Failed to store key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Encryption is not configured on the server","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"operationId":"listByokKeys","summary":"List connected providers (never key material)","tags":["Account"],"responses":{"200":{"description":"Connected providers","content":{"application/json":{"schema":{"type":"object","properties":{"keys":{"type":"array","items":{"type":"object","properties":{"provider":{"type":"string"},"kind":{"type":"string","enum":["llm","sender","revenue"]},"connected":{"type":"boolean"},"updatedAt":{"type":"string","format":"date-time","nullable":true}}}},"configured":{"type":"boolean"},"encConfigured":{"type":"boolean"}}}}}},"401":{"description":"No tenant context","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/byok/keys/{provider}":{"delete":{"operationId":"deleteByokKey","summary":"Remove a stored provider credential","tags":["Account"],"parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"deleted":{"type":"boolean"},"provider":{"type":"string"},"note":{"type":"string"}}}}}},"400":{"description":"Unknown provider","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No tenant context","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Failed to delete key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/status":{"get":{"operationId":"status","summary":"Public status feed","description":"Readiness, version, uptime and subsystem/provider availability. Always 200. This is a status feed; the infrastructure gates are /health and /ready. `providers` carries capability booleans only; vendor names are never published here. `status` is read from persisted probe history: a component reads `unknown` when it has no recorded probe or its newest probe is older than the staleness limit.","tags":["Meta"],"security":[],"parameters":[{"name":"history_minutes","in":"query","required":false,"description":"Observation history window in minutes (default 60, max 1440).","schema":{"type":"integer","minimum":1,"maximum":1440}}],"responses":{"200":{"description":"Status snapshot","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"service":{"type":"string"},"version":{"type":"string"},"uptimeSeconds":{"type":"integer"},"timestamp":{"type":"string","format":"date-time"},"cacheAgeMs":{"type":"integer"},"subsystems":{"type":"object","properties":{"supabase":{"type":"object","additionalProperties":true},"redis":{"type":"object","additionalProperties":true},"queues":{"type":"object","additionalProperties":true}}},"providers":{"type":"object","additionalProperties":true},"status":{"type":"object","description":"Persisted status (R1-06): overall, store, stale_after_ms, internal_readiness, external_reachability, provider_degradation, incidents, history.","properties":{"overall":{"type":"string","enum":["ok","degraded","down","unknown"]},"store":{"type":"string","enum":["persisted","not_configured","unavailable"]},"stale_after_ms":{"type":"integer"},"internal_readiness":{"type":"object","additionalProperties":true},"external_reachability":{"type":"object","additionalProperties":true},"provider_degradation":{"type":"object","additionalProperties":true},"incidents":{"type":"array","items":{"type":"object","additionalProperties":true}},"history":{"type":"object","additionalProperties":true}}},"error":{"type":"string","description":"Present as 'status-unavailable' on a degraded response"}}}}}}}}},"/v1/pricing":{"get":{"operationId":"pricing","summary":"Public pricing catalog","description":"Plans + per-endpoint credit model. No authentication required.","tags":["Billing"],"security":[],"responses":{"200":{"description":"Pricing catalog","content":{"application/json":{"schema":{"type":"object","properties":{"currency":{"type":"string"},"billing":{"type":"string"},"plans":{"type":"array","items":{"type":"object"}},"credit_model":{"type":"array","items":{"type":"object"}}}}}}}}}},"/v1/usage":{"get":{"operationId":"getUsage","summary":"Current month usage and quota","tags":["Account"],"responses":{"200":{"description":"Usage stats","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Tenant not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`usage_unavailable`: usage could not be read (never a made-up zero)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/billing/usage":{"get":{"operationId":"getBillingUsage","summary":"Current month usage and quota (alias of GET /v1/usage)","description":"Served by the same handler as GET /v1/usage.","x-alias-of":"/v1/usage","tags":["Account"],"responses":{"200":{"description":"Usage stats","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Tenant not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`usage_unavailable`: usage could not be read (never a made-up zero)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/billing/checkout":{"post":{"operationId":"billingCheckout","summary":"Create a Stripe Checkout session","tags":["Billing"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["plan"],"properties":{"plan":{"type":"string","description":"A paid plan: `hobby` | `pro` | `scale` (the legacy ids `starter` | `business` are accepted too)."},"uiMode":{"type":"string","enum":["hosted","embedded"],"default":"hosted","description":"`hosted` needs successUrl + cancelUrl; `embedded` needs returnUrl."},"successUrl":{"type":"string","format":"uri"},"cancelUrl":{"type":"string","format":"uri"},"returnUrl":{"type":"string","format":"uri"}}}}}},"responses":{"200":{"description":"Hosted: `{ url, uiMode }`. Embedded: `{ clientSecret, sessionId, uiMode }`.","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"clientSecret":{"type":"string"},"sessionId":{"type":"string"},"uiMode":{"type":"string","enum":["hosted","embedded"]}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/billing/portal":{"get":{"operationId":"billingPortal","summary":"Create a Stripe Billing Portal session","tags":["Billing"],"responses":{"200":{"description":"Stripe Billing Portal URL","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri"}}}}}},"400":{"description":"No billing account found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/health":{"get":{"operationId":"health","summary":"Health check","description":"No authentication required.","tags":["Meta"],"security":[],"responses":{"200":{"description":"API is healthy","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"version":{"type":"string"},"metering":{"type":"boolean","description":"True when this deployment charges credits and enforces quotas on billable calls."}}}}}}}}},"/openapi.json":{"get":{"operationId":"getOpenApiSpec","summary":"OpenAPI 3.1 specification (this document)","description":"Public endpoint. No authentication required.","tags":["Meta"],"security":[],"responses":{"200":{"description":"OpenAPI JSON document","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/docs":{"get":{"operationId":"apiExplorer","summary":"Interactive API explorer (Scalar)","description":"Public, no-auth. Returns an HTML page with the Scalar API reference / playground rendered against this OpenAPI document.","tags":["Meta"],"security":[],"responses":{"200":{"description":"HTML API explorer","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/agent-onboarding/SKILL.md":{"get":{"operationId":"agentOnboardingSkill","summary":"Agent self-onboarding skill (Markdown)","description":"Public, no-auth. Returns a text/markdown guide an AI agent fetches to onboard itself onto SuperScraper (get a key → core calls → MCP → CLI → index).","tags":["Meta"],"security":[],"responses":{"200":{"description":"SKILL.md content","content":{"text/markdown":{"schema":{"type":"string"}}}}}}},"/v1/keys/provision":{"post":{"operationId":"provisionKey","summary":"Self-serve API key provisioning","description":"Public, no key needed. Mints a free-tier tenant and its first API key, so an agent can bootstrap itself. IP rate-limited. The key is shown once.","tags":["Account"],"security":[],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"label":{"type":"string","maxLength":60,"description":"A name for the key and tenant (default \"agent\")"},"email":{"type":"string","format":"email","description":"Optional contact email for the tenant"}}},"example":{"label":"research-agent"}}}},"responses":{"201":{"description":"New API key (shown once) and the free plan budget","content":{"application/json":{"schema":{"type":"object","properties":{"apiKey":{"type":"string","description":"`ss_live_…`. Send it as `x-api-key` (or Bearer) on /v1/* calls."},"tenantId":{"type":"string"},"tier":{"type":"string","enum":["free"]},"credits":{"type":"object","properties":{"scrape":{"type":"integer"},"enrich":{"type":"integer"},"kyb":{"type":"integer"},"state":{"type":"string","enum":["ok","grant_pending","grant_failed","reconciling"]},"period":{"type":"string","example":"2026-09"}}},"quota":{"type":"object","properties":{"pages":{"type":"integer"},"extractions":{"type":"integer"}}},"creditsGranted":{"type":"array","items":{"type":"string"}},"note":{"type":"string"}}}}}},"403":{"description":"Self-serve provisioning is disabled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Too many provisioning requests","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Provisioning backend unavailable, or the credit grant could not be recorded (no key was created)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/enrich":{"post":{"operationId":"enrich","summary":"Enrich a domain: emails, phones, tech stack, team, social","description":"Crawls up to `maxPages` pages (default 10, max 20). Runs deterministic extraction then optionally escalates to LLM for team pages.","tags":["Enrichment"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"domain":{"type":"string","minLength":1,"description":"Domain or URL to enrich, e.g. \"acme.com\""},"maxPages":{"type":"integer","exclusiveMinimum":true,"minimum":0,"maximum":20,"default":10,"description":"Max pages to crawl (homepage + priority paths), hard cap 20"},"includeEmails":{"type":"boolean","default":true},"includeTech":{"type":"boolean","default":true},"includeTeam":{"type":"boolean","default":true},"includeBrand":{"type":"boolean","default":false,"description":"Include a brand kit (colors/fonts/logo) extracted from the homepage"}},"required":["domain"],"additionalProperties":true,"description":"POST /v1/enrich request body (mounted at /v1/enrich in index.ts — the enrich.ts route itself is `/`)."},"example":{"domain":"stripe.com"}}}},"responses":{"200":{"description":"Enrichment result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnrichWebsiteResponse"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Homepage fetch failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v2/scrape":{"post":{"operationId":"migration_scrape","summary":"Migration door: runs as `scrape`","description":"Compatibility path for an existing integration built on a Firecrawl-compatible v2 client. Runs the canonical `scrape` operation with the same auth, rate limits, quota and pricing. Coverage: partial. formats markdown, rawHtml, html (served as the unprocessed page HTML, with a warning), links, json (with a schema), summary, screenshot, branding. actions wait(ms)/click/write/press/scroll/screenshot. Refused: other formats, wait-for-selector, executeJavascript, pdf/scrape actions, redactPII, lockdown, profile. Ignored with a warning: mobile, proxy, location, parsers, skipTlsVerification, timeout. For new integrations use the /v1 operation.","tags":["Migration door"],"x-migration-side-door":true,"x-canonical-operation":"scrape","x-compat-status":"partial","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"responses":{"200":{"description":"`{ success: true, … }` in the v2 wire format."},"400":{"description":"`{ success: false, error, code }`: invalid request or an option this door refuses (`unsupported_format`, `unsupported_option`, `unsupported_action`)."},"401":{"description":"Missing, invalid or revoked API key."},"402":{"description":"Quota or credit balance exhausted."},"429":{"description":"Rate limit exceeded (shared with /v1)."}}}},"/v2/search":{"post":{"operationId":"migration_search","summary":"Migration door: runs as `search`","description":"Compatibility path for an existing integration built on a Firecrawl-compatible v2 client. Runs the canonical `search` operation with the same auth, rate limits, quota and pricing. Coverage: partial. web results only; news/images are reported as unsupported (never served from web results). scrapeOptions supports markdown only. location and highlights are ignored with a warning. For new integrations use the /v1 operation.","tags":["Migration door"],"x-migration-side-door":true,"x-canonical-operation":"search","x-compat-status":"partial","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"responses":{"200":{"description":"`{ success: true, … }` in the v2 wire format."},"400":{"description":"`{ success: false, error, code }`: invalid request or an option this door refuses (`unsupported_format`, `unsupported_option`, `unsupported_action`)."},"401":{"description":"Missing, invalid or revoked API key."},"402":{"description":"Quota or credit balance exhausted."},"429":{"description":"Rate limit exceeded (shared with /v1)."}}}},"/v2/map":{"post":{"operationId":"migration_map","summary":"Migration door: runs as `map`","description":"Compatibility path for an existing integration built on a Firecrawl-compatible v2 client. Runs the canonical `map` operation with the same auth, rate limits, quota and pricing. Coverage: partial. url, search, limit. sitemap, includeSubdomains, ignoreQueryParameters, location are ignored with a warning. For new integrations use the /v1 operation.","tags":["Migration door"],"x-migration-side-door":true,"x-canonical-operation":"map","x-compat-status":"partial","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"responses":{"200":{"description":"`{ success: true, … }` in the v2 wire format."},"400":{"description":"`{ success: false, error, code }`: invalid request or an option this door refuses (`unsupported_format`, `unsupported_option`, `unsupported_action`)."},"401":{"description":"Missing, invalid or revoked API key."},"402":{"description":"Quota or credit balance exhausted."},"429":{"description":"Rate limit exceeded (shared with /v1)."}}}},"/v2/crawl":{"post":{"operationId":"migration_crawl_start","summary":"Migration door: runs as `crawl_start`","description":"Compatibility path for an existing integration built on a Firecrawl-compatible v2 client. Runs the canonical `crawl_start` operation with the same auth, rate limits, quota and pricing. Coverage: partial. url, limit, includePaths, excludePaths, maxDiscoveryDepth, crawlEntireDomain, ignoreRobotsTxt, webhook (url only). scrapeOptions.formats markdown only. Refused: prompt, zeroDataRetention:true, other formats. Other options ignored with a warning. For new integrations use the /v1 operation.","tags":["Migration door"],"x-migration-side-door":true,"x-canonical-operation":"crawl_start","x-compat-status":"partial","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"responses":{"200":{"description":"`{ success: true, … }` in the v2 wire format."},"400":{"description":"`{ success: false, error, code }`: invalid request or an option this door refuses (`unsupported_format`, `unsupported_option`, `unsupported_action`)."},"401":{"description":"Missing, invalid or revoked API key."},"402":{"description":"Quota or credit balance exhausted."},"429":{"description":"Rate limit exceeded (shared with /v1)."}}}},"/v2/crawl/{id}":{"get":{"operationId":"migration_crawl_status","summary":"Migration door: runs as `crawl_status`","description":"Compatibility path for an existing integration built on a Firecrawl-compatible v2 client. Runs the canonical `crawl_status` operation with the same auth, rate limits, quota and pricing. Coverage: supported. status, completed, total, data (pages once the crawl completes, read from the job store or our object storage). No pagination: all stored pages in one response. A completed crawl whose pages cannot be read answers 502 results_unavailable, never empty data. For new integrations use the /v1 operation.","tags":["Migration door"],"x-migration-side-door":true,"x-canonical-operation":"crawl_status","x-compat-status":"supported","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"`{ success: true, … }` in the v2 wire format."},"400":{"description":"`{ success: false, error, code }`: invalid request or an option this door refuses (`unsupported_format`, `unsupported_option`, `unsupported_action`)."},"401":{"description":"Missing, invalid or revoked API key."},"402":{"description":"Quota or credit balance exhausted."},"429":{"description":"Rate limit exceeded (shared with /v1)."}}},"delete":{"operationId":"migration_crawl_cancel","summary":"Migration door: runs as `crawl_cancel`","description":"Compatibility path for an existing integration built on a Firecrawl-compatible v2 client. Runs the canonical `crawl_cancel` operation with the same auth, rate limits, quota and pricing. Coverage: supported. cancels a queued or running crawl. For new integrations use the /v1 operation.","tags":["Migration door"],"x-migration-side-door":true,"x-canonical-operation":"crawl_cancel","x-compat-status":"supported","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"`{ success: true, … }` in the v2 wire format."},"400":{"description":"`{ success: false, error, code }`: invalid request or an option this door refuses (`unsupported_format`, `unsupported_option`, `unsupported_action`)."},"401":{"description":"Missing, invalid or revoked API key."},"402":{"description":"Quota or credit balance exhausted."},"429":{"description":"Rate limit exceeded (shared with /v1)."}}}},"/v2/batch/scrape":{"post":{"operationId":"migration_batch","summary":"Migration door: runs as `batch`","description":"Compatibility path for an existing integration built on a Firecrawl-compatible v2 client. Runs the canonical `batch` operation with the same auth, rate limits, quota and pricing. Coverage: partial. always asynchronous. format markdown only (the async worker stores markdown); ignoreInvalidURLs, maxConcurrency, webhook (url only). Refused: other formats (including rawHtml), appendToId, zeroDataRetention:true, x-idempotency-key (not honoured, so refused rather than risk a double charge). For new integrations use the /v1 operation.","tags":["Migration door"],"x-migration-side-door":true,"x-canonical-operation":"batch","x-compat-status":"partial","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"responses":{"200":{"description":"`{ success: true, … }` in the v2 wire format."},"400":{"description":"`{ success: false, error, code }`: invalid request or an option this door refuses (`unsupported_format`, `unsupported_option`, `unsupported_action`)."},"401":{"description":"Missing, invalid or revoked API key."},"402":{"description":"Quota or credit balance exhausted."},"429":{"description":"Rate limit exceeded (shared with /v1)."}}}},"/v2/batch/scrape/{id}":{"get":{"operationId":"migration_job_get","summary":"Migration door: runs as `job_get`","description":"Compatibility path for an existing integration built on a Firecrawl-compatible v2 client. Runs the canonical `job_get` operation with the same auth, rate limits, quota and pricing. Coverage: supported. status, completed, total, data. Batch jobs only (a crawl id answers 404). For new integrations use the /v1 operation.","tags":["Migration door"],"x-migration-side-door":true,"x-canonical-operation":"job_get","x-compat-status":"supported","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"`{ success: true, … }` in the v2 wire format."},"400":{"description":"`{ success: false, error, code }`: invalid request or an option this door refuses (`unsupported_format`, `unsupported_option`, `unsupported_action`)."},"401":{"description":"Missing, invalid or revoked API key."},"402":{"description":"Quota or credit balance exhausted."},"429":{"description":"Rate limit exceeded (shared with /v1)."}}}},"/v2/parse":{"post":{"operationId":"migration_parse","summary":"Migration door: runs as `parse`","description":"Compatibility path for an existing integration built on a Firecrawl-compatible v2 client. Runs the canonical `parse` operation with the same auth, rate limits, quota and pricing. Coverage: partial. multipart `file` + JSON `options`. formats markdown only. Other options ignored with a warning. For new integrations use the /v1 operation.","tags":["Migration door"],"x-migration-side-door":true,"x-canonical-operation":"parse","x-compat-status":"partial","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary"},"options":{"type":"string","description":"JSON-encoded options"}}}}}},"responses":{"200":{"description":"`{ success: true, … }` in the v2 wire format."},"400":{"description":"`{ success: false, error, code }`: invalid request or an option this door refuses (`unsupported_format`, `unsupported_option`, `unsupported_action`)."},"401":{"description":"Missing, invalid or revoked API key."},"402":{"description":"Quota or credit balance exhausted."},"429":{"description":"Rate limit exceeded (shared with /v1)."}}}}},"tags":[{"name":"Scraping","description":"Scrape, map, batch and screenshot"},{"name":"Extraction","description":"Structured extraction and document parsing"},{"name":"Crawl","description":"Async multi-page crawling and job status"},{"name":"Search","description":"Web search"},{"name":"Enrichment","description":"Domain enrichment, phone verification and batch enrichment"},{"name":"Context","description":"Brand, company resolution, the business graph and the directory"},{"name":"Account","description":"Keys, usage, quota, credits and bring-your-own-key credentials"},{"name":"Billing","description":"Plans, checkout and the billing portal"},{"name":"Meta","description":"Health, status, spec and discovery endpoints"},{"name":"Migration door","description":"Compatibility paths for moving an existing integration that speaks a Firecrawl-compatible v2 wire format. Same auth, limits and pricing as /v1. Use /v1 for new work."}],"x-migration-side-door":{"description":"A compatibility door for teams moving an existing integration. Not the primary API. Deferred routes answer 404 `operation_deferred`."}}