# Tech Stack Lookup — CMS, Ecommerce & Analytics Detection (`accountable_eel/tech-stack-lookup`) Actor

Find out what a website is built on — CMS, ecommerce platform, JS framework, analytics, CDN, payments and more — from its own HTML and headers, no third-party API. Paste a list of domains, press Start. Charged only for sites where something is detected.

- **URL**: https://apify.com/accountable\_eel/tech-stack-lookup.md
- **Developed by:** [Adrian Voss](https://apify.com/accountable_eel) (community)
- **Categories:** Lead generation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 successful lookups

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

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Tech Stack Lookup — CMS, Ecommerce & Analytics Detection

You give this actor a list of websites. For each one, it fetches the live page, reads the HTML
and response headers, and tells you what the site is built on — CMS, ecommerce platform, JS
framework, analytics, tag manager, marketing tool, chat widget, CDN, host, payment processor, and
security service. No third-party lookup API and no stored database sit between you and the
answer: every check is a fresh fetch of the exact page you asked about, matched against a
hand-written signature list. The list covers 37 named technologies grouped into 11 categories —
`cms`, `ecommerce`, `framework`, `analytics`, `tag-manager`, `marketing`, `chat`, `cdn`, `hosting`,
`payments`, and `security` — and each row tells you exactly which of those matched, and why
(the `Server` header, the `X-Powered-By` header, or a generator meta tag, where present).

### Who it's for

The accountable\_eel catalogue sells company intelligence columns for outbound. Each actor takes a
list of domains or company identifiers and returns one flat, stably-named row per input —
firmographics, registry IDs, tech stack, email route, hiring activity — the shape a Clay table, an
n8n workflow, or an AI agent can consume without post-processing. Pricing is pay-per-event and
per-domain: a few tenths of a cent for a row that was actually found, and nothing for a
miss, so a list that doesn't enrich costs you next to nothing. Where an official source exists —
VIES, GLEIF, SEC EDGAR, Brønnøysund, PRH, RDAP — it is queried directly instead of scraped. No
seat licence, no monthly minimum, no credit system to decode. This one detects a site's technology
stack straight from its response headers and HTML — no separate lookup database, no DNS record
involved.

### Why this one

- **Reads the page's own signal, not a database of it.** Script tags, meta generator tags,
  JS-variable and CSS-class fingerprints, and response headers (`Server`, `X-Powered-By`,
  `CF-Ray`, `X-Vercel-Id`) — the same category of signal Wappalyzer and BuiltWith are built on,
  fetched fresh on every run instead of read from a stored snapshot.
- **37 signatures across 11 categories, kept deliberately small.** Every signature requires an
  asset path, a JS variable, or a CSS class — something only present if the page is actually
  built on that stack — not a loose brand-name text match. (stripe.com's own customer-logo
  carousel mentions "WooCommerce" in alt text; that alone doesn't trigger a match here.)
  High-precision over broad-but-noisy is the trade-off, and it's a deliberate one.
  See "vs. alternatives" below for what that trade-off costs you.
- **Never charged for a miss.** A site that blocks the request, doesn't match any signature, or
  isn't a valid domain/URL still gets a row explaining why — and costs nothing.
  See "Price" below for the found-row rate.
- **One row per site or one row per technology, your choice.** Turn on "One row per technology
  detected" in the Input tab and the same run produces one row per detected technology instead of
  one grouped row — you're still charged once per site either way.
- **No proxies to configure, no headless browser needed.** This is a plain HTTP fetch through
  Apify's default proxy; the page's raw HTML and headers are enough for every signature here.

### What you get

Every row starts with the same five fields, then adds the technology fields you selected in
"Which columns do you want?" (all included by default).

| Field | Type | Description |
|---|---|---|
| `query` | text | The website you submitted, echoed back |
| `found` | boolean | `true` if at least one technology was detected on this page |
| `status` | text | `OK` on a match; `NOT_FOUND`, `BLOCKED`, `BAD_FORMAT`, or `REQUEST_FAILED` on a miss |
| `message` | text | Plain-English reason for a miss — only present when `found` is `false` |
| `scrapedAt` | ISO 8601 datetime | When the page was fetched |
| `detectedCount` | number | How many technologies were detected on this page (default one-row-per-site output) |
| `detected` | array | Every technology detected, as `{"name": "...", "category": "..."}` objects (default one-row-per-site output) |
| `name` | text | One technology's name — populated when "One row per technology detected" is on, in place of `detected` |
| `category` | text | That technology's category: `cms`, `ecommerce`, `framework`, `analytics`, `tag-manager`, `marketing`, `chat`, `cdn`, `hosting`, `payments`, or `security` — populated alongside `name` |
| `server` | text | The page's `Server` response header, if present |
| `poweredBy` | text | The page's `X-Powered-By` response header, if present |
| `generatorMetaTag` | text | The content of `<meta name="generator">`, if the page has one |

Deselect any of the technology fields in the Input tab's column picker to drop them from every
row — `query`, `found`, `status`, `message`, and `scrapedAt` always stay.

### Price

$5 per 1,000 websites, plus a $0.005 start fee. Misses (`found:false`) are never charged.

1,000 domains through this actor: **~$5.62** if every one is found ($0.00562 per found row at the
FREE tier, plus a $0.00005 actor-start charge), less if some aren't and less again on a paid Apify
plan. The same 1,000 rows through a credit-based enrichment platform: **$80–$400**. A site where
nothing matches, or that blocks the request, is never billed — you only pay for a row that
actually names a technology.

### How to use

1. **In the Apify Console.** Open the actor page and click **Start** — the `websites` field is already pre-filled with a working example. Results land in the run's dataset as soon as each item is found.
2. **Via the API.** Call it directly with a POST request — no Console needed once you have an API token:
   ```bash
   curl "https://api.apify.com/v2/acts/accountable_eel~tech-stack-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \
     -X POST \
     -H "Content-Type: application/json" \
     -d '{"websites":["stripe.com","shopify.com"]}'
   ```
3. **On a schedule.** Save this actor as an Apify **Task** with the input you want, then add a **Schedule** (hourly, daily, weekly) so it runs on its own — no server of your own required.

1) Open the Input tab and paste your list of websites into "Websites to check" — one per line,
   as a bare domain (`stripe.com`) or a full URL (`https://stripe.com`).
2) Leave "Try it first" on for your first run — it caps the run at 5 sites so you can see the
   output shape before spending on the full list.
