# Website Tech Stack (`openrows/website-tech-stack`) Actor

Detect the CMS, frameworks, analytics, CDN and server behind any URL list from HTML and response headers alone — no browser, no per-domain markup.

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

## Pricing

$3.00 / 1,000 analysed urls

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

Detect the CMS, JavaScript/web frameworks, analytics, CDN and web server behind any list of
URLs — a fast, honest technology-stack detector for growth, sales-ops, SEO and competitive
research.

Give it a list of URLs and it delivers a clean dataset (JSON, CSV, Excel, or through the API)
of the technologies each page is built on. HTTP-only, no browser: one plain request per URL, so
runs are fast and cheap.

**Site-agnostic.** You supply the URLs; this Actor only fetches what you point it at. See
"Data and compliance" below.

### What data you get

For every URL the Actor returns:

| Field          | Description                                                                        |
| -------------- | ---------------------------------------------------------------------------------- |
| `url`          | The URL that was analysed                                                          |
| `scrapedAt`    | ISO 8601 timestamp the result was written to the dataset                           |
| `fetchedAt`    | ISO 8601 timestamp the page was fetched                                            |
| `server`       | Detected web server / reverse proxy, or the raw `Server` header if nothing matched |
| `cms`          | Detected CMS technologies (array)                                                  |
| `frameworks`   | Detected JS / web / UI frameworks (array)                                          |
| `analytics`    | Detected analytics and tag-manager technologies (array)                            |
| `cdn`          | Detected CDN technologies (array)                                                  |
| `technologies` | Every detected technology; shape depends on input (see below)                      |
| `httpHeaders`  | A small, fixed subset of the page's own response headers (see below)               |

`technologies[]` is, by default, `{ name, category, version, confidence }` per technology. With
`compactOutput: true` it drops to `{ name, category }`. With `categoriesOnly: true` it drops
further, to a flat list of the distinct category names detected (no technology names, no
versions) — the smallest, cheapest shape.

`httpHeaders` is exactly these response header names, when present on the page: `server`,
`x-powered-by`, `via`, `x-generator`, `x-drupal-cache`, `x-varnish`, `x-cache`, `cf-ray`,
`cf-cache-status`, `x-shopify-stage`, `x-aspnet-version`, `x-aspnetmvc-version`,
`x-turbo-charged-by`, `x-sourcemap` — never the full header set, never `Set-Cookie`.

### Input

| Option               | Type             | Default          | Description                                                           |
| -------------------- | ---------------- | ---------------- | --------------------------------------------------------------------- |
| `urls`               | array of strings | —                | Pages to analyse, one per line. One result per URL. Required.         |
| `maxItems`           | integer          | `100`            | Stop after this many results (max 10,000). Also your cost cap.        |
| `categoriesOnly`     | boolean          | `false`          | Return only the distinct technology categories per URL, nothing more. |
| `compactOutput`      | boolean          | `false`          | Drop `httpHeaders` and per-technology version/confidence detail.      |
| `proxyConfiguration` | object           | Apify datacenter | Proxy settings                                                        |

`urls` takes plain URL strings, one per line in the Console editor (or a plain array of strings
through the API) — not the "Load URLs from a file" bulk-import mode some other Actors offer.

Example input:

```json
{
    "urls": ["https://docs.apify.com/", "https://example.com/"],
    "maxItems": 50
}
```

### Output example

This is a real result from this Actor's own run against `https://wordpress.org/` (not invented):
no Cloudflare and no jQuery front that page, and it carries Google Tag Manager, not Google
Analytics.

```json
{
    "url": "https://wordpress.org/",
    "scrapedAt": "2026-09-22T20:40:51.597Z",
    "fetchedAt": "2026-09-22T20:40:51.597Z",
    "server": "Nginx",
    "cms": ["WordPress"],
    "frameworks": [],
    "analytics": [],
    "cdn": [],
    "technologies": [
        { "name": "WordPress", "category": "CMS", "version": "7.2", "confidence": 100 },
        { "name": "Google Tag Manager", "category": "Tag managers", "version": null, "confidence": 100 },
        { "name": "Nginx", "category": "Web servers", "version": null, "confidence": 100 }
    ],
    "httpHeaders": { "server": "nginx" }
}
```

