# Website Technology Detector – Tech Stack, CMS & Changes (`far003/website-technology-detector`) Actor

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.

- **URL**: https://apify.com/far003/website-technology-detector.md
- **Developed by:** [Francesco Antonio Russo](https://apify.com/far003) (community)
- **Categories:** Lead generation, Developer tools, Open source
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$3.00 / 1,000 site analyses

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Website Technology Detector – Tech Stack, CMS & Changes

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 `invalid` records 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.txt` or shows nothing recognisable costs nothing — and is still returned as a record that says why.
- **"What changed since last time."** With `trackChanges` on, 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 (`cpe` is included), and a scheduled change log (`trackChanges`) when something is added, removed or updated.

### Quick start

```json
{
  "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`:

```json
{
  "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 `signalsNotChecked` says `js`.
- **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 get `blocked`, 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 be `blocked` now and `ok` a 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 only `HSTS`, `HTTP/3`, `HTTP/2` or `Open Graph` are found is `ok` and **not** charged (see Pricing); other common entries such as "Priority Hints" do count. Use `excludeCategories` (e.g. `["Miscellaneous", "Security"]`) to drop them from the record and from the charge, or filter on `technologies[].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 plain `http://` and the record says so in `warnings`; an input that starts with `https://` is never downgraded. A DNS failure is not retried on http (then `www.` is tried, as before).
- **One site, one memory.** `trackChanges` is keyed like the duplicate check: `python.org`, `www.python.org` and `http://python.org` share 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 `rulesVersion` of 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`](https://github.com/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

```js
// 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);
```

```python
## Python
from apify_client import ApifyClient

client = 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_TOKEN
Content-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.

# Actor input Schema

## `urls` (type: `array`):

Websites to analyse: full URLs or bare domains (example.com is read as https://example.com/). One record per website. Entries that are not web addresses are reported as "invalid", not as a run error.

## `includeEvidence` (type: `boolean`):

Add to every technology the proof of the detection: which header, cookie, meta tag, script, selector or DNS record matched, and the value that matched.

## `minConfidence` (type: `integer`):

Technologies below this confidence are left out. Weak signals add up: two 50 % signals make 100 %. 50 hides single weak hints, 0 shows everything, 100 shows only certain detections.

## `excludeCategories` (type: `array`):

Category names to leave out, e.g. Miscellaneous and Security (Open Graph, HSTS, HTTP/3…). A technology is removed only when ALL its categories are excluded; removed technologies are not counted and do not make a site chargeable. Names are case-insensitive; see technologies\[].categories for the names.

## `checkDns` (type: `boolean`):

Also read the TXT, MX, NS, SOA and CNAME records of the site and its main domain. They reveal e-mail providers, DNS/CDN providers and verification tags that the page itself does not show. No extra HTTP request.

## `respectRobotsTxt` (type: `boolean`):

Do not read a page that robots.txt forbids to this bot (TechDetectorBot). The site is then reported as "robots_disallowed" for free. robots.txt is read in any case: it is also a detection signal.

## `trackChanges` (type: `boolean`):

Remember the technologies of every site (in a named key-value store) and report what was added, removed or changed version since the previous analysis of the same URL. A blocked or unreachable site never counts as "removed".

## `maxConcurrency` (type: `integer`):

Different sites are read in parallel; requests to the same site are at least one second apart.

## `timeoutSecs` (type: `integer`):

Limit for each HTTP request (robots.txt, the page).

## Actor input object example

```json
{
  "urls": [
    "https://wordpress.org",
    "https://react.dev"
  ],
  "includeEvidence": true,
  "minConfidence": 50,
  "excludeCategories": [],
  "checkDns": true,
  "respectRobotsTxt": true,
  "trackChanges": false,
  "maxConcurrency": 10,
  "timeoutSecs": 20
}
```

# Actor output Schema

## `sites` (type: `string`):

No description

## `summary` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "urls": [
        "https://wordpress.org",
        "https://react.dev"
    ],
    "excludeCategories": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("far003/website-technology-detector").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "urls": [
        "https://wordpress.org",
        "https://react.dev",
    ],
    "excludeCategories": [],
}

# Run the Actor and wait for it to finish
run = client.actor("far003/website-technology-detector").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "urls": [
    "https://wordpress.org",
    "https://react.dev"
  ],
  "excludeCategories": []
}' |
apify call far003/website-technology-detector --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,far003/website-technology-detector"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/KhfaDYDD2a2AQm8GN/builds/IswZO3ib2gK92Io6Y/openapi.json
