Website Tech Stack Detector & Tracking Pixel Checker avatar

Website Tech Stack Detector & Tracking Pixel Checker

Pricing

from $1.80 / 1,000 domain scanneds

Go to Apify Store
Website Tech Stack Detector & Tracking Pixel Checker

Website Tech Stack Detector & Tracking Pixel Checker

Detect tracking pixels in bulk: Meta (Facebook) Pixel, TikTok, Google Analytics (GA4), Google Tag Manager, Google Ads tag. A website tech stack detector and technology lookup: CMS, Shopify, CRM, consent, mail technologies per domain as JSON or CSV. No-ads-tag filter.

Pricing

from $1.80 / 1,000 domain scanneds

Rating

0.0

(0)

Developer

Solid Fetch

Solid Fetch

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

2 days ago

Last modified

Share

Detect tracking pixels in bulk: a marketing pixel detector and website tech stack detector for agencies and sales teams. Paste a list of domains, or call it with one domain from an AI agent, and see which of them use Meta Pixel (Facebook Pixel), TikTok Pixel, Google Analytics (GA4), Google Tag Manager or a Google Ads tag, plus the website technologies behind each site, such as CMS, shop platform, consent tool, CRM and e-mail marketing. It is a website technology lookup that returns the tech stack of each domain as JSON or CSV, for example to enrich a list of 500 domains with the marketing tools they use, or to find the Shopify stores on your list that have no analytics installed. You get one flat row per domain, ready for CSV. $3.00 per 1,000 domains on the free plan; blocked sites are free.

{ "domains": ["hubspot.com", "allbirds.com", "techcrunch.com"] }

One row of the result (shortened, see Output for all fields):

{ "domain": "acme.example", "status": "scanned", "hasGoogleAdsTag": true, "hasGA4": false, "hasGTM": false, "hasMetaPixel": true, "adTagIds": ["AW-123456789", "meta:123456789012345"], "cms": "WordPress", "leadSignals": ["runs Google Ads tag", "Google Ads tag without Google Analytics"] }

Blocked and unreadable sites are free, and pixel IDs are included: the Google Ads AW- id, the GA4 G- id, the Meta Pixel ID, the Tag Manager GTM- id and more come back as plain values in adTagIds, not only a yes/no. About 290 detection rules.

Paste a list of domains and get one flat row for each: which ad and analytics tags the homepage carries (Google Ads, GA4, Google Tag Manager, Meta Pixel, LinkedIn Insight, TikTok, Microsoft UET, Hotjar and more), which CMS or shop platform it runs on, which consent platform, CRM, chat and e-mail marketing tools it loads, who hosts its mail (Google Workspace, Microsoft 365, other), whether it has SPF and DMARC, and how old the domain is. Costs $3.00 per 1,000 domains on the free plan ($1.80 per 1,000 on Gold and above). Blocked sites, errors, repeats and domains left out by your filter are free.

Built for agencies and lead generation: the filters onlyWith and onlyWithout turn a prospect list into a target list, for example "only domains with no Google Ads tag" or "WordPress sites with no GA4 and no consent platform". A leadSignals column says in plain words what each site is missing.

