# Company Tech Stack & Domain Signals (`signalscouter/company-tech-stack-signals`) Actor

Paste company domains, get one row each: website tech stack (CMS, e-commerce, analytics, marketing tools, CDN, hosting), mail provider and SPF/DMARC/DKIM grade, SaaS verification records, RDAP domain age and registrar, and hiring platform with open-job count. Public data only.

- **URL**: https://apify.com/signalscouter/company-tech-stack-signals.md
- **Developed by:** [Signal Scout](https://apify.com/signalscouter) (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

$2.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

## Company Tech Stack & Domain Signals

Give it a list of company domains. You get **one row per domain** that shows what the company runs and how it operates, built only from public signals:

- **Website technologies:** CMS, e-commerce platform, analytics, tag managers, marketing automation, ad pixels, chat, payments, consent tools, A/B testing, CDN, hosting, frameworks and more. There are about 400 fingerprints in 31 categories, and each detection carries a confidence score and the evidence string it came from.
- **Email and DNS:** mailbox provider (Google Workspace, Microsoft 365 and others), email gateways, SPF services (which tools send mail as the domain), DMARC policy and vendor, DKIM on common selectors, MTA-STS, TLS-RPT and BIMI, plus an **A–F email-security grade**.
- **SaaS footprint:** domain-verification TXT records, such as Atlassian, HubSpot, Stripe, OpenAI and Miro.
- **Domain facts (RDAP):** registrar, creation, update and expiry dates, domain age in years, status flags and DNSSEC.
- **Hiring signal:** which applicant tracking system the company uses (35 ATSes are recognised) and, for 12 of them, the **number of open jobs** taken from the public job board.
- **Website basics:** title, meta description, language, HTTP security headers and an optional `security.txt` check.

*By signalscouter.*

### Who it's for

- **Sales and RevOps:** filter accounts by technology (for example "uses Shopify and Klaviyo", or "on HubSpot, not Salesforce"), and prioritise companies that are hiring.
- **Agencies and SaaS vendors:** find prospects on a competitor's or partner's platform.
- **Security and deliverability teams:** audit SPF, DMARC and DKIM posture across a portfolio of domains.
- **Investors and analysts:** monitor a list of companies for stack changes and hiring momentum over time.

### Input

The minimum input is a list of domains. URLs and email addresses are accepted too; they are normalised to the registrable domain using the Public Suffix List (`https://www.shop.example.co.uk/x` becomes `example.co.uk`, and `jane@acme.com` becomes `acme.com`).

**Duplicates are merged by registrable domain.** `example.co.uk`, `shop.example.co.uk` and `jane@example.co.uk` produce **one** row, analysed and charged **once**. The row's `inputs` field lists every original input that was merged into it, and `input` holds the first one. The analysis is of the registrable domain's own website: if one of your inputs is the domain itself or its `www` host, that page (path included) is used, otherwise the homepage `https://<domain>/`. A subdomain such as `jobs.stripe.com` therefore gives you the `stripe.com` row rather than a separate one. Sites on shared platforms listed in the Public Suffix List, such as `acme.myshopify.com` or `foo.github.io`, count as their own domains, so different shops stay separate (their RDAP `lookupStatus` is `platform_subdomain`, because the registration belongs to the platform). `RUN_SUMMARY.duplicateInputsMerged` tells you how many inputs were merged.

```json
{
  "domains": ["webflow.com", "allbirds.com", "monzo.com", "https://www.pret.co.uk/en-GB"],
  "checks": ["tech", "dns", "emailSecurity", "rdap", "ats"],
  "technologyFilter": [],
  "onlyChanges": false
}
```

| Field | Default | What it does |
|---|---|---|
| `domains` | | Domains, URLs or emails. |
| `datasetId` / `datasetField` | `domain` | Reads domains from another dataset, for example the output of a lead-list Actor. |
| `checks` | tech, dns, emailSecurity, rdap, ats | Turn off what you don't need to make runs faster. `securityTxt` is optional. |
| `pagesPerDomain` | 1 | Number of pages to fetch (1–5). Values above 1 add a few linked first-party pages, such as pricing or contact, which catches more tools. |
| `atsProbeSlugs` | true | If the site doesn't link to its job board, checks whether `boards.greenhouse.io/<name>` or `jobs.ashbyhq.com/<name>` exists. A match is accepted only when the site has a careers page and the board name matches the site title. |
| `technologyFilter` / `categoryFilter` | | Output only domains that match, for example `["Shopify"]` or `["chat"]`. Filtered-out domains are **not charged**. |
| `onlyChanges` | false | Monitoring mode. It compares each domain with the previous run (stored in the named key-value store `company-tech-signals-state`) and outputs only domains that changed, with `added`, `removed` and `changedFields`. Unchanged domains are **not charged**. |
| `maxDomains` | 0 (all) | A cap for testing. Applied after duplicates are merged. |
| `maxConcurrency` | 10 | Number of domains processed in parallel. |
| `requestTimeoutSecs` | 15 | Timeout per HTTP request. |
| `perHostDelaySecs` | 0.5 | Minimum gap between requests to the same host. A larger robots.txt `Crawl-delay` (up to 10 s) takes priority. |

### Output

There is one dataset row per domain. A shortened real row from a test run (October 2026):

```json
{
  "recordType": "domain",
  "domain": "webflow.com",
  "finalUrl": "https://webflow.com/",
  "httpStatus": 200,
  "title": "Webflow: The agentic web platform for modern businesses",
  "language": "en",
  "technologies": [
    {"name": "Webflow", "category": "cms", "confidence": 100, "evidence": "meta generator: Webflow; asset https://cdn.prod.website-files.com"},
    {"name": "Marketo", "category": "marketingAutomation", "confidence": 90, "evidence": "asset //go.webflow.com/js/forms2/js/forms2.min.js"},
    {"name": "Intellimize", "category": "abTesting", "confidence": 90, "evidence": "asset https://cdn.intellimize.co/snippet/…js"},
    {"name": "Google Tag Manager", "category": "tagManager", "confidence": 90, "evidence": "asset https://www.googletagmanager.com/ns.html?id=GTM-…"}
  ],
  "technologyCount": 14,
  "cms": "Webflow",
  "cdn": "Cloudflare",
  "hosting": "Amazon Web Services",
  "hostingSource": "ipAsn",
  "marketingAutomation": ["Marketo"],
  "httpSecurity": {"hsts": true, "csp": true, "xFrameOptions": true, "score": 4},
  "ats": {"platform": "ashby", "boardUrl": "https://jobs.ashbyhq.com/webflow", "openJobs": 17, "detectedVia": "careersLink"},
  "dns": {
    "dnsProvider": "Amazon Route 53",
    "mxProvider": "Google Workspace",
    "txtVerifications": ["Atlassian", "HubSpot", "Miro", "OpenAI", "Stripe", "…"]
  },
  "emailSecurity": {
    "spfPolicy": "neutral (?all)",
    "spfServices": ["Google Workspace", "Mailjet", "Zendesk", "Salesforce", "Mailgun"],
    "dmarcPolicy": "reject",
    "dmarcVendors": ["Red Sift OnDMARC"],
    "dkimFound": true,
    "grade": "B"
  },
  "rdap": {"registrar": "MarkMonitor Inc.", "createdAt": "2003-03-31T19:16:33+00:00", "ageYears": 23.6, "dnssec": false, "lookupStatus": "ok"},
  "partial": false,
  "checkErrors": null,
  "durationSecs": 7.0
}
```

Flat columns (`cms`, `ecommerce`, `analytics`, `chat`, `payments` and so on) make spreadsheet filtering easy. The full list is in the **Overview** and **Technologies** views of the dataset. Other views cover **Email security**, **Domain and hiring**, **Changes** (monitoring mode) and **Errors**.

**Errors** get their own row (`recordType: "error"`, with an `error` code such as `INVALID_DOMAIN` or `DOMAIN_NOT_FOUND`) and are **never charged**. If one check fails but others work (for example, the website blocks bots but DNS and RDAP succeed), the row is still delivered with `partial: true`, and `checkErrors` says what failed, such as `{"web": "HTTP_403"}`.

A `RUN_SUMMARY` record in the default key-value store reports success rate, timings, request counts and how many domains were filtered, unchanged or stopped at your spending limit.

#### Email-security grade

| Grade | Meaning |
|---|---|
| A | SPF `-all`/`~all`, DMARC `reject`/`quarantine` at 100%, and DKIM found |
| B | DMARC enforced, with either strict SPF or DKIM |
| C | SPF and DMARC published, but DMARC not enforced (`p=none`) or only weakly backed |
| D | Only SPF or only DMARC |
| F | Neither |

DKIM is checked only on 16 common selectors, so "DKIM not found" does **not** prove DKIM is missing.

### Pricing

Pay per event: **$2.00 per 1,000 domains**. Each successful domain row is one `domain-analyzed` event at $0.002.

You pay only for delivered domain rows. These are not charged:

- error rows (invalid or nonexistent domains)
- domains removed by your technology or category filters
- unchanged domains in monitoring mode

There is no separate charge per run start or per dataset item.

The Actor respects your maximum spending limit and stops when it is reached. Every domain that was not delivered because of the limit is counted in `RUN_SUMMARY.droppedAtChargeLimit`: both domains that were in progress when the limit was hit and domains that never started. `RUN_SUMMARY` also reports `chargeableResults`, `charged` and `errorRowsNotCharged`, so you can check every bill.

### Performance

In local tests on 46 real, varied domains (SaaS, e-commerce, media, UK retail, government, non-profits):

- **100%** of real domains returned a row; 85% (39 of 46) had full website analysis.
- The rest were blocked by bot protection or unreachable over HTTPS. Those rows still have DNS, email and RDAP data, and are marked `partial`.
- Median time was about 6 s per domain (most of it is deliberate politeness delay). With the default concurrency of 10, 25 domains finish in about 17–21 s.
- A domain behind an aggressive bot wall can take up to about 20–60 s before timing out. Each domain has a hard time limit, so the run never hangs.

#### Run time, memory and timeout for large lists

Throughput on the Apify platform was measured on a 152-domain list at the default concurrency of 10 (October 2026). The Actor is CPU-bound, and Apify gives a run CPU in proportion to its memory, so memory sets the speed:

| Memory | Time per domain | 1,000 domains | Domains per hour |
|---|---|---|---|
| 4 GB (default) | ~0.9 s | ~15 min | ~3,500–4,000 |
| 1 GB | ~2.5 s | ~45 min | ~1,400 |
| 512 MB | ~5 s | ~1.5 h | ~700 |

Platform usage per domain was the same at every memory size (about $0.15 per 1,000 domains), so the default is the fast one. Memory is capped at 4 GB because the Actor can't use more than one CPU core, so more memory would only cost more.

**Timeout:** the default run timeout is **4 hours**, which fits about 14,000 domains at 4 GB. One hour handles roughly 3,500 domains, so **lists of roughly 3,500+ domains need a timeout longer than 1 hour**. Keep that in mind if you or an integration set a 1-hour timeout. Rule of thumb: **timeout (seconds) ≈ number of domains × 1 at 4 GB (× 3 at 1 GB, × 6 at 512 MB) + 120**. If a run times out, rows already delivered are kept (and charged); split very long lists into several runs.

### Limitations (please read)

- **No JavaScript rendering.** Detection uses the HTML, headers, cookies and URLs that are visible in the page source. Tools loaded later by a tag manager or bundled into app JavaScript can be missed. For example, a site might load its analytics only through Google Tag Manager. Expect solid coverage of CMS, e-commerce, CDN, hosting, frameworks and most marketing tags, and lower coverage of tags loaded inside GTM.
- **Bot-protected sites** (some large retail, travel and fintech sites) return 403 or a challenge page. Those rows have header-level detection only, plus DNS, email and RDAP, and are marked `blockedByBotProtection: true`.
- **RDAP is unavailable for some TLDs**, for example `.de`, `.io` and `.so`, where `lookupStatus` is `unsupported_tld`. Registrars also differ in which dates they publish.
- **Inline-URL detections** (confidence 80) come from URLs found inside inline scripts. They can occasionally come from allowlists or embed code rather than an active tool.
- **ATS:** open-job counts are available for Greenhouse, Lever, Ashby, Workable, Workday, BambooHR, Recruitee, Teamtailor, Personio, Breezy, Pinpoint and Rippling. The other 23 ATSes (iCIMS, SmartRecruiters, SuccessFactors, Taleo and others) are detected but not counted. Lever is found only when the site links to it. Slug guessing is deliberately conservative, so some companies' boards are missed rather than misattributed.
- Fingerprints are signalscouter's own, written and maintained by hand. Niche tools may be missing. Request them in the Issues tab.

### Legal and data use

- **Public data only.** The Actor reads public website pages, public DNS records (over DNS-over-HTTPS), public RDAP registry data and public ATS job-board APIs. It uses no logins and no paid or private data sources, and it doesn't bypass bot protection.
- **robots.txt is respected** on every host, including each redirect hop, and `Crawl-delay` is honoured. Requests are rate-limited per host and identified with the user-agent `signalscouterBot/1.0`.
- **No personal data.** The Actor doesn't output registrant contacts (including RDAP contact data), names, emails or phone numbers. DMARC report addresses are reduced to their domain, and `security.txt` reports contact types only.
- You are responsible for using the results in line with the law and the terms of the sites you analyse.

### More signalscouter Actors

| Actor | What it gives you |
|---|---|
| [Career Site Jobs Scraper](https://apify.com/signalscouter/career-site-jobs-scraper) | Jobs from 12 ATSes (Greenhouse, Lever, Ashby, Workday, Workable and more) in one schema, from company domains |
| [Workday Jobs Scraper](https://apify.com/signalscouter/workday-jobs-scraper) | Jobs from any `myworkdayjobs.com` career site, including sites with more than 2,000 jobs |
| [UK Public Sector Jobs Scraper](https://apify.com/signalscouter/uk-public-sector-jobs-scraper) | NHS Jobs, Teaching Vacancies and DWP Find a Job |
| [SEC Form D Funding Signals](https://apify.com/signalscouter/sec-form-d-funding-signals) | Newly funded US companies from SEC Form D filings |

### Support and contact

For bugs or fingerprint requests, use the Issues tab on this Actor's page. For anything else, email **signalscouter.data@gmail.com**.

### Changelog

- **0.2:** inputs are de-duplicated by registrable domain (Public Suffix List), with all original inputs kept in `inputs`. Platform subdomains such as `acme.myshopify.com` stay separate. `droppedAtChargeLimit` now also counts domains that never started. The default timeout is now 4 hours, with memory capped at 4 GB. Monitoring state is keyed on the registrable domain.
- **0.1:** first version.

# Actor input Schema

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

One per line. Bare domains (stripe.com), full URLs (https://www.notion.so/product) or work email addresses (only the domain part is used). Inputs are de-duplicated by registrable domain (Public Suffix List): example.co.uk, shop.example.co.uk and jane@example.co.uk become one row, analysed and charged once, with every original input listed in its 'inputs' field. Sites on shared platforms (acme.myshopify.com, foo.github.io) stay separate. Run time: about 1 second per domain at the default 4 GB memory, so one hour handles roughly 3,500 domains; lists of roughly 3,500+ domains need a timeout longer than 1 hour. The default timeout is 4 hours (about 14,000 domains). Rule of thumb: timeout in seconds = domains x 1 at 4 GB (x 3 at 1 GB, x 6 at 512 MB) + 120.

## `datasetId` (type: `string`):

ID of an Apify dataset to read extra domains from, e.g. the output of another Actor or an imported CRM export.

## `datasetField` (type: `string`):

Field name in each dataset item that contains the domain or URL.

## `checks` (type: `array`):

Which signal groups to collect. The price per domain is the same whichever you choose.

## `pagesPerDomain` (type: `integer`):

Homepage only (1) is usually enough. Up to 5 adds same-site pages such as /pricing or /about to catch tools loaded only there. More pages = slower runs.

## `atsProbeSlugs` (type: `boolean`):

If the site doesn't link to its job board, try the company name on Greenhouse, Ashby and Lever. Matches are flagged detectedVia = 'slugProbe' because a same-named board can belong to a different company.

## `technologyFilter` (type: `array`):

Case-insensitive partial match on technology name, mail provider or hiring platform, e.g. 'Shopify', 'HubSpot', 'Microsoft 365', 'greenhouse'. Domains that don't match are skipped and not charged.

## `categoryFilter` (type: `array`):

Keep domains that use at least one technology of the chosen categories.

## `onlyChanges` (type: `boolean`):

Remembers each domain's stack in a named key-value store ('company-tech-signals-state'). The first run outputs every domain (changes.firstSeen = true); later runs output only domains whose technologies, mail provider, DNS host, DMARC policy, hiring platform or registrar changed. Unchanged domains are not charged.

## `maxDomains` (type: `integer`):

Stop after this many domains (0 = no limit).

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

Domains processed in parallel. Requests to any single website are always sequential.

## `requestTimeoutSecs` (type: `integer`):

Per HTTP request. Slow sites beyond this are reported as TIMEOUT.

## `perHostDelaySecs` (type: `number`):

Politeness gap between consecutive requests to one host. A longer robots.txt Crawl-delay (up to 10 s) is honoured automatically.

## Actor input object example

```json
{
  "domains": [
    "stripe.com",
    "allbirds.com",
    "notion.so"
  ],
  "datasetField": "domain",
  "checks": [
    "tech",
    "dns",
    "emailSecurity",
    "rdap",
    "ats"
  ],
  "pagesPerDomain": 1,
  "atsProbeSlugs": true,
  "onlyChanges": false,
  "maxDomains": 0,
  "maxConcurrency": 10,
  "requestTimeoutSecs": 15,
  "perHostDelaySecs": 0.5
}
```

# Actor output Schema

## `overview` (type: `string`):

Dataset on the Overview view: one row per domain with CMS, e-commerce, hosting, mail provider, email grade, domain age and hiring platform.

## `technologies` (type: `string`):

Dataset on the Technologies view: every detected technology with category, version, confidence and evidence.

## `emailSecurity` (type: `string`):

Dataset on the Email & DNS view: mail provider, SPF/DMARC/DKIM, SaaS verification records and IP network.

## `domainAndHiring` (type: `string`):

Dataset on the Domain age & hiring view: registrar, registration date and hiring platform with open jobs.

## `errors` (type: `string`):

Inputs for which nothing could be collected (never charged).

## `runSummary` (type: `string`):

RUN_SUMMARY record: counts, success rate, median seconds per domain, requests made.

# 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": [
        "stripe.com",
        "allbirds.com",
        "notion.so",
        "monzo.com",
        "bbc.co.uk"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("signalscouter/company-tech-stack-signals").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": [
        "stripe.com",
        "allbirds.com",
        "notion.so",
        "monzo.com",
        "bbc.co.uk",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("signalscouter/company-tech-stack-signals").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": [
    "stripe.com",
    "allbirds.com",
    "notion.so",
    "monzo.com",
    "bbc.co.uk"
  ]
}' |
apify call signalscouter/company-tech-stack-signals --silent --output-dataset

```

## MCP server setup

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

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/Suy7aPfvsYJWMDkwv/builds/Xq2l8cuwmDWJ5CzWR/openapi.json
