# Website Tech Gap Scanner (`lergassy/website-tech-gap-scanner`) Actor

Scan websites for their technology stack and the capabilities they are missing: booking, live chat, CRM, payments, analytics, ad pixels and call tracking, plus email and phone. Export scraped data, run the scraper via API, schedule and monitor runs, or integrate with other tools or AI workflows.

- **URL**: https://apify.com/lergassy/website-tech-gap-scanner.md
- **Developed by:** [Matvey](https://apify.com/lergassy) (community)
- **Categories:** Lead generation, Business, AI
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $14.00 / 1,000 website scanneds

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

### What is Website Tech Gap Scanner?

**Website Tech Gap Scanner** turns a list of websites into a **qualified prospect list**. For every domain it returns the **technology stack**, the **capabilities the website is missing**, whether the company is **paying for traffic right now**, and the **email and phone** to reach it. Paste domains from Google Maps, a directory, a CRM export or a spreadsheet, press Start, and download the result as JSON, CSV or Excel — or pull it through the Apify API into n8n, Make, Zapier, Google Sheets or an AI agent.

Most **website technology lookup** tools tell you what a website *has*. That is the less useful half. Outbound email does not convert on "I see you use WordPress" — it converts on *"I noticed you are running Meta ads but a visitor cannot book an appointment on your site."* This scraper returns both halves plus the contact, so a row is a reason to write, not a trivia card.

**No API key, no login, no proxy, no browser.** One HTTP request per website, so a list of ten thousand domains costs what the pricing tab says.

### What data does Website Tech Gap Scanner extract?

| Field | Example | What it is for |
|---|---|---|
| `technologies` | `["Google Tag Manager", "Meta Pixel", "Shopify", "Yotpo"]` | Every tool detected: CMS, ecommerce, payments, booking, chat, CRM, analytics, ad pixels, call tracking, reviews |
| `technologiesByCategory` | `{"ecommerce": ["Shopify"], "ads_pixel": ["Meta Pixel"]}` | The same list grouped, so a filter can ask "any booking vendor at all?" |
| `gaps` | `["no_online_booking", "no_live_chat"]` | Machine-readable keys of what the website lacks — your opening line |
| `gapLabels` | `["No online booking / scheduling widget"]` | The same gaps as sentences you can paste into an email |
| `runsPaidAds`, `adPlatforms` | `true`, `["Meta Pixel", "Google Ads Conversion"]` | Real money is being spent on traffic right now |
| `usesCallTracking` | `true` | Paid marketing where the conversion is a phone call, not a checkout |
| `paidMarketingSignal` | `true` | Either of the two above — the single "has budget" flag |
| `adAccountIds` | `{"metaPixelIds": ["1758221267590067"], "googleAdsIds": ["AW-786649265"]}` | Advertising account IDs read out of the tag container |
| `prospectScore`, `prospectTier` | `90`, `hot` | 0–100 fit score bucketed into hot / warm / cold |
| `bookingSignals` | `["free consultation", "book online"]` | Scheduling wording found in the page text, even when no vendor was identified |
| `city`, `stateCode`, `postalCode` | `Las Vegas`, `NV`, `89146` | The address the business publishes on its own home page, so you can segment outreach by geography |
| `locationSource` | `schema.org` | Where the address was read from: schema.org markup or the page text |
| `primaryEmail`, `emails` | `help@allbirds.com` | Best address first: the company's own domain, general inboxes over side mailboxes |
| `phones` | `["+14809178400"]` | Taken from `tel:` links only, so free-text digits never leak in |
| `social` | `{"linkedin": "…", "instagram": "…"}` | Profile links found on the home page |
| `tagManagerContainers` | `["GTM-TH8KRSBJ"]` | Which Google Tag Manager containers the site loads |
| `confidence` | `high` / `medium` / `low` | `low` means the page was too thin to judge — an anti-bot wall, not an empty answer |
| `status`, `error` | `ok`, `unreachable` | Every submitted domain comes back, so nothing disappears silently |

#### Which gaps can it detect?

`no_online_booking`, `booking_unverified`, `no_live_chat`, `no_crm`, `no_analytics`, `no_ads_pixel`, `no_email_capture`, `no_online_payments`, `no_reviews_widget`, `no_contact_form`, `no_https`, `not_mobile_ready`, `no_meta_description`.

### The part other tech stack detectors miss

Modern websites no longer put their marketing tags in the HTML. **Google Tag Manager injects them at runtime**, so a plain HTML scan reports "no advertising pixel" for almost every serious advertiser — exactly backwards.

This scraper fetches each website's **public GTM container** and reads the tag list inside it. On a test set of twenty ecommerce and local-business domains, that lifted ad-pixel detection from **0 of 20 to 9 of 20** — the same websites, the same fetch budget, a completely different answer. The advertising account IDs in `adAccountIds` come from the same place.

#### Three states of booking, not two

"No vendor detected" is not the same as "no online booking". A website may use a vendor nobody lists, or keep scheduling on an inner page. So the booking result comes back in three states:

- a booking vendor was **identified**
- `booking_unverified` — the site advertises scheduling ("book online", "free consultation", "free estimate") but no vendor was attributed
- `no_online_booking` — no vendor and no mention anywhere

Only the third is safe to write an email about. On a sample of 287 real dental practices, taking "no vendor" at face value would have overstated the target segment **6.6 times**.

#### Vertical vendors, not just Calendly

Clinics, contractors and law firms do not schedule with Calendly. The scanner ships signatures for the vendors they actually run: **NexHealth, Zocdoc, LocalMed, Dentrix Ascend, Denticon, Curve Dental, Weave, RevenueWell, Solutionreach, PatientPop, Cherry, Sunbit, CareCredit, JobNimbus, AccuLynx, ServiceTitan, Jobber, Housecall Pro, Roofr, HOVER, Hearth, Clio, Lawmatics, MyCase, Filevine, Smokeball, LawPay, Ngage, Intaker, Smith.ai, CallRail** and more — 144 signatures across 14 categories.

On those 287 dental practices exactly **one** used Calendly. Without the vertical list the booking gap would have been meaningless.

### How much does it cost to scrape website technologies?

Pay-per-event: **$0.02 per website scanned**, which is **$20 per 1,000 websites**. Cheaper tiers apply automatically on paid Apify plans.

**You are only charged for websites that were actually read.** Unreachable domains, invalid domains and rows filtered out by your own settings are free. There is no start fee and no per-page fee — the number of pages read never changes your bill.

| Your list | Cost |
|---|---|
| 100 websites | $2 |
| 1,000 websites | $20 |
| 10,000 websites | $200 |

The Apify **free plan** includes $5 of monthly usage, which is about **250 websites per month at no cost**. No credit card is needed to try it.

### How to scan websites with Website Tech Gap Scanner

1. Open the Actor and press **Try for free**.
2. Paste your domains into **🌐 Websites**, one per line. The three prefilled examples run in about three seconds if you just want to see the output.
3. Optionally narrow the list: **💰 Only companies spending on traffic**, **🕳️ Only websites missing ALL of these**, **🎯 Minimum prospect score**.
4. Press **Start** and download the dataset as JSON, CSV or Excel — or call the run through the API.

![Website Tech Gap Scanner input form with the websites field, gap filters and prospect score](https://raw.githubusercontent.com/lergassy/apify-actor-assets/main/website-tech-gap-scanner/website-tech-gap-scanner-input-form.png)

### ⬇️ Input

```json
{
  "domains": ["allbirds.com", "roofingbybruce.com", "bostondentalgroup.com"],
  "onlyPaidTraffic": true,
  "onlyWithGaps": ["no_online_booking"],
  "minProspectScore": 60
}
```

#### Filtering so you only pay for rows you will use

- **`onlyPaidTraffic`** keeps only companies with an advertising pixel or call tracking.
- **`onlyWithGaps`** keeps only websites lacking *every* capability you select. Selling booking software? `no_online_booking`. Selling chat or intake? `no_live_chat`.
- **`minProspectScore`** at `60` returns the hot tier only.
- **`onlyStates`** keeps only websites whose published address is in the states you list (two-letter codes).

Filtered rows are never written to the dataset and never billed.

#### Advanced

- **`expandTagManager`** (on by default) reads the public GTM container. Turning it off makes runs slightly faster and ad detection far worse.
- **`maxConcurrency`** raises throughput on long lists.
- **`requestTimeoutSecs`** controls how long a slow website is given.

### ⬆️ Output

One row per website. Failed rows carry `status` and `error` so nothing disappears from your list.

![Website Tech Gap Scanner output table with prospect tier, score, ad platforms, gaps and email](https://raw.githubusercontent.com/lergassy/apify-actor-assets/main/website-tech-gap-scanner/website-tech-gap-scanner-output-table.png)

```json
{
  "domain": "allbirds.com",
  "status": "ok",
  "confidence": "high",
  "prospectScore": 90,
  "prospectTier": "hot",
  "runsPaidAds": true,
  "paidMarketingSignal": true,
  "adPlatforms": ["Meta Pixel", "Google Ads Conversion", "TikTok Pixel"],
  "adAccountIds": { "googleAdsIds": ["AW-786649265"], "ga4Ids": ["G-XXXXXXXX"] },
  "technologies": ["Google Tag Manager", "Google Analytics 4", "Meta Pixel", "Shopify", "Yotpo"],
  "gaps": ["no_online_booking", "no_live_chat", "no_crm"],
  "gapLabels": ["No online booking / scheduling widget", "No live chat or messaging widget"],
  "city": "Beverly Hills",
  "stateCode": "CA",
  "postalCode": "90211",
  "locationSource": "page text",
  "primaryEmail": "help@allbirds.com",
  "phones": [],
  "tagManagerContainers": ["GTM-TH8KRSBJ"]
}
```

### Use cases for website technology and gap data

#### Agencies selling a specific service

Filter on the gap your service fills and on `paidMarketingSignal`, and every row is a company that already spends on traffic and lacks exactly what you sell. The email writes itself, and `gapLabels` gives you the sentence.

#### SaaS vendors displacing a competitor

`technologies` names the incumbent. Filter for websites running a competing product and you have a switch list; filter for websites running none and you have a greenfield list.

#### Outreach segmented by geography

`stateCode` and `city` come from the address the business publishes itself, so a nationwide list splits into territories without a separate enrichment step. Measured on 151 live US law-firm websites, **69% published a parsable address** — 51% in schema.org markup, 53% in the page text, with overlap. The remaining 31% come back with `stateCode: null` rather than a guess, and the `onlyStates` filter drops them.

#### Lead qualification before outreach

Run a Google Maps or directory export through the scanner and drop everything below a score. A smaller, hotter list protects your sending domain better than volume ever will.

#### AI agents and enrichment pipelines

Flat, predictable fields and a one-call API make this easy to wire into an agent that qualifies inbound domains, or into a nightly n8n job that enriches new CRM rows.

### Integrations

Run it from the **API**, from the **Python** or **JavaScript** Apify client, on a **schedule**, or through **n8n, Make, Zapier and Google Sheets**. It also works as a tool over the **Apify MCP server**, so an AI agent can call it directly.

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_API_TOKEN>")
run = client.actor("lergassy/website-tech-gap-scanner").call(run_input={
    "domains": ["allbirds.com", "bostondentalgroup.com"],
    "onlyPaidTraffic": True,
})
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(row["domain"], row["prospectTier"], row["gapLabels"])
```

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

const client = new ApifyClient({ token: '<YOUR_API_TOKEN>' });
const run = await client.actor('lergassy/website-tech-gap-scanner').call({
    domains: ['allbirds.com', 'bostondentalgroup.com'],
    minProspectScore: 60,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items);
```

### Troubleshooting

- **A row came back with `confidence: "low"`.** The website answered but served almost nothing — usually an anti-bot page or a JavaScript-only shell. Treat it as "unknown", not as "no technologies".
- **A gap looks wrong.** The scanner reads the home page. A booking widget that lives only on `/appointments` reads as `booking_unverified` when the site mentions scheduling, and as a gap when it does not. For high-value lists, treat gaps as a hypothesis worth one click.
- **No advertising pixel on a company you know advertises.** Check that `expandTagManager` is on. Tags injected by other managers, such as server-side GTM or Segment, are not expanded.
- **`stateCode` is null.** The website publishes no machine-readable address on its home page. The location is never guessed from the domain name or the phone number.
- **A domain is `unreachable`.** The website did not answer over HTTPS, `www` or HTTP within your timeout. Raise `requestTimeoutSecs` for slow hosts.

### ❓ FAQ

#### Is it legal to scrape website technology data?

The scanner reads publicly served pages and public tag-manager containers, the same files any visitor's browser downloads. It uses no login and no personal account. Contact details are the ones the business publishes on its own website. Laws differ by country and by how you use the data — consult a lawyer for your case, and respect GDPR/CAN-SPAM when you email anyone.

#### How is this different from BuiltWith or Wappalyzer?

Those tell you what is installed. This also tells you what is **missing**, whether the company is **spending on ads** (by reading the tag container, not just the HTML), scores the prospect, and returns the contact — and it knows the vertical booking and CRM vendors that generic detectors have never heard of.

#### How many websites can I scan in one run?

There is no fixed cap. Measured throughput is about **one website per second at the default concurrency of 10**, and about **three per second at concurrency 40** — so a thousand-domain list takes roughly fifteen minutes on the defaults, or six minutes with concurrency raised. Raise **⚡ Websites in parallel** for long lists.

#### Can it tell me where each company is located?

Yes, when the website says so. `city`, `stateCode` and `postalCode` are read from schema.org markup or the published address in the page, and `onlyStates` filters on them. About 7 in 10 US business websites carry a parsable address; the rest return null rather than a guess.

#### Does one row equal one website?

Yes. One row per submitted domain, including the ones that failed — and failed rows are free.

#### Can I use it with the Apify API?

Yes. Start runs, poll status and read the dataset over the REST API, or use the Python and JavaScript clients as shown above.

#### Can I use it through an MCP server?

Yes. The Actor is available as a tool through the Apify MCP server, so an AI agent can call it with the input schema alone.

#### Can I integrate it with other apps?

Yes — n8n, Make, Zapier, Google Sheets, Slack and webhooks are all supported by the Apify platform.

#### Do I need a proxy?

No. The scanner makes plain HTTP requests and needs no proxy, no browser and no API key.

### Your feedback

Found a technology it should detect, or a vendor missing from a vertical? Open an issue on the Actor's **Issues** tab — vendor signatures are quick to add, and requests get shipped. Ratings and reviews are read and answered.

### You might also like

| Actor | What it does |
|---|---|
| [Website Contacts API](https://apify.com/lergassy/website-contacts-api) | Deep contact extraction: emails, phones, WhatsApp, socials and postal address from a whole website |
| [Email Verifier & Phone Number Validator](https://apify.com/lergassy/email-phone-verifier) | Validate the addresses and numbers before you send |
| [US New Business Leads](https://apify.com/lergassy/us-business-filings) | Newly registered US companies, straight from state and city registries |
| [Shopify Store Scraper](https://apify.com/lergassy/shopify-store-intel) | Emails, apps and catalogue figures for Shopify stores |
| [Jobs API](https://apify.com/lergassy/jobs-api) | Job postings from Indeed, LinkedIn and company career sites — a hiring intent signal |

# Actor input Schema

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

Add the websites to scan, one per line. Both <code>example.com</code> and <code>https://www.example.com/page</code> work — everything is reduced to the domain.

## `onlyPaidTraffic` (type: `boolean`):

Keep only websites with an advertising pixel or a call-tracking script — companies that are paying for visitors right now.

## `onlyWithGaps` (type: `array`):

Keep only websites that lack every capability you select. Selling booking software? Pick <b>no\_online\_booking</b>. Selling chat or intake? Pick <b>no\_live\_chat</b>.

## `onlyStates` (type: `array`):

Keep only websites whose published address is in one of these states. Use two-letter codes, for example <code>TX</code>. About 7 in 10 websites publish an address — the rest are dropped by this filter, so leave it empty unless you need the geography.

## `minProspectScore` (type: `integer`):

Drop websites scoring below this. 60 and above is the <b>hot</b> tier: paying for traffic and missing obvious capabilities.

## `expandTagManager` (type: `boolean`):

Reads each website's public GTM container to reveal advertising pixels that never appear in the HTML. Keep this on — without it most advertisers look like they run no ads at all.

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

How many websites to read at the same time. Raise it for long lists.

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

How long to wait for a slow website before giving up on it.

## Actor input object example

```json
{
  "domains": [
    "allbirds.com",
    "roofingbybruce.com",
    "bostondentalgroup.com"
  ],
  "onlyPaidTraffic": false,
  "onlyWithGaps": [],
  "onlyStates": [],
  "minProspectScore": 0,
  "expandTagManager": true,
  "maxConcurrency": 10,
  "requestTimeoutSecs": 20
}
```

# Actor output Schema

## `prospects` (type: `string`):

One row per website, with its stack, the capabilities it lacks, whether it is paying for traffic, and how to reach it.

# 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": [
        "allbirds.com",
        "roofingbybruce.com",
        "bostondentalgroup.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("lergassy/website-tech-gap-scanner").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": [
        "allbirds.com",
        "roofingbybruce.com",
        "bostondentalgroup.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("lergassy/website-tech-gap-scanner").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": [
    "allbirds.com",
    "roofingbybruce.com",
    "bostondentalgroup.com"
  ]
}' |
apify call lergassy/website-tech-gap-scanner --silent --output-dataset

```

## MCP server setup

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

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/anoEfzYw1HAjyRVUm/builds/eqJjelDq7cuO5Xz8f/openapi.json