(Google Tag Manager's own category is "Tag managers", not "Analytics" — it does not appear in the
`analytics` summary field, only in `technologies[]`.)

A run summary (items pushed, items skipped by category — including `robotsDisallowed` and
`robotsUnknown` — pages fetched, failed requests by category, stop reason, and a `robots` block
with the per-host outcomes — see "How robots.txt is handled" below) is stored in the run's
key-value store under the key `OUTPUT`.

### Pricing

**Pay per result.** You are charged per result and nothing else: no platform usage, no compute
units. Retries and failed requests are free. The current rate is on the *Pricing* tab of this
Actor — that is the only place it is set, so no figure is repeated here.

- `maxItems` caps the number of results, and therefore the cost, of a run.
- The run also stops when it reaches the *maximum total charge* you set for the run in Apify
  Console or through the API.

### Tips

- Start with `maxItems: 20` to check the fields before a large run.
- Use `categoriesOnly: true` when you only need to know *whether* a site runs a CMS, uses a CDN,
  etc. — not which one or which version. It is the cheapest way to read the output, though the
  price per result is the same; it only shrinks the payload.
- A handful of well-known frameworks (Angular 2+, Svelte, Ember.js, Adobe Analytics, and a few
  others — see Limitations) are not detectable from HTML and headers alone and are not in this
  Actor's fingerprint set.

### Limitations

- Detection is HTTP-only: one page load, no headless browser. Technologies whose only reliable
  signal is a DOM property, a JavaScript global, or a live probe request are not detected. The
  vendored fingerprint set (72 technologies — see below) was filtered down to exactly the
  technologies detectable this way; a small number of well-known frameworks that are *only*
  detectable via the DOM/JS (Angular 2+, Svelte, Ember.js, Material UI, Adobe Analytics,
  Cloudflare Turnstile, Remix, Bulma) are out of scope for this reason, not an oversight.
- Linked CSS/JS assets are matched by **filename/path only** (as referenced in the page's own
  HTML) — their contents are never fetched. A technology whose only signature lives inside a
  script's body (not its filename, the page's HTML, headers, meta tags or cookie names) will not
  be detected.
- Single-page apps that render most of their markup client-side may show fewer signals than a
  server-rendered page, because only the HTML delivered on the initial GET is inspected.
- Pages behind a login are not accessible and are not scraped.

**Source caveats.** The fingerprint database is a curated subset (72 of several thousand
upstream entries) of a community-maintained, open technology-detection database — coverage is
necessarily narrower than a full commercial tool's, and a technology's absence from the output
does not always mean the site doesn't use it (see Limitations above). See "Third-party data"
below for the source and its license.

### Data and compliance

This Actor is **site-agnostic**: you choose the URLs, and you are responsible for the sites you
point it at. Within that:

- It fetches only the URLs you give it — nothing is discovered, crawled or followed beyond them.
- It identifies itself honestly with an `openrows/website-tech-stack` User-Agent and sends no
  headers that imitate a browser (no `sec-ch-*`, no `sec-fetch-*`, no Chrome fingerprint).
- It does not bypass access controls, solve CAPTCHAs, or retry past a block (401/403/429 is
  recorded and the request is abandoned, not retried under a different identity).
- It collects **no personal data**. The output is technology names, versions, categories and a
  small fixed subset of response headers (server/cache/CDN signals only). Cookie *names* are
  used as a detection signal (e.g. `_ga` implies Google Analytics); cookie **values** are never
  read or stored, and no `Set-Cookie` value ever reaches the output.

#### How robots.txt is handled

Before the first page on a host is fetched, the Actor fetches that host's `robots.txt` once and
obeys it for the rest of the run. Rules are read for the product token `openrows`, falling back
to the `*` group when the file does not name us, with `*` and `$` wildcards and the standard
"longest matching rule wins" precedence. A `Crawl-delay` set for us is honoured, up to 10 seconds.
When a page redirects, the URL the redirect actually leads to is checked again against its own
host's rules before anything is read from it, so a redirect cannot carry the Actor onto a site,
or a path, that its owner puts off limits.

