Say you have a list of 500 company websites and you want to know which run WordPress, which are on Shopify, who uses Cloudflare or Google Tag Manager. That's a typical prospecting, competitor-research or migration-audit question. Services like BuiltWith are great but priced for heavier use. For a quick, coarse pass, a single HTTP request per site gets you surprisingly far.
This post walks through doing that with an Apify actor I built, Website Tech Stack Detector. Disclosure: it's my actor and I earn a little when it's used.
How it works
For each URL it makes one plain HTTP request (no browser, no JavaScript execution), follows up to 5 redirects, and looks at:
- response headers (
server,cf-ray,x-vercel-id, ...) <meta name="generator">- script URLs and a few well-known HTML markers (
__NEXT_DATA__,/wp-content/) - cookie names (values are never read or stored)
About 60 technologies are recognised: CMS (WordPress, Drupal, Wix...), ecommerce (Shopify, WooCommerce, Magento...), analytics/tag managers, CDN/hosting, web servers, JS frameworks. Every detection includes the evidence that triggered it, so you can judge it yourself. It respects robots.txt by default.
Price
Pay-per-event: $0.005 per URL analyzed (at time of writing), so 500 sites is about $2.50. You're only charged for URLs fetched successfully (HTTP below 400). Timeouts, DNS failures, 4xx/5xx, non-public URLs and robots.txt skips are free. Compute is included.
Input
{
"urls": [
{ "url": "https://wordpress.org" },
{ "url": "https://nextjs.org" },
{ "url": "https://www.drupal.org" },
{ "url": "example.com" }
],
"respectRobotsTxt": true,
"maxConcurrency": 5,
"timeoutSecs": 20
}
Bare domains are accepted. For 500 sites, paste or generate the urls array from your spreadsheet; maxConcurrency goes up to 20.
Real output
I ran the actor's source locally on 10 URLs: 8 analyzed, 2 not analyzed and not charged (a 404 page and http://127.0.0.1, which is rejected as non-public). Two of the results:
[
{
"input": "https://nextjs.org",
"finalUrl": "https://nextjs.org",
"httpStatus": 200,
"technologies": [
{
"name": "Vercel",
"category": "Hosting",
"evidence": "header x-vercel-id"
},
{
"name": "Next.js",
"category": "JS framework",
"evidence": "script src /\\/_next\\/static\\//"
}
],
"capturedAt": "2026-10-16T05:53:25.120Z"
},
{
"input": "https://www.drupal.org",
"finalUrl": "https://new.drupal.org/",
"httpStatus": 200,
"technologies": [
{
"name": "Drupal",
"category": "CMS",
"evidence": "meta generator \"Drupal 11 (https://www.drupal.org)\""
},
{
"name": "Google Tag Manager",
"category": "Tag manager",
"evidence": "html GTM noscript iframe"
},
{
"name": "Fastly",
"category": "CDN",
"evidence": "header x-served-by"
},
{
"name": "Apache",
"category": "Web server",
"evidence": "header server"
},
{
"name": "jQuery",
"category": "JS library",
"evidence": "script src /jquery([.-]\\d[\\d.]*)?(\\.min)?\\.js/"
}
],
"capturedAt": "2026-10-16T05:53:25.120Z"
}
]```
{% endraw %}
Failed rows come back with an {% raw %}`error`{% endraw %} field instead of technologies, so you can retry or drop them.
## Be realistic about accuracy
This is a coarse tool, and my test run shows why:
- {% raw %}`https://www.shopify.com`{% endraw %} returned only **Cloudflare**. A single request saw nothing else. Absence of a detection is never proof a technology isn't used.
- {% raw %}`github.com`{% endraw %} came back as GitHub hosting plus Contentful, not a full stack.
- No JavaScript is executed, so tech that appears only after client-side rendering, or only on other pages, is missed. No version numbers are reported (except what a generator tag states).
- Sites behind bot walls or logins may return a block page. No proxies or captcha solving are used.
It's best for questions like "which of these 500 are on WordPress / Shopify / Cloudflare?", where the signal is in headers or the HTML, and weaker for building a complete technographic profile. I didn't run a 500-URL batch for this post, so I won't quote timing for one. Try 20 of your own sites first and compare with what you know.
## Call it from code
Small lists, one synchronous request:
{% raw %}
```bash
curl -X POST \
"https://api.apify.com/v2/acts/pennydata~website-tech-stack-detector/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"urls":[{"url":"https://wordpress.org"},{"url":"https://nextjs.org"}]}'
For 500 URLs use an async run with the Python client:
from apify_client import ApifyClient
client = ApifyClient("YOUR_APIFY_TOKEN")
urls = [{"url": u.strip()} for u in open("sites.txt") if u.strip()]
run = client.actor("pennydata/website-tech-stack-detector").call(
run_input={"urls": urls, "maxConcurrency": 10}
)
rows = client.dataset(run["defaultDatasetId"]).list_items().items
wordpress = [r["input"] for r in rows if "WordPress" in r.get("technologyNames", [])]
print(len(wordpress), "WordPress sites out of", len(rows))
technologyNames is a flat list for easy filtering; technologies has name, category and evidence per detection.
Wrap-up
For a few dollars you can tag a whole prospect list by CMS, ecommerce platform, CDN and analytics, with the evidence attached. Treat it as a first-pass filter and verify anything important by hand.
Written with AI (Claude).
Top comments (0)