# Website Tech Stack & Hosting Detector (`everyotherfriday/tech-stack-detector`) Actor

Detect CMS, frameworks, analytics, ecommerce tools, CDN/hosting signals and email providers from public website and DNS evidence. Batch up to 1,000 domains and export structured findings with confidence and evidence. Uses 211 rules; no JavaScript execution.

- **URL**: https://apify.com/everyotherfriday/tech-stack-detector.md
- **Developed by:** [Paul Vasquez](https://apify.com/everyotherfriday) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$10.00 / 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.

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 & Hosting Detector

### what it does

Turn a list of public websites into structured technology observations for
prospecting, audits, or market research. Supply up to 1,000 domain names or
HTTP(S) URLs. Bare domains use HTTPS; supplied paths are preserved. The actor
follows redirects, captures the final URL and HTTP status, and inspects server
headers, generator metadata, script URLs, stylesheet links, cookie names, and
HTML markers. It does not save cookie values or execute JavaScript.

The vendored, original MIT-licensed fingerprint set covers CMS products,
frameworks, analytics, tag managers, CDNs, hosting, commerce, payments, chat,
email providers, and marketing tools (211 original product/category rules). Optional network enrichment uses Google DNS-over-HTTPS
for MX and Team Cymru ASN DNS records. The `networkName` field is kept for compatibility and is always null.
Failures in optional enrichment are warnings. An ASN identifies the visible
edge network; it does not establish where a site's hidden origin is hosted.

The default input contains Wikipedia, Python.org, and WordPress.org, with
homepage-only mode enabled. Disabling that mode fetches robots.txt and at most
one same-origin internal page permitted for TechStackDetector. Robots failures
other than 404/410 prevent the internal fetch. Redirect targets are checked
against the same policy. Concurrency is fixed at ten domains.

### Use cases

Find CMS migration prospects, identify payment integrations, segment leads
by commerce platform, or compare public infrastructure signals across a list
of sites. Each submitted entry produces one dataset item, including failures,
so downstream workflows can join results back to inputs. Duplicate inputs
remain separate entries. Arrays summarize categories while `technologies`
retains the evidence source and confidence for each finding. Unknown
technologies produce empty arrays rather than invented results.

- Sales development teams can group prospect domains by observed CMS before assigning migration outreach.
- Web agencies can screen client homepages for visible analytics and tag-manager integrations before an audit.
- Partnership teams can identify commerce-platform signals when building an integration prospect list.
- Infrastructure teams can compare visible CDN and edge-network observations across a managed domain inventory.

### sample output

Illustrative abbreviated output, not a measurement of the example domain:

```json
{
  "input": "example.org",
  "domain": "example.org",
  "finalUrl": "https://example.org/",
  "statusCode": 200,
  "success": true,
  "server": "cloudflare",
  "cms": ["WordPress"],
  "cdn": ["Cloudflare"],
  "technologies": [
    {"name": "WordPress", "category": "cms", "confidence": 100,
     "evidence": ["meta.generator"]}
  ],
  "confidence": 100,
  "warnings": [],
  "error": null
}
```

Confidence ranges from zero to 100 and expresses rule strength, not measured
probability. The top-level value is the maximum finding confidence, not a
statement that the entire stack was identified. HTTP error pages can still
contain useful CDN evidence; inspect `success` and `statusCode` first.

### pricing

The proposed price is **$0.01 per submitted domain entry**, including failed
lookups. Three defaults cost $0.03 at this event price; 1,000 entries cost $10.
The runner calls `Actor.charge(event_name='domain-analyzed', count=1)` once
after storing each item. Local runs do not collect payment. The declaration
is in `.actor/pay_per_event.json`; it is publication configuration, not a
claim that the CLI automatically deploys pricing. Before publishing, configure
this event in Console and remove synthetic dataset/start charges to preserve
the advertised single-event price. No account, token, login, push, or billing
configuration was performed for this local build.

### FAQ

**How do I run it?** From this folder in PowerShell, use the existing
Python environment (no Apify CLI is required):

```powershell
$env:APIFY_LOCAL_STORAGE_DIR = Join-Path $PWD 'storage/local-run'
New-Item -ItemType Directory -Force "$env:APIFY_LOCAL_STORAGE_DIR/key_value_stores/default" | Out-Null
Copy-Item INPUT.json "$env:APIFY_LOCAL_STORAGE_DIR/key_value_stores/default/INPUT.json"
.\.venv\Scripts\python.exe -m src
.\.venv\Scripts\python.exe -m unittest discover -s tests -v
```

On a fresh checkout only, first create the environment with `python -m venv .venv`
and install dependencies with `.\.venv\Scripts\python.exe -m pip install -r requirements.txt`.
Use a fresh storage directory when you want results separated from earlier runs.
The SDK reads the local key-value-store INPUT, so copying the requested JSON matters.

Apify stores local dataset output under `$env:APIFY_LOCAL_STORAGE_DIR/datasets/default/`. The
Python module is `src`. Input defaults also exist in code so an absent local
input still uses the three public sites. `.actor/input_schema.json` provides
Store prefill. `timeoutSeconds` is the per-request socket timeout, from one to
60 seconds; DNS resolution and total domain work can take longer.

**Does one failure stop the run?** Website and enrichment failures become
result errors or warnings. Invalid top-level input and SDK storage/charging
failures still fail the run, so infrastructure problems are not disguised as
website failures. Dataset storage and charging are sequential, not a single
transaction; forced termination between them requires reconciliation.

**Where do the fingerprints come from?** `vendor/PROVENANCE.md` describes the
original collection and license. It contains 211 independently authored product/category rules,
not the full Wappalyzer database. Patterns and their confidence values live in
each rule's `signals` list. A product can appear in multiple relevant categories
(for example, Shopify in CMS and commerce). New signals can be added with regression fixtures.

### limitations

No headless browser, login, proxy rotation, CAPTCHA solving, or JavaScript
execution is included. Consent-gated and dynamically injected tags may be
missed. Cookie matching only sees response Set-Cookie names. Sites can spoof
headers and metadata. Responses are capped at two MiB. MX reflects the final
website hostname, which can have no mail records even when its parent does.
Free enrichment endpoints may throttle or fail; ASN data may lag
DNS lookups are not cached.
Public-address checks are a basic guard, not DNS-rebinding isolation.

Local validation status is documented in `VALIDATION.md`. The default sites
were successfully checked locally with ten additional sites on 2026-09-26.
This is not a hosted Store test or validation of production billing. Network
availability and website behavior cannot be guaranteed by prefilled input.

### Example output

One real dataset row from a platform run on 2026-09-26, trimmed by omitting fields without changing retained values:

```json
{
  "input": "https://www.wordpress.org",
  "finalUrl": "https://wordpress.org/",
  "statusCode": 200,
  "success": true,
  "server": "nginx",
  "metaGenerators": ["WordPress 7.2-alpha-63914"],
  "technologies": [
    {"name": "WordPress", "category": "cms", "confidence": 100, "evidence": ["html", "meta.generator"]},
    {"name": "Automattic (edge network)", "category": "hosting", "confidence": 85, "evidence": ["asn"]},
    {"name": "Google Tag Manager", "category": "tagManagers", "confidence": 95, "evidence": ["html"]}
  ],
  "cms": ["WordPress"],
  "tagManagers": ["Google Tag Manager"],
  "hosting": ["Automattic (edge network)"],
  "network": {"mx": ["smtp1-dca.wordpress.org", "smtp2-dca.wordpress.org"], "asn": 2635, "asnName": "AUTOMATTIC - Automattic, Inc, US"},
  "confidence": 100,
  "error": null
}
```

Every detection lists the evidence it came from. A site that matches no rule returns empty arrays, which means "nothing recognised", not "uses nothing".

**Pricing example:** 1,000 submitted domain entries, including failures x $0.01 per `domain-analyzed` event = **$10.00 in event fees**, using `.actor/pay_per_event.json`. Local runs do not bill.

# Actor input Schema

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

One public domain or HTTP(S) URL per entry; maximum 1,000.

## `homepageOnly` (type: `boolean`):

Disable to also fetch robots.txt and up to one allowed internal page. Supplied URL paths are preserved.

## `timeoutSeconds` (type: `integer`):

Socket timeout for each HTTP request; not a whole-domain deadline.

## `lookupNetwork` (type: `boolean`):

Use free Google DNS-over-HTTPS and Team Cymru ASN DNS.

## Actor input object example

```json
{
  "domains": [
    "https://www.wikipedia.org",
    "https://www.python.org",
    "https://www.wordpress.org"
  ],
  "homepageOnly": true,
  "timeoutSeconds": 15,
  "lookupNetwork": true
}
```

# Actor output Schema

## `results` (type: `string`):

All dataset items as JSON.

## `resultsCsv` (type: `string`):

All dataset items as CSV.

# 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": [
        "https://www.wikipedia.org",
        "https://www.python.org",
        "https://www.wordpress.org"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("everyotherfriday/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": [
        "https://www.wikipedia.org",
        "https://www.python.org",
        "https://www.wordpress.org",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("everyotherfriday/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": [
    "https://www.wikipedia.org",
    "https://www.python.org",
    "https://www.wordpress.org"
  ]
}' |
apify call everyotherfriday/tech-stack-detector --silent --output-dataset

```

## MCP server setup

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