- **Disallowed URLs are never requested.** They are reported under `skipped.robotsDisallowed` in
  the run summary, so you can see exactly how many of your input URLs the site puts off limits.
- **No `robots.txt` (404 or 410) means no rules**, and the host is crawled normally. It is counted
  as `robotsAbsent` in the summary.
- **A `robots.txt` we cannot read means the host is skipped, not crawled.** Any other response —
  401, 403, 429, a 5xx, a timeout, a network failure, or a 200 that turns out to be an error or
  login page — leaves us without the site's rules, and this Actor will not guess. Every URL on
  that host is skipped and counted under `skipped.robotsUnknown`, and the host and the status
  that stopped us are listed under `robots.unknownHosts` in the summary. If a host you supplied
  returns no results, look there first.

**Removal requests.** If you believe this Actor's output is wrong or exposes something it
should not, open an issue on the Actor's *Issues* tab and state the URL(s) concerned.

You are responsible for using this Actor, and the pages it fetches on your behalf, in line with
the terms of each site you point it at and the laws that apply to you.

#### Third-party data: the fingerprint database

Technology detection is powered by a curated subset of the **WebAppAnalyzer** technology
fingerprint database (<https://github.com/enthec/webappanalyzer>), the community-maintained
continuation of the original Wappalyzer `technologies.json` (Wappalyzer's client went
closed-source in August 2023; the fingerprint data was continued as an open project). It is
used here under its **GPL-3.0** license, vendored (never fetched from a third party at run
time) with full attribution and the license text included in this Actor's source. Full
provenance, license reasoning and the exact field subset kept: see `THIRD_PARTY_NOTICES.md` and
`FINGERPRINTS_LICENSE.txt` in this Actor's source.

### Licence

This Actor's own source code is available under the **GNU General Public License v3.0
(GPL-3.0)** — see the `LICENSE` file in this Actor's source for the full text. It is licensed
this way because it vendors fingerprint data from the **WebAppAnalyzer** project
(<https://github.com/enthec/webappanalyzer>), which is itself GPL-3.0-licensed; see "Third-party
data" above and `THIRD_PARTY_NOTICES.md` for the full provenance and license reasoning.

### Support

Report bugs and request fields on the *Issues* tab of this Actor. Include the run ID.

# Actor input Schema

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

Pages to analyse, one per line, one result per URL. Each URL is fetched once (HTML + response headers); nothing else is crawled or followed. Paste URLs directly — bulk "Load URLs from a file" import is not supported.

## `maxItems` (type: `integer`):

Stop after this many results. You are charged per result, so this is also your cost cap. Maximum 10,000 per run; split a bigger job across runs.

## `categoriesOnly` (type: `boolean`):

Return only the distinct technology categories detected per URL (e.g. "CMS", "Analytics"), not individual technology names, versions or confidence scores. Cheapest, smallest output for buyers who only need the shape of the stack.

## `compactOutput` (type: `boolean`):

Drop the httpHeaders field and the per-technology version/confidence detail (name + category only). Ignored when "Categories only" is on.

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

Proxy settings. Apify datacenter proxies are the default and are enough for most sites.

## `maxRequestsPerCrawl` (type: `integer`):

Hard cap on pages fetched, including ones a site's robots.txt turns away (those spend a request but produce no result). Each input URL is fetched at most once, so this defaults to the larger of Max results and the number of URLs given, which leaves room for every good URL even when some others in the list are robots-disallowed.

## Actor input object example

```json
{
  "urls": [
    "https://docs.apify.com/",
    "https://example.com/"
  ],
  "maxItems": 100,
  "categoriesOnly": false,
  "compactOutput": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

No description

## `summary` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

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

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "urls": [
        "https://docs.apify.com/",
        "https://example.com/"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("openrows/website-tech-stack").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://docs.apify.com/",
        "https://example.com/",
    ],
    "proxyConfiguration": { "useApifyProxy": True },
}

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

```

## MCP server setup

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

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/ey9qZlwvhS6NemhRs/builds/4TwMFG6QjhV2XgmMM/openapi.json
