# Tech Stack, Email Provider & SaaS Detector by Domain (`lakadev/website-tech-stack-detector`) Actor

Use when you have company domains and need their email provider, SaaS tools and tech stack (CMS, analytics, chat, payments). Reads DNS plus raw home page HTML and public scripts, no browser; robots.txt respected; evidence per finding. $0.002/domain, $0.005 with page.

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

## Pricing

from $2.00 / 1,000 domain analyzed (dns)s

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

## Tech Stack, Email Provider & SaaS Detector by Domain

Give it company domains. For each one it returns, **with the evidence for every finding**:

- the **email provider** (Google Workspace, Microsoft 365, Zoho Mail, OVHcloud Mail...), read from MX records;
- the **SaaS tools the domain declares in DNS**: services allowed to send its e-mail (SPF), verification tokens (Atlassian, HubSpot, DocuSign, OpenAI...), CNAME records of a few well-known subdomains;
- the **website technologies** found in the raw HTML of the home page, in up to 3 scripts served by the site itself and in its public Google Tag Manager container: CMS (WordPress, Shopify, Webflow...), analytics and tag managers, chat widgets (Zendesk, Intercom...), payment tools (Stripe, PayPal...), consent tools, frameworks, web server. Each one says where it was found (`source`);
- the **DNS provider**, the **hosting platform** and the **CDN** when DNS reveals them;
- the **SPF** and **DMARC** policies.

One action, one row per domain, structured JSON. DNS first: the e-mail provider, SPF/DMARC and SaaS proofs that page scanners do not give. It reads public DNS records and the public home page (after checking `robots.txt`), with the User-Agent `LakadevStackBot/1.0 (+https://apify.com/lakadev/website-tech-stack-detector)`. No browser, no JavaScript executed, no API key, no login, no proxy.

### When to use it

- **Qualify B2B prospects.** From a list of domains, find who runs WordPress or Shopify, loads HubSpot or Intercom, or sends mail through Salesforce or SendGrid, then keep the ones that fit your offer. Stripe is rarely visible on a home page (it loads at checkout), so a Stripe finding mostly comes from DNS and means an account, not live payments.
- **Find the email provider of a domain.** Google Workspace, Microsoft 365, Zoho, OVHcloud, Proton, a security gateway such as Mimecast or Proofpoint: the answer comes from MX records, with the MX host as proof.
- **Spot the customers of a SaaS competitor.** Domains that declare Zendesk, HubSpot, Atlassian or Salesforce in their DNS, or whose home page names their chat widget, are likely users or past users. A DNS verification token proves the domain has an account with the service, not that it uses it today.
- **Audit e-mail posture** at scale: SPF final mechanism (`~all`, `-all`), DMARC policy (`none`, `quarantine`, `reject`).
- **Feed an AI agent** that needs a structured answer about a company's stack instead of reading raw HTML.

It is not a replacement for a full technology database: it knows 54 website technologies and 34 DNS services (lists below).