3) Decide whether you want one row per site (default — `detected` holds the full list) or one row
   per technology (turn on "One row per technology detected"); either way you're charged once per
   site.
4) Optionally narrow the "Which columns do you want?" list if you only need a subset of fields, or
   set "Only keep rows containing" / "Skip rows containing" to filter by keyword.
5) Turn "Try it first" off and press Start to run the full list.

Typical uses: qualifying a prospect list by platform before you pitch a platform-specific service
(who's on Shopify vs. WooCommerce vs. a custom build), checking what analytics, CDN, and payments
stack a competitor runs, or auditing a portfolio of sites you manage to confirm each one still runs
the stack you expect and catch any that quietly migrated.

### Input

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

One domain or URL per line. Accepted formats: stripe.com, https://stripe.com.

### Sample output

By default you get **one row per website**, with every technology detected listed together (turn
on "One row per technology detected" in the Input tab to get one row per technology instead):

| query | found | status | detectedCount | detected | name | category | server | poweredBy | generatorMetaTag | scrapedAt |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| https://stripe.com | true | OK | 1 | \[{"name":"Next.js","category":"framework"}] | <technology> | <category> | nginx |  |  | 2026-08-26T06:00:20.288Z |

A real row, one-row-per-site mode:

| query | found | status | detectedCount | detected |
| --- | --- | --- | --- | --- |
| shopify.com | true | OK | 3 | Shopify (ecommerce), React (framework), Cloudflare (cdn) |

A site nothing on the detection list matches gets a row with `found: false` and a `status`/
`message` explaining why, and is never charged.

### Use it from Clay, n8n, Make, or an AI agent

This actor runs synchronously over plain HTTP — call it directly from a script, a workflow tool, or an AI agent, no Apify Console needed once you have an API token.

```bash
curl "https://api.apify.com/v2/acts/accountable_eel~tech-stack-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"websites":["stripe.com","shopify.com"]}'
```

**n8n.** Add an HTTP Request node: Method `POST`, URL `https://api.apify.com/v2/acts/accountable_eel~tech-stack-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>`, Body Content Type `JSON`, JSON Body `{"websites":["stripe.com","shopify.com"]}` (swap in an expression from an earlier node for a real value).

**Clay.** Add an "HTTP API" column: Method `POST`, URL `https://api.apify.com/v2/acts/accountable_eel~tech-stack-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>`, Body `{"websites":["{{website}}"]}`, mapping the row's website into the `websites` array.

**MCP.** In Claude, Cursor, or any MCP client with the Apify MCP server, ask for "Tech Stack Lookup | Apify" — the agent will find and run this actor.

### Tips

- Run "Try it first" on a handful of domains you already know the stack for, to sanity-check the
  detection against sites you can verify by hand before you run a full list.
- If you only care about one category — say, ecommerce platform — narrow "Which columns do you
  want?" to `detectedCount` and `detected`, then filter on the category client-side; you're still
  charged the same per-site rate regardless of column count.
- A `BLOCKED` status means the site returned a 403, 429, or 503 to the request — retrying later or
  lowering "Max concurrency" sometimes gets through; a site that blocks automated traffic
  consistently will keep coming back `BLOCKED`.
- Turn on "One row per technology detected" when you're loading results into a table you plan to
  filter or group by category — it's easier to query `category = 'cms'` across many rows than to
  parse a nested `detected` array per site.
- Keep "Max concurrency" conservative on a large list. This target has no browser fallback, so a
  site that starts blocking you costs more in wasted requests than a slower, steadier crawl.

### vs. alternatives

| | What it costs | What you get | Trade-off |
|---|---|---|---|
| **This actor** (`tech-stack-lookup`) | $0.00562 per found row (FREE tier, less on paid tiers), $0.00005 actor start, nothing for a miss | One row per site (or per technology) — CMS, ecommerce platform, JS framework, analytics, marketing, chat, CDN, hosting, payments, and security tooling, read live from the page's own HTML and headers | 37 hand-picked signatures, chosen for precision over breadth. A stack that isn't on the list, or that's invisible in HTML/headers (server-side-only tooling), won't show up. |
| **BuiltWith** | From $295/month | Historic tech data going back years, plus lead lists filtered by technology | If you need "every site that added Shopify last quarter", that's a database question and BuiltWith answers it. This answers "what is this one domain running, right now". |
| **Clay** | $0.08–$0.40 per enriched row in credits, on top of a seat | A whole enrichment workspace — waterfalls across dozens of providers, plus the table and the sequencing around it | If you want one place that does everything and you're not counting rows, that's Clay. This is one column, priced per column, callable *from* Clay via its HTTP step. |
| Doing it yourself | Your time + maintaining a signature list, handling redirects and blocked responses, and re-testing signatures as sites change their bundlers | The same data | This actor's signature list, the miss-classification (blocked vs. not-found vs. bad-format), and the retry/concurrency handling are the maintenance burden it absorbs. |

Prices for third-party tools are their published list prices as of August 2026 and are not tracked
here — check the vendor before relying on the comparison.

### FAQ

**Why is a row empty, or why does `found` say `false`?**
Either the input doesn't look like a domain or URL (`status: BAD_FORMAT`), the site blocked the
request (`status: BLOCKED`, usually HTTP 403/429/503), the request failed after retries
(`status: REQUEST_FAILED`), or none of the technologies on the detection list matched
(`status: NOT_FOUND`). Check the `message` column for the specific reason. None of these are
billed.

**Am I charged for a miss?**
No. `Actor.charge()` only fires when at least one technology is detected on the page. A blocked
site, an unrecognized stack, or a malformed input all produce a row (unless you turn on "Hide rows
with no result") and none of them cost anything.

**Is this a live check or a stored database?**
Live. Every run fetches the current page and its current response headers — there's no cached
snapshot behind it. That also means a site that changed its stack since the last time you checked
will show the new one, and a site with intermittent blocking may return a different `status` on
different runs.

**Does this handle rate limits or blocking?**
Each request goes through Apify's proxy and retries up to twice on failure. If a site consistently
blocks automated requests, it'll come back `BLOCKED` regardless of retries — lowering "Max
concurrency" can help on sites that are rate-limiting rather than blocking outright, but a site
with strong bot-detection will stay `BLOCKED`.

**Do I need to configure proxies?**
No. The actor uses Apify's default proxy configuration out of the box; there's nothing to set up
unless you want to supply your own proxy group.

**Is this GDPR-relevant?**
No personal data is involved. This reads a website's own public HTML and HTTP response headers —
technical metadata about the site's infrastructure, not information about any individual.

**Does it see technologies added by JavaScript after the page loads?**
Only what's present in the initial HTML response — this doesn't run a headless browser or execute
client-side JavaScript. That covers most of what's on the detection list, since frameworks like
Next.js and Nuxt.js leave a fingerprint (`__NEXT_DATA__`, `__NUXT__`) in the HTML the server sends,
and most CMS, analytics, and marketing tags load via a `<script src>` tag that's also present in
that first response. A technology that's injected entirely by client-side code after the initial
render — with no trace in the server-rendered HTML or headers — won't be picked up.

**Can I schedule this to re-check the same list on a cadence?**
Yes — set up an Apify Task with a schedule (daily, weekly, whatever cadence fits) and point it at
this actor with a saved input. Each run is a fresh fetch, so a scheduled task is how you'd catch a
site migrating off one platform onto another.

**Can an AI agent call this directly?**
Yes. It's registered on the Apify MCP server — an agent in Claude, Cursor, or another MCP client
can find and run it by name ("Tech Stack Lookup | Apify"), or you can call the REST endpoint shown
above from any script or workflow tool.

### Related actors

- [Shopify Store Lookup](https://apify.com/accountable_eel/shopify-store-lookup) — once you know a
  site runs Shopify, pull its storefront details: theme, apps, currency, and more.
- [Company Domain Enrichment](https://apify.com/accountable_eel/company-domain-enrichment) —
  broader firmographic and technical enrichment across multiple sources per domain.
- [Domain RDAP Lookup](https://apify.com/accountable_eel/domain-rdap-lookup) — registration data
  (registrar, creation date, nameservers) for the same domain, from the official RDAP source.

# Actor input Schema

## `websites` (type: `array`):

One domain or URL per line. Accepted formats: stripe.com, https://stripe.com. You're only charged for the ones we actually find — a miss costs nothing.

## `testRun` (type: `boolean`):

Turn this on to test your input on a small sample before running the full list. Turn it off to process everything.

## `onlyFound` (type: `boolean`):

Only keep rows where something was actually found. Misses are always free, whether or not you show them here.

## `includeKeywords` (type: `array`):

Optional. Only keep results that mention at least one of these words (e.g. a job title, a city, a product name). Leave empty to keep everything.

## `excludeKeywords` (type: `array`):

Optional. Drop any result that mentions one of these words. Leave empty to skip nothing.

## `maxResults` (type: `integer`):

Optional. Stop the run once this many results have been found — useful for a quick, cheap sample. Leave blank for no limit.

## `columns` (type: `array`):

Choose which pieces of information to include in each result row. All are included by default.

## `expandRows` (type: `boolean`):

When on, each technology detected found gets its own row instead of being grouped under its website. You're still only charged once per website, no matter how many rows it produces.

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

Parallel requests. Keep conservative — this target has no browser fallback, so getting blocked costs more than slow-and-steady.

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

Apify Proxy config. Residential recommended for anti-bot-sensitive targets.

## Actor input object example

```json
{
  "websites": [
    "stripe.com",
    "shopify.com"
  ],
  "testRun": false,
  "onlyFound": false,
  "includeKeywords": [],
  "excludeKeywords": [],
  "columns": [
    "detectedCount",
    "detected",
    "name",
    "category",
    "server",
    "poweredBy",
    "generatorMetaTag"
  ],
  "expandRows": false,
  "maxConcurrency": 5,
  "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 = {
    "websites": [
        "stripe.com",
        "shopify.com"
    ],
    "includeKeywords": [],
    "excludeKeywords": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("accountable_eel/tech-stack-lookup").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 = {
    "websites": [
        "stripe.com",
        "shopify.com",
    ],
    "includeKeywords": [],
    "excludeKeywords": [],
}

# Run the Actor and wait for it to finish
run = client.actor("accountable_eel/tech-stack-lookup").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 '{
  "websites": [
    "stripe.com",
    "shopify.com"
  ],
  "includeKeywords": [],
  "excludeKeywords": []
}' |
apify call accountable_eel/tech-stack-lookup --silent --output-dataset

```

## MCP server setup

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

```

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/EoWLjauepGg2hBPpS/builds/TRvtqYcWpsdMQA9i3/openapi.json
