REST API reference

12 endpoints, one shape: JSON in, JSON out. Bearer-token auth. Rate-limited per API key. Every response typed via the OpenAPI 3.1 spec at /openapi.json.

Authentication

All authenticated requests carry a Bearer token:

Authorization: Bearer ws_...

Get a key at webscans.com/settings/keys. Public endpoints (search, domain, similar) work without a key at reduced rate limits.

Base URL

https://webscans.com

Errors & rate limits

StatusMeaningResponse
200OKJSON payload
401 / 403Invalid or missing key{"detail": "..."}
402Insufficient credits (paid tier)Header: X-Credits-Needed
404Domain not in index & couldn't fetch live{"detail": "..."}
429Rate limit exceededHeader: Retry-After
503Backend saturated — retryHeader: Retry-After: 3
5xxServer error — safe to retry with backoff

Rate limits: 30 req/min (free), 300 req/min (pro), 3,000 req/min (enterprise). Search-result row caps: 100 / 10,000 / 1,000,000 respectively.

OpenAPI spec

Full machine-readable spec: GET /openapi.json. Swagger UI: /docs. Import into Postman / Insomnia / your codegen tool of choice.

GET/search
Keyword + filter search across 13.3M homepages.
ParamTypeNotes
qstring, requiredKeyword or phrase
modephrase|any|singleDefault phrase
techstringe.g. HubSpot, Shopify, React
industrystringe.g. Financial Services
countryISO-2US, GB, DE, ...
tldstringWithout dot: com, io, ai
hasemail|phone|pricing|blog|ecommerce|contactFeature filter
limitint1–500, default 100
offsetintPagination
curl
Python
TypeScript
curl "https://webscans.com/search?q=solar%20panels&tech=HubSpot&country=US&limit=25" \
  -H "Authorization: Bearer ws_..."
from webscans import WebScans
ws = WebScans(api_key="ws_...")
r = ws.search("solar panels", tech="HubSpot", country="US", limit=25)
for hit in r.results:
    print(hit.domain, hit.industry)
const res = await fetch(
  "https://webscans.com/search?q=solar+panels&tech=HubSpot&country=US&limit=25",
  { headers: { Authorization: "Bearer ws_..." }},
);
const { results } = await res.json();

GET /search/count

GET/search/count?q=hubspot
Total matching-domain count for a query. Cheap — no rows returned.
curl "https://webscans.com/search/count?q=hubspot"
# → {"query":"hubspot","total_results":19433}

GET /autocomplete

GET/autocomplete?q=stri&limit=6
Suggestions as you type. ?mode=domain returns only domain-name suggestions.

GET /domain/{d}

GET/domain/stripe.com
Full 178-field cached profile. Returns 404 if not indexed — use /lookup or /enrich to fetch live.
curl https://webscans.com/domain/stripe.com | jq '.industry, .tech_stack'
# "Finance & Banking"
# "Next.js, Schema.org, ContactPoint Schema, ..."

GET /domain/{d}/competitors

GET/domain/{d}/competitors?limit=25
Lookalike / competitor discovery via FAISS embedding similarity (gte-Qwen2-7B-instruct, 3584-dim). Scored 0–1 cosine.
curl "https://webscans.com/domain/figma.com/competitors?limit=10"
# {
#   "domain": "figma.com",
#   "method": "faiss_embedding",
#   "total_found": 10,
#   "competitors": [
#     {"domain": "linear.app",   "score": 0.799, "match_type": "lookalike", ...},
#     {"domain": "composio.dev", "score": 0.792, "match_type": "lookalike", ...},
#     ...
#   ]
# }

GET /lookup/{d}

GET/lookup/{d}?force=false
Cache-first, with live-crawl fallback (curl_cffi → Playwright). Older two-stage cascade; use /enrich for the new race-based version with CC-WET.

GET /enrich/{d}

GET/enrich/{d}?wait_ms=800&force=false
Real-time enrichment cascade: cache (12-mo TTL) → parallel race of live curl_cffi + Common Crawl WARC (first byte wins) → Playwright browser → Bright Data residential proxy. Sub-1s p95 target.
curl "https://webscans.com/enrich/anthropic.com?wait_ms=800"
# {
#   "domain": "anthropic.com",
#   "source": "cached",          // cached | live_curl | cc_wet | playwright | brightdata | none
#   "elapsed_ms": 8,
#   "fetch_ms": 0,
#   "cache_age_days": 42,
#   "enriched_columns": 131,
#   "profile": { ... 178 fields ... }
# }

Params: wait_ms (100–15000, default 800) caps the race budget. force=true bypasses cache. allow_playwright / allow_brightdata disable those tiers if you want budget-safe.

GET /search/download

GET/search/download?q=...&tier=pro
Streaming CSV export of search results. All 178 columns. Sorted by score DESC.

Free tier: 100 rows. Pro: 10K. Enterprise: 1M.

GET/bulk-search/download?keywords=k1,k2,k3&tier=pro
Comma-separated keywords → single dedup'd CSV. Globally sorted by score DESC across all keywords.

POST /bulk-lookup

POST/bulk-lookup
Upload a CSV of domains (up to 100K rows), receive an enriched CSV with all 178 columns per domain.
curl -X POST https://webscans.com/bulk-lookup \
  -H "Authorization: Bearer ws_..." \
  -F "file=@my_domains.csv" \
  -o enriched.csv

GET /my-companies/export

GET/my-companies/export
Export the caller's saved-company list. Paid tier — costs 1 credit per row.

POST /copilot/query

POST/copilot/query
Natural-language search. An agentic Claude loop picks the right tool (search / lookup / similar), executes it, and returns the raw results plus a text summary.
curl -X POST https://webscans.com/copilot/query \
  -H "Content-Type: application/json" \
  -d '{"message":"find SaaS companies in California using HubSpot"}'
# {
#   "answer": "Here are 8 SaaS companies ...",
#   "tool_call": {"name":"search_companies","args":{...}},
#   "results": {...},
#   "elapsed_ms": 4520,
#   "model": "claude-sonnet-4-6"
# }