# Tech Stack Detector & Change Monitor - Wappalyzer Alternative (`optalgin/tech-stack-monitor`) Actor

Detect the technologies behind any list of websites — CMS, ecommerce, analytics, frameworks, CDN, email provider (DNS) — and track changes between runs. 7,600+ open fingerprints. Pay only per site analyzed.

- **URL**: https://apify.com/optalgin/tech-stack-monitor.md
- **Developed by:** [Evyatar ventura](https://apify.com/optalgin) (community)
- **Categories:** Lead generation, SEO tools, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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

## Tech Stack Detector & Change Monitor

Find out what any list of websites is built with — and get notified when it changes.

Give it domains. For each site you get the CMS, ecommerce platform, analytics and tag managers, JavaScript frameworks, CDN, web server, security/WAF, payment and marketing tools, **plus the email provider and SaaS verifications from DNS** (MX, SPF/TXT, NS). 7,600+ open fingerprints, versions where detectable, and a confidence score for every match.

### Why this one

- **Gets through where plain scrapers get a 403.** Every site is fetched with a real Chrome TLS/HTTP2 fingerprint, which alone opens sites like medium.com and nytimes.com that block Python clients. If a site still answers with a 403 or a bot-challenge page, it is retried once through a residential proxy in the site's own country (`.co.il` → Israel, `.de` → Germany), which opens geo-fenced and IP-reputation blocks. You pay the small `site-unblocked` event only when that retry actually returns a real page.
- **Browser mode when it pays off.** About 900 of the 7,600 fingerprints only exist in the browser (`window.*` objects, runtime-injected chat widgets, heatmaps and marketing tags, XHR calls). In `auto` mode the Actor renders a site in headless Chromium only when the static pass found little, so bulk runs stay fast and cheap; `always` renders everything. Browser detections carry `via: ["browser"]`.
- **Honest about blocks.** Soft blocks that return HTTP 200 with a "Bot Manager" or challenge page (Radware, Akamai, Cloudflare, Imperva) are recognised and flagged with a `warning` instead of being reported as a site with three technologies. JavaScript challenges are not solved (no browser is used), and are reported as such.
- **Change monitoring built in.** Set a `monitorId` (e.g. `competitors`) and schedule the Actor. Every result tells you which technologies were **added**, **removed**, or **changed version** since the last run. Changes are confirmed on two consecutive runs by default (`confirmChanges`), so flapping tags such as bot protection or A/B-test scripts don't spam you; first sightings are shown in `pendingChanges`. Turn on `outputOnlyChanges` and your dataset becomes a clean change feed — perfect for sales triggers ("prospect just moved to Shopify") and competitor tracking.
- **Built for bulk.** Thousands of domains per run, one GET per site, no browser, duplicates removed automatically. A `SUMMARY` record gives the market share of every technology across your list.
- **DNS detection.** Sees Google Workspace / Microsoft 365, email-sending tools and DNS hosts even when the homepage reveals nothing.
- **Fair billing.** `site-analyzed` per site that returned a page; `site-unblocked` only when the residential retry turned a blocked site into a real page; `site-browser-scan` only when a site was actually rendered. Unreachable sites and sites that stay blocked cost nothing extra.
- **Evidence on demand.** `includeEvidence` shows exactly which header, cookie, meta tag, script or DNS record triggered each detection.

### How it compares (measured, not claimed)

Head-to-head on 28 sites (Israeli retailers, Shopify stores, WordPress publishers, Drupal universities, SaaS, government) against the Store's most-used Wappalyzer alternative, September 2026. Generic header/meta detections (HSTS, Open Graph, CSP…) excluded on both sides.

| | This Actor | Most-used alternative |
|---|---|---|
| Technologies seen directly on the page | **214** (+31 clearly labelled as implied) | 63 |
| Sites where the platform itself (CMS / shop / framework) was identified | **22 of 28** | 4 of 28 |
| Sites returned as HTTP 403 (no result) | **0** (5 opened via residential retry) | 5 |
| Time for 35 sites | 75 s, all done | timed out at 5 min after 28 |
| Change tracking, DNS, versions, evidence | yes | no |

Examples: Shopify was identified on all 4 Shopify stores in the sample (fox.co.il, allbirds, kyliecosmetics, fashionnova) and Magento on castro.com; the alternative returned only "Cloudflare" for each of them. What the alternative found and this Actor did not: mostly GA4 inferred from any `gtag` call and CDN vendors from headers — 22 detections in total, several of them false positives (e.g. "Intercom" from marketing copy that mentions Intercom).

With `browserMode: auto` on the same list, 12 of 35 sites triggered a render (JS-heavy pages where the static pass found little) and gained 31 technologies visible only in the browser — ad-tech stacks, TikTok/Pinterest pixels, Microsoft Clarity, Segment, Sentry, MobX, React Router — while the other 23 sites stayed static and cheap. The whole run took 113 seconds.

Limitations, stated plainly: without browser mode, about 900 of the 7,600 fingerprints are not reachable; with it they are, at the cost of a few seconds per rendered site. JavaScript challenges (Akamai, AWS WAF) are recognised and flagged; headless Chromium gets through some of them, not all. Blocking is probabilistic — a site that opens today can be blocked tomorrow, which is why blocked pages never overwrite your monitoring snapshot.

### Speed and cost

Measured on 1,000 domains from the Tranco list (ranks 2,000–3,000, i.e. ordinary sites, not just giants), `browserMode: auto`, default settings: about 1,000 sites in 20 minutes at 4 GB, peak memory 1.3 GB, no crashes; roughly a quarter of the sites triggered a browser render. Static-only runs are several times faster. A run survives platform migrations: finished sites are checkpointed and never analysed or billed twice.

Unreachable inputs are common in real lists (in that sample, 20% of apex domains had no DNS record at all — CDN and tracking domains). They are reported with an `error` and cost nothing.

Need a lead list rather than a full audit? Set `onlyIfDetected` (e.g. `["Shopify"]` or `["Ecommerce"]`) and the dataset contains only the matching sites.

### Input

```json
{
  "urls": ["shopify.com", "https://www.example.org/pricing"],
  "monitorId": "competitors",
  "outputOnlyChanges": false,
  "detectDns": true,
  "minConfidence": 50
}
```

### Output (one item per site)

```json
{
  "domain": "example.com",
  "finalUrl": "https://www.example.com/",
  "status": 200,
  "fetchedVia": "direct",
  "unblocked": false,
  "technologyCount": 14,
  "directCount": 11,
  "technologies": [
    {"name": "WordPress", "version": "6.5.2", "confidence": 100, "categories": ["CMS", "Blogs"], "website": "https://wordpress.org", "via": ["page"]}
  ],
  "byCategory": {"CMS": ["WordPress"], "Analytics": ["Google Analytics"]},
  "firstCheck": false,
  "changed": true,
  "changes": {"added": ["Shopify"], "removed": ["WooCommerce"], "versionChanged": [{"name": "WordPress", "from": "6.4", "to": "6.5.2"}]}
}
```

`via` tells you how each technology was found: `page` (the website itself), `implied` (e.g. WordPress implies PHP; set `includeImplied: false` to drop these) or `dns` (MX/SPF/TXT verification records — these reveal SaaS tools the company uses, not what the website runs on). Page detections are listed first.

The dataset has two views: **Overview** (one row per site) and **One row per technology** (ready for spreadsheets / CRM import).

### How it works and its limits

Detection is static: HTTP headers, cookies, meta tags, HTML, script URLs, inline scripts and CSS, the static DOM and DNS records. Technologies that can only be seen by executing JavaScript in a browser (about 900 of the 7,600 fingerprints) are not detected — this keeps the Actor fast and cheap. Sites behind aggressive bot protection may return a block page; such results (HTTP errors and suspiciously tiny pages) are flagged with a `warning`, are not compared against your monitor snapshot and never overwrite it — so a block page can't show up as "everything was removed".

Monitoring state lives in a named key-value store `techmon-<monitorId>` in your own Apify account. Delete it to reset a watch-list.

### License and attribution

Technology fingerprints come from [enthec/webappanalyzer](https://github.com/enthec/webappanalyzer), the community continuation of the open-source Wappalyzer database, used under GPL-3.0. This Actor's code is also released under GPL-3.0. Not affiliated with Wappalyzer or BuiltWith.

# Actor input Schema

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

Domains or URLs, one per line. Bare domains are fine (https:// is assumed). Duplicates by hostname are removed.

## `onlyIfDetected` (type: `array`):

Technology names or categories (e.g. Shopify, WordPress, Ecommerce, HubSpot). Only sites where at least one of these was detected are written to the dataset. All sites are still analysed and billed; this just keeps your dataset to the matches.

## `monitorId` (type: `string`):

Give your watch-list a name (e.g. 'competitors'). The last known stack of every site is kept in a named key-value store in YOUR account, and each result reports added / removed technologies and version changes since the previous run. Schedule the Actor to get a change feed.

## `outputOnlyChanges` (type: `boolean`):

With a monitor ID: output only sites whose stack changed (or that are checked for the first time). Every analyzed site is still billed.

## `confirmChanges` (type: `boolean`):

Report an added/removed technology only after it is seen in two consecutive checks. Filters out flapping tags (bot protection, A/B tests, rotating ads). First sightings appear in `pendingChanges`.

## `detectDns` (type: `boolean`):

Also look at MX / TXT (SPF, verification) / NS / SOA records to detect email providers, DNS hosts and SaaS verifications.

## `unblock` (type: `boolean`):

Every site is first fetched with a real Chrome TLS fingerprint (free). If it still comes back as HTTP 403 or a bot-challenge page, it is retried once through a residential proxy located in the site's own country (e.g. .co.il → Israel). The extra `site-unblocked` event is charged only when the retry actually returns a real page. Sites that stay blocked are marked with a `warning` and cost nothing extra.

## `proxyCountry` (type: `string`):

ISO country code (e.g. IL, DE, US) to use for every proxy retry instead of the one inferred from the domain.

## `browserMode` (type: `string`):

Renders the page in headless Chromium to see what only JavaScript reveals: window.\* fingerprints (about 900 technologies are detectable only this way), scripts injected at runtime (chat widgets, heatmaps, marketing tags), XHR/fetch calls and JS-set cookies. 'auto' scans a site only when the static pass found 4 or fewer technologies on the page or hit a JavaScript challenge. Each rendered site adds the `site-browser-scan` event. Needs ≥ 2 GB run memory; 4 GB (the default) renders two pages at once.

## `browserConcurrency` (type: `integer`):

How many pages to render at once. Automatically capped to one page per 2 GB of run memory (default run memory is 4 GB → 2 pages); raise the run memory to render more in parallel.

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

Drop detections below this confidence (0–100).

## `includeImplied` (type: `boolean`):

Technologies inferred from others rather than seen directly (WordPress implies PHP and MySQL; Amazon S3 implies AWS) are marked via: \["implied"]. Turn this off to keep only what was observed on the page or in DNS.

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

Add which header / cookie / meta tag / script / DNS record triggered each detection.

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

How many sites to fetch in parallel.

## `requestTimeoutSecs` (type: `integer`):

Per-site HTTP timeout. Slow sites are retried once over plain http.

## Actor input object example

```json
{
  "urls": [
    "apify.com",
    "shopify.com",
    "wordpress.org"
  ],
  "onlyIfDetected": [],
  "outputOnlyChanges": false,
  "confirmChanges": true,
  "detectDns": true,
  "unblock": true,
  "browserMode": "auto",
  "browserConcurrency": 3,
  "minConfidence": 50,
  "includeImplied": true,
  "includeEvidence": false,
  "maxConcurrency": 20,
  "requestTimeoutSecs": 20
}
```

# Actor output Schema

## `results` (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": [
        "apify.com",
        "shopify.com",
        "wordpress.org"
    ],
    "onlyIfDetected": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("optalgin/tech-stack-monitor").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": [
        "apify.com",
        "shopify.com",
        "wordpress.org",
    ],
    "onlyIfDetected": [],
}

# Run the Actor and wait for it to finish
run = client.actor("optalgin/tech-stack-monitor").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": [
    "apify.com",
    "shopify.com",
    "wordpress.org"
  ],
  "onlyIfDetected": []
}' |
apify call optalgin/tech-stack-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,optalgin/tech-stack-monitor"
        }
    }
}
```

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/cmDiPIFWvplGqmtzw/builds/G4MWshjfqJhdatCwY/openapi.json