What it does and does not do

  • One public page. For each domain the actor makes one plain HTTPS GET of the homepage, follows redirects (at most 5), and reads the HTML and the response headers. It checks the site's robots.txt first and does not fetch the page when the file disallows it.
  • No bot-protection bypass, no headless browser. The actor never bypasses bot protection and never runs a headless browser. If a site answers 403, 429 or a Cloudflare, DataDome or captcha page, the actor does not try again, rotate addresses or run a browser. The domain gets a free row with status: "blocked" and the reason. The same goes for robots.txt refusals, parked pages and unreachable sites.
  • Polite. Five domains at a time at most, a 15 second timeout, no retries, an honest User-Agent (SolidfetchStackScanner/1.0), and a redirect is never followed to an IP address or a local name.
  • No personal data. The actor reads script, link and meta tags and response headers. It does not read, keep or output e-mail addresses, phone numbers, names or page text. An e-mail address in your list is refused as an input. Domain age comes from RDAP; only the dates and the registrar company name are read, never the registrant.
  • Tag Manager containers are read too. Most real sites load their ad tags through Google Tag Manager, so the HTML alone would call them "no ads tag". When the homepage names a container (GTM-XXXX), the actor also fetches the public container script that every visitor's browser loads (googletagmanager.com/gtm.js?id=GTM-XXXX, one request per container for the whole run, at most 3 containers per page) and looks inside it for Google Ads conversion and remarketing tags, GA4, Meta Pixel, LinkedIn, TikTok, Microsoft UET, Hotjar and the rest of the rule list. Each technology says where it was found (source: html or gtm), and viaGtm is true when something was found only in a container. A container is a list of tags, not proof that they fire: a paused tag, or a tag that waits for a consent choice or a page rule, is listed too.
  • When a container cannot be read (the script answers an error, times out or is too large), the row has gtmUnread: true. Every tag boolean that would be false is then null (unknown), the lead signal reads tags may be inside GTM (container could not be read), and the row gets no "no ads tag" or "no analytics" signal. A filter such as onlyWithout: ["googleAdsTag"] leaves such a domain out, because the scan cannot back up "without". A container is asked for twice (429, 5xx and timeouts are retried once) and a failure that may pass is not remembered for the next domain. If the page shows no tag other than Tag Manager itself, the row is a free unreadable row; if it shows other tags, they stay in the row and the row is charged.
  • When the homepage is a JavaScript app (built with Next.js, Nuxt, Gatsby or Angular, or a React, Vue or Svelte root with little text) and its HTML holds no tag, the row is a free unreadable row with every tag boolean null: the actor does not run scripts, and such sites often add their tags from scripts. onlyWithout does not return these rows as having no tag.
  • Only what the page and its containers show. Tags that appear only after a click, on other pages or from a tag manager other than Google's are not seen.

What people use it for

  • A target list of sites with no ads tag. onlyWithout: ["googleAdsTag"] keeps the sites that do not show a Google Ads tag. Add anyAdTag to skip every site with an ad pixel. "No ads tag" means none was visible in the homepage HTML or its Tag Manager containers. Sites that load tags through their own platform loader or server-side tagging (large Shopify stores, for example) can still run ads, so check big brands with the Google Ads Transparency actor before you pitch.
  • Audit hooks for a pitch. Google Ads tag but no GA4, still on Universal Analytics (stopped collecting in 2023), ad pixels with no consent platform, a WordPress version from the 4.x era, no SPF or DMARC for a domain that sends mail.
  • Sorting a client or prospect list by platform. onlyWith: ["Shopify"], ["WordPress"], ["HubSpot"].
  • Competitor and market mapping. Which sites in a niche run Meta Pixel, TikTok, LinkedIn Insight, a chat widget or a CRM.
  • Mail and domain context. Mail provider (Google Workspace or Microsoft 365 for a "migrate or secure" pitch), DMARC policy, and a new domain (under a year) or an established one (10+ years).

Use it together with the Google Ads Transparency Center check

Our Google Ads Transparency Center scraper answers a different question for the same list of domains: does the domain advertise on Google right now (domainCheck mode, hasAds, activeLast30Days)? This actor answers what the site itself carries. Put both columns next to each other and the list sorts itself:

Transparency Center (hasAds)This actor (hasGoogleAdsTag, hasGTM)What it means
advertises, active last 30 daysno Google Ads tag, container read (gtmUnread: false)Advertises on Google per the Transparency Center but has no conversion tag in the page. Conversion tracking is missing or set up elsewhere: the strongest audit pitch.
advertiseshasGoogleAdsTag: null, gtmUnread: trueThe container could not be read, so tracking may sit inside it. Check before pitching.
advertisesGoogle Ads tag, no GA4Spends on ads without Google Analytics next to it.
no ads listedhas a Google Ads tagTag left behind from a past campaign, or ads not listed in the Transparency Center. A "restart or clean up" conversation.
no ads listedno ad tag at allCold lead: a business that does not advertise yet.

Run the two actors on the same domain list and join on domain in a spreadsheet, Clay, Make or n8n:

import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const domains = ['acme.example', 'globex.example'];
const ads = await client.actor('solidfetch/google-ads-transparency-scraper').call({ mode: 'domainCheck', domains, region: 'PL' });
const stack = await client.actor('solidfetch/marketing-stack-scanner').call({ domains });
const hasAds = new Map((await client.dataset(ads.defaultDatasetId).listItems()).items.map((row) => [row.domain, row.hasAds]));
const { items } = await client.dataset(stack.defaultDatasetId).listItems();
const pitch = items.filter((row) => hasAds.get(row.domain) === true && row.hasGoogleAdsTag === false);