**How to read the results:** an absence proves nothing. "No chat widget found" means none appears in the HTML, the scripts read or the GTM container, not that the site has none (tools loaded by the site's own JavaScript, or after consent, are invisible). If `page.status` is not `fetched`, an empty `technologies` means "not read", not "none".

### Price

Pay per event, no platform usage billed on top:

| Event | Price | When |
|---|---|---|
| Domain analyzed (DNS) | **$0.002** | the domain exists and its DNS was analyzed (status `ok`) |
| Home page analyzed | **$0.003** more | the home page was fetched and analyzed (`page.status = "fetched"`); covers its scripts and GTM container |
| Actor start | $0.00005 | once per run (Apify's standard start event) |

Besides the tiny start fee, a full row costs $0.005; a DNS-only row (`fetchHomepage: false`) costs $0.002. **Nothing is charged** for invalid items, unknown domains (`not_found`), lookup errors, or for the page when it is `robots_disallowed`, `blocked`, `http_error` or `unreachable`. If your maximum cost per run is reached, the run stops cleanly at the first row it can no longer pay for.

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `domains` | array of strings (1 to 1,000) | required | Domains (`apify.com`), URLs (`https://www.shopify.com/fr`) or e-mail addresses (`name@wordpress.org`). The domain is extracted, `www.` is removed, duplicates are merged. |
| `includeSubdomainWww` | boolean | `true` | Also look up `www.<domain>` in DNS to detect hosting and CDN. |
| `fetchHomepage` | boolean | `true` | Read the home page and its scripts (robots.txt first) to detect website technologies. `false`: no HTTP request to the site, DNS only. |

```json
{
  "domains": ["apify.com", "https://www.shopify.com/fr", "someone@wordpress.org", "zendesk.com"],
  "includeSubdomainWww": true,
  "fetchHomepage": true
}
```

Domains are normalized with IDNA 2008 (UTS 46): `Bücher.de` becomes `xn--bcher-kva.de`. Subdomains are analyzed as given.

### Output

One row per domain, in input order (after merging duplicates). Real run of 2026-10-01 on `webflow.com`, shortened (`saas` had 14 items and keeps 5). Three technologies come from the Google Tag Manager container (`source: "gtm"`), including the chat tool Qualified. Real values change over time.

```json
{
  "input": "webflow.com",
  "domain": "webflow.com",
  "status": "ok",
  "emailProvider": {
    "name": "Google Workspace",
    "evidence": "MX aspmx.l.google.com"
  },
  "mx": [
    "aspmx.l.google.com",
    "alt1.aspmx.l.google.com",
    "alt2.aspmx.l.google.com",
    "aspmx2.googlemail.com",
    "aspmx3.googlemail.com"
  ],
  "spf": {
    "record": "v=spf1 include:_spf.google.com include:spf.mailjet.com include:mail.zendesk.com include:_spf.salesforce.com include:mailgun.org ?all",
    "includes": [
      "_spf.google.com",
      "spf.mailjet.com",
      "mail.zendesk.com",
      "_spf.salesforce.com",
      "mailgun.org"
    ],
    "all": "?all"
  },
  "dmarc": {
    "policy": "reject",
    "record": "v=DMARC1; p=reject; sp=none; rua=mailto:***@inbox.ondmarc.com,mailto:***@webflow.com,mailto:***@dmarc.mailgun.org,mailto:***@inbox.ondmarc.com; ruf=mailto:***@inbox.ondmarc.com,mailto:***@dmarc.mailgun.org,mailto:***@inbox.ondmarc.com; aspf=r; pct=100; fo=1; ri=3600"
  },
  "saas": [
    {
      "name": "Google Workspace",
      "category": "email",
      "evidence": "MX aspmx.l.google.com"
    },
    {
      "name": "Salesforce",
      "category": "crm",
      "evidence": "SPF include:_spf.salesforce.com"
    },
    {
      "name": "SendGrid",
      "category": "email-sending",
      "evidence": "CNAME s1._domainkey.webflow.com s1.domainkey.u2859722.wl223.sendgrid.net"
    },
    {
      "name": "Stripe",
      "category": "payments",
      "evidence": "TXT stripe-verification=081188df8f2b18461958…"
    },
    {
      "name": "Zendesk",
      "category": "support",
      "evidence": "SPF include:mail.zendesk.com"
    }
  ],
  "dnsProvider": {
    "name": "Amazon Route 53",
    "evidence": "NS ns-1078.awsdns-06.org"
  },
  "hosting": null,
  "cdn": null,
  "nameservers": [
    "ns-1078.awsdns-06.org",
    "ns-1722.awsdns-23.co.uk",
    "ns-344.awsdns-43.com",
    "ns-958.awsdns-55.net"
  ],
  "page": {
    "status": "fetched",
    "url": "https://webflow.com/",
    "httpStatus": 200
  },
  "technologies": [
    {
      "name": "Cloudflare",
      "category": "cdn",
      "version": null,
      "source": "page",
      "evidence": "HEADER cf-ray: a43baf32dddb1fd3-IAD"
    },
    {
      "name": "Google Tag Manager",
      "category": "tag-manager",
      "version": null,
      "source": "page",
      "evidence": "HTML https://www.googletagmanager.com/ns.html?id=GTM-55XXQM3F\" he"
    },
    {
      "name": "jQuery",
      "category": "js-library",
      "version": "3.5.1",
      "source": "page",
      "evidence": "SCRIPT https://d3e54v103j8qbb.cloudfront.net/js/jquery-3.5.1.min.dc5e7f18c8.js?s…"
    },
    {
      "name": "LinkedIn Insight Tag",
      "category": "advertising",
      "version": null,
      "source": "gtm",
      "evidence": "GTM GTM-55XXQM3F snap.licdn.com"
    },
    {
      "name": "Meta Pixel",
      "category": "advertising",
      "version": null,
      "source": "gtm",
      "evidence": "GTM GTM-55XXQM3F connect.facebook.net/"
    },
    {
      "name": "Qualified",
      "category": "chat",
      "version": null,
      "source": "gtm",
      "evidence": "GTM GTM-55XXQM3F js.qualified.com"
    },
    {
      "name": "Webflow",
      "category": "cms",
      "version": null,
      "source": "page",
      "evidence": "META generator Webflow"
    }
  ],
  "failedLookups": [],
  "checkedAt": "2026-10-01T13:06:22Z",
  "error": null
}
```

How to read it: `emailProvider` is Google Workspace because the MX hosts are Google's. The `saas` list shows Stripe and Zendesk from DNS (tokens, SPF): accounts, not proof of live use. Qualified is listed with `source: "gtm"`: it is configured in the site's tag manager container, which does not guarantee it is active on the home page.

#### Fields

- `input`: the item as given. E-mail addresses are masked (`***@x.com`).
- `domain`: normalized domain; `null` when the status is `invalid`.
- `status`: `ok`, `not_found` (the domain does not exist or has no records), `invalid` (not a domain), `error` (lookups failed and nothing was found). Only `ok` is billed.
- `emailProvider`: `{name, evidence}`. `null` with an empty `mx` means the domain itself receives no mail (the company may use another domain: `notion.so` has no MX, its mail is on `notion.com`). `Unknown` means the MX host is not in our list: read `mx` and `saas`, since a mail gateway such as Proofpoint or Mimecast can sit in front of Google or Microsoft. `None (null MX)` means the domain declares it does not receive e-mail (RFC 7505).
- `mx`: MX hosts sorted by priority, then name.
- `spf`: `record`, `includes` (the `include:` targets of that record, no recursion), `all` (final `all` mechanism); `null` without SPF.
- `dmarc`: `policy` (value of `p=`) and the `record`; `null` without DMARC.
- `saas`: services the domain declares in DNS: `name`, `category` and `evidence` (`"<TYPE> <value>"`).
- `dnsProvider`, `hosting`, `cdn`: `{name, evidence}` or `null`.
- `nameservers`: NS targets, sorted.
- `failedLookups`: lookups that failed (`"MX x.com"`). A row with status `ok` can be partial: `mx = []` with no `MX x.com` here means "no MX"; with it, "MX unknown".
- `page`: `status` is `fetched` (2xx page received and analyzed), `robots_disallowed` (robots.txt forbids the page, or answers 5xx), `blocked` (401, 403, 429, or an anti-bot challenge page: nothing is bypassed), `http_error` (other HTTP failure, too many redirects), `unreachable` (no usable answer, timeout, non-public address) or `not_fetched`. `url` is the last page URL requested, `httpStatus` its response code.
- `technologies`: empty unless `page.status` is `fetched`. Each item: `name`, `category`, `version` (only for generators, jQuery file names and `server` headers, else `null`), `source` (`page`: the page HTML and headers; `script`: one of the site's own scripts; `gtm`: a Google Tag Manager container) and `evidence` (`page`: `HEADER name: value`, `COOKIE name`, `META generator ...`, `SCRIPT url`, `LINK url` or `HTML excerpt`; `script`: `SCRIPT <script url> <pattern>`; `gtm`: `GTM <container id> <pattern>`). `gtm` means "configured in the site's GTM container", not necessarily active on the home page. If several sources find the same technology, `page` wins over `script`, then `gtm`. The `status` of the row stays the DNS status.
- `checkedAt`: UTC time of the analysis. `error`: cause in English when status is not `ok`.

No e-mail address appears in the output: anything that looks like one is rendered `***@domain`.

### What it detects

Generated from the signature files shipped with the Actor.

**DNS services (34), by category** (`saas` field and `emailProvider`):

- **email**: Fastmail, Google Workspace, iCloud Mail, Microsoft 365, OVHcloud Mail, Proton Mail, Zoho Mail
- **security**: GlobalSign, Jamf, Mimecast, Proofpoint
- **analytics**: Google Search Console, Mixpanel
- **dev**: Atlassian, OpenAI
- **support**: Atlassian Statuspage, Zendesk
- **payments**: Stripe
- **marketing**: HubSpot, Mailchimp, Marketo
- **social**: Meta
- **other**: Apple
- **productivity**: Adobe, DocuSign, Zapier, Zoom
- **crm**: Salesforce
- **email-sending**: Amazon SES, Brevo, Mailchimp Transactional, Mailgun, Postmark, SendGrid

**DNS providers, hosting and CDN** (from NS, CNAME and IP ranges):

- **dns_providers**: Cloudflare, Amazon Route 53, Google Cloud DNS, Azure DNS, OVHcloud, GoDaddy, Gandi, Namecheap, NS1, Akamai
- **hosting**: Vercel, Netlify, GitHub Pages, Shopify, Squarespace, Wix, Heroku, WP Engine
- **cdn**: Cloudflare, Fastly, Amazon CloudFront, Akamai

**Website technologies (54), by category** (`technologies` field; looked for in the HTML, the scripts and the GTM container):

- **cms**: Drupal, Ghost, Joomla, Squarespace, Webflow, Wix, WordPress
- **ecommerce**: BigCommerce, Magento, PrestaShop, Shopify, WooCommerce
- **analytics**: Google Analytics, Hotjar, Matomo, Microsoft Clarity, Plausible, Segment
- **tag-manager**: Google Tag Manager
- **advertising**: LinkedIn Insight Tag, Meta Pixel
- **chat**: Crisp, Drift, Gladly, Gorgias, HubSpot Chat, Intercom, LiveChat, Qualified, Tawk.to, Tidio, Zendesk
- **marketing**: HubSpot, Klaviyo
- **payments**: Adyen, Klarna, PayPal, Shop Pay, Stripe
- **consent**: Axeptio, Cookiebot, Didomi, OneTrust
- **framework**: Gatsby, Next.js, Nuxt
- **js-library**: jQuery, React, Vue.js
- **cdn**: Cloudflare
- **hosting**: Netlify, Vercel
- **server**: Apache HTTP Server, Nginx

Anything not in these lists is not reported by name: an unrecognized mail provider is `Unknown`, an unrecognized host or CDN is `null`.

### Calling it from an AI agent

Through the Apify MCP server (`https://mcp.apify.com`): the agent finds the Actor with `search-actors` (for example "find the email provider and tech stack of a list of company domains"), reads its input schema and prices with `fetch-actor-details`, then runs it with `call-actor`:

```json
{
  "actor": "lakadev/website-tech-stack-detector",
  "input": {"domains": ["apify.com", "gitlab.com"], "fetchHomepage": true}
}
```

`call-actor` waits at most about 45 seconds. A local run of 20 domains took about 8 seconds, but keep batches small for a direct call; for longer lists start the run asynchronously and read the dataset afterwards. Running an Actor requires an Apify token (or an x402 or Skyfire payment where available).

Through the API, synchronously:

```bash
curl -X POST "https://api.apify.com/v2/acts/lakadev~website-tech-stack-detector/run-sync-get-dataset-items" \
  -H "Authorization: Bearer $APIFY_TOKEN" -H "Content-Type: application/json" \
  -d '{"domains": ["apify.com", "gitlab.com"]}'
```

### Limits

- **No JavaScript is executed.** Besides the raw home page, the Actor reads as text the first 3 scripts hosted on the site's own domain (or its subdomains) and up to 2 public Google Tag Manager containers (`googletagmanager.com/gtm.js?id=GTM-...`) found in the page, and looks for the same patterns in them. `robots.txt` is checked for each origin read. A tool loaded by code that this does not reveal stays invisible: **an absence proves nothing**. A DNS token proves an account, not a current use (Stripe, for instance, rarely shows on a home page).
- One page request per domain (plus redirects, `robots.txt`, up to 3 site scripts and 2 GTM containers). A failing script is ignored. A page larger than 2 MB is analyzed on its first 2 MB, stays `fetched` and is billed. Cookies are never sent back.
- Pages behind anti-bot protection are reported as `blocked`; nothing is bypassed (no proxy, no captcha solving). Measured on 100 well-known sites from the Apify platform (October 2026): 67 pages read, 22 `blocked`, 8 `robots_disallowed`, 3 `unreachable`. The DNS part works for every existing domain.
- `robots.txt` is respected: a page it forbids is `robots_disallowed`, and not billed.
- No recursion into SPF `include:` records.
- No climb to the parent domain: `blog.x.com` is analyzed as is, without inherited NS or DMARC.
- No public suffix list: `co.uk` is analyzed as a domain.
- Detection relies on built-in signature lists (34 DNS services, 54 page technologies), not on a database of thousands of technologies.
- DNS goes to the public resolvers 1.1.1.1 and 8.8.8.8 (one try each, 3 seconds per try). The HTTP part takes at most 12 seconds per domain, and addresses that are not public are refused.
- At most 1,000 domains per run.
- If the run is restarted (platform migration), it resumes at the next row to write: no row is written or billed twice.

### Legal and good manners

The Actor reads public DNS records and the public home page. It identifies itself with its User-Agent, reads and follows `robots.txt` (RFC 9309) before any page request, does not bypass protections, and never sends cookies. Some sites forbid automated browsing in their terms or in free text; check that your use is allowed. The output contains company-level technical data, and e-mail addresses are masked. Source code: MIT license.

### FAQ

**Does it run JavaScript or a browser?** No. It reads the raw HTML, headers, DNS, a few of the site's own scripts and the public GTM container as text. Tools injected only at run time are not seen.

**It found no chat widget or no Stripe. Does the site have none?** Not necessarily. Stripe usually loads at checkout, not on the home page, and chat widgets are often loaded after consent. Use the DNS findings and `page.status` to judge.

**What if the site blocks it?** The page is reported as `blocked` or `robots_disallowed` and you are not charged for the page (nor for its scripts). DNS findings are still returned.

**Am I charged for failures?** No: only rows with status `ok` are billed, plus the page event when the page was actually read.

**Does a SaaS found in DNS mean the company uses it today?** Not necessarily. Verification tokens and SPF entries can outlive the contract. Treat them as strong hints, and read the `evidence`.

**Is the same input always the same output?** DNS findings are deterministic for the same DNS data; page findings depend on what the home page serves at that moment.

**How many domains can I send?** Up to 1,000 per run.

# Actor input Schema

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

Domains to analyze, 1 to 1,000. Each item can be a bare domain ("apify.com"), a URL ("https://www.shopify.com/fr") or an e-mail address ("name@wordpress.org"; only the domain is kept and the address is masked in the output). The "www." prefix is removed and duplicates are merged. Subdomains are analyzed as given ("blog.example.com" is not reduced to "example.com"). An invalid item gives a row with status "invalid", free of charge.

## `includeSubdomainWww` (type: `boolean`):

Default true. Also look up www.<domain> in DNS, which is where most sites show their hosting provider and CDN. Set to false to read the bare domain only (fields hosting and cdn may then stay null).

## `fetchHomepage` (type: `boolean`):

Default true. Read the home page once (robots.txt first), plus up to 3 scripts of the site and its public Google Tag Manager container, as text (no browser, no JavaScript executed) to detect website technologies such as WordPress, Shopify, Google Analytics, chat widgets. Absence proves nothing. Adds $0.003 per domain when the page was read. Set to false for DNS only (email provider, SaaS, DNS, host): no HTTP request to the site and only $0.002 per domain.

## Actor input object example

```json
{
  "domains": [
    "apify.com",
    "wordpress.org",
    "shopify.com"
  ],
  "includeSubdomainWww": true,
  "fetchHomepage": true
}
```

# Actor output Schema

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

All rows of the run (one per domain), as JSON items.

# 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": [
        "apify.com",
        "wordpress.org",
        "shopify.com"
    ]
};

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

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

```

## MCP server setup

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