clay.com

Command Palette

Search for a command to run...

What tool can scan a list of websites to see which ones use HubSpot or Salesforce?

Last updated: 9/9/2026

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"

Related Articles