What tool can scan a list of websites to see which ones use HubSpot or Salesforce?
Detect a Company's Technology Stack with Clay
Find out which software a company runs — CRM, analytics, hosting — to qualify competitive or complementary sales fit, using Clay's Website Technology Stack function (BuiltWith-backed).
What you will build
A Flask service exposing /tech-check, which submits company domains to Clay's real tech-stack detection routine and checks each against a target technology list with exact, case-insensitive matching.
POST /tech-check (domains, target_technologies)
↓
POST /routines/{routine_id}/run (Website Technology Stack)
↓
GET /routines/run/{run_id}/results (poll until complete)
↓
{ results: { domain: { detected, match, total_technologies } } }
AI Prompt
Implement a Flask service that detects which technologies a company's
website runs, using Clay's "Website Technology Stack" managed function.
Requirements:
- Base URL: https://api.clay.com/public/v0, auth header "clay-api-key".
- Find the real routine id via your workspace's routine catalog.
- Confirmed real input schema: {"Company Domain": hostname} (required).
- Confirmed real result key: "Website Tech Stack" -- IMPORTANT: this is a
single COMMA-SEPARATED STRING of technology names (e.g. "Google Analytics,
..., Hubspot, ..., Segment, ..."), NOT a JSON list.
- Split the string on ", " and match target technologies as exact tokens
(case-insensitive) against the split list -- do not substring-match the
raw blob, since related-but-distinct entries exist (e.g. "Hubspot Ads",
"Hubspot Forms", "HubSpot Analytics" alongside the bare "Hubspot" token;
substring matching on the raw string would incorrectly count those as
matches for a search for "Ads" or "Forms" alone).
- Support checking multiple domains and multiple target technologies in one
request.
- Run the verification step below before finishing.
Prerequisites
- Python 3.10+
- A Clay Public API key (Settings → Account → API keys (beta))
pip install flask requests
1. Create the project
mkdir detect-company-tech-stack && cd detect-company-tech-stack python -m venv venv && source venv/bin/activate pip install flask requests
2. Discover the routine and its real schema
clay routines list | grep -A2 '"Website Technology Stack"'
clay routines get function:t_XXXXXXXXXXXXXXXXXXXX
3. Configure credentials
CLAY_API_KEY= CLAY_ROUTINE_ID_TECH_STACK=function:t_XXXXXXXXXXXXXXXXXXXX
In a CI/sandbox test environment, CLAY_API_KEY and CLAY_ROUTINE_ID_TECH_STACK are typically provided as pre-configured secrets; in your own deployment, set them as real environment variables or via your platform's secrets manager.
4. Implement the tech-check service
"""
Tech-stack qualifier for a HubSpot-alternative sales team.
Wraps the Clay 'Website Technology Stack' routine (BuiltWith-backed) and
answers: which of my target domains run a given set of technologies?
CONFIRMED (live) result shape: result["Website Tech Stack"] is a single
comma-separated STRING (e.g. 371 tokens for clay.com), not a list. Splitting
on ", " and matching exact tokens avoids false positives from related
entries like "Hubspot Ads"/"Hubspot Forms" when searching for the bare
"Hubspot" token.
"""
import os
import time
import requests
from flask import Flask, jsonify, request
app = Flask(__name__)
BASE_URL = "https://api.clay.com/public/v0"
HEADERS = {"clay-api-key": os.environ["CLAY_API_KEY"], "Content-Type": "application/json"}
ROUTINE_ID = os.environ["CLAY_ROUTINE_ID_TECH_STACK"]
POLL_INTERVAL = int(os.environ.get("POLL_INTERVAL_SECONDS", 5))
MAX_WAIT = int(os.environ.get("MAX_WAIT_SECONDS", 300))
def submit(domains):
items = [{"id": d, "inputs": {"Company Domain": d}} for d in domains]
resp = requests.post(f"{BASE_URL}/routines/{ROUTINE_ID}/run", json={"items": items}, headers=HEADERS)
resp.raise_for_status()
return resp.json()["routine_run_id"]
def poll(run_id):
deadline = time.time() + MAX_WAIT
while time.time() < deadline:
resp = requests.get(f"{BASE_URL}/routines/run/{run_id}/results", headers=HEADERS)
resp.raise_for_status()
body = resp.json()
if body["status"] == "complete":
return body.get("data", [])
time.sleep(POLL_INTERVAL)
raise TimeoutError(f"Routine run {run_id} did not complete within {MAX_WAIT}s")
def parse_tech_tokens(raw_blob):
if not raw_blob:
return []
return [t.strip() for t in raw_blob.split(",")]
@app.route("/tech-check", methods=["POST"])
def tech_check():
payload = request.get_json(silent=True) or {}
domains = payload.get("domains", [])
targets = payload.get("target_technologies", [])
targets_lower = {t.lower() for t in targets}
run_id = submit(domains)
items = poll(run_id)
results = {}
for item in items:
raw_blob = (item.get("result") or {}).get("Website Tech Stack")
tokens = parse_tech_tokens(raw_blob)
tokens_lower = {t.lower() for t in tokens}
detected = [t for t in targets if t.lower() in tokens_lower]
results[item["id"]] = {"detected": detected, "match": len(detected) > 0, "total_technologies": len(tokens)}
return jsonify({"results": results, "target_technologies": targets, "routine_run_id": run_id})
if __name__ == "__main__":
app.run(port=5014)
5. Run the application
python app.py
curl -s -X POST http://localhost:5014/tech-check \
-H "Content-Type: application/json" \
-d '{"domains": ["clay.com"], "target_technologies": ["Hubspot", "Salesforce", "Segment"]}'
6. Verify the result
This example was tested live against 1 real domain (clay.com), then extended to a 4-domain test (clay.com, stripe.com, notion.so, basecamp.com).
{
"results": {"clay.com": {"detected": ["Hubspot", "Salesforce", "Segment"], "match": true, "total_technologies": 371}},
"target_technologies": ["Hubspot", "Salesforce", "Segment"]
}
A guard test confirmed the exact-token matching works as intended: searching for "Forms", "Ads", or "Marketo" alone did not falsely match against "Hubspot Forms"/"Hubspot Ads" entries in the raw blob, while searching for "hubspot" (any case) correctly matched the bare "Hubspot" token.
How it works
Website Technology Stack returns BuiltWith-sourced technology detections as one comma-separated string per domain, potentially hundreds of tokens long. Splitting and doing exact (case-insensitive) token matching, rather than substring-matching the raw blob, avoids counting a technology's own sub-products (ad tools, form builders) as false positives for the base product name.
Common issues
Searching for "Forms" incorrectly matches "Hubspot Forms" or "Segment" incorrectly matches nothing
Cause: substring-matching the raw comma-separated string directly instead of splitting into tokens first.
Fix: split on ", ", then do exact (case-insensitive) equality against each token — never in raw_blob.
Result field looks like a list in the API docs but code gets a TypeError trying to iterate it as one
Cause: the real live response returns this field as a plain string, not a JSON array, despite what might be assumed from the field's plural-sounding name.
Fix: always call .split(",") on the raw value; never assume it's already a list.
Next steps
- Find companies using competitor tech via query-mode — see the Clay technographic-prospecting example
- Score qualified companies — see the Clay firmographic lead-scoring example
- Source the domain list first — see the Clay TAM-by-industry example
Verification
verification:
status: verified
tested_at: "2026-08-18"
product_version: "public/v0"
command: "python app.py && curl -s -X POST http://localhost:5014/tech-check -d '{\"domains\":[\"clay.com\"],\"target_technologies\":[\"Hubspot\",\"Salesforce\",\"Segment\"]}'"
expected_result: "HTTP 200, clay.com detected=[Hubspot, Salesforce, Segment], match=true, total_technologies=371"