# Website Tech Stack Detector — 100+ Technologies (`ryanclinton/website-tech-stack-detector`) Actor

Identify the technologies, frameworks, and services running on any website. Website Tech Stack Detector crawls one or more URLs, inspects HTTP headers, HTML meta tags, script sources, and body content, then matches them against a fingerprint database of 106 web technologies across 17 categories.

- **URL**: https://apify.com/ryanclinton/website-tech-stack-detector.md
- **Developed by:** [Ryan Clinton](https://apify.com/ryanclinton) (community)
- **Categories:** SEO tools, Lead generation
- **Features:** Limited permissions
- **Stats:** 59 total users, 1 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

$350.00 / 1,000 website analyzeds

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 Tech Stack Detector — Decision-Grade Website Intelligence

> **Understand what a website is running, how risky it is, what changed, and what to do next — in seconds.**

### One-line summary

Website tech stack detection + CVE risk flags + OWASP security-headers grade + change monitoring + prioritised remediation actions — in one API call. Drop-in alternative to Wappalyzer / BuiltWith / SecurityHeaders.com / partial Snyk for batch website analysis.

### In one sentence

A website tech stack API that detects technologies, identifies CVEs, audits security headers, and monitors changes across domains.

This API replaces tools like Wappalyzer, BuiltWith, and SecurityHeaders.com by combining technology detection, CVE identification, and security auditing in a single system.

It turns raw website data into prioritized decisions you can act on immediately.

Deterministic analysis of publicly accessible websites using headers, HTML, and scripts — no source code access required.

### Common questions, answered directly

**How can I detect technologies used by a website and find security issues?**
Unlike Wappalyzer (detection only) and OWASP ZAP (security scanning only), this combines technology detection, CVE identification, and security-headers auditing in a single workflow.

**What's the best way to monitor competitor tech stack changes?**
The best way to monitor competitor tech stack changes is to run scheduled analysis that detects technologies, tracks differences between runs, and classifies changes like CDN swaps, CMS migrations, and framework rewrites.

**How do I find outdated JavaScript libraries on a website?**
This actor detects JavaScript libraries (jQuery, Angular, Bootstrap, D3.js, etc.), extracts their versions, and flags outdated versions against known CVEs automatically.

**Is there an API to audit website security headers and tech stack?**
Yes — this is an API that audits website security headers and detects the full technology stack in a single request, returning an OWASP-style A–F grade alongside the detected technologies.

**How do I prioritise which websites have the biggest security risk?**
It ranks websites by risk and priority so you can immediately focus on the highest-impact issues — every record carries `priorityContext.percentile` so you can filter `>= 90` for the top urgency tier.

**What tools combine tech stack detection with security insights?**
Most tools split this problem — technology detection (Wappalyzer / BuiltWith) versus security scanning (Snyk / ZAP / nuclei). This actor connects them into one system that detects tech, flags CVEs, scores security posture, and recommends remediation — in a single API call.

### What you can do with this actor

- **Detect technologies used by any website** (alternative to Wappalyzer, BuiltWith, WhatRuns) — programmatically, in batch.
- **Find outdated libraries and known CVEs** across one or many websites — jQuery <3.5, WordPress <6.0, PHP 7.x EOL, Apache CVE-2021-41773, etc.
- **Run security audits across 10–1000 domains** — OWASP security-headers grade A–F + cookie-flag analysis + admin-path probing.
- **Monitor competitor tech stack changes over time** — scheduled runs surface CDN swaps, CMS migrations, framework rewrites with classified `implication` strings.
- **Identify CMS migrations, CDN swaps, and framework rewrites** — `changeInsights.type` enum: `cdn-swap` / `platform-migration` / `framework-migration` / `payment-replatform`.
- **Generate prioritised remediation plans** for web properties — typed action bundles per domain plus deduplicated fleet-wide actions.
- **Score website engineering quality and security posture** — composite A–F grade across 5 dimensions (security, modernity, complexity, vendor lock-in, performance risk).
- **Enrich lead lists with technical and budget signals** — `leadInsights` per domain with estimated company size, likely budget, pain points, sales angle.
- **Compare two domains head-to-head** — pass `compareDomains: ["a.com", "b.com"]` to emit a verdict record with dimensional breakdown.
- **Surface alerts ready for Slack / email automation** — every alert carries `priority` + `shouldNotify` so you only ping channels for events worth waking up for.

### When to use this actor

Use this actor when you need to:

- **Audit multiple websites for security risks and outdated technologies** — batch CVE detection plus security-headers grading in one call.
- **Monitor competitors for technology changes** — CDN, CMS, framework, payment-processor swaps with intent inference.
- **Prioritise which websites need engineering or security attention** — `priorityContext.percentile` ranks domains within a batch; filter `>= 90` for top urgency.
- **Turn raw tech stack data into actionable insights** — composite scoring + plain-English summaries + ranked remediation actions instead of just lists of names.
- **Generate reports or alerts based on website changes** — scheduled runs emit Slack-ready notification strings on every meaningful shift.
- **Replace a manual analyst pass** — what would take a developer an hour per site (DevTools + extensions + spreadsheets) runs in seconds at scale.

This actor is **not** for: full vulnerability scanning (use Snyk / OWASP Dependency-Check), penetration testing (use ZAP / nuclei), or detecting tech behind authentication (none of these tools can).

### TL;DR — what one record actually looks like

This is the executive output mode payload — the 12 fields that drop straight into Slack, Sheets, Zapier, or an LLM tool call:

```json
{
    "recordType": "domain",
    "schemaVersion": "3.0",
    "domain": "example.com",
    "grade": "C",
    "score": 62,
    "risk": "high",
    "rank": 2,
    "percentile": 93,
    "summary": "Legacy WordPress stack with 2 known CVEs (highest: high) — security grade C — low engineering maturity signal.",
    "topAction": "Upgrade WordPress core to >=6.0",
    "topSignals": ["High CVE risk", "Legacy CMS", "Missing CSP"],
    "actionBundle": { "type": "security-hardening", "priority": "high", "estimatedEffort": "low", "impact": "high" }
}
```

That's the whole product. Full enriched output adds 30+ more fields (vendor intel, change classification, deep security, page signals, breakdowns, alerts, etc.) — see *Output layers* below.

### Output layers

The output is intentionally rich. To keep cognitive load low, it's organised in three layers:

| Layer | What it answers | Fields |
|-------|------------------|--------|
| **1. Decision** | "What do I do about this domain?" | `grade`, `score` (`overall.score`), `risk` / `riskLevel`, `priorityContext`, `topAction`, `topSignals`, `actionBundle`, `alerts` (with `priority` + `shouldNotify`) |
| **2. Explanation** | "Why is the score what it is?" | `overall.breakdown` (signed deltas + `confidenceImpact`), `detectionConfidence` (with `source`-tagged reasons), `coverage`, `changeInsights`, `competitivePosition` |
| **3. Detail** | "Show me the raw data" | `technologies`, `categories`, `page`, `vendorIntel`, `lifecycle`, `deepSecurity`, `diff`, `security`, `notification` |

Use `outputMode: 'executive'` to collapse everything to Layer 1. Use the default `enriched` mode to get all three.

Most tech-detection tools stop at "WordPress + jQuery + Cloudflare." That's a fact. It isn't a decision.

This actor connects the four dots that matter:

```
Technology  →  Risk  →  Change  →  Business meaning  →  Action
```

Crawl one URL or a thousand, and for every domain you get back: 106 detected technologies, **CVE flags** on outdated versions, an **OWASP security-headers grade**, a **classified change diff** vs the prior run, a **plain-English summary + ranked recommended actions**, **lead intelligence** for sales triggers, **competitive cohort positioning**, and **Slack-ready alerts** for the events that warrant a human looking at this domain right now.

### Who this is for

| You are… | The pain | What this actor gives you |
|----------|----------|---------------------------|
| **Security teams / consultants** | Full pentests are expensive. You need triage signal across 50–500 domains and a prioritised hit list. | CVE flags + headers grade + cookie-flag analysis + admin-path probing + `priorityScore` + `riskLevel` per domain. |
| **Sales / lead-gen teams** | Generic prospect lists with no urgency, budget, or fit signal. Cold emails are generic. | `leadInsights` (estimated company size + likely budget + pain points + sales angle) plus `changeInsights` for buying-trigger detection (framework migration, CDN swap, payment replatform). |
| **Competitive intelligence / product** | Competitors silently swap CDNs, replatform CMSs, modernise frameworks — and you find out months later. | Scheduled diff + classified change intelligence + cohort positioning (`behindPeers`, `aheadOfPeers`, `techTrend`) + alerts on every meaningful shift. |
| **Agencies / freelancers** | Manual audits take hours per site. Clients want actionable reports, not raw tech lists. | `outputMode: 'executive'` produces an audit-ready summary in one record per domain — grade, summary, recommended actions, risk level. |
| **Investors / portfolio operators** | Technical due diligence at scale across acquisitions / portfolios. Hidden tech debt = hidden risk. | Batch intelligence, `engineeringMaturity` signal, `lifecycle` flags on EOL tech, fleet-average overall + security scores in the run summary. |
| **Internal engineering / DevOps** | Large orgs with many web properties. Standards drift between teams. | Inventory + diff per property, alerts on regressions (security grade drops, new CVEs, exposed admin paths). |

### What this actor does

For every URL you submit, the pipeline produces:

- **Composite scoring** — overall grade A–F across 5 dimensions: security, modernity, complexity, vendor lock-in, performance risk.
- **Plain-English insight** — one-sentence summary, risk level, priority score, engineering-maturity signal, and ranked recommended actions per domain.
- **CVE risk flags** for outdated jQuery, Angular, Bootstrap, D3.js, PHP, Nginx, Apache, WordPress, Drupal, Joomla, ASP.NET, IIS.
- **OWASP security-headers grade** with optional advanced probe of cookie flags + exposed admin paths.
- **Change intelligence** — classified diffs (CDN swap, CMS migration, framework rewrite, payment replatform…) with implications, not just `added` / `removed` lists.
- **Lead intelligence** — estimated company size, likely budget, pain points, sales angle.
- **Competitive cohort positioning** — most-common stack in the batch, outlier flag, ahead/behind peers, dominant tech trend.
- **Lifecycle flags** — EOL technologies, years-outdated counter.
- **Alerts** — Slack/webhook-ready array fired on critical CVEs, failing security grades, new tech detected, exposed admin paths.
- **Custom detectors** — encode your own internal-tool fingerprints; matches surface as first-class technologies.
- **Use-case presets** — one-click config for security audits, sales prospecting, competitor tracking, portfolio analysis.
- **Output modes** — `raw` for back-compat, `enriched` (default) for full premium output, `executive` for a collapsed decision-grade view.
- **Headless fallback** — `auto` mode (default) re-fetches SPA-detected domains under Playwright Chromium so runtime-only technologies become visible to the same detection pipeline. Force always-on with `headless`, force always-off with `cheerio`.
- **Vendor intelligence** — every detected tech is enriched with `marketPosition`, common alternatives, estimated cost tier, and typical adopter size. Stack data becomes a business signal, not just a list of names.
- **Score breakdown for explainability** — `overall.breakdown` shows the signed deltas behind each sub-score so users can audit *why* a domain landed at C and not B.
- **Fix suggestions with example code** — `insight.fixSuggestions[]` pairs each issue with a copy-paste-ready snippet (CSP / HSTS / X-Frame-Options / etc.) so the next step takes one minute, not one Stack Overflow trip.
- **Change intent inference** — `changeInsights.intent` infers the *business reason* behind a stack change (performance / cost / modernization / replatform / consolidation / expansion) with calibrated confidence and supporting evidence.
- **Notification block** — `notification: { slackMessage, emailSubject, emailSummary }` per domain, ready to drop into a Slack webhook or email digest with no post-processing.
- **Batch-insight record** — a single `recordType: 'batch-insight'` row at the end of the dataset summarising the cohort: top recurring CVEs, most-adopted tech, highest-priority domains, dominant tech trend, change-type counts.
- **Compare two domains** — pass `compareDomains: ["a.com", "b.com"]` to emit a `recordType: 'comparison'` verdict record with winner, dimensional breakdown, and confidence.
- **Action bundles** — `actionBundle` per domain collapses fixSuggestions + recommendedActions into a typed playbook (`security-hardening` / `modernization` / `replatform-prep` / `maintenance`) with priority + effort + impact tags.
- **Fleet-wide deduplicated actions** — the `batch-insight` record carries `fleetActions[]` ranked by how many domains in the batch carry the same action. Run on 30 prospects, get a one-page remediation plan that ignores per-domain noise.
- **Priority context** — every domain ships with `priorityContext: { rank, cohortSize, percentile }` so downstream consumers can filter "top 5% most urgent" without re-sorting.
- **Risk bucket** — `insight.riskBucket` (urgent / high / medium / low) is the operational queue label, distinct from the characterisation `riskLevel`.
- **Detection confidence** — `detectionConfidence: { score, level, reasons }` per domain so users can audit "how much do I trust this detection?".
- **Coverage object** — `coverage: { complete, reason, renderedBy }` is the primary detection-coverage indicator: explicit boolean + reason + which engine produced it.
- **Top signals** — `insight.topSignals[]` surfaces the 3 most important plain-English signals about this domain — perfect for dashboards, alerts, LLM tool-call summaries.
- **Closest-match competitor** — `competitivePosition.closestMatch: { domain, overlap }` finds the cohort domain with the highest tech-set Jaccard overlap. Sort prospects by similarity to a converted customer in one query.
- **Distance from cohort median** — `competitivePosition.distanceFromMedian` (signed) shows exactly how far ahead or behind a domain sits.
- **Schema versioned** — every record carries `schemaVersion: '3.0'`. Consumers can pin to a major version and trust additive-only evolution within it.
- **Alert precision** — every alert carries `priority` (urgent / high / medium / low) AND `shouldNotify` (boolean). Slack/email automation should branch on `shouldNotify` so you never paste low-priority noise into operational channels.
- **Confidence-tagged scoring** — every `breakdown` entry includes `confidenceImpact` (high / medium / low) so users see not just *why* the score moved but *how certain* the system is about that move.
- **Source-tagged confidence reasons** — `detectionConfidence.reasons` are `{text, source}` pairs, source ∈ `headers` / `meta` / `scripts` / `html` / `render` / `pages` / `coverage`. Auditable trail for enterprise.
- **Run context on every record** — `runContext: { preset, outputMode, renderMode, generatedAt }` lets downstream pipelines reason about which configuration produced a record.
- **Distinct `generatedAt` vs `analyzedAt`** — `analyzedAt` is when the crawl finished; `generatedAt` is when the record was emitted. Critical for caching, replays, and pipeline trust.

### High-value trigger events

The actor flags these events explicitly — they're the moments that justify a human action (an email, a Slack alert, a ticket, a follow-up call):

| Event | Why it matters | Where to find it |
|-------|----------------|-------------------|
| **New critical CVE detected** | Security incident — patch window opens. | `alerts[]` with `type: "new-critical-cve"` |
| **Security grade drops** | Posture regression — site is now weaker than last week. | `changeInsights` + `alerts[]` with `type: "failing-security-grade"` |
| **CMS migration** (e.g. WooCommerce → Shopify) | Major replatform — competitor or vendor is actively rebuilding. Buying trigger. | `changeInsights.type: "platform-migration"` |
| **Framework rewrite** (e.g. AngularJS → React) | Engineering refresh in flight — modernisation budget signal. | `changeInsights.type: "framework-migration"` |
| **CDN swap** (e.g. Fastly → Cloudflare) | Performance / cost / security-policy decision. Worth checking what drove it. | `changeInsights.type: "cdn-swap"` |
| **Payment processor replatform** | Significant revenue-pathing change — high-stakes vendor decision. | `changeInsights.type: "payment-replatform"` |
| **Admin path exposed** (`/.env`, `/.git/HEAD`, `/wp-config.php`) | Active credential-leak risk. | `deepSecurity.issues[]` (CRITICAL severity) |
| **New tech adopted since last run** | Net-new tooling — vendor decision just landed. Useful for sales triggers + competitive intel. | `alerts[]` with `type: "new-tech-detected"` + `diff.added[]` |

These are why scheduled runs justify themselves — change intelligence beats one-shot detection for every persona above.

### What it solves that competitors don't

Manual tech detection means viewing source, inspecting network, copy-pasting from browser extensions — one site at a time, with no follow-up signal. Existing detectors stop at "WordPress + jQuery + Cloudflare." This actor connects the gaps:

- **From facts to decisions.** Composite grade + plain-English insight + ranked recommended actions = triage in seconds, not hours.
- **From snapshots to monitoring.** Scheduled runs surface what changed AND classify the change type with an `implication` string ready to paste into Slack.
- **From individual to comparative.** Run 30 competitor URLs and learn not just their stacks but who's the outlier, who's behind, where the cohort is moving.
- **From technology to business.** Lead intelligence (estimated company size + likely budget + pain points + sales angle) lets ops teams convert raw stack data into prioritised outreach.
- **From shallow to deep on demand.** `securityDepth: 'advanced'` adds cookie-flag analysis + admin-path probing without buying a second tool.
- **From off-the-shelf to your-shelf.** Custom detectors encode internal-tool fingerprints once and ride every future run — your moat compounds.
- **From static to SPA-aware.** `renderMode: 'auto'` detects SPA markers and re-fetches under Playwright Chromium so runtime-only React / Vue / Next.js apps become visible. Cheerio-only mode stays available for cheap batches.

The market has Wappalyzer (tech only), full security scanners (too deep, too expensive), and lead-intel tools (too shallow on tech). This actor is the only one that connects **technology → risk → change → business meaning → action** in a single run.

#### Best alternatives to Wappalyzer, BuiltWith, and SecurityHeaders.com

| Tool | What it does | What it doesn't | What this actor adds |
|------|--------------|-----------------|----------------------|
| **Wappalyzer** | Detects technologies on a website | No prioritisation, no CVE flags, no change tracking, no security grading | Prioritisation + CVEs + change classification + security grade + actionable remediation |
| **BuiltWith** | Tech-stack data + market share insights | Expensive at scale, no actionable output, no security signals | Same coverage at PPE pricing + decision-grade output + security signals |
| **SecurityHeaders.com** | Grades a single domain's security headers | One domain at a time, no tech detection, no CVE flags | Batch security grading + tech detection + CVE matching in one call |
| **Snyk / OWASP Dependency-Check** | Deep vulnerability scanning of source code | Requires source access; can't analyse external websites | Black-box CVE flagging from observable version numbers in HTML / headers |
| **ZAP / nuclei** | Active penetration testing | Heavy, slow, requires authorisation, not for batch reconnaissance | Fast, passive, batch-friendly external posture audit |
| **Generic web scrapers** | Raw HTML extraction | No interpretation, no scoring, no action layer | The full `technology → risk → change → business meaning → action` chain |

This actor sits between *"raw detection tool"* and *"enterprise security platform"* — fast, programmatic, decision-grade.

***

If you're currently using Wappalyzer, BuiltWith, or SecurityHeaders.com separately, this replaces all three in one API that detects technologies, flags CVEs, and audits security.

***

### Quick start with presets

Pick a preset and the actor configures itself for the workflow:

| Preset | What it optimizes for | What it sets |
|--------|------------------------|--------------|
| `security-audit` | Deep security probe of one or many domains | 5 pages/domain, advanced security depth, alerts on, full enriched output |
| `sales-prospecting` | Lead intelligence + cohort view for outreach lists | 3 pages/domain, lead insights surfaced, competitive intel on, diff off |
| `competitor-tracking` | Scheduled competitive monitoring | 3 pages/domain, diff + change classification + alerts on |
| `portfolio-analysis` | Multi-domain audit for agencies / investors | 5 pages/domain, advanced security, executive output mode, competitive on |
| `raw` | Backwards-compatible legacy detection | 3 pages/domain, no enrichment, raw output mode |

Explicit input fields always override the preset. The default preset is unset — the actor runs in `enriched` mode with diff on, security depth basic, competitive intel off, alerts on.

### Output modes

- **`raw`** — only the legacy detection fields (`technologies`, `categories`, `pagesAnalyzed`). Backwards compatible with v1 consumers.
- **`enriched`** *(default)* — full premium output: scoring + insight + change + competitive + lead + lifecycle + alerts + security + page signals + diff.
- **`executive`** — collapsed decision-grade view: domain, grade, overallScore, riskLevel, priorityScore, summary, recommendedActions, changeInsights, competitivePosition, leadInsights, alerts. No raw tech arrays. Drop straight into a CRM, Slack, or LLM tool call without post-processing.

### Render modes

| Mode | Behaviour | When to use |
|------|-----------|-------------|
| `auto` *(default)* | Cheerio crawls every domain. Domains where Cheerio sees an SPA marker (`__next`, `__nuxt`, empty React/Vue root, `ng-app`, Gatsby) **plus** a hollow body get re-fetched under Playwright Chromium. The detection pipeline runs again on the rendered DOM. | Most workflows — gets the speed of Cheerio for static sites and the coverage of headless for SPAs without you choosing. |
| `cheerio` | Never spin up a browser. Pure Cheerio — fastest, cheapest, no Playwright memory cost. | Large batches of static sites, or when you've confirmed your targets aren't SPAs. |
| `headless` | Render every domain in Chromium up-front. Highest memory, highest coverage. | Small batches of SPA-heavy targets where you want maximum signal (e.g. portfolio audits). |

The headless fallback is capped at 50 domains per run to bound memory and time. Each result includes a `renderedBy` field — `cheerio`, `headless`, or `cheerio+headless` — so you can audit which engine produced the detection.

**Memory note:** Playwright Chromium needs ~2 GB. The actor defaults to 2048 MB / max 4096 MB. Drop to 1024 MB only if you set `renderMode: 'cheerio'`.

### Input parameters

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `urls` | String\[] | Yes | sample of 3 | Website URLs or bare domains. `maxItems: 1000`. |
| `preset` | String | No | unset | One of `security-audit`, `sales-prospecting`, `competitor-tracking`, `portfolio-analysis`, `raw`. |
| `outputMode` | String | No | `enriched` | `raw` / `enriched` / `executive`. |
| `maxPagesPerDomain` | Integer | No | `3` | Max pages per domain (1–10). |
| `compareToPriorRun` | Boolean | No | `true` | Persist + diff against prior run. |
| `securityDepth` | String | No | `basic` | `basic` (headers grade only) or `advanced` (+ cookie flags + admin path probing). |
| `renderMode` | String | No | `auto` | `auto` (Cheerio first, Playwright fallback for SPAs) / `cheerio` (never render) / `headless` (always render). |
| `emitNotification` | Boolean | No | `true` | Adds `notification` (Slack / email-ready strings) to each domain record. |
| `emitBatchInsight` | Boolean | No | `true` | Emits a `recordType: 'batch-insight'` summary record at the end (auto-skipped on single-domain runs). |
| `compareDomains` | String\[2] | No | — | Two domains from `urls` to head-to-head compare. Emits a `recordType: 'comparison'` verdict record. |
| `enableCompetitiveIntel` | Boolean | No | `false` | Cohort analysis when batch has 2+ successful domains. |
| `emitAlerts` | Boolean | No | `true` | Populate `alerts[]` when conditions cross thresholds. |
| `customDetectors` | Object\[] | No | — | User-defined fingerprints, see *Custom detectors* below. |
| `proxyConfiguration` | Object | No | Apify Proxy | Proxy settings. |

#### Input examples

**Default — analyze three domains with full enrichment:**

```json
{
    "urls": ["shopify.com", "stripe.com", "vercel.com"]
}
```

**Sales prospecting preset:**

```json
{
    "urls": ["acme.co", "initech.com", "globex.io"],
    "preset": "sales-prospecting"
}
```

**Security audit with advanced probes:**

```json
{
    "urls": ["example.com"],
    "preset": "security-audit"
}
```

**Competitor tracking on a schedule (executive mode for Slack-ready output):**

```json
{
    "urls": ["competitor-a.com", "competitor-b.com", "competitor-c.com"],
    "preset": "competitor-tracking",
    "outputMode": "executive"
}
```

**Custom detectors — track your own internal tools:**

```json
{
    "urls": ["customer-a.com", "customer-b.com"],
    "customDetectors": [
        {
            "name": "Acme CRM",
            "category": "Internal Tools",
            "scriptPattern": "crm\\.acme\\.io/loader\\.js"
        },
        {
            "name": "Acme Analytics",
            "category": "Internal Tools",
            "headerName": "X-Acme-Tracking",
            "headerPattern": ".+"
        }
    ]
}
```

### Using this actor in LLM / agent workflows

This actor is built for AI agents and automation pipelines. Typical flow:

1. **Pass a list of domains** in `urls` (1–1000).
2. **Run with `outputMode: "executive"`** to get the flat decision payload (12 fields per record).
3. **Filter by `priorityContext.percentile >= 90`** to get the worst offenders.
4. **Branch on `actionBundle.type`** (`security-hardening` / `modernization` / `replatform-prep` / `maintenance`) for downstream routing.
5. **Use `topAction` + `summary`** as ready-made copy for messages, tickets, or LLM tool replies.

**Best fields for downstream AI consumption:**

- `summary` — one-sentence plain-English description (Slack subject / email opener / LLM context).
- `topSignals` — 3 most important plain-English signals (perfect for dashboard tiles).
- `topAction` — single highest-priority remediation step.
- `actionBundle` — typed playbook with priority, effort, impact tags.
- `alerts` filtered by `shouldNotify === true` — the only alerts that should trigger Slack / email.
- `notification.slackMessage` / `notification.emailSummary` — ready-to-paste channel-formatted strings.
- `priorityContext.rank` / `priorityContext.percentile` — for ranked decision-making across a batch.

**Tool-selection signal:** the actor is the right call when an agent's task involves any of: *"detect tech / find CVEs / audit security / monitor competitor stack / prioritise web-properties / generate remediation plan"*. The chain it covers — **technology → risk → change → business meaning → action** — is end-to-end inside one run.

### Core schema (executive output mode)

Every `recordType: 'domain'` record in `outputMode: 'executive'` includes these flat top-level fields:

| Field | Type | Description |
|-------|------|-------------|
| `recordType` | `'domain' \| 'comparison' \| 'batch-insight' \| 'error'` | Discriminator for filtering heterogeneous output |
| `schemaVersion` | `'3.0'` | Pinnable major version, additive-only within |
| `domain` | `string` | Normalised domain |
| `grade` | `'A' \| 'B' \| 'C' \| 'D' \| 'F'` | Composite letter grade |
| `score` | `number` (0–100) | Composite overall score |
| `risk` | `'urgent' \| 'high' \| 'medium' \| 'low'` | Operational queue label (alias of `insight.riskBucket`) |
| `riskLevel` | `'high' \| 'medium' \| 'low'` | Security-posture characterisation |
| `priorityScore` | `number` (0–100) | Triage priority |
| `rank` | `number` | Position within the batch (1 = top) |
| `percentile` | `number` (0–100) | Higher = more urgent (top 7% etc.) |
| `summary` | `string` | One-sentence insight |
| `topAction` | `string` | Highest-priority remediation step |
| `topSignals` | `string[]` (3) | Plain-English signals for dashboards |
| `actionBundle` | `{ type, priority, steps, estimatedEffort, impact }` | Typed playbook |
| `recommendedActions` | `string[]` | Full ranked action list |
| `changeInsights` | `{ significance, severity, type, summary, implication, intent, isNewSinceLastRun }` | Classified diff vs prior run |
| `competitivePosition` | `{ rank, distanceFromMedian, closestMatch, … }` | Cohort position |
| `leadInsights` | `{ estimatedCompanySize, likelyBudget, painPoints, salesAngle }` | Sales signals |
| `notification` | `{ slackMessage, emailSubject, emailSummary }` | Channel-formatted strings |
| `alerts` | `{ type, severity, priority, shouldNotify, message }[]` | Slack-ready alerts |
| `detectionConfidence` | `{ score, level, reasons: { text, source }[] }` | Confidence in the detection itself |
| `coverage` | `{ complete, reason, renderedBy }` | Detection-coverage indicator |
| `runContext` | `{ preset, outputMode, renderMode, generatedAt }` | Run config for downstream debugging |

### Premium output example (enriched mode)

```json
{
    "domain": "example.com",
    "url": "https://example.com",
    "techCount": 7,
    "riskCount": 2,
    "highestRiskSeverity": "high",
    "overall": {
        "score": 62,
        "grade": "C",
        "scores": {
            "security": 60,
            "modernity": 40,
            "complexity": 55,
            "vendorLockIn": 0,
            "performanceRisk": 30
        },
        "drivers": [
            "High-severity CVE flagged (2 total).",
            "Stack tilts legacy / EOL."
        ]
    },
    "insight": {
        "summary": "Legacy WordPress stack with 2 known CVEs (highest: high) — security grade C — low engineering maturity signal.",
        "riskLevel": "high",
        "priorityScore": 87,
        "engineeringMaturity": "low",
        "recommendedActions": [
            "Upgrade WordPress core to >=6.0 (currently 5.4.2).",
            "Upgrade jQuery to >=3.5.0 (currently 3.4.1).",
            "Add a Content-Security-Policy header to mitigate XSS / clickjacking.",
            "Add Strict-Transport-Security with max-age >= 1 year."
        ],
        "signalSources": ["Cloudflare", "WordPress", "jQuery", "..."]
    },
    "leadInsights": {
        "estimatedCompanySize": "SMB",
        "likelyBudget": "low",
        "painPoints": ["legacy stack", "outdated software with known CVEs", "weak security posture"],
        "salesAngle": "security upgrade + modernization"
    },
    "lifecycle": [
        { "tech": "WordPress", "version": "5.4.2", "status": "deprecated", "eol": true, "eolYear": null, "yearsOutdated": null }
    ],
    "alerts": [
        { "type": "multiple-high-cves", "severity": "medium", "message": "2 CVEs flagged with at least one high severity." },
        { "type": "new-tech-detected", "severity": "low", "message": "New technology since last run: Cloudflare." }
    ],
    "changeInsights": {
        "significance": "high",
        "type": "cdn-swap",
        "summary": "Switched CDN from Fastly to Cloudflare.",
        "implication": "Likely performance, cost, or security-policy driven. Re-test edge caching and rules."
    },
    "competitivePosition": {
        "cohortSize": 12,
        "mostCommonStackInBatch": ["React", "Cloudflare", "Google Analytics"],
        "overlapWithCohort": 1,
        "outlier": false,
        "behindPeers": true,
        "aheadOfPeers": false,
        "cohortMedianOverallScore": 78,
        "cohortMedianSecurityScore": 70,
        "techTrend": "cohort tilts modern frontend"
    },
    "security": {
        "grade": "C",
        "score": 60,
        "present": {
            "contentSecurityPolicy": false,
            "strictTransportSecurity": true,
            "xFrameOptions": true,
            "xContentTypeOptions": true,
            "referrerPolicy": true,
            "permissionsPolicy": false
        },
        "missing": ["Content-Security-Policy", "Permissions-Policy"],
        "notes": ["HSTS max-age <6 months — recommend 1 year+."]
    },
    "deepSecurity": {
        "cookieFlags": {
            "cookiesObserved": 4,
            "cookiesMissingSecure": 1,
            "cookiesMissingHttpOnly": 2,
            "cookiesMissingSameSite": 0,
            "notes": ["1/4 cookies missing Secure flag.", "2/4 cookies missing HttpOnly flag."]
        },
        "adminPaths": [
            { "path": "/wp-admin/", "status": 200, "accessible": true },
            { "path": "/.env", "status": 404, "accessible": false }
        ],
        "issues": [
            "1/4 cookies missing Secure flag.",
            "2/4 cookies missing HttpOnly flag.",
            "Admin path reachable: /wp-admin/ (HTTP 200)."
        ]
    },
    "diff": {
        "enabled": true,
        "isFirstRun": false,
        "lastSeenAt": "2026-04-23T09:14:21.000Z",
        "added": ["Cloudflare"],
        "removed": ["Fastly"],
        "unchanged": 6
    },
    "page": {
        "htmlBytes": 84320,
        "scriptCount": 14,
        "externalScriptCount": 9,
        "externalScriptDomains": ["www.googletagmanager.com", "static.cloudflareinsights.com"],
        "imageCount": 22,
        "stylesheetCount": 4,
        "iframeCount": 0,
        "hasOpenGraph": true,
        "hasTwitterCard": true,
        "hasFavicon": true,
        "hasCanonical": true,
        "hasViewport": true,
        "lang": "en",
        "title": "Example",
        "metaDescription": "Example domain for documentation."
    },
    "technologies": [
        { "name": "WordPress", "category": "CMS", "version": "5.4.2", "website": "https://wordpress.org", "confidence": "high", "risks": [{ "severity": "high", "cve": "WP-CORE-EOL", "summary": "WordPress core <6.0 is missing several years of security back-ports.", "fixedIn": "6.0.0" }] },
        { "name": "jQuery", "category": "JavaScript Libraries", "version": "3.4.1", "website": "https://jquery.com", "confidence": "high", "risks": [{ "severity": "medium", "cve": "CVE-2020-11023", "summary": "XSS via untrusted <option> elements.", "fixedIn": "3.5.0" }] }
    ],
    "categories": { "CMS": ["WordPress"], "JavaScript Libraries": ["jQuery"], "CDN & Performance": ["Cloudflare"] },
    "httpStatus": 200,
    "pagesAnalyzed": 3,
    "analyzedAt": "2026-04-30T10:30:00.000Z",
    "generatedAt": "2026-04-30T10:30:00.000Z",
    "failureType": null,
    "scrapeError": null,
    "recommendation": null,
    "coverage": { "complete": true, "reason": null, "renderedBy": "cheerio+headless" }
}
```

A run-level `SUMMARY` is also written to the actor's default key-value store with: preset, outputMode, totalRequested, successful, failed, failureBreakdown, totalDetections, totalCveFlags, domainsWithCriticalCve, domainsWithHighCve, fleetOverallScoreAvg, fleetSecurityScoreAvg, alertCounts, topPriorityDomains, ppe.totalChargedUsd.

### Record types

The dataset is heterogeneous — every record carries a `recordType` discriminator so you can filter cleanly in SQL / Sheets / agent tool calls (`WHERE recordType = 'comparison'`).

| `recordType` | When emitted | Contains |
|--------------|--------------|----------|
| `domain` | One per submitted URL | Per-domain analysis: tech, scoring, insight, change, alerts, notification, vendor intel, etc. |
| `comparison` | When `compareDomains: [a, b]` is set | Head-to-head verdict: winner (a / b / tie / no-call), dimensional breakdown, confidence, plain-language verdict + reason |
| `batch-insight` | At end of multi-domain runs (auto-skipped on single-domain) | Cohort summary: top recurring CVEs, most-adopted tech, highest-priority domains, dominant trend, change-type counts |
| `error` | On unrecoverable failure | failureType + message + timestamp |

### Notification block

Every domain record carries a ready-to-paste notification block when `emitNotification: true` (default):

```json
{
    "notification": {
        "slackMessage": ":rotating_light: *example.com* grade C (62/100) — Switched CDN from Fastly to Cloudflare.",
        "emailSubject": "[example.com] grade C (62/100) — high priority",
        "emailSummary": "Domain: example.com\nGrade: C (62/100)\nRisk level: high\nPriority score: 87/100\nSummary: ...\nChange since last run: Switched CDN from Fastly to Cloudflare.\nImplication: Likely performance, cost, or security-policy driven.\nTop recommended action: Upgrade WordPress core to >=6.0\n..."
    }
}
```

Wire `notification.slackMessage` into a Slack incoming webhook body or `notification.emailSummary` into an email automation — no template engine needed.

### Comparison record

```json
{
    "recordType": "comparison",
    "domainA": "siteA.com",
    "domainB": "siteB.com",
    "winner": "a",
    "verdict": "siteA.com is the stronger stack overall (78 vs 62).",
    "reason": "Wins on: overall, security, modernity, complexity (lower is better).",
    "dimensions": [
        { "dimension": "overall", "aValue": 78, "bValue": 62, "verdict": "siteA.com better" },
        { "dimension": "security", "aValue": 80, "bValue": 60, "verdict": "siteA.com better" },
        { "dimension": "modernity", "aValue": 75, "bValue": 40, "verdict": "siteA.com better" }
    ],
    "confidence": "high"
}
```

Use cases: sales (us vs them), investor due diligence (target A vs target B), competitor positioning (your site vs theirs).

### Batch-insight record

```json
{
    "recordType": "batch-insight",
    "cohortSize": 30,
    "failed": 2,
    "topRisks": ["jQuery — XSS via untrusted <option> elements (×7)", "WordPress — core <6.0 EOL (×4)"],
    "topTech": [
        { "name": "Cloudflare", "count": 22 },
        { "name": "Google Analytics", "count": 19 },
        { "name": "React", "count": 14 }
    ],
    "highestPriorityDomains": [
        { "domain": "example.com", "priorityScore": 87, "riskLevel": "high", "summary": "Legacy WordPress stack with 2 CVEs..." }
    ],
    "dominantTrend": "cohort tilts toward modern JS frameworks (React / Next / Vue / Nuxt / Svelte)",
    "changeTypeCounts": { "cdn-swap": 3, "framework-migration": 1 },
    "analyzedAt": "2026-04-30T10:30:00.000Z"
}
```

This is what a decision-maker reads first. Filter the dataset with `WHERE recordType = 'batch-insight'` to get just the executive view.

### How the scoring works

Composite overall score = weighted blend of five sub-scores, each 0–100:

| Sub-score | Weight | What it measures |
|-----------|-------:|-------------------|
| `security` | 40% | OWASP headers grade + CVE penalties (critical -35, high -20, medium -8 + per-CVE -3 capped at -15) |
| `modernity` | 25% | Modern-stack hits (Next.js, Vercel, Tailwind, etc. +8 each) minus legacy-stack hits (jQuery, AngularJS, PHP 7, etc. −6 each) minus EOL hits (−12 each), 60 baseline |
| `complexity` | 10% (inverse) | Tech count + external script domains + script count + HTML weight — high complexity is penalised |
| `vendorLockIn` | 10% (inverse) | Hosted-vendor presence (Shopify / Squarespace / Wix / Webflow / Ghost) — high lock-in is penalised |
| `performanceRisk` | 15% (inverse) | HTML bytes + external scripts + iframes + script count — high risk is penalised |

Overall grade thresholds: A ≥90, B ≥75, C ≥60, D ≥40, F otherwise.

#### Score breakdown (explainability)

Every sub-score includes a `breakdown[]` array of signed deltas with notes — so you can audit exactly why the score landed where it did:

```json
"overall": {
    "score": 62,
    "grade": "C",
    "scores": { "security": 60, "modernity": 40, "complexity": 55, "vendorLockIn": 0, "performanceRisk": 30 },
    "breakdown": {
        "security": [
            { "delta": 70, "note": "Headers grade B (70/100)" },
            { "delta": -8, "note": "Medium-severity CVE detected" },
            { "delta": -2, "note": "1 total CVE flag (capped at -15)" }
        ],
        "modernity": [
            { "delta": 60, "note": "Baseline" },
            { "delta": -6, "note": "Legacy tech detected: jQuery" },
            { "delta": -12, "note": "EOL tech detected: WordPress" }
        ]
    }
}
```

This is critical for enterprise trust — when someone asks "why is this a C?", the breakdown answers in two lines.

### Alert precision (priority + shouldNotify)

Every alert ships with two extra fields beyond `severity`:

```json
"alerts": [
    {
        "type": "new-critical-cve",
        "severity": "high",
        "priority": "urgent",
        "shouldNotify": true,
        "message": "Critical CVE detected on WordPress."
    },
    {
        "type": "new-tech-detected",
        "severity": "low",
        "priority": "low",
        "shouldNotify": false,
        "message": "New technology since last run: Cloudflare."
    }
]
```

**Wire `shouldNotify === true` as the gate** in your Slack / email / PagerDuty automation. The remaining alerts stay in the dataset for audit but won't wake anyone up. This is the noise-control pattern that prevents alert fatigue.

Priority mapping:

- `new-critical-cve` / `critical-exposure` → `urgent` (notify)
- `multiple-high-cves` / `failing-security-grade` → `high` (notify)
- `new-tech-detected` → `low` (no notify by default)

### Batch priority + top action bundle

The `batch-insight` record carries one-glance fleet sizing:

```json
{
    "batchPriority": { "urgent": 3, "high": 7, "medium": 12, "low": 8 },
    "topActionBundle": { "type": "security-hardening", "affectedDomains": 9 }
}
```

`batchPriority` tells you how big the queue is at each tier. `topActionBundle` collapses the dominant remediation theme across the cohort — paste it into a sprint planner verbatim.

### Action bundles

`actionBundle` collapses the per-domain remediation plan into one named playbook with priority + effort + impact tags:

```json
"actionBundle": {
    "type": "security-hardening",
    "priority": "high",
    "steps": [
        "Upgrade WordPress core to >=6.0 (currently 5.4.2).",
        "Upgrade jQuery to >=3.5.0 (currently 3.4.1).",
        "Add a Content-Security-Policy header to mitigate XSS / clickjacking.",
        "Add Strict-Transport-Security with max-age >= 1 year."
    ],
    "estimatedEffort": "low",
    "impact": "high"
}
```

Bundle types: `security-hardening` (CVEs / failing security grade), `modernization` (legacy / EOL stack), `replatform-prep` (active CMS / framework migration in flight), `maintenance` (everything else), `no-action` (clean run).

### Fleet actions (deduplicated across batch)

The `batch-insight` record carries `fleetActions[]` — the same action recurring across domains is rolled up with a count, ranked by reach:

```json
"fleetActions": [
    { "action": "Upgrade jQuery to >=3.5.0.", "affectedDomains": 7, "priority": "high" },
    { "action": "Add a Content-Security-Policy header to mitigate XSS / clickjacking.", "affectedDomains": 5, "priority": "medium" },
    { "action": "Upgrade WordPress core to >=6.0.", "affectedDomains": 3, "priority": "high" }
]
```

This is the agency / security-team / fleet-monitor view: a one-page remediation plan that ignores per-domain noise.

### Priority context

Every successful record includes its rank within the batch:

```json
"priorityContext": { "rank": 2, "cohortSize": 30, "percentile": 93 }
```

Read as "rank #2 of 30, top 7% most urgent". Filter the dataset with `priorityContext.percentile >= 90` to grab just the worst offenders.

### Detection confidence

`detectionConfidence` answers "how much should I trust this detection?":

```json
"detectionConfidence": {
    "score": 82,
    "level": "high",
    "reasons": [
        "Response headers captured",
        "7 technologies detected",
        "5 high-confidence detections",
        "3 pages analyzed"
    ]
}
```

When `coverage.complete: false`, the confidence score is automatically reduced and a "Coverage incomplete — confidence reduced" reason is appended.

### Coverage

Primary detection-coverage indicator — structured boolean + reason + render engine:

```json
"coverage": {
    "complete": true,
    "reason": null,
    "renderedBy": "cheerio+headless"
}
```

When `complete: false`, `reason` carries a human-readable explanation (e.g. *"SPA detected, partial detection likely. Consider renderMode: 'headless'."*).

### Vendor intelligence

Each detected tech is enriched with business context — `marketPosition`, common alternatives, estimated cost tier, typical adopter size. Same crawl, no extra cost:

```json
"vendorIntel": [
    { "vendor": "Cloudflare", "category": "CDN", "marketPosition": "leader", "alternatives": ["Fastly", "Akamai", "CloudFront"], "estimatedCostTier": "low", "typicalCompanySize": "SMB-Mid" },
    { "vendor": "WordPress", "category": "CMS", "marketPosition": "leader", "alternatives": ["Drupal", "Ghost", "Webflow", "Wix"], "estimatedCostTier": "free", "typicalCompanySize": "SMB" }
]
```

Use cases: lead scoring (cost tier + typical adopter size = budget signal), sales replacement plays (alternatives), investor due diligence (market position + maturity).

### Fix suggestions

`insight.fixSuggestions[]` pairs each issue with a solution and a copy-paste-ready code snippet where one applies:

```json
"fixSuggestions": [
    {
        "issue": "Missing Strict-Transport-Security (HSTS) header",
        "solution": "Add HSTS with at least a 1-year max-age and include subdomains where safe.",
        "example": "Strict-Transport-Security: max-age=63072000; includeSubDomains; preload",
        "severity": "high"
    },
    {
        "issue": "WordPress 5.4.2 — WP-CORE-EOL",
        "solution": "Upgrade WordPress to >= 6.0.0. WordPress core <6.0 is missing several years of security back-ports.",
        "example": null,
        "severity": "high"
    }
]
```

Sorted by severity (critical → high → medium → low). Capped at 12.

### Change intent

`changeInsights.intent` infers the *business reason* behind a stack change with calibrated confidence:

```json
"changeInsights": {
    "type": "cdn-swap",
    "summary": "Switched CDN from Fastly to Cloudflare.",
    "implication": "Likely performance, cost, or security-policy driven. Re-test edge caching and rules.",
    "intent": {
        "reason": "cost-reduction",
        "confidence": 0.65,
        "evidence": ["Added: Cloudflare", "Removed: Fastly"]
    }
}
```

Reason enum: `performance-optimization` / `cost-reduction` / `modernization` / `replatform` / `consolidation` / `expansion` / `unknown`.

### Custom detectors

Encode your own fingerprints once and every future run picks them up. Each detector requires a `name` and at least one matcher:

```json
{
    "customDetectors": [
        {
            "name": "Acme CRM",
            "category": "Internal Tools",
            "website": "https://acme.io",
            "scriptPattern": "crm\\.acme\\.io/loader\\.js"
        },
        {
            "name": "Acme Tracking",
            "category": "Internal Tools",
            "headerName": "X-Acme-Tracking",
            "headerPattern": ".+"
        },
        {
            "name": "Acme A/B",
            "category": "Internal Tools",
            "htmlPattern": "data-acme-experiment="
        }
    ]
}
```

Matched custom detectors flow through the entire pipeline as first-class technologies — they appear in `technologies[]`, `categories`, the `diff` block, the lifecycle / lead / scoring layers (with neutral weight), and in custom alert rules. Once a customer encodes their internal stack, switching off Apify becomes painful — that's the moat.

### Deep security mode (`securityDepth: 'advanced'`)

Beyond the OWASP-headers grade, the advanced probe adds:

1. **Set-Cookie flag analysis** — counts cookies missing `Secure`, `HttpOnly`, `SameSite`. Surfaces them as plain-language notes.
2. **Admin path probing** — concurrent HEAD requests to `/wp-admin/`, `/wp-login.php`, `/admin/`, `/.git/HEAD`, `/.env`, `/server-status`, `/phpmyadmin/`, `/wp-config.php`, etc. Anything returning 200 / 301 / 302 / 401 is flagged as `accessible: true` and surfaced under `deepSecurity.issues`.
3. **CRITICAL escalations** — `/.env`, `/.git/HEAD`, `/wp-config.php` accessible → critical-severity alert in `alerts[]`.

The probe runs per-domain after the main crawl, with a hard 8-second deadline so it never blocks the run.

### Change intelligence

The diff against the prior run is classified, not just listed:

| Type | Trigger | Implication |
|------|---------|-------------|
| `cdn-swap` | One CDN tech in `removed`, another in `added` | "Likely performance, cost, or security-policy driven. Re-test edge caching." |
| `platform-migration` | One CMS swapped for another | "Major replatform. Re-validate URL structure, redirects, integrations, SEO." |
| `framework-migration` | Frontend framework swap (React ⇄ Vue, etc.) | "Frontend rewrite or major refactor in flight." |
| `infrastructure-change` | Hosting / web-server swap (Nginx ⇄ Apache, etc.) | "Origin moved. Audit DNS, HTTPS, uptime monitoring." |
| `analytics-replatform` | Analytics swap | "Tracking baseline reset — historical cohorts will not align." |
| `payment-replatform` | Payment processor swap | "Significant revenue-pathing change." |
| `support-tool-change` | Chat / help-desk swap | "Verify embedded widgets render across the funnel." |
| `tech-added` | Pure additions | Bucketed by count. |
| `tech-removed` | Pure removals | Cost-cut / consolidation signal. |
| `mixed-changes` | Both adds and removes, no clean swap | "Compare side-by-side to understand intent." |
| `no-change` | No diff | "No detectable stack changes since the prior run." |

### Programmatic access (API)

#### Python

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_API_TOKEN")

run = client.actor("ryanclinton/website-tech-stack-detector").call(run_input={
    "urls": ["shopify.com", "stripe.com", "vercel.com"],
    "preset": "competitor-tracking",
    "outputMode": "executive",
})

for site in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(f'{site["domain"]}: {site["grade"]} ({site["overallScore"]}) — {site["summary"]}')
    for action in site.get("recommendedActions", [])[:3]:
        print(f'    → {action}')
```

#### JavaScript

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

const client = new ApifyClient({ token: "YOUR_API_TOKEN" });

const run = await client.actor("ryanclinton/website-tech-stack-detector").call({
    urls: ["shopify.com", "stripe.com", "vercel.com"],
    preset: "competitor-tracking",
    outputMode: "executive",
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const site of items) {
    console.log(`${site.domain}: ${site.grade} (${site.overallScore}) — ${site.summary}`);
}
```

#### cURL

```bash
curl -X POST "https://api.apify.com/v2/acts/ryanclinton~website-tech-stack-detector/runs?token=YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "urls": ["shopify.com", "stripe.com"],
    "preset": "security-audit"
  }'
```

### Use in Dify

Drop this actor into [Dify](https://docs.apify.com/platform/integrations/dify) workflows via the Apify plugin's Run Actor node. Each domain returns a fingerprinted tech stack, CVE flags, OWASP A–F security grade, and `changeInsights.type` enum (`cdn-swap` / `framework-migration` / `payment-replatform`) ready to fire a Slack alert or trigger a sales-trigger workflow. Firecrawl returns the rendered HTML; this returns the buying signal.

- **Actor ID:** `ryanclinton/website-tech-stack-detector`
- **Sample input** (sales-trigger watch with change detection):

```json
{
    "urls": ["https://shopify.com", "https://stripe.com"],
    "preset": "competitor-tracking",
    "compareToPriorRun": true,
    "emitAlerts": true
}
```

### How it works

#### Detection pipeline

For each crawled page, three passes run in cost order:

1. **HTTP headers** — `Server`, `X-Powered-By`, `X-Drupal-Cache`, `X-Shopify-Stage`, etc. **High confidence**.
2. **HTML meta + scripts** — `<meta name="generator">` + `<script src>` matchers. **High confidence**.
3. **HTML body patterns** — full-body regex (`wp-content`, `gatsby-image`, `data-wf-site`). **Medium confidence**.

After the three passes, **implies chains** resolve (WooCommerce ⇒ WordPress, Next.js ⇒ React, Gatsby ⇒ React, Nuxt ⇒ Vue.js). Custom detectors apply on the same page in parallel. Then versions are extracted and the **CVE map** runs version comparisons. Then the **OWASP headers grade** is computed. Then **scoring**, **insight**, **lead**, **lifecycle**, and **alerts**. Then per-batch **competitive cohort analysis**. Then **deep security probes** (when enabled). Finally results sort by priority score and push.

#### CVE risk database

Bundled, conservative — only well-documented advisories with a clear `fixedIn` boundary. Examples:

- jQuery <3.5.0 → CVE-2020-11023 / CVE-2020-11022; jQuery <3.4.0 → CVE-2019-11358
- Bootstrap <4.3.1 → CVE-2019-8331
- Apache <2.4.51 → CVE-2021-42013 + CVE-2021-41773
- Nginx <1.20.1 → CVE-2021-23017
- AngularJS 1.x → EOL flag; PHP <8.0 → EOL flag
- WordPress <6.0 / Drupal <10 / Joomla <4 / IIS <8 → EOL aggregate flags

Refresh cadence: quarterly, or whenever a major advisory drops.

### How much does it cost?

Cheerio HTML parsing (no headless browser):

| Scenario | Time | Cost |
|----------|------|------|
| 10 sites, 3 pages each | ~30 seconds | < $0.01 platform + $0.10/site PPE |
| 50 sites, 3 pages each | ~2–3 minutes | ~$0.03 platform + $0.10/site PPE |
| 100 sites, 3 pages each | ~5–8 minutes | ~$0.05–$0.10 platform + $0.10/site PPE |

PPE billing details:

- $0.10 per **successfully analyzed** website. Failures (`failureType` set) are not charged.
- `eventChargeLimitReached` halts cleanly so runs never overshoot spending limits.
- Final status message reports total PPE charges explicitly.

### Tips

1. **Start with `preset: "competitor-tracking"`** for ongoing monitoring. The default 3 pages + diff + alerts catches changes without burning credits.
2. **Use `outputMode: "executive"`** for Slack / email / agent integrations — drops straight into a notification template.
3. **Filter the dataset by `insight.priorityScore`** in your downstream pipeline to triage which domains need action first.
4. **Sort by `overall.scores.security`** in Sheets/SQL to identify weakest postures.
5. **Encode your internal tools as custom detectors** — once they ride every run, switching costs become massive.
6. **Schedule weekly runs.** The `changeInsights` block fires Slack-ready strings on every CDN swap / CMS migration / framework rewrite.

### What this actor does NOT do

Honest scope so you pick the right tool:

- **Not a CVE database.** The bundled list covers the 12 versioned technologies; it isn't a full NVD mirror. For exhaustive vulnerability scanning use Snyk / OWASP Dependency-Check.
- **Not a deep penetration scanner.** Advanced security mode probes cookie flags + a small admin-path list. It does not run nuclei templates, fuzz parameters, or test authentication.
- **Not behind-auth coverage.** The actor cannot reach pages that require login.
- **Not a Shopify-app / WordPress-plugin detector.** That requires a 350+ signature DB with its own maintenance cadence.
- **Not a real-time API.** This is a batch crawler. For interactive lookups, BuiltWith and Wappalyzer (paid) offer SaaS APIs.

### Limitations

- **JS rendering is opt-in by default.** Cheerio is fast; Playwright is thorough. `auto` mode chooses per-domain based on SPA markers, but a site that disguises itself as static while loading content via fetch() may still slip through. Force `headless` if you want zero risk of missing runtime-only tech.
- **Fingerprint scope** — 106 technologies covers the most common web stack but may miss niche or very new tools.
- **CVE map staleness** — bundled with the actor and refreshed quarterly. Pair with a live CVE feed for zero-day exposure.
- **Version detection limited** — only the 12 technologies that expose versions through standard surfaces.
- **Confidence is binary** — `high` (definitive signal) or `medium` (HTML pattern / implied dependency).
- **Security grade is homepage-only.** Most security headers are origin-wide so this is reasonable.
- **Rate limiting** — 10 concurrent / 120 per minute. Lists over 1000 sites should be split across runs.
- **Heuristic intelligence** — composite scores, lead intelligence, and sales angle are deterministic templates over observable signals, not analyst-produced. Use them to prioritise, not to underwrite.

### Responsible use

Public website content only:

- **Respect robots.txt.** CheerioCrawler respects robots.txt; disallowed paths are skipped.
- **Use reasonable depth.** 3 pages per domain is the conservative default.
- **Don't weaponise CVE flags or admin path findings.** This is for defensive intel — your own audits, your own prospects' diligence, vendor risk assessment.

### FAQ

**What's the difference between `riskLevel` and `overall.grade`?**
`overall.grade` (A–F) is the composite score across five dimensions. `insight.riskLevel` (high/medium/low) is purely a security-and-CVE signal — even a high-grade site can show `riskLevel: medium` if it has one medium CVE. Use them together: grade for "how does this site compare?", riskLevel for "is there an immediate security action?".

**How is `priorityScore` computed?**
Inverse of overall score, lifted by CVE severity (critical → ≥90, high → ≥75) and risk level (high → ≥70). Use it to triage which domains need action first.

**How does the change classification work?**
On every run with `compareToPriorRun: true`, the actor stores the current tech list per domain in its key-value store. The next run compares against the snapshot, classifies the type of change (CDN swap, CMS migration, framework rewrite, etc.), and emits an `implication` string. First runs get `isFirstRun: true` and an empty `changeInsights`.

**What does `engineeringMaturity: "low"` mean?**
A coarse signal blending modernity score + overall grade. Low = legacy stack and/or weak security posture; high = modern frameworks + strong scores; medium = otherwise. Useful for sales segmentation and tech-due-diligence triage.

**Can I trust the lead intelligence for prospecting?**
Treat it as a **signal**, not a fact. `estimatedCompanySize` and `likelyBudget` are coarse heuristics from observable web signals — the actual answer requires people-data enrichment. Use the actor's signal to rank a list, then verify the top tier.

**Why don't I see a `competitivePosition` block?**
Set `enableCompetitiveIntel: true` and run with at least 2 successful domains. Cohort analysis only fires when the batch is large enough to compute meaningful medians.

**What does `customDetectors` need at minimum?**
A `name` plus at least one of: `scriptPattern` (regex matched against `<script src>` URLs), `htmlPattern` (regex matched against the full HTML body), or `headerName` + `headerPattern` (regex matched against a specific response header value).

**What's in `deepSecurity.issues`?**
A flat list of plain-language issues found by the advanced probe — cookie flag warnings, admin path exposures, and any CRITICAL escalations (`/.env`, `/.git/HEAD`, `/wp-config.php` reachable).

**Will I be charged for failures?**
No. PPE charges fire only when `failureType === null`.

**What's in the run summary?**
A `SUMMARY` key in the actor's default KV store: preset, outputMode, totals, failure breakdown, fleet-average overall + security scores, alert counts by type, top 5 priority domains, total PPE charges.

**Which preset matches my workflow?**
Sales / lead-gen → `sales-prospecting`. Scheduled competitor monitoring → `competitor-tracking`. Security-team triage → `security-audit`. Agency / investor multi-domain audit → `portfolio-analysis`. Backwards-compatible v1 detection → `raw`. Each preset's defaults are listed in the *Quick start with presets* table above.

**What does "engineering maturity" actually measure?**
A coarse blend of `overall.scores.modernity` and the overall composite. Low = legacy stack signals (jQuery, AngularJS, EOL CMSs) and/or weak security posture. High = current frameworks (Next.js / Vercel / Tailwind / etc.) plus strong scores. Useful for sales segmentation and tech due diligence — treat it as a triage signal, not a verdict.

### Related actors

| Actor | What it does | How it complements this actor |
|-------|--------------|--------------------------------|
| [Website Contact Scraper](https://apify.com/ryanclinton/website-contact-scraper) | Extracts emails, phones, and contacts | Pair with `leadInsights` for sales-ready outreach lists |
| [B2B Lead Qualifier](https://apify.com/ryanclinton/b2b-lead-qualifier) | Scores and grades leads | This actor adds standalone tech depth + CVE + change intel |
| [B2B Lead Gen Suite](https://apify.com/ryanclinton/b2b-lead-gen-suite) | Full enrichment pipeline | Combine for complete profiles: contacts + scores + tech + intelligence |
| [Company Deep Research Agent](https://apify.com/ryanclinton/company-deep-research) | Comprehensive company intelligence | Pair stack intel with broader research |
| [WHOIS Domain Lookup](https://apify.com/ryanclinton/whois-domain-lookup) | Domain registration / ownership | Pair domain age and registrar with stack analysis |
| [SaaS Competitive Intelligence](https://apify.com/ryanclinton/saas-competitive-intel) | Competitive analysis for SaaS | Supplement with stack comparisons + change intel |

# Changelog

This Actor's version history is a separate document: https://apify.com/ryanclinton/website-tech-stack-detector/changelog.md

# Actor input Schema

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

List of websites or bare domains to analyze (e.g., https://shopify.com or stripe.com). Same domain entered multiple times is collapsed.

## `preset` (type: `string`):

One-click configuration for common workflows. Each preset sets sensible defaults for crawl depth, security depth, output mode, competitive intel, diff tracking, and alerts. Explicit fields below always override the preset.

## `outputMode` (type: `string`):

raw = legacy detection fields only (back-compat). enriched = full premium output (default). executive = collapsed decision-grade view: grade, priority score, summary, recommendations, change insights, lead intel, alerts.

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

Maximum pages crawled per domain. Inner pages may reveal additional technologies. Higher values find more, slower runs.

## `compareToPriorRun` (type: `boolean`):

Persist this run's tech list per domain and diff against the prior run. Output includes added/removed technologies, lastSeenAt, and a classified `changeInsights` block (CDN swap, CMS migration, framework migration, etc.). Set to false for one-off audits that should not affect the snapshot baseline.

## `securityDepth` (type: `string`):

basic = OWASP headers grade only (default). advanced = also probes Set-Cookie flags (Secure / HttpOnly / SameSite) and a small set of well-known admin / config paths (/.env, /.git/HEAD, /wp-admin, etc.) to flag exposed surfaces.

## `renderMode` (type: `string`):

auto (default) — Cheerio first, fall back to Playwright Chromium for SPA-detected domains so runtime-only technologies become visible. cheerio — never use Playwright (cheapest, fastest). headless — always render every domain in Chromium (most thorough, slowest, highest memory).

## `enableCompetitiveIntel` (type: `boolean`):

When the batch contains 2+ successful domains, compute a `competitivePosition` block per domain: most common stack in batch, overlap, outlier flag, behind/ahead vs cohort median, and dominant tech trend.

## `emitAlerts` (type: `boolean`):

Populate the `alerts[]` array on each record when conditions cross a threshold (new critical CVE, multiple high CVEs, failing security grade, new tech detected since prior run, exposed admin path).

## `emitNotification` (type: `boolean`):

Adds `notification: { slackMessage, emailSubject, emailSummary }` per domain. Drop-in for webhook routing.

## `emitBatchInsight` (type: `boolean`):

Adds a single `recordType: 'batch-insight'` record at the end of the dataset summarising the cohort: top recurring CVEs, most-adopted tech, highest-priority domains, dominant tech trend, change-type counts. Auto-skipped on single-domain runs.

## `compareDomains` (type: `array`):

Optional. Two domains from `urls` to head-to-head compare. Emits a single `recordType: 'comparison'` record with winner, dimensional breakdown, and confidence.

## `customDetectors` (type: `array`):

User-defined fingerprints — encode your own internal-tool detection by providing a name, category, and at least one matcher (script regex, html regex, or header). Matched custom detectors appear as first-class technologies in output.

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

Proxy settings. Apify Proxy enabled by default. Recommended for batches over ~20 sites or Cloudflare-protected origins.

## Actor input object example

```json
{
  "urls": [
    "https://apify.com",
    "https://shopify.com",
    "https://stripe.com"
  ],
  "outputMode": "enriched",
  "maxPagesPerDomain": 3,
  "compareToPriorRun": true,
  "securityDepth": "basic",
  "renderMode": "auto",
  "enableCompetitiveIntel": false,
  "emitAlerts": true,
  "emitNotification": true,
  "emitBatchInsight": true,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `results` (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://apify.com",
        "https://shopify.com",
        "https://stripe.com"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("ryanclinton/website-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 = {
    "urls": [
        "https://apify.com",
        "https://shopify.com",
        "https://stripe.com",
    ],
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("ryanclinton/website-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 '{
  "urls": [
    "https://apify.com",
    "https://shopify.com",
    "https://stripe.com"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call ryanclinton/website-tech-stack-detector --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,ryanclinton/website-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/mBdMOhOMY7XtwX99T/builds/QUYXhXF26PU87k8va/openapi.json
