# Website Tech Stack Detector: BuiltWith & Wappalyzer Alternative (`groundtruth/tech-stack-detector`) Actor

Find out what a website runs on. Give it domains or URLs and get the CMS, ecommerce platform, analytics, CDN, server, email provider and 6,000+ other technologies, with the evidence for each and a link to the page it came from.

- **URL**: https://apify.com/groundtruth/tech-stack-detector.md
- **Developed by:** [Ground Truth](https://apify.com/groundtruth) (community)
- **Categories:** Developer tools, Lead generation, SEO tools
- **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.

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

Give it a list of domains or URLs. For each one it fetches the homepage once and tells you what the site runs on: CMS, ecommerce platform, analytics, tag manager, CDN, web server, hosting, frameworks, payment processors and the email provider, from a rule set of more than 6,700 technologies. Every record shows the evidence for each detection and links to the page it came from.

### Use cases

- **Sales and lead qualification.** Find every company in a list that runs Shopify, WordPress or a given analytics tool, then sort by what they use.
- **Competitive and market research.** See which platforms a set of competitor or customer sites are built on.
- **Agency and migration prospecting.** Spot outdated versions, sites without a CDN, or stores on a platform you replace.
- **Data enrichment.** Add a technology column to a spreadsheet of domains. Flat fields (`cms`, `ecommerce`, `analytics`, `cdn`, `email_provider` and others) drop straight into a CSV.
- **AI agents.** An agent that is asked "what does this company use?" can call this with a domain and get a structured, checkable answer in about a second, with no browser to drive.

### What it does not do

- It does not run a browser or JavaScript. Technologies that can only be seen by executing JavaScript in a page (for example a library found only as a global variable) are not detected, and a site that builds its whole page with JavaScript shows only what is in its response headers, its static markup and its DNS.
- It reads one page per domain, the one you give it. It does not crawl, log in, or get around bot protection. If a site answers with an error or a challenge page, that response is analyzed as it is, `fetch_status` says `http_error`, and the result may be incomplete.
- It does not collect or output emails, phone numbers, names or any page text. Page content is read only to match technology signatures and is never copied into the output.
- Detection is evidence-based, not certain. A confidence score is given for each technology and the evidence type is listed so you can judge it. A technology that does not leave a trace in the page, headers or DNS cannot be seen.
- DNS TXT records show that a domain verified itself with a service (for example a mail or collaboration tool). Those appear in `technologies` with `evidence: ["dns:TXT"]` and are kept out of the flat fields.

### Sample output

Input:

```json
{ "domains": ["nginx.org"] }
```

One record (shortened to two technologies):

```json
{
  "input": "nginx.org",
  "domain": "nginx.org",
  "final_url": "https://nginx.org/",
  "status": 200,
  "fetch_status": "ok",
  "fetch_note": "",
  "technologies": [
    { "name": "Nginx", "categories": ["Web servers", "Reverse proxies"], "version": "1.31.3",
      "confidence": 100, "evidence": ["headers"], "website": "https://nginx.org/en" },
    { "name": "HSTS", "categories": ["Security"], "version": null,
      "confidence": 100, "evidence": ["headers"], "website": "https://www.rfc-editor.org/rfc/rfc6797#section-6.1" }
  ],
  "technology_count": 2,
  "cms": [], "ecommerce": [], "analytics": [], "tag_managers": [], "cdn": [],
  "web_server": ["Nginx"], "hosting": [], "email_provider": [],
  "javascript_frameworks": [], "web_frameworks": [], "programming_languages": [],
  "payment_processors": [], "mx_hosts": [],
  "fetched_at": "2026-10-18T09:00:00Z",
  "verify_url": "https://nginx.org/",
  "fingerprints": "692e0ec"
}
```

**How to check it yourself.** Open the `verify_url`, or run `curl -I https://nginx.org/` and read the `Server` header: it prints `nginx` and a version number, which is the evidence behind the `Nginx` entry and its version. The nginx documentation says nginx puts its version in that header. For a Shopify store, view the page source and search for `cdn.shopify.com`. For email, look up the domain's MX records with any DNS tool and compare to `mx_hosts`.

**A site that cannot be reached** returns a clean record and is not charged:

```json
{ "input": "nonexistent-domain-abc123xyz987.com", "domain": "nonexistent-domain-abc123xyz987.com",
  "final_url": null, "status": null, "fetch_status": "unreachable",
  "fetch_note": "the domain name does not resolve", "technologies": [], "technology_count": 0,
  "verify_url": "https://nonexistent-domain-abc123xyz987.com/" }
```

(The real record also carries the empty flat fields shown above.)

### Configuration

| Input | Type | Required | Default | Description |
|---|---|---|---|---|
| `domains` | list of text | yes | `["wordpress.org"]` | Domains or page URLs. A bare domain is fetched over https, and over http if https is not available. Up to 1000 per run. Repeats are fetched once. |
| `minConfidence` | integer 0 to 100 | no | 50 | Only report technologies at or above this confidence. Weak single signals score under 50. Use 0 to see everything. |
| `includeDns` | true or false | no | true | Look up MX, TXT and NS records. This is how the email provider is found. Turn off for the fastest run. |

Only the default web ports (80 and 443) are fetched. IP addresses, internal hostnames, URLs with a username or password, and non-web schemes are refused with a message, and are not charged.

#### Output fields

| Field | Meaning |
|---|---|
| `input`, `domain` | What you gave, and the domain in ASCII form with a leading `www.` removed |
| `final_url`, `status` | The page that answered after redirects, and its HTTP status (`null` if nothing answered) |
| `fetch_status` | `ok`, `http_error` (the site answered with an error status, analyzed anyway), `not_html`, or `unreachable` |
| `fetch_note` | Plain-language note when something limited the result |
| `technologies` | List of `name`, `categories`, `version` (if found), `confidence` (0 to 100), `evidence` (`url`, `html`, `text`, `css`, `scripts`, `scriptSrc`, `meta`, `headers`, `cookies`, `dom`, `dns:MX`, `dns:TXT`, `dns:NS`, `implied`) and `website` |
| `technology_count` | Length of `technologies` |
| `cms`, `ecommerce`, `analytics`, `tag_managers`, `cdn`, `web_server`, `hosting`, `email_provider`, `javascript_frameworks`, `web_frameworks`, `programming_languages`, `payment_processors` | Flat lists of technology names, for spreadsheets. `email_provider` comes from MX records only |
| `mx_hosts` | Up to 5 mail server hostnames, lowest preference first |
| `fetched_at` | UTC time of the fetch |
| `verify_url` | The page that was fetched, or attempted if it failed |
| `fingerprints` | Short id of the rule-set version used |

#### What can go wrong

| `fetch_status` or message | What it means | What to do |
|---|---|---|
| `unreachable`, "the domain name does not resolve" | The name has no address | Check the spelling |
| `unreachable`, "did not answer within 15 seconds" | The site is down or blocking | Try again later |
| `http_error` with status 403 or 429 | The site refused the request or showed a bot check | Results come from headers and DNS only. Do not rely on a short list |
| `not_html` | The address is a file, not a page | Give the page URL |
| "certificate could not be verified" in `fetch_note` | The page was read anyway; its TLS certificate is invalid | Treat the site as misconfigured |
| Entry skipped in the run log | The entry was not a valid public domain or URL | Fix it. It is not charged |

### Pricing

Pay per event: **$0.0015 per domain analyzed**, about $1.50 per 1,000 domains, plus Apify's small start fee per run. A domain is charged only if the site answered over HTTP, including an error page that was still analyzed. Unreachable sites, duplicates and invalid entries are free.

### Where the rules come from

Detection uses the community-maintained Wappalyzer rule set, [enthec/webappanalyzer](https://github.com/enthec/webappanalyzer), licensed GPL-3.0, vendored at commit `692e0ec` (2026-10-16), with a few documented additions. See `SOURCES.md` for the license, the list of what is and is not included, and where each test value came from.

# Actor input Schema

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

One or more domains or page URLs, for example wordpress.org or https://www.example.com/pricing. A bare domain is fetched over https (and over http if https is not available). Up to 1000 per run. Repeats are fetched once.

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

Only report technologies detected with at least this confidence (0 to 100). Weak single signals score below 50, so 50 filters them out. Use 0 to see everything.

## `includeDns` (type: `boolean`):

Look up MX, TXT and NS records for each domain. This is how the email provider is found, and it also reveals services the domain has verified. Turn off for the fastest run.

## Actor input object example

```json
{
  "domains": [
    "wordpress.org",
    "https://www.allbirds.com/"
  ],
  "minConfidence": 50,
  "includeDns": true
}
```

# Actor output Schema

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

No description

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

No description

## `runDetails` (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 = {
    "domains": [
        "wordpress.org"
    ]
};

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

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

```

## MCP server setup

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