A domain list of 1,000 costs you the Transparency Center price for the domain check (only the domains that advertise are charged there, see that actor's page) plus $3.00 here ($1.80 on Gold and above), or less when a filter leaves domains out.

How to use it

A list:

{
"domains": ["hubspot.com", "allbirds.com", "techcrunch.com"]
}

A pasted column (one per line, or separated by commas, semicolons or tabs), with URLs allowed:

{
"text": "acme.example\nhttps://www.globex.example/about\ninitech.example"
}

Only the sites with no Google Ads tag in the page and no GA4:

{
"domains": ["acme.example", "globex.example", "initech.example"],
"onlyWithout": ["googleAdsTag", "ga4"]
}

WordPress sites that load an ad pixel but have no consent platform:

{
"domains": ["acme.example", "globex.example"],
"onlyWith": ["WordPress", "anyAdTag"],
"onlyWithout": ["consentManager"]
}

A field name or a filter word the actor does not know stops the run with a hint such as "onlyWithout" ... "googleAdTag" (did you mean "googleAdsTag"?), before anything is requested or charged, so a typo cannot give you a run that ignored a setting.

Input

FieldWhat it does
domainsList of domains. URLs are reduced to the domain, www. is dropped, an entry may hold several domains separated by commas, semicolons or line breaks. Up to 10,000 entries per run. E-mail addresses are refused.
textA text blob with one domain per line, or separated by commas, semicolons or tabs. Used together with domains.
onlyWithKeep a domain only if it has every thing listed. Feature keys or technology names, see below.
onlyWithoutKeep a domain only if it has none of the things listed.
saveFailedDomainsOn by default: blocked, unreachable, parked, unreadable, redirected and robots.txt-refused domains get a free row with the reason. Turn it off to save only scanned domains.
followCrossDomainRedirectsOff by default. When the homepage redirects to a different registrable domain (old-brand.com to new-brand.com), the row is free with status: "redirected" and nothing of the other site is attributed to your domain. Turn on to read the other site and attribute its tags to the input domain: the row is then charged, and redirectedTo shows the host. The registrable domain is the last two labels of the host (three under co.uk, com.au and similar), without the Public Suffix List.

Filter words (case and spaces do not matter, so googleadstag and Google Ads Tag both work):

Feature keyTrue when
googleAdsTaga Google Ads tag (AW- id, conversion script) is in the page
ga4Google Analytics 4 (G- id)
universalAnalyticsthe legacy Universal Analytics (UA- id, analytics.js)
gtmGoogle Tag Manager
metaPixelMeta (Facebook) Pixel
linkedinInsightLinkedIn Insight Tag
tiktokPixelTikTok Pixel
microsoftUetMicrosoft Advertising UET tag
hotjarHotjar
anyAdTagany ad pixel: Google Ads, Meta, LinkedIn, TikTok, Microsoft, Pinterest, Snapchat, X, Reddit, Criteo, AdSense
anyAnalyticsany analytics or session recording tool
consentManagera cookie consent platform (Cookiebot, OneTrust, Usercentrics and others). Google Consent Mode alone does not count
chatToola chat or support widget (Intercom, Drift, Zendesk, Crisp and others)
crma CRM, marketing automation or e-mail marketing tool (HubSpot, Klaviyo, Mailchimp and others)
ecommercea shop platform (Shopify, WooCommerce, Magento, BigCommerce, PrestaShop)
mx, spf, dmarcthe domain has MX records, an SPF record, a DMARC record
googleWorkspace, microsoft365mail is hosted there
oldCmsan outdated CMS version is shown: WordPress below 6, Drupal below 9, Joomla below 4

Any technology name from the detection list works too: WordPress, Shopify, Wix, HubSpot, Klaviyo, Intercom, Cookiebot, Hotjar and so on.

A filter only sees what the scan could check. If a DNS query failed, the SPF and DMARC answers are unknown (null), and such a domain is left out of both spf filters rather than listed with a claim the scan could not back up.

Domains left out by a filter are scanned, but not saved and not charged. The summary counts them.

Output

One row per input that is saved, in input order. The row is flat, so the CSV export works as is. An example of the row shape (invented domain):

{
"domain": "acme.example",
"status": "scanned",
"error": null,
"leadSignals": ["runs Google Ads tag", "Google Ads tag without Google Analytics", "established domain (10+ years)"],
"hasGoogleAdsTag": true,
"hasGA4": false,
"hasUniversalAnalytics": false,
"hasGTM": false,
"hasMetaPixel": true,
"hasLinkedInInsight": false,
"hasTikTokPixel": false,
"hasMicrosoftUet": false,
"hasHotjar": false,
"hasAnyAdTag": true,
"hasAnyAnalytics": false,
"hasConsentManager": true,
"hasChatTool": false,
"hasCrm": true,
"hasEcommerce": false,
"adTagIds": ["AW-123456789", "hubspot:1234567", "meta:123456789012345"],
"cms": "WordPress",
"consentManager": "Cookiebot",
"technologyNames": ["Google Ads tag", "Meta Pixel", "Cookiebot", "HubSpot", "WordPress", "Cloudflare"],
"technologies": [{ "name": "WordPress", "category": "cms", "version": "6.4.3" }],
"emailProvider": "Google Workspace",
"mxHost": "aspmx.l.google.com",
"hasMx": true,
"hasSpf": true,
"hasDmarc": true,
"dmarcPolicy": "reject",
"dnsStatus": "ok",
"domainCreated": "2012-03-04",
"domainAgeYears": 14.6,
"domainExpires": "2027-05-01",
"registrar": "Example Registrar Inc.",
"rdapStatus": "ok",
"httpStatus": 200,
"finalUrl": "https://acme.example/",
"redirectedTo": null,
"input": "acme.example",
"duplicateOf": null,
"scannedAt": "2026-10-27T12:00:00.000Z"
}
FieldMeaning
domain, inputThe normalised domain (lower case, ASCII). input is the parsed domain too, never the pasted text, so a URL with a login or a query string is not stored. An entry that is not a domain and has an at sign or six or more digits is hidden.
statusscanned, or why there is no scan: blocked, robots-disallowed, robots-unavailable, unreachable, timeout, domain-not-found, http-error, not-html, too-many-redirects, redirect-blocked, parked, unreadable, redirected, duplicate, invalid-domain.
errorThe reason in a sentence. null for a scanned row.
leadSignalsShort sentences about what the scan means for a pitch: runs Google Ads tag, no ads tag, tags may be inside GTM (container could not be read), ad pixels but no Google Ads tag, Google Ads tag without Google Analytics, no analytics, still on Universal Analytics (stopped collecting in 2023), tracking tags without consent manager, old CMS: WordPress 4.9.8, online shop, no CRM or chat tool, new domain (under 1 year), established domain (10+ years), no SPF record, no DMARC record, DMARC policy is none (monitoring only).
hasGoogleAdsTag, hasGA4, hasUniversalAnalytics, hasGTM, hasMetaPixel, hasLinkedInInsight, hasTikTokPixel, hasMicrosoftUet, hasHotjarOne boolean per key tag, true when the page or a Tag Manager container shows it. null when the page was not read (blocked, unreachable) or a container could not be read, never false, so an unknown site is not mistaken for a site with no tag.
hasAnyAdTag, hasAnyAnalytics, hasConsentManager, hasChatTool, hasCrm, hasEcommerceSummary booleans over the whole rule list.
adTagIdsPublic tag ids found in the page: AW- (Google Ads), G- (GA4), GTM-, UA-, and meta:, linkedin:, tiktok:, uet:, hotjar:, hubspot:, klaviyo:, pinterest:, snap:, taboola:, quora:, adroll:, awin:, statcounter:, smartlook:, pendo:, piwikpro:, tealium:, cfwa: (Cloudflare Web Analytics token) plus the id, and DC- for a Floodlight tag. The Meta Pixel ID is meta: plus the number. These ids are published in every visitor's page source.
technologies, technologyNamesEvery technology found with its category (advertising, analytics, tag-manager, session-recording, consent, cms, website-builder, ecommerce, chat-support, crm-marketing, email-marketing, scheduling, payments, hosting-cdn, framework) and the version when the site shows it. technologyNames is the same as plain names, easy to filter in a spreadsheet.
cms, consentManagerThe main platform name and the consent platform name, for a quick column.
viaGtm, gtmContainers, gtmUnreadWhether a tag was found only inside a Tag Manager container, the container ids the page loads, and whether a container could not be read (then false tag booleans are null). Each entry of technologies also has source: html or gtm.
emailProvider, mxHost, hasMxMail provider read from the MX hosts: Google Workspace, Microsoft 365, a named provider such as Zoho Mail, a gateway such as Proofpoint (gateway) (what sits behind a gateway is not visible), Other, or None.
hasSpf, hasDmarc, dmarcPolicyWhether the domain publishes SPF and DMARC records, and the DMARC policy (none, quarantine, reject). null when the DNS query failed.
dnsStatusok, nxdomain or error.
domainCreated, domainAgeYears, domainExpires, registrar, rdapStatusFrom the registry's public RDAP service. rdapStatus is ok, unsupported-tld (many country domains have no RDAP service), not-found, rate-limited or error. Dates are null then, and the row is still charged.
pageTruncatedtrue when the page was larger than the actor reads (1.5 MB) and was cut: a tag that would be false is null, because it may be after the cut. A cut page with no tag found in the part read is a free unreadable row.
httpStatus, finalUrl, redirectedToThe answer for the homepage, its final address without the query string, and the host when the site redirects to another domain.
duplicateOfThe domain this row repeats.
scannedAtWhen the scan ran.

The run also writes a SUMMARY record with the counts by status, how many rows were charged, left out by a filter and free, and how many DNS, page and RDAP lookups were made.

Pricing

Pay per event, prices on the Pricing tab.

EventCharged for
domain-scannedEach domain whose homepage was read (status: "scanned"), is not a repeat of an earlier one, and passes your filter.

Free: blocked sites (403, 429, bot-protection pages), unreadable pages (empty or tiny answers, redirect-only pages, empty app shells, JavaScript apps with no tag in their HTML, HTTP 202, a page whose Tag Manager container could not be read or that was cut when nothing was found in the part read), redirected rows, robots.txt refusals, unreachable and timed-out sites, domains that do not exist, parked pages, HTTP errors, repeats, entries that are not domains, and every domain left out by onlyWith or onlyWithout. Rows past your maximum cost per run are not saved.

A run's last line adds it up, for example 812 domains scanned and charged as domain-scanned. free: 140 scanned but left out by the filter, 38 that could not be scanned (blocked, unreachable, no page), 10 repeats. 1,000 inputs.

Limits

  • Homepage only. A tag that is only on a checkout, a landing page or a blog post is not seen.
  • Other tag managers. Google Tag Manager containers are read. Adobe Launch, Tealium, Segment and similar are detected as technologies, but what they load is not read.
  • Server-side and consent-gated tags. A tag that waits for a consent choice, or that is served from the site's own domain (server-side tagging), may not match a rule.
  • Rule list, not a guess. About 290 rules written for this actor from the install snippets and script hosts vendors publish in their own documentation (none copied from another fingerprint list, no third-party rule file loaded): ad pixels and networks, analytics and A/B testing, session recording, consent platforms, CMS, page builders and frameworks, shop platforms and shop apps, chat and booking widgets, CRM, lead capture and e-mail marketing, payments, hosting and CDN. A technology that is not on the list is not reported. The version, when shown, comes from the site's generator tag.
  • Blocked is common. Large sites and many small ones behind bot protection answer 403. Expect a share of free blocked rows. The actor does not work around it.
  • RDAP coverage. The IANA bootstrap lists a service for most generic and many country domains, not all. Registries limit the request rate: the actor spaces requests out and skips a registry that says to slow down.
  • A subdomain is looked up in DNS and fetched as given. Its domain age is read from the registered domain above it.
  • Up to 10,000 entries per run. About 3 domains a second, so a 5,000-domain list takes roughly half an hour.

Privacy

Your domain list is your data. The actor does not log it (the log holds counts only), keeps it nowhere except the dataset of the run, and sends it only to the domains themselves, to DNS, to Google's public Tag Manager script address for the container ids the pages name (the ids, not your domains), and to the registry of each domain's top-level domain for the domain-age lookup. The run's restart record holds a hash of the list and counters, not domains.

The output holds no personal data: no e-mail addresses, phone numbers or names from pages, and no registrant data from RDAP.

When a run fails, an error report with the error message (your domains replaced by a placeholder) and the shape of the input (which fields are set, how long the lists are, which filter words), never your domains or your results, is sent to the developer to fix bugs.

Did this help? A review on this page, or an issue saying what is missing, helps other people find it.