Website Technology Detector – Tech Stack, CMS & Changes
Pricing
$3.00 / 1,000 site analyses
Website Technology Detector – Tech Stack, CMS & Changes
Detect the CMS, frameworks, analytics, CDN and e-commerce platform of any site from one HTTP request. Every detection comes with its evidence; failed sites are free; track what changed since the last run.
Pricing
$3.00 / 1,000 site analyses
Rating
0.0
(0)
Developer
Francesco Antonio Russo
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
4 days ago
Last modified
Share
Give it a list of websites. For each one it makes one request to the page (no browser), reads robots.txt and the DNS records, and returns one JSON record: the CMS, e-commerce platform, web server, CDN, analytics, JavaScript frameworks, programming language and everything else it can recognise — each with a version (when the page shows one), a confidence and the evidence: which header, cookie, meta tag, script or DNS record matched, and the value that matched.
Four things it does on purpose:
- Every detection shows its proof. You can check any finding in seconds, and you can see why a weak one is weak.
- A list with no valid address is not a failed run. Entries that are not web addresses come back as free
invalidrecords and the run ends normally; only a run in which every site was unreachable ends as failed (a dead list cannot be told from a network outage). - Failed sites are free. A site that blocks the request, does not exist, is not a web page, forbids bots in
robots.txtor shows nothing recognisable costs nothing — and is still returned as a record that says why. - "What changed since last time." With
trackChangeson, a scheduled run tells you which technologies were added, removed or changed version on each site. A site that was unreachable or blocked in one run is never reported as "technology removed". - The rules are dated and sourced. Every record carries the version of the rule set it was checked against, and this README says where the rules come from and how many of them can actually work without a browser.
| Technologies known | 7,628 rules; 6,728 of them can be detected without a browser (6,680 directly from a signal this Actor reads, 48 more only as something another detection implies), plus 10 supplementary patterns written for this Actor (listed in src/extra-rules.json). The other 900 need a real browser (JavaScript variables, network calls) and are not claimed |
| Request budget per site | robots.txt + the page (+ redirects) + a few DNS lookups. At least 1 second between requests to the same site; different sites in parallel |
| Throughput | 200 different sites in about 3 minutes at concurrency 10 (roughly 1 site per second, ~4,000 sites an hour; measured). A run of 152 sites with DNS on took 51 s, peak resident memory 464 MB (macOS). Memory does not grow with the number of sites: the data of a site (robots.txt, DNS records, the page) is released as soon as its record is delivered; a simulated run of 20,000 sites stays flat. The Actor asks for 1,024 MB by default (512 MB is the minimum; it measured 464 MB peak). Speed depends on the load of the machine and of the sites: a run of 30 sites took 185 s on a busy machine |
| Speed | the analysis of a typical page takes 60–700 ms; a page at the 2 MB limit about 1.2 s (measured on a laptop; a whole run of 25 sites took about 10 s) |
| Honest about blocking | no disguised browser, no proxies, no retry tricks. If a site refuses, you get blocked and whatever its response headers reveal |
Who it is for
- Agencies and consultants auditing or pitching sites: which CMS, e-commerce platform, analytics and CDN a prospect or a client runs, with the proof to show.
- Sales ops and lead enrichment: turn a list of domains into columns (
cms,ecommerce,webServer,cdn,analytics) for a spreadsheet or CRM; failed domains are free and come back with the reason. - Security and inventory teams: which sites of a portfolio run which software and which version (
cpeis included), and a scheduled change log (trackChanges) when something is added, removed or updated.
Quick start
{"urls": ["wordpress.org", "https://react.dev", "example-shop.com/en/"],"trackChanges": true}
Domains without https:// are fine. Schedule it daily or weekly with trackChanges on to get a change log of the technologies of your list.
Pricing — what you pay, what is free
One event, site-analysis (title "Site analysis"): $0.003 per site analysis — charged when the page of a site was read and analysed and at least one technology with scope page was found that is not one of the trivial findings every site has (HSTS, HTTP/3, HTTP/2, Open Graph). No fee per run, none for starting, none for failures, none for sites where only DNS-based (domain) entries were found. The platform usage of the run (compute, storage) is included in the event price, as with every pay-per-event Actor.
| Result for a site | status | Charged? |
|---|---|---|
HTML page read and analysed, at least one page technology found | ok | yes — $0.003 |
HTML page read and analysed, nothing recognised (after minConfidence and excludeCategories), or only domain entries from DNS, or only trivial findings (HSTS, HTTP/3, HTTP/2, Open Graph) | ok | no |
401 / 403 / 429 / 503, any answer with an AWS WAF captcha / challenge / block header (a 405 included), or an anti-bot or block page (Cloudflare, AWS WAF, Akamai, Imperva, PerimeterX, DataDome and similar; headers-only result) | blocked | no |
| DNS failure, connection refused, TLS error, timeout, other 4xx / 5xx | unreachable | no |
| PDF, JSON, image… instead of a web page | not_html | no |
robots.txt forbids the page to this bot | robots_disallowed | no |
| An input line that is not a web address | invalid | no |
| Sites not reached because your spending limit was hit | listed as skipped in SUMMARY | no |
Cost cap: number of sites × $0.003. If a run reaches your spending limit it stops, and the sites it did not analyse are listed in the SUMMARY record as skipped — they are not delivered as failures.
Input
| Field | Default | Meaning |
|---|---|---|
urls | — | Websites: full URLs or bare domains (example.com is read as https://example.com/); an email address (user@example.com) is invalid, not a website. Duplicates are removed (example.com, www.example.com and http://example.com are one site; the first is kept); entries that are not web addresses (mailto:, a bare number such as 12345…) become invalid records, they do not stop the run. A page that is only a stub handing over to another address with <meta http-equiv="refresh" content="0; url=…"> (delay up to 5 s) is followed one hop, with the same checks (robots.txt, private addresses); redirectChain lists it with status: "meta-refresh". A bare domain whose apex does not answer (no DNS record, refused, TLS error, timeout) is tried once more as www. and the record says so in warnings |
includeEvidence | true | Add the proof to every technology. Off keeps records small |
minConfidence | 50 | 0–100, a filter on the output only: the engine evaluates the same way whatever the value (so "what changed" is never affected by it). Weak signals add up (two 50 % hints make 100 %). 0 shows everything, 100 only certain detections |
checkDns | true | Also read TXT / MX / NS / SOA / CNAME records of the site and its main domain. See the note on DNS below |
excludeCategories | [] | Category names to leave out, e.g. ["Miscellaneous", "Security"] (this removes Open Graph, HSTS, HTTP/3… ). A technology is removed only when all its categories are excluded; removed technologies do not count and do not make a site chargeable. Names are case-insensitive; the names are in technologies[].categories |
respectRobotsTxt | true | Do not read a page that robots.txt forbids to TechDetectorBot. robots.txt is read in any case: it is also a detection signal |
trackChanges | false | Remember the technologies of each URL (named key-value store website-technology-detector-state) and report added / removed / version changes. Only what both runs looked at is compared: the stricter minConfidence of the two runs, the current excludeCategories, and DNS-based entries only if DNS was read in both (changes.comparedScopes tells which scopes were compared). Changing options or a failing DNS lookup never produces fake changes |
maxConcurrency | 10 | 1–20 sites in parallel (requests to the same site are always spaced) |
timeoutSecs | 20 | 5–60, per request |
A non-numeric number stops the run before anything is charged; a number out of range is clamped.
Output
One dataset record per input. Real example (cern.ch, first run with trackChanges, 12 October 2026). Shortened: the real record lists 16 technologies (2 are shown, each with at most 3 pieces of evidence) and more categories in byCategory:
{"url": "https://cern.ch/","finalUrl": "https://home.cern/","status": "ok","httpStatus": 200,"error": null,"redirectChain": [{"url": "https://cern.ch/","status": 302}],"technologies": [{"name": "WordPress","slug": "wordpress","categories": ["CMS", "Blogs"],"version": "6.9.9","confidence": 100,"website": "https://wordpress.org","description": "WordPress is a free and open-source content management system written in PHP a…","cpe": "cpe:2.3:a:wordpress:wordpress:*:*:*:*:*:*:*:*","scope": "page","evidence": [{"source": "meta","detail": "<meta generator> = \"WordPress 6.9.9\""},{"source": "scriptSrc","detail": "script src https://home.cern/wp-includes/js/dist/vendor/react.min.js?ver=18.3.1.1"},{"source": "script","detail": "inline script: …en-GB\",\"is_rtl\":0,\"name\":\"English\",\"url\":\"https://home.cern/\",\"flag\":\"http://home.cern/wp-content/mu-plugins/polylangpro/vendor/wpsyntex/polylang/flags/gb.png\",\"current_lang\":true,\"no…"}]},{"name": "PHP","slug": "php","categories": ["Programming languages"],"version": "8.4.25","confidence": 100,"website": "https://php.net","description": "PHP is a general-purpose scripting language used for web development.…","cpe": "cpe:2.3:a:php:php:*:*:*:*:*:*:*:*","scope": "page","evidence": [{"source": "header","detail": "X-Powered-By: PHP/8.4.25"},{"source": "script","detail": "inline script: …diaUploadMiddleware ); wp.apiFetch.nonceEndpoint = \"https://home.cern/wp-admin/admin-ajax.php?action=rest-nonce\"; //# sourceURL=wp-api-fetch-js-after moment.updateLocale( 'en_GB', {…"},{"source": "implied","detail": "implied by WordPress"}]}],"technologyCount": 16,"pageTechnologyCount": 16,"domainTechnologyCount": 0,"technologyNames": ["Google Font API", "jQuery", "jQuery Migrate", "Matomo Analytics", "Max Mega Menu", "Moment.js", "MySQL", "Nginx", "OpenResty", "PHP", "Priority Hints", "RSS", "WordPress", "WordPress Block Editor", "WordPress Site Editor", "WordPress Super Cache"],"byCategory": {"Analytics": ["Matomo Analytics"],"CMS": ["WordPress"],"Programming languages": ["PHP"],"Web servers": ["Nginx", "OpenResty"]},"cms": "WordPress","ecommerce": null,"webServer": "OpenResty","cdn": null,"analytics": ["Matomo Analytics"],"javascriptFrameworks": null,"programmingLanguages": ["PHP"],"signalsChecked": ["headers", "cookies", "meta", "scriptSrc", "scripts", "html", "dom", "url", "text", "robots", "dns", "css"],"signalsNotChecked": ["js", "xhr", "certIssuer", "probe", "dom.properties", "css (external stylesheets)"],"dnsChecked": true,"truncated": false,"warnings": [],"changes": {"firstRun": true,"comparedScopes": ["page", "domain"],"added": [],"removed": [],"versionChanged": [],"previousCheckedAt": null},"rulesVersion": "eea872a (2026-09-16)","checkedAt": "2026-10-12T13:23:24.099Z","responseTimeMs": 826,"charged": true}
| Field | Notes |
|---|---|
url, finalUrl, redirectChain | what you gave (normalized), where the page was actually read, and the redirects followed |
status, httpStatus, error | see the table above; error says in plain words why a site could not be fully analysed |
technologies[] | sorted by confidence, then name. scope is page when at least one proof comes from the HTTP response, the HTML or robots.txt (or the technology is implied by a page one), domain when every proof is a DNS record: services the organisation has linked to the domain in DNS, not necessarily used on this page. version is null when the page does not show one. cpe is the identifier used by vulnerability databases, when the rules know it |
technologies[].evidence[] | source is header, cookie, meta, scriptSrc, script, html, dom, url, text, robots, dns, css or implied; detail names the header / selector / pattern and the value that matched (shortened to 200 characters). implied says which detected technology implies this one (e.g. WordPress implies PHP and MySQL) |
technologyCount, pageTechnologyCount, domainTechnologyCount, technologyNames, byCategory | counts (total = page + domain) and flat lists for spreadsheets; the lists contain both scopes, filter on technologies[].scope if you only want one |
cms, ecommerce, webServer, cdn, analytics, javascriptFrameworks, programmingLanguages | convenience fields built from page technologies only (cdn also accepts domain ones: a CDN or DNS provider is a fact about the domain, but never library or storage hosts such as jsDelivr, cdnjs, Google Hosted Libraries, unpkg or Amazon S3, which stay in technologies); ecommerce takes "Squarespace Commerce" or "Wix eCommerce" only when the page also shows another sign of a shop (their upstream rules fire on the platform alone); null when nothing matched. The single-value ones (cms, ecommerce, webServer) hold the best technology of that category: real products before generic hints such as "Cart Functionality", then the most confident (a hint under 50 % never fills them, even with minConfidence: 0; it stays in technologies), with a detected version only as a tie-breaker |
signalsChecked, signalsNotChecked, dnsChecked | exactly what was and was not looked at for this site (a blocked site has no page body to look at). dnsChecked is true only when every DNS query got an answer (a failed query is retried once); if only some did, it is false, a warning says so, the entries that did arrive are still listed, and trackChanges neither compares nor remembers the DNS part of that run (no false "removed") |
truncated, warnings | pages over 2 MB are cut at 2 MB and say so |
changes | only with trackChanges and only for ok records: { firstRun, comparedScopes, added[{name, version, scope}], removed[{name, version, scope}], versionChanged[{name, from, to}], previousCheckedAt }; null otherwise |
rulesVersion, checkedAt, responseTimeMs, charged | rule-set version, ISO 8601 UTC time of the check, page request time, whether this record triggered the paid event |
The SUMMARY record (key-value store) holds the totals (records by status, charged, free, skipped, resumed), the list of skipped sites with the reason, the stop reason (spending limit, timeout, abort) and the rule statistics.
What is looked at, and what is not
| Signal | Evaluated? | How |
|---|---|---|
| Response headers | yes | all headers, names case-insensitive |
| Cookies | yes | from every Set-Cookie header |
<meta> tags | yes | name, property and http-equiv |
Script URLs (scriptSrc) | yes | src of every <script>, resolved to absolute URLs |
Inline script code (scripts) | yes | inline <script> bodies only, length-limited |
HTML source (html) | yes | first 450,000 and last 150,000 characters of long pages |
DOM selectors, element text and attributes (dom) | yes | CSS selectors on the parsed page; rules that read JavaScript properties of DOM nodes are ignored |
Page URL (url) | yes | the final URL after redirects |
Visible text (text) | yes | first 100,000 characters |
robots.txt content (robots) | yes | when the site has one |
DNS TXT / MX / NS / SOA / CNAME (dns) | yes | for the host and its registrable domain; turn off with checkDns |
Inline <style> CSS (css) | yes | inline styles only |
JavaScript variables at runtime (js) | no | needs a browser |
Network calls made by the page (xhr) | no | needs a browser |
TLS certificate issuer (certIssuer) | no | needs a TLS handshake read |
Extra probe URLs (probe) | no | would be extra requests |
| External stylesheets | no | would be extra requests |
Rules that depend on a technology (requires, requiresCategory) are only evaluated once that technology is detected; implies is followed recursively (an implied technology never gets more confidence than the one that implies it); excludes removes the technology a confident one rules out. If several rules match the same technology their confidences add up to at most 100.
A note on DNS. TXT records often contain verification tags that SaaS products ask a company to publish (openai-domain-verification=…, atlassian-domain-verification=…, SPF entries). They are real findings, but they are tagged scope: "domain": services the organisation has linked to the domain in DNS, not necessarily used on this page. They do not feed the convenience fields (except cdn) and they are never charged. If you do not want them at all, set checkDns to false.
Limits — read this before you rely on it
- Known gaps of the community rules. Some technologies are recognised upstream only through a JavaScript variable (for example Microsoft Clarity, New Relic or Intercom on some pages): where a static marker exists and we verified it, it is in the supplementary patterns; for the others a miss is possible, and the record's
signalsNotCheckedsaysjs. - No browser. Technologies that only show up when JavaScript runs (many single-page-app libraries, tag managers that inject other tags, widgets loaded after the page) can be missed. The sites you test are returned with a list of what was not checked; absence of a technology is never proof that a site does not use it.
- Sites that block bots. If a site answers 401 / 403 / 429 / 503 or an anti-bot challenge page (including the AWS WAF JavaScript challenge, which comes as an empty-titled
202; Akamai's "page unavailable" block page; a "not available in your region" geoblock; a shop that says it does not serve your country, and any page with no title, no text and nothing but scripts, even a 256 KB one such as Nordstrom's F5 interstitial, whatever its status), you getblocked, free, with what the headers reveal (often the CDN or WAF). Some sites (booking.com is one) challenge only some requests: the same address can beblockednow andoka minute later; run it again. Nothing is bypassed: no fake browser, no proxy, no retries to get around a refusal. - Only the page you give. One page per site (usually the home page). Technologies used only on other pages are not seen.
- Pages over 2 MB are cut at 2 MB (
truncated: true). - A rule is a heuristic. A header or a script name suggests a technology, it does not prove it: e.g. a mention of an Amazon storage bucket in a Content-Security-Policy header is reported as such. Read the evidence.
- Low-value entries. Items such as "Open Graph", "HSTS", "HTTP/3" or "Priority Hints" are technologies in the rule set (scope
page) and appear on most sites. They carry little information. A site where onlyHSTS,HTTP/3,HTTP/2orOpen Graphare found isokand not charged (see Pricing); other common entries such as "Priority Hints" do count. UseexcludeCategories(e.g.["Miscellaneous", "Security"]) to drop them from the record and from the charge, or filter ontechnologies[].categories. - DNS rebinding. The check that refuses private addresses resolves the host name, and the HTTP request resolves it again; a host that answers differently the second time is not caught. The impact is limited to the Actor's own container.
- Bare domains use
https://. If a bare domain (typed without a scheme, no path or port) cannot be reached over https (TLS handshake failure, refused, reset, timeout), it is tried once over plainhttp://and the record says so inwarnings; an input that starts withhttps://is never downgraded. A DNS failure is not retried on http (thenwww.is tried, as before). - One site, one memory.
trackChangesis keyed like the duplicate check:python.org,www.python.organdhttp://python.orgshare one history (histories saved by earlier versions under the exact URL are still read). Two schedules on the same site share it. - Robots.txt unreadable (HTTP 5xx) is treated as "disallow", as RFC 9309 requires; no robots.txt (404) means no restrictions.
- Private and loopback addresses are refused at every redirect.
- Rule quality is the quality of a community rule set: occasional false positives and stale rules exist. The
rulesVersionof every record tells you which version produced it.
Where the detection rules come from
The detection rules are the open-source rule set of the Wappalyzer project as continued by the community repository enthec/webappanalyzer — commit eea872af449e207e055398f7369d11ee48c8ea03, dated 16 September 2026, 7,628 technologies. They are included unchanged in the vendor/webappanalyzer/ folder of this Actor (rule files, category and group lists, the licence and a SOURCE.md with the commit; the third-party logos of that repository are not copied). This Actor is independent: it has no affiliation with, and is not endorsed by, Wappalyzer or Enthec. The rules are licensed GPL-3.0; the code of this Actor (the engine, the runner, the schemas) is released under GPL-3.0-or-later, and the source code of this Actor is available under GPL-3.0-or-later; ask through the Issues tab. The original Wappalyzer rules were MIT-licensed; the community fork used here is GPL-3.0. If the rule set is updated, the new commit and date appear in rulesVersion and here.
How many rules work without a browser is computed on every start from the rules themselves (a technology counts when at least one of its signals is one this Actor reads, directly or through implies, and — for plugins — when what it requires is itself detectable): 6,728 of 7,628. Our own supplementary patterns (10, in src/extra-rules.json) only add to the community rules: they cover cases where the community rule looks for a JavaScript variable although the page also carries a static marker (for instance /_next/static/ for Next.js, the inline Google Tag Manager snippet, /_nuxt/, Segment, Hotjar, Microsoft Clarity and the VitePress generator tag). Each was added only after checking on real sites that the community rule missed it, and has a test with a positive and a negative (prose) case. The run log prints the same numbers, and the number of rule patterns skipped because they did not compile (0 in this version).
Use the output
// Apify client (JavaScript)import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: process.env.APIFY_TOKEN });const run = await client.actor('far003/website-technology-detector').call({urls: ['wordpress.org', 'https://react.dev'],trackChanges: true,});const { items } = await client.dataset(run.defaultDatasetId).listItems();for (const site of items) console.log(site.url, site.status, site.cms, site.technologyNames.length);
# Pythonfrom apify_client import ApifyClientclient = ApifyClient(token)run = client.actor("far003/website-technology-detector").call(run_input={"urls": ["wordpress.org", "https://react.dev"],"includeEvidence": True,"minConfidence": 50,})for site in client.dataset(run["defaultDatasetId"]).iterate_items():print(site["url"], site["status"], site["cms"], site["webServer"])
n8n / Make / Zapier. Use the Apify node or module with this Actor, or call the synchronous endpoint from an HTTP Request node:
POST https://api.apify.com/v2/acts/far003~website-technology-detector/run-sync-get-dataset-items?token=YOUR_TOKENContent-Type: application/json{ "urls": ["{{ $json.domain }}"], "includeEvidence": false }
The response is the list of records; filter on status == "ok" and read cms, ecommerce or technologyNames. For a weekly "what changed" digest, schedule the Actor with trackChanges: true and send only records whose changes.added, changes.removed or changes.versionChanged is not empty.
FAQ
Why is a site blocked when I can open it in my browser? Many sites refuse requests that do not come from a full browser. This Actor identifies itself honestly (TechDetectorBot) and does not try to look like one, so some sites will always be blocked. Blocked sites are free.
Why did I get a technology I do not see on the page? It came from a header, a cookie, a script URL, a DNS record or robots.txt. Open evidence to see which. Entries with scope: "domain" come from DNS only: services the organisation has linked to the domain, not necessarily used on this page — see the note above, or turn checkDns off.
Why is a technology I know the site uses missing? Probably it only appears when JavaScript runs, or only on another page, or its confidence is below minConfidence. Lower minConfidence to 0 to see weak hints.
What does confidence mean? Each rule match carries a weight; weights of different matches of the same technology add up to at most 100. An implied technology (WordPress → PHP) takes the weight of the implication, never more than the technology that implies it.
Does trackChanges cost extra? No. The first run of a URL has nothing to compare with (firstRun: true); from the second run on, changes lists the differences. A blocked or unreachable site neither updates the memory nor produces "removed".
What happens to a run that is restarted by the platform? Sites already in the dataset are not analysed or charged again.
Is it allowed? The Actor reads public pages the way a search engine does, obeys robots.txt by default, sends one request at a time per site and identifies itself in the User-Agent: Mozilla/5.0 (compatible; TechDetectorBot/0.1; +https://apify.com/far003/website-technology-detector). You are responsible for how you use the results.