# Tech Stack Detector: Hidden SaaS, EOL & CVE Check (`urlwright/tech-stack-detector`) Actor

Find what any website runs on: CMS, frameworks, analytics, hosting. Plus the SaaS behind the domain from public DNS, and end-of-life or potentially vulnerable versions. Failed domains are free. $1.50–$3.00 per 1,000 domains.

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

## Pricing

from $1.50 / 1,000 domain analyzeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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: Hidden SaaS, EOL & CVE Check

**Urlwright** — *Little tools for the web.*

Find what any website runs on: CMS, frameworks, analytics, hosting. Plus the SaaS behind the domain from public DNS, and end-of-life or potentially vulnerable versions. Many domains per run; one bad domain never fails the run, and you only pay for domains that could be analyzed.

Default User-Agent: `UrlwrightBot/1.0 (tech-stack; +https://urlwright.com/bot)` (`UrlwrightBot` for robots.txt matching).

### What it does

Give it a list of domains (or the dataset of a [Sitemap URL Extractor](https://apify.com/urlwright/sitemap-url-extractor) run). For each site you get one row with:

1. **Technologies** – CMS, frameworks, e-commerce, analytics, tag managers, CDN, hosting, web server and more. ~7,600 open fingerprints plus our own rules, each hit with confidence, version (when visible) and the **evidence** that triggered it.
2. **Hidden SaaS stack** – mail provider, sending services and SaaS verified on the domain, from public DNS (MX, SPF, TXT verifications, DMARC, NS). Mapped to service names; raw tokens never appear in the dataset. DNS is also returned for blocked or robots-disallowed hosts, free of charge.
3. **Lifecycle** – end-of-life status and "latest in cycle" from [endoflife.date](https://endoflife.date) for exactly detected versions.
4. **Potentially affected versions** – known vulnerable JavaScript libraries ([retire.js](https://retirejs.github.io/retire.js/)) and version-range matches against the NVD, with [CISA KEV](https://www.cisa.gov/known-exploited-vulnerabilities-catalog) highlighted. Reported **only when the version was detected exactly**; server packages without distro information are skipped by default.
5. **Privacy & security hints** – third-party hosts, external Google Fonts, trackers, consent manager, imprint/privacy links, HSTS/CSP and other security headers, certificate issuer and expiry.
6. **Changes since the last run** (optional) – added/removed technologies and SaaS, version changes. Schedule it and get alerts via Apify integrations.

Main-platform accuracy in our benchmark: **96%** (26 of 27 reachable well-known sites, 21 Oct 2026, passive HTTP + DNS).

### Fair and polite by design

- **You only pay for analyzed domains** (`status: ok`). Blocked, offline, timed-out, robots.txt-disallowed and otherwise failed domains are free. No start fee.
- **Passive and read-only.** We load what a browser would load (the page, its scripts, `robots.txt`) plus public DNS. No path probing, no login, no exploit checks.
- **Polite.** Honors `robots.txt` for `UrlwrightBot`, at most 1 request per second per site, honest User-Agent with a contact page.
- **No personal data.** No emails, phone numbers or names in the output. DNS verification values are mapped to service names and dropped.

### Good for

Sales & RevOps (enrich and segment accounts by stack) · Agencies (find relaunch and maintenance candidates) · Security teams & MSPs (passive outdated-software overview of a portfolio) · Market research (technology shares across a list) · AI agents (stable JSON via the Apify API or MCP).

### Input example

Store console prefill: `domains: ["example.com"]`.

```json
{
  "domains": ["example.com", "https://shop.example.com/de/"],
  "maxPagesPerDomain": 1,
  "checks": ["technologies", "dns", "lifecycle", "vulnerabilities", "privacy", "security"],
  "minConfidence": 50,
  "includeImplied": true,
  "compareWithPrevious": false,
  "maxConcurrency": 30,
  "requestTimeoutSecs": 20,
  "outputFormat": "nested"
}
```

- `domains` – domains or URLs; normalized and deduplicated (www and apex count once).
- `maxPagesPerDomain` – homepage plus up to 4 extra pages. Extras come from your URLs, a `sitemapDatasetId`, or internal links on the homepage (one per template; robots-allowed; never guessed paths).
- `checks` – technologies are always on; switching others off saves runtime, not money.
- `onlyIfDetected` – emit only domains that use one of these technologies (others are analyzed but not charged).
- `compareWithPrevious` + `monitorName` (+ `outputOnlyChanges`) – monitoring between scheduled runs.
- `includeServerPackageCves` – also match nginx/Apache/PHP/OpenSSL versions without distro markers (off by default; distro packages are often patched).
- `outputFormat` – `nested` (one row per domain) or `flat` (one row per technology).

Recommended memory: **1024 MB** (default concurrency 30).

### Output

#### Output example (dataset item, shortened)

```json
{
  "domain": "example.com",
  "status": "ok",
  "httpStatus": 200,
  "finalUrl": "https://www.example.com/",
  "summary": {
    "cms": "WordPress",
    "analytics": ["Google Analytics"],
    "tagManager": "Google Tag Manager",
    "cdn": "Cloudflare",
    "webServer": "Nginx",
    "mailProvider": ["Microsoft 365"],
    "dnsProvider": ["Cloudflare DNS"]
  },
  "technologies": [
    {
      "name": "WordPress",
      "categories": ["CMS", "Blogs"],
      "confidence": 100,
      "version": "6.4.3",
      "versionPrecision": "exact",
      "evidence": [{ "type": "meta:generator", "match": "WordPress 6.4.3" }]
    },
    {
      "name": "jQuery",
      "categories": ["JavaScript libraries"],
      "confidence": 100,
      "version": "1.8.3",
      "versionPrecision": "exact",
      "evidence": [{ "type": "scriptSrc", "match": "jquery-1.8.3.min.js" }]
    }
  ],
  "hiddenStack": {
    "mailProviders": ["Microsoft 365"],
    "emailSendingServices": ["HubSpot"],
    "verifiedServices": ["Atlassian", "DocuSign"],
    "dmarcPolicy": "quarantine",
    "note": "TXT verifications show a service was verified at some point; not proof of current use."
  },
  "lifecycle": [
    { "product": "wordpress", "version": "6.4.3", "cycle": "6.4", "isEol": true, "latestInCycle": "6.4.5", "behindInCycle": true }
  ],
  "vulnerabilities": {
    "status": "checked",
    "potentiallyAffected": [
      {
        "technology": "jQuery",
        "version": "1.8.3",
        "source": "nvd+retire.js",
        "cveCount": 6,
        "kevCount": 1,
        "maxCvss": 6.9,
        "topCves": [{ "id": "CVE-2020-11023", "cvss": 5.5, "kev": true, "url": "https://nvd.nist.gov/vuln/detail/CVE-2020-11023" }],
        "wording": "Version matches the affected range of known vulnerabilities. Patches may have been backported; verify before acting."
      }
    ]
  },
  "charged": true,
  "chargedExtraPages": 0
}
```

Values are illustrative. Every row also carries `privacy`, `security`, `performance`, `accessibility`, `pagesAnalyzed`, `redirects`, `dataVersion` and, with monitoring, `changes`.

#### Statuses

`ok` (charged) · `blocked` · `robots_disallowed` · `dns_error` · `connection_error` · `tls_error` · `timeout` · `http_error` · `not_html` · `too_many_redirects` · `invalid_input` – all non-`ok` statuses are free. Each row has `charged: true|false`.

#### Run summary

Key-value store record `RUN_SUMMARY`: totals, `byStatus`, `charged`, `chargedEvents` (`domain-analyzed`, `extra-page`), EOL and potentially-affected counts, and `truncated` / `stoppedByChargeLimit` when a run stopped early.

### Pricing

Pay per event (USD). **No start fee.** Recommended minimum **Max total charge** per run: **$0.02**.

| Event | When | FREE | BRONZE | SILVER | GOLD |
|---|---|---:|---:|---:|---:|
| `domain-analyzed` (primary) | Each domain with `status: ok` (DNS, lifecycle, vulnerability and hint checks included) | 0.0030 | 0.0025 | 0.0020 | 0.0015 |
| `extra-page` | Each additional page (beyond the first) that loaded successfully | 0.0008 | 0.0007 | 0.0005 | 0.0004 |

**$3.00 → $1.50 per 1,000 domains** and **$0.80 → $0.40 per 1,000 extra pages**, depending on your Apify plan. Prices only go down as your plan goes up; Platinum and Diamond pay the Gold price.

Errors are free: blocked, offline, timed-out, robots.txt-disallowed and other failed domains are **not** charged. Domains filtered out by `onlyIfDetected` or `outputOnlyChanges` are not charged either.

**Bronze examples:** 1,000 domains, homepage only: $2.50 · 1,000 domains with 2 extra pages each: $2.50 + $1.40 = $3.90.

### Limitations

- Plain HTTP, no browser: technologies injected only by JavaScript at runtime may be missed. Server-side software (databases, back-end languages) is usually invisible or only implied.
- CDNs and reverse proxies can hide the origin server. Sites with strict bot protection are reported as `blocked` (free).
- DNS verifications show that a service was verified at some point, not that it is still used.
- **Vulnerability data is a hint, not a finding.** A match means the detected version falls inside the affected range of a published vulnerability. Vendors and Linux distributions often backport fixes, so a site can be patched while showing an old version. We never test whether a vulnerability is exploitable. Verify before acting.
- HTML is read up to 3 MB per page; at most 5 pages per domain.

### No warranty

Results are generated automatically from public signals and third-party data (fingerprints, endoflife.date, retire.js, NVD, CISA KEV). They can be incomplete or wrong. They are provided as-is, without warranty, and are no substitute for a security assessment. Use them at your own judgment; Urlwright accepts no liability for decisions based on them.

### FAQ

#### Why is my dataset row `blocked` or `timeout`?

The site refused automated requests (bot protection, e.g. a 403 challenge page) or did not answer in time. These rows still contain public DNS data where available and are **free**. Try again later or with `requestTimeoutSecs` raised; we do not try to get around bot protection.

#### What does a run with 1,000 domains cost?

On **Bronze**, 1,000 analyzed domains with one page each: `1000 × 0.0025 = $2.50`. Failed domains cost nothing. Extra pages are charged only if you raise `maxPagesPerDomain`.

#### Does it respect robots.txt?

Yes. We fetch `robots.txt` first and honor rules for `UrlwrightBot` (and `*`), including on redirect targets. Disallowed domains get `status: robots_disallowed`, plus DNS data, free of charge.

#### Why don't I see CVEs for a technology with a known vulnerability?

We only report version-range matches when the version was detected **exactly** (e.g. from a file name or generator tag). Without a reliable version there is no claim. Server packages (nginx, Apache, PHP, OpenSSL) are skipped unless you set `includeServerPackageCves`, because distro builds are usually patched.

#### Can I monitor changes?

Enable `compareWithPrevious` with a `monitorName` and schedule the Actor. Each row gets `changes` (added/removed technologies and SaaS, version changes); `outputOnlyChanges` emits only domains that changed.

#### Does it store personal data?

No. The output contains technologies, DNS-derived service names and technical hints. No emails, names or phone numbers; DNS verification values are dropped after mapping.

### Data sources & credits

- Technology fingerprints: [webappanalyzer](https://github.com/enthec/webappanalyzer) by Enthec & contributors (GPL-3.0), plus Urlwright rules.
- JavaScript library vulnerabilities: [retire.js](https://github.com/RetireJS/retire.js) (Apache-2.0).
- Lifecycle data: [endoflife.date](https://endoflife.date) (MIT).
- Known Exploited Vulnerabilities: [CISA KEV](https://www.cisa.gov/known-exploited-vulnerabilities-catalog) (CC0-1.0).
- This product uses data from the NVD API but is not endorsed or certified by the NVD.

More tools and open numbers: [urlwright.com](https://urlwright.com) · Bot details: [urlwright.com/bot](https://urlwright.com/bot)

# Actor input Schema

## `domains` (type: `array`):

Domains or URLs (example.com, https://shop.example.com/de/). Normalized and deduplicated. Required unless sitemapDatasetId is set. Max 50,000 per run.

## `sitemapDatasetId` (type: `string`):

Optional Apify dataset ID from Sitemap URL Extractor (C04). Up to maxPagesPerDomain representative URLs are sampled per domain.

## `maxPagesPerDomain` (type: `integer`):

Homepage plus up to 4 extra pages. Extras come from input/sitemap URLs and/or internal links on the homepage (one URL per first path segment; robots-allowed, same registrable domain; no guessed paths). Each extra page with HTTP 2xx bills as extra-page.

## `checks` (type: `array`):

Optional subset of checks. technologies is always on. Disabling others saves runtime but not the per-domain price.

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

Drop technology hits below this confidence (0–100).

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

Emit implied technologies (e.g. PHP from WordPress) marked implied: true.

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

Only emit rows where at least one of these technology names was detected. Other domains are still analyzed but not output and not charged.

## `compareWithPrevious` (type: `boolean`):

Diff against the last run with the same monitorName.

## `monitorName` (type: `string`):

Named KV store suffix urlwright-techstack-<monitorName> (a-z, 0-9, hyphen; max 40).

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

When compareWithPrevious is on, only emit domains with changes. Unchanged domains are still analyzed but not output and not charged.

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

Parallel domains. Per-host spacing is always ≥ 1 request/second.

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

HTTP timeout per request.

## `proxyConfiguration` (type: `object`):

Optional Apify Proxy (datacenter recommended). Residential is not recommended for cost.

## `includeServerPackageCves` (type: `boolean`):

When true, also match NVD ranges for nginx/Apache/PHP/OpenSSL etc. without distro markers (may be distro-patched). Default off.

## `outputFormat` (type: `string`):

nested = one row per domain (default). flat = same data; use the Technologies flat dataset view.

## Actor input object example

```json
{
  "domains": [
    "example.com"
  ],
  "maxPagesPerDomain": 1,
  "checks": [
    "technologies",
    "dns",
    "lifecycle",
    "vulnerabilities",
    "privacy",
    "security",
    "performance",
    "accessibility"
  ],
  "minConfidence": 50,
  "includeImplied": true,
  "compareWithPrevious": false,
  "monitorName": "default",
  "outputOnlyChanges": false,
  "maxConcurrency": 30,
  "requestTimeoutSecs": 20,
  "includeServerPackageCves": false,
  "outputFormat": "nested"
}
```

# Actor output Schema

## `domains` (type: `string`):

Dataset items: technologies, hidden SaaS stack, lifecycle, vulnerabilities, privacy/security flags.

## `runSummary` (type: `string`):

JSON summary of the run (status counts, charges, truncation).

# 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 = {
    "domains": [
        "example.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("urlwright/tech-stack-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 = { "domains": ["example.com"] }

# Run the Actor and wait for it to finish
run = client.actor("urlwright/tech-stack-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 '{
  "domains": [
    "example.com"
  ]
}' |
apify call urlwright/tech-stack-detector --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,urlwright/tech-stack-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/FLNgN0pRdPjOV1713/builds/W0uBLHgVDMgD9hSDP/openapi.json
