If you sell a Shopify app, every store not running Shopify is a wasted call. If you sell a WordPress security plugin, you want the sites on WordPress, ideally on an old version. If you are watching a competitor, the day their marketing site swaps one analytics tool for another, or moves from a hosted CMS to a framework, tells you something about what they are building next.
All of those start from the same question: given a URL, what is it built with? That question has a fairly reliable answer for most public sites, and a surprisingly unreliable one for some. This lesson is about getting the answer and knowing which kind you got.
01How detection actually works
A detector does not log into anything or ask the site. It fetches the page like a browser would and matches what comes back against a database of fingerprints. A WordPress site loads scripts from /wp-content/ and /wp-includes/. A Shopify store sets recognisable cookies and loads assets from Shopify's CDN. A site behind Cloudflare returns a server: cloudflare header. Google Tag Manager leaves a specific script snippet in the HTML. Each fingerprint is a pattern on one part of the response.
Website Tech Stack Detector (techstackdetector) does exactly this and tells you which parts matched. It is a GET with a url parameter, and at 30 credits it is one of the more expensive calls in this course, which shapes how you use it.
curl "https://api.apiverve.com/v1/techstackdetector?url=https%3A%2F%2Fwordpress.org" \
-H "x-api-key: YOUR_API_KEY"Look at detectedBy on each technology, and at MySQL, which is implied: true with an empty detectedBy.
When we ran this against wordpress.org it returned 13 technologies. A few of them show the full range of evidence you will see:
| Technology | Categories | confidence | implied | detectedBy |
|---|---|---|---|---|
| WordPress | CMS, Blogs | 100 | false | html, scriptSrc, headers, meta, dom |
| Nginx | Web servers, Reverse proxies | 100 | false | headers |
| PHP | Programming languages | 100 | false | scripts |
| Google Tag Manager | Tag managers | 100 | false | html |
| MySQL | Databases | 100 | true | (empty) |
The detectedBy list is the evidence, and it is the most underused field in the response. WordPress matched on five independent signals; it would take a deliberate effort to fake that. Nginx matched on one response header. MySQL matched on nothing at all: it is implied, because WordPress requires a MySQL-compatible database. Nobody saw MySQL. The fingerprint database knows that where there is WordPress there is a MySQL or MariaDB server somewhere, and it reports that inference.
Now run it against a site with almost nothing to fingerprint:
curl "https://api.apiverve.com/v1/techstackdetector?url=https%3A%2F%2Fexample.com" \
-H "x-api-key: YOUR_API_KEY"Three results: Cloudflare, Cloudflare DNS and HTTP/3. No CMS, no framework.
example.com returns three technologies: Cloudflare (detected from headers and dns), Cloudflare DNS (from dns) and HTTP/3. That is not a failure. It is a static page behind a CDN, and the honest answer is that the CDN is all a visitor can see.
02Reading confidence, implied and version
Three fields decide how much weight a detection deserves.
confidence runs from 0 to 100. Some fingerprints are definitive (a header that only one product sets); others are suggestive (a CSS class name that one framework popularised and others copied). A fingerprint that matches weakly produces a lower number. For anything that triggers an action, like adding a lead to a campaign, set a threshold and stick to it. Starting at 75 and tuning from your own false positives is reasonable.
implied means "inferred, not observed". Inferences are usually right, but they are a different kind of claim. If your sales copy says "we noticed you run MySQL", and the inference came from WordPress on a host that runs MariaDB, the prospect knows you guessed. Filter out implied technologies whenever the result will be quoted back to a human.
version is a premium field, and even on plans that include it, it is often null. In the run above, WordPress came back with a version and the block editor did not. A version only appears when the page reveals it, and well-run sites hide them on purpose. Never build a segment like "sites on an outdated version" and treat the absence of a version as "unknown, include anyway": you would be emailing every security-conscious site you found. A cpe identifier (also premium) is there for matching against vulnerability databases when a version is known.
A detection comes back with confidence: 100, implied: true and an empty detectedBy. What does that tell you?
03Where detection is blind
The API's own limitations are worth reading carefully, because each one maps to a mistake teams make.
Server-side software behind a CDN or proxy is usually invisible. A site on Cloudflare, Fastly or a load balancer returns the edge's headers, not the origin's. You will see the CDN; you often will not see the web server, the language or the hosting provider behind it. "No backend detected" means "not visible", never "none".
Bot challenges hide the whole site. Some sites answer automated requests with a challenge page. The detector then sees only the CDN. The response tells you when this happened: check statusCode. A 403 or 503 with a short technology list is a challenge, not a minimalist stack. Record it as blocked and do not retry in a loop.
JavaScript-injected technology needs rendering. By default, mode is static: the detector reads the HTML the server sent. Single-page apps and tag-manager-loaded tools often only appear after scripts run. The render parameter (premium) loads the page in a headless browser first and also reads JavaScript globals. It is slower, so use it when a static result looks thin for a site you know is an app, not as a default.
Redirects are followed. Compare finalUrl with url. A request for example-shop.com that ends on a marketplace profile page describes the marketplace, not the shop.
Pages over 5 MB are rejected. Rare for a homepage, common for some generated report pages. Point the detector at the homepage.
There is also time. The fingerprintsUpdatedAt field tells you when the fingerprint database was last refreshed. If you store detections, store that timestamp with them; a technology that "appears" between two runs might be a site change or a new fingerprint.
04Enrich a lead list
Here is a realistic job: a CSV of company domains from a signup form or a trade-show list, and a requirement to tag each one with its CMS, ecommerce platform and analytics so sales can prioritise. This is Python 3.9+ with requests.
import csv
import os
import sys
import time
import requests
KEY = os.environ["APIVERVE_API_KEY"]
WANTED = {"CMS", "Ecommerce", "Analytics", "JavaScript frameworks", "Tag managers"}
MIN_CONFIDENCE = 75
def detect(domain):
r = requests.get(
"https://api.apiverve.com/v1/techstackdetector",
params={"url": f"https://{domain}"},
headers={"x-api-key": KEY},
timeout=45,
)
if r.status_code == 429:
if r.headers.get("x-api-remaining-credits") == "0":
sys.exit("Out of credits for the month; stopping.")
time.sleep(int(r.headers.get("retry-after", "10")))
return detect(domain)
body = r.json()
if body.get("status") != "ok":
return {"domain": domain, "state": "error", "detail": body.get("error")}
data = body["data"]
if data["statusCode"] >= 400:
return {"domain": domain, "state": "blocked", "detail": data["statusCode"]}
tags = {}
for t in data["technologies"]:
if t["implied"] or t["confidence"] < MIN_CONFIDENCE:
continue
for cat in t["categories"]:
if cat in WANTED:
tags.setdefault(cat, []).append(t["name"])
return {"domain": domain, "state": "ok", "detail": "; ".join(f"{k}: {', '.join(v)}" for k, v in sorted(tags.items()))}
with open(sys.argv[1], newline="") as f_in, open("enriched.csv", "w", newline="") as f_out:
writer = csv.DictWriter(f_out, fieldnames=["domain", "state", "detail"])
writer.writeheader()
for row in csv.DictReader(f_in):
writer.writerow(detect(row["domain"].strip().lower()))A few deliberate choices in there. Calls run one at a time; enrichment is a batch job, nobody is waiting, and serial calls keep you well under the rate limit. Out-of-credits stops the job instead of hammering a 429 for the rest of the file. Blocked sites get their own state so sales knows the difference between "no CMS" and "couldn't look". And the category filter drops the long tail of detections (fonts, performance hints, RSS) that nobody prioritises a lead on.
The single most effective cost control is not in the script: deduplicate domains before you start, and cache results for weeks. Companies change their stack every year or two, not every day. Re-detecting a domain you looked at last Tuesday costs 30 credits to learn nothing. Key the cache by registrable domain, not by full URL, and expire entries after 30 to 60 days.
Your enrichment run reports 'no CMS' for 30% of domains. Most of them returned statusCode: 403 and only a CDN. What is the right fix?
05Competitive analysis: track changes, not snapshots
A single detection of a competitor is trivia. A series of them is a signal. Store the set of slug values per run and diff consecutive runs; that is all the logic you need.
// Node 18+, ES module
const KEY = process.env.APIVERVE_API_KEY;
export async function stackSlugs(url) {
const qs = new URLSearchParams({ url });
const res = await fetch(`https://api.apiverve.com/v1/techstackdetector?${qs}`, {
headers: { 'x-api-key': KEY },
signal: AbortSignal.timeout(45000),
});
const body = await res.json();
if (body.status !== 'ok') throw new Error(body.error);
return new Set(
body.data.technologies.filter((t) => !t.implied && t.confidence >= 75).map((t) => t.slug)
);
}
export function diff(previous, current) {
return {
added: [...current].filter((s) => !previous.has(s)),
removed: [...previous].filter((s) => !current.has(s)),
};
}Run it weekly per competitor, persist the slug set, and alert on any non-empty diff. A new payment provider, a switched analytics tool or a migration to a new frontend framework appears as a two-line change. Be sceptical of single-run flickers: a technology loaded by a tag manager can appear and disappear depending on what a visitor was served, so require a change to persist across two runs before you announce it to anyone.
06Use it responsibly
Everything a detector reports is public: it is what any browser receives. That makes it fair to use for research. It does not make it fair to use carelessly. Keep a few lines in mind:
- Do not present inferences as facts to the prospect. "Looks like you run Shopify" with evidence is fine; claims about versions or vulnerabilities in a cold email read as a threat.
- Respect the sites you look at. Weekly checks of a few competitors are research. Daily runs across thousands of domains you have no relationship with is crawling, and needs the same restraint as any crawler.
- Remember what the data is not. It describes a website, not a company. A company's marketing site might be run by an agency on a stack the company itself never touches.
The next lesson moves from the page to the domain itself: who registered it, when, where its DNS points and whether its certificate is healthy. Continue with domain intelligence.