Tech Stack & Buying Signal Enrichment avatar

Tech Stack & Buying Signal Enrichment

Pricing

from $10.00 / 1,000 domain scanneds

Go to Apify Store
Tech Stack & Buying Signal Enrichment

Tech Stack & Buying Signal Enrichment

Detect what any website is built with: CMS, ecommerce, payments, analytics, tag managers, CDN, hosting, ATS, email and JavaScript frameworks, with evidence for every detection. Get a vendor_change record when a site swaps a tool. Pay per domain scanned, no subscription.

Pricing

from $10.00 / 1,000 domain scanneds

Rating

0.0

(0)

Developer

Flux Pull

Flux Pull

Maintained by Community

Actor stats

0

Bookmarked

1

Total users

0

Monthly active users

3 days ago

Last modified

Share

Give this Actor a list of domains and get back the full technology stack behind each one — CMS, ecommerce platform, payment processors, analytics, tag managers, CDN, hosting, applicant tracking system, email provider, security headers, JavaScript frameworks, live chat, A/B testing, CRM and cookie-compliance tooling — resolved from HTML, response headers, cookies, DNS records and the TLS certificate.

It also does the part a one-shot lookup cannot: change detection. Every scan is compared against your previous scan of the same domain, and when the stack actually moves — and the next scan confirms it — a vendor_change record is appended to a change-feed dataset that spans every run. That is how a sales team learns that a prospect just left Shopify, dropped Segment or installed a new tag manager — on the day it happens, rather than at the next quarterly data refresh.

You pay per domain scanned, not per month. A new Apify account comes with $5 of free platform credit every month, and this Actor draws on it like any other Store Actor — about 500 domains scanned before you pay anything, with no credit card required.

What you get for each domain

Field groupWhat it contains
TechnologiesEvery match from a 7,613-technology, 109-category fingerprint database — name, slug, categories, version where visible, a 0–100 confidence score, and the evidence that produced it
Headline fieldsFlat cms, ecommerce, emailProvider, ats and techCount columns, so a CSV export is usable without unpacking nested JSON
Category summaryGrouped view: analytics, payments, CDN, hosting, tag manager, live chat, CRM, A/B testing, cookie compliance, security, JavaScript frameworks
DNSMX, NS, CNAME, A records, SPF include: targets, third-party TXT verification records (which name the SaaS tools a company has actually signed up for), the inferred email provider, and whether a DMARC policy is published
TLSCertificate issuer and organisation, validity window, SAN count, wildcard flag, negotiated TLS version and ALPN
Security postureHSTS and its max-age, CSP, X-Frame-Options, X-Content-Type-Options, referrer and permissions policy
Change recordadded, removed, versionChanged, dnsChanged, tlsIssuerChanged, plus a before/after summary — only when something really changed, and only once a second scan agrees. On the row as changes and as flat changesAdded/changesRemoved/changesUpgraded/changesPending columns; collected in a change-feed dataset that spans every run
ProvenanceThe exact fingerprint-database commit each row was produced with, so a disputed detection is reproducible

Every detection carries its evidence: which field matched (header, cookie, meta, scriptSrc, html, dns, tls, robots, js, dom, xhr, css, implies …), the pattern that fired, and the matched text. Nothing is a black box.

How to detect what a website is built with

  1. Open the Actor and paste your domains into Domains — bare hosts (allbirds.com), full URLs, internationalised domains and ports all work, and duplicates are removed. For long lists use Domains (pasted list) or point Domains file URL at a public text or CSV file.
  2. Leave everything else on its defaults. DNS, DMARC and TLS lookups are on and cost no bandwidth; renderJs is off; change detection is on.
  3. Click Start. A Tier 1 scan takes about 0.8–1.7 seconds per domain and 20 run in parallel by default. A 15-domain smoke list completed at 38 domains per minute at half that concurrency, so a list of a thousand is minutes of work, not hours.
  4. Read the results in the Overview dataset view, or export to CSV, JSON, Excel or Google Sheets. The dataset holds exactly one row per domain you asked for. Changes since your last run appear in the Vendor changes view, and are also collected in a change-feed dataset of their own that keeps growing across runs.

The first run of a domain records a baseline and never reports a change — there is nothing to compare it against yet. From the second run on, you get the difference, and by default a difference has to show up in two consecutive scans before it counts.

Minimal input

{
"domains": ["allbirds.com", "vercel.com", "wix.com"]
}

A realistic input

{
"domains": ["allbirds.com", "techcrunch.com", "wix.com", "1password.com/careers"],
"maxConcurrency": 20,
"timeoutSecs": 12,
"includeDns": true,
"includeTls": true,
"includeWellKnown": true,
"minConfidence": 51,
"trackChanges": true,
"renderJs": false
}

Pricing: how much does a website technology lookup cost?

This Actor is billed pay per event. You are charged for the events below and for nothing else — the platform compute the run consumes is not billed to you on top of them. (Apify offers a separate "pay per event + usage" mode that does bill platform usage separately; it is not used here.) That includes proxy bandwidth: the datacenter proxy you can switch on in Proxy configuration is paid for out of the event price, not charged to you on top of it. Residential proxies are the one thing this pricing cannot absorb — see Do I need a proxy? — so a run configured with the RESIDENTIAL group stops immediately and explains why, rather than scanning and billing you for it.

EventCharged whenPrice
apify-actor-startOnce per run, charged by the platform at its default rate: one event per GB of run memory, and Apify waives the first 5 seconds of compute$0.00005 per GB
domain_scannedAn HTTP response of any status came back, or at least one technology was detected$0.010
domain_failedNo HTTP response at all and zero technologies$0.001
domain_renderedA JavaScript render completed and produced signals — charged in addition to domain_scanned$0.025
vendor_changeThe stack differs from your stored snapshot and the next scan agrees — charged once per changed domain, never per changed technology$0.070

Try it free. Apify's free plan includes $5 of platform usage credit each month and asks for no credit card, and pay-per-event Actors draw on that credit like everything else in the Store. At $0.010 a scan that is roughly 500 domains at no cost, or roughly 140 with renderJs: true at $0.035 a domain. Set dryRun: true to price a list exactly before any of the credit is spent.

A 4xx or 5xx is a result, not a failure: headers, DNS and TLS still ship and usually still carry detections, so those rows bill as domain_scanned. domain_failed is the tenth-of-a-price case where nothing answered and nothing was found.

Worked examples

1,000 domains, one run, no JavaScript rendering

ItemQuantityUnitTotal
Actor start1 run at 4 GB$0.00005 per GB$0.0002
domain_scanned1,000$0.010$10.00
Run total$10.00

1,000 domains, one run, renderJs: true

ItemQuantityUnitTotal
Actor start1 run at 4 GB$0.00005 per GB$0.0002
domain_scanned1,000$0.010$10.00
domain_rendered1,000$0.025$25.00
Run total$35.00

500 domains monitored daily for 30 days, 2% of them changing during the month

ItemQuantityUnitTotal
Actor start30 runs at 4 GB$0.00005 per GB$0.006
domain_scanned500 × 30 = 15,000$0.010$150.00
vendor_change2% of 500 = 10$0.070$0.70
Month total$150.71

Change detection is the cheap part of monitoring: the scans cost the money, and a change bills once per domain no matter how many tools moved. Suppression rules (below) make sure a daily schedule does not bill the same change thirty times, and a move has to show up in two consecutive scans before it bills at all — so a site that shuffles its ad tags on every visit is not a line on your invoice.

Set Dry run to compute everything and charge nothing — that is how to price a large run before you commit to it. You can also cap a run's spend with Apify's maximum-cost setting.

Input reference

FieldTypeDefaultWhat it does
domainsarrayDomains or URLs to scan. Bare hosts, full URLs, IDN and ports accepted; duplicates removed
domainsTextstringThe same list pasted as text, split on newlines, commas, semicolons or spaces
domainsUrlstringPublic URL of a plain-text or CSV-first-column file, one domain per line (max 50 MB)
maxDomainsinteger0Hard cap on domains processed this run; 0 means no cap
maxConcurrencyinteger20Parallel domain scans
timeoutSecsinteger10Per-HTTP-request timeout
followWwwbooleantrueIf the apex domain fails, retry the www. variant and vice versa
includeDnsbooleantrueTXT / MX / NS / CNAME / SOA lookups — no bandwidth cost, and the source of email-provider and SaaS-verification signals
includeDmarcbooleantrueOne extra TXT query for _dmarc.<domain>
includeTlsbooleantrueCertificate issuer, validity, SAN count, wildcard, TLS version
includeWellKnownbooleanfalseTwo extra small requests for /robots.txt and /sitemap.xml
inlineJsHeuristicbooleantrueApproximate JavaScript-variable detection by searching inline script text; capped at confidence 50
minConfidenceinteger50Drop detections below this score. 51 excludes every inline-JS-only result
categoriesFilterarray[]Report only these categories. Detection always runs in full, so implied technologies stay correct
renderJsbooleanfalseTier 2. Render the page in headless Chromium. Extra charge; needs at least 2 GB of run memory
renderConcurrencyinteger4Tier 2. Parallel browser contexts, clamped by run memory
renderTimeoutSecsinteger25Tier 2. The browser's budget for one page — loading it and reading it. Separate from timeoutSecs, which bounds one HTTP request. Heavy pages behind a proxy want 20–40
renderBlockResourcesstringdefaultTier 2. default blocks images, media and fonts; aggressive also blocks stylesheets (and the css detections with them); none downloads everything
trackChangesbooleantrueCompare against your last scan and emit vendor_change records
snapshotStoreNamestringtech-stack-snapshotsNamed key-value store holding your scan history. Use different names for separate tracking sets
changesDatasetNamestringtech-stack-changesNamed dataset collecting every vendor_change record across runs — your change feed. Created the first time a change is confirmed
changeMinConfidenceinteger75A technology must score at least this to count as added or removed
changeConfirmScansinteger2Consecutive scans a move must survive before it counts as a change and is billed. Candidates wait in changes.pending. 1 bills on the first scan that sees a difference
purgeSnapshotsbooleanfalseEmpty the snapshot store before the run — how you honour a deletion request or start a tracking set over
dryRunbooleanfalseCompute changes, leave history untouched, charge nothing
proxyobjectproxy offApify Proxy configuration. Not needed for normal use. Datacenter groups and your own proxyUrls are supported; the RESIDENTIAL group is not
userAgentstring""Leave empty for a realistic Chrome fingerprint, or set your own

Output examples

A scanned domain

One domain_result row per domain. Trimmed here — marks where lists were cut:

{
"type": "domain_result",
"domain": "allbirds.com",
"finalUrl": "https://www.allbirds.com/",
"status": 200,
"redirectChain": [{ "url": "https://allbirds.com", "status": 301 }],
"scannedAt": "2026-09-15T11:43:11.328Z",
"durationMs": 1223,
"tier": 1,
"cms": "Backdrop",
"ecommerce": "Shopify",
"emailProvider": "Microsoft 365",
"ats": null,
"techCount": 30,
"changed": true,
"changeCount": 3,
"changes": {
"previousScannedAt": "2026-09-14T11:39:15.053Z",
"added": [{ "name": "PayPal", "categories": ["Payment processors"] }],
"removed": [{ "name": "Adyen", "categories": ["Payment processors"] }],
"versionChanged": [],
"dnsChanged": false,
"tlsIssuerChanged": false,
"pending": { "added": ["Hotjar"], "removed": [], "versionChanged": [] },
"confirmScans": 2
},
"categorySummary": {
"ecommerce": "Shopify",
"payments": ["PayPal", "Catch", "Shop Pay"],
"cdn": "Cloudflare",
"tagManager": "Google Tag Manager",
"emailProvider": "Microsoft 365",
"liveChat": "Gladly",
"security": ["HSTS", "reCAPTCHA"],
"jsFrameworks": ["ExtJS", "Vue.js"]
},
"technologies": [
{
"name": "Shopify",
"slug": "shopify",
"categories": [{ "id": 6, "name": "Ecommerce" }],
"confidence": 100,
"evidence": [
{ "type": "meta", "pattern": "shopify-digital-wallet", "match": "/11044168/digital_wallets/dialog" },
{ "type": "scriptSrc", "pattern": "cdn\\.shopify\\.com", "match": "cdn.shopify.com" }
],
"website": "https://shopify.com",
"saas": true
}
],
"dns": {
"mx": ["allbirds-com.mail.protection.outlook.com"],
"cname": "shops.myshopify.com",
"txtVerifications": [
{ "vendor": "DocuSign", "record": "docusign=7de1885c-…" },
{ "vendor": "Miro", "record": "miro-verification=523f3ed99ca1…" }
],
"spfIncludes": ["allbirds_com._es.easydmarc.com", "Microsoft 365"],
"provider": "Microsoft 365",
"dmarcPresent": true
},
"tls": {
"issuerOrg": "Let's Encrypt",
"validTo": "2026-11-08T22:33:31.000Z",
"tlsVersion": "TLSv1.3",
"alpn": "h2"
},
"security": { "hsts": true, "csp": true, "xFrameOptions": "DENY" },
"fingerprintsCommit": "2b5eed3a5eede8b2415fcd0b27c003d625ad88d3"
}

changes is the whole comparison with your previous scan of this domain, carried on the row itself — so one row per domain is all you ever have to read. changes.pending is the waiting room: Hotjar was seen in a new state once, it is not counted in changeCount, and it costs nothing unless the next scan agrees.

The same comparison is repeated as four flat columns — changesAdded, changesRemoved, changesUpgraded ("Sentry 7.0.0 → 7.12.1") and changesPending ("+Hotjar", "-Datadog") — because a CSV export and the Console table cannot read nested JSON. They are what the Vendor changes view displays, and they are absent on a first scan, when there is genuinely nothing to compare.

The DNS block is worth a second look. cname: shops.myshopify.com is the ecommerce platform confirmed at the infrastructure layer; mx names the email provider; and every txtVerification is a SaaS product the company went to the trouble of verifying a domain for. None of that costs a page load.

Monitor technology changes over time

Set a schedule, leave trackChanges on, and the Actor keeps a private history of every domain you have scanned. Snapshots live in a named key-value store on your Apify account — tech-stack-snapshots by default, renameable with snapshotStoreName — hashed into 256 buckets, written through on every scan, kept for 180 days. Each domain keeps one history per tier, so a rendered scan is only ever compared with your previous rendered scan and a Tier-1 scan with your previous Tier-1 one.

Your change feed is a dataset of its own

Every run's dataset holds exactly one row per domain you asked for — nothing else. Confirmed changes go somewhere better: a named dataset on your own account, tech-stack-changes by default and renameable with changesDatasetName, which the Actor appends to run after run. That store is your change feed. Point a webhook, a schedule, a spreadsheet or a BI tool at it and you have every vendor move this account has ever detected in one place, instead of one fragment per run expiring on its own retention clock. It is created the first time a change is confirmed, so an account that has never seen one never has an empty dataset lying around, and each run's OUTPUT records its name, id and how many records that run contributed.

A record looks like this:

{
"type": "vendor_change",
"domain": "allbirds.com",
"scannedAt": "2026-09-15T11:43:11.328Z",
"previousScannedAt": "2026-09-15T11:39:15.053Z",
"added": [
{ "name": "Google Tag Manager", "categories": ["Tag managers"] },
{ "name": "PayPal", "categories": ["Payment processors"] }
],
"removed": [{ "name": "Adyen", "categories": ["Payment processors"] }],
"versionChanged": [],
"dnsChanged": false,
"tlsIssuerChanged": false,
"changeCount": 3,
"summaryBefore": { "ecommerce": "Shopify", "emailProvider": "Microsoft 365", "techCount": 30 },
"summaryAfter": { "ecommerce": "Shopify", "emailProvider": "Microsoft 365", "techCount": 30 }
}

The same information is on the domain's own row under changes, so you can work purely from the one-row-per-domain dataset if you prefer.

A change has to happen twice

A technology seen in a new state once is a candidate, not a change. It is reported on the row under changes.pending, it does not move changed or changeCount, and it is not billed. The next scan decides: if it agrees, the change is confirmed, written to the change feed and charged once; if it contradicts it, the candidate is dropped and you never hear about it. changeConfirmScans sets how many consecutive scans are needed — 2 by default, up to 5, or 1 to be told the moment anything differs.

This exists because the noisiest changes are all true. An ad-heavy site loads a different subset of its ad-tech tags on every visit, so AppNexus, Tapad, Hotjar, Cxense, Rubicon Project, Facebook Pixel, DoubleClick Floodlight and Datadog really do come and go between two scans minutes apart — no confidence floor, category rule or scan-quality check can tell that from a real migration, because nothing about the detection is wrong. Persistence can: a company that adopts a tool still has it tomorrow. A real move therefore costs you one extra scan cycle to hear about, and a flicker costs you nothing at all.

A change is only a change when it is worth acting on, so several rules sit between "the lists differ" and "you are billed":

RuleEffect
First scanNever bills a change. There is no "before"
ConfirmationA move must be seen in the same new state changeConfirmScans scans in a row (default 2). Until then it sits in changes.pending, costs nothing, and disappears silently if the next scan disagrees
Confidence floorOnly technologies at changeMinConfidence (default 75) or above take part. The default output floor is 50, so a low-confidence heuristic hit can never bill a change on its own
Partial scanIf a scan sees fewer than half the technologies of the previous snapshot — a WAF challenge, a timeout — removals are suppressed (changes.suppressed: "partial_scan")
No responseA scan that never reached the site reports additions from DNS and TLS but never removals ("no_response"). What the page would have shown is unknown, not gone — and your stored history keeps it, so the next healthy scan does not report the site as freshly installed
24-hour repeatAn identical change set inside 24 hours is suppressed ("repeat"), so a daily schedule bills once per real change
FlapA change that exactly undoes the previous one is suppressed ("flap"). Sites answering from several edge nodes otherwise oscillate forever
Page characteristicsTechnologies filed only under "Miscellaneous" — HTTP/3, Open Graph, PWA, RSS — stay on the row but never bill a change. They depend on which edge node answered
Certificate authoritiesTechnologies filed only under "SSL/TLS certificate authorities" — DigiCert, Let's Encrypt, Sectigo and the rest — stay on the row but never bill a change. Which authority you see depends on which edge node answered and on the renewal cycle, not on a decision anyone made
Separate history per tierA rendered scan is compared only with your previous rendered scan, never with a Tier-1 one. A browser sees tools a static fetch cannot, so mixing the two would bill you for the change of instrument. Switching renderJs on starts a second history, and its first scan is a first scan
DNS and TLS movesNever bill on their own. changes.tlsIssuerChanged is reported and never charged; changes.dnsChanged covers MX and NS only — a real email-provider or DNS-host move — while A and CNAME records are left out, because they differ per edge node. A charge always means a technology was added, removed or upgraded
Version appearing from nowhereA version is only an upgrade when both snapshots carried one. A technology that merely gains a version is not billed

To delete your history, run once with purgeSnapshots: true, or delete the tech-stack-snapshots store from Storage in Apify Console. Either way, every domain then looks like a first scan and nothing is billed for a change. The change feed is a separate dataset and is left alone; delete it from Storage → Datasets if you want the record of past changes gone too.

When to turn on JavaScript rendering

Tier 1 — the default — fetches one page (body capped at 1.5 MB) plus DNS and TLS. No browser, and that is why it is fast and cheap.

Tier 2 (renderJs: true) loads the page in real headless Chromium and reads what only a browser can see: the actual values of 5,594 JavaScript property paths, 1,787 DOM selectors evaluated by a real CSS engine, the hosts the page calls over fetch/XHR, scripts injected after load, and stylesheet text. Tier 2 is a superset of Tier 1 — it merges the rendered DOM with the transport body rather than replacing it, so a paid render can never lose evidence the free fetch already had.

Measured technology counts:

DomainTier 1Tier 2Tier 1 at minConfidence: 51Tier 2 at minConfidence: 51
allbirds.com30381324
vercel.com37432735
wix.com45503238

Turn it on when you care about client-side tools that never appear in HTML: animation and personalisation libraries, wallet buttons, affiliate and attribution scripts, tag managers that inject other tags, consent platforms loaded after interaction.

Leave it off for platform, CDN, hosting, email and ATS attribution. Those come from headers, DNS and TLS, where a render adds nothing but cost.

Memory. Rendering needs at least 2048 MB of run memory. Below that the run stays on Tier 1, every row is stamped renderSkipped: "insufficient_memory", and no domain_rendered is charged — you are never billed for a render that did not happen. Above the floor, each concurrent render needs about 1 GB on top of the first GB:

slots = min(renderConcurrency, max(1, floor((memoryMbytes − 1024) / 1024)))
Run memoryEffective parallel renders (at renderConcurrency: 4)
2048 MB1
3072 MB2
4096 MB3
5120 MB and above4

Asking for more renders than the memory affords is not an error — the run just does them in smaller batches. 4096 MB is the comfortable setting for rendering.

Time. One render gets renderTimeoutSecs (default 25 s) to load the page and read it — navigation, the settle, and the in-page probe all share that one budget, so a page that pins its main thread costs one domain rather than a run. A render that runs out stays on Tier 1 with renderSkipped: "render_failed" and charges nothing extra. Most pages finish in 3.5–4.7 seconds; heavy pages, and anything behind a proxy, want 20–40.

timeoutSecs is a different number and stays small: it bounds a single HTTP request, and a browser loading a page with a hundred subresources is not a single HTTP request.

Bandwidth. Images, media and fonts are blocked at the network layer by default, which brings a render down to roughly 1.6–3.4 MB per page (measured: allbirds 3.4 MB, vercel 1.6 MB, wix 2.5 MB). renderBlockResources: "aggressive" drops stylesheets too — cheaper and faster, at the cost of the ~200 technologies detected from stylesheet text.

Recommendations and tips

  • minConfidence: 51 is the one-flag precision mode. It drops every detection that rests only on the inline-JS heuristic, which is capped at exactly 50. Verified over allbirds, vercel and wix: 0 inline-JS-only technologies survive, while technologies that have real evidence and an inline-JS hit keep both. The default of 50 favours recall.
  • Leave DNS and TLS on. They cost no bandwidth, they work even when the site blocks you, and SPF include: targets plus TXT verification records are among the strongest buying signals available anywhere.
  • Turn on includeWellKnown if you want robots-derived detections and sitemap hints; it costs two small requests per domain.
  • Treat ats as a bonus, not a headline. Applicant tracking systems are almost never linked from a homepage — companies link /careers on their own domain and the vendor host appears one click deeper. Pass example.com/careers explicitly when you want ATS coverage; that is how 1password.com/careers resolves to Ashby.
  • Use categoriesFilter to narrow the output, not the detection. Detection always runs in full so implied technologies stay correct; the filter only trims what is reported.
  • Rehearse big runs with dryRun: true. Everything is computed and pushed; nothing is charged.
  • Keep separate tracking sets apart with snapshotStoreName — one store per customer list, per territory, per campaign.

Use it as a tech stack API

The Actor supports Standby mode: a warm container that answers HTTP requests directly, with no run to start and no queue to wait for. Your Standby URL is per-Actor and stable across runs — copy it from the Endpoints tab on the Actor's detail page in Apify Console rather than building it by hand. Requests authenticate exactly like the Apify API, with Authorization: Bearer <your token>.

# One domain, with DNS and TLS.
curl -H "Authorization: Bearer $APIFY_TOKEN" \
'https://<your-standby-url>/?domain=allbirds.com&dns=1&tls=1'
# The same request with a JavaScript render.
curl -H "Authorization: Bearer $APIFY_TOKEN" \
'https://<your-standby-url>/?domain=allbirds.com&render=1'
# Up to 25 domains in one call. The body takes the whole input object.
curl -X POST -H "Authorization: Bearer $APIFY_TOKEN" \
-H 'content-type: application/json' \
-d '{"domains":["vercel.com","wix.com"],"includeDns":true,"includeTls":true}' \
'https://<your-standby-url>/'
# Liveness and readiness.
curl -H "Authorization: Bearer $APIFY_TOKEN" 'https://<your-standby-url>/health'

GET / returns a single domain_result. POST / returns { "count": 2, "results": [ … ] }. GET /health returns readiness, the loaded fingerprint commit, in-flight requests and the run's event totals. GET /openapi.json serves the full OpenAPI contract, which is also what renders the interactive Endpoints tab.

Query parameters mirror the batch input: domain, dns, dmarc, tls, wellKnown, inlineJs, render, renderConcurrency, track, minConfidence, categories (comma-separated), timeoutSecs, followWww, userAgent.

Errors come back as JSON with an error object carrying a code and a message: 400 for an invalid request, 402 once the run's pay-per-event charge limit is reached, 429 when the container is already running its maximum number of scans or, for render=1, when every render slot is busy (RENDER_BUSY, with a Retry-After header: retry after a few seconds, or give the Standby container more memory for more slots), and 504 when a scan takes longer than 30 seconds. A scan that runs into the 504 still finishes in the background and is still billed, so treat a 504 as a signal to lower timeoutSecs for that domain or to scan it in batch mode instead.

Measured on a warm container:

RequestTime
Tier 10.65–0.87 s
First render=1 in a container (loads the browser)4.3–5.0 s
Subsequent render=13.5–3.8 s

Chromium is loaded lazily: a container that only ever answers Tier-1 requests never starts a browser, and /health reports tier2.loaded so you can confirm it.

Scheduling and integrations

Because this is an Apify Actor, everything the platform offers comes for free: schedules (scan your watchlist every morning), webhooks on run completion, exports to CSV, JSON, XML, Excel and Google Sheets, and the standard Apify integrations — Zapier, Make, Slack, Airbyte, LangChain and the rest — plus the Apify API and the JavaScript and Python SDKs for anything custom.

A practical monitoring setup: one scheduled daily run over your account list with trackChanges: true, and a webhook on run completion that reads the tech-stack-changes dataset — your change feed, which accumulates across every run — and posts what is new into Slack or your CRM. Each run's OUTPUT says how many records it added, so a webhook can skip the read entirely when the answer is zero. The run's own dataset stays one row per domain, and its Vendor changes view shows the same comparison per domain if you would rather work from there.

FAQ

Is there a free trial?

Effectively, yes, and it is Apify's rather than ours. Every new account is on the free plan, which includes $5 of platform usage credit each month and requires no credit card, and pay-per-event Actors like this one draw on that credit the same way the rest of the Store does. At $0.010 per Tier 1 scan that is about 500 domains a month at no cost, or about 140 with renderJs: true. Run with dryRun: true first to see exactly what a list would cost before any of the credit is spent.

How much does it cost to scan 10,000 domains?

$100.00 for a Tier 1 run: 10,000 × $0.010, plus a run-start fee of a fraction of a cent. With renderJs: true it is $350.00, because each render adds $0.025 on top of the scan. Domains that answer nothing at all bill $0.001 instead of $0.010.

How accurate is it, and what does the confidence score mean?

Confidence is a 0–100 score aggregated from the evidence behind a detection. A response header, a cookie, a meta tag, a script URL, a DNS record or a real JavaScript global is hard evidence and typically scores 100. The inline-JS heuristic — searching inline script text for a variable name that a browser would have to run the page to confirm — is capped at exactly 50 and always tagged as inlineJs evidence. Setting minConfidence: 51 therefore removes every detection that rests only on a guess, which is the precision mode. Every row also records the fingerprint-database commit it was produced with, so a disputed detection can be reproduced exactly.

How is this different from BuiltWith?

BuiltWith sells subscriptions — its published plans start at $295 per month and go to $995 per month (checked 2026-09-15) — and its main product is a large historical index you query. This Actor is priced per domain scanned, at $0.010 for a Tier 1 scan, with no subscription and no minimum. It scans live at the moment you ask rather than serving you a cached crawl, and it returns the evidence for every detection. What it does not have is BuiltWith's multi-year historical archive: your history here starts the first time you scan a domain. If you need "what did this site use in 2019", that is a different product. If you need "what is this site using now, and tell me when it changes", this is the cheaper answer and a reasonable BuiltWith alternative for prospecting and enrichment workloads.

How is this different from Wappalyzer?

Wappalyzer is best known as a browser extension and an API over the same open fingerprint tradition. This Actor uses the community fingerprint dataset maintained at enthec/webappanalyzer — the continuation of the open Wappalyzer rule set — pinned to an exact commit, with a detection engine written from scratch. As a Wappalyzer alternative the practical differences are: DNS, DMARC and TLS signals that a page-only scanner does not collect; explicit evidence on every detection; batch and warm-API modes on the same codebase; and change detection between scans, which no lookup-only tool can provide.

Can I use it as an API?

Yes, two ways. Standby mode gives you a warm HTTP endpoint that answers a single domain in under a second (see above). The regular Apify API lets you start a run, poll it and read the dataset, which is the better fit for large batches. Both are covered by your Apify token.

Can I schedule it and get alerted when something changes?

Yes. Use an Apify schedule for the run and a webhook on run completion, then read the tech-stack-changes dataset — the change feed every run appends to, so your alerting reads one growing store rather than hunting through per-run datasets. OUTPUT.changesDataset names it and says how many records the run added. Only genuinely changed domains produce a vendor_change record: a move has to repeat on the next scan before it counts, and the suppression rules above mean a daily schedule bills and reports a given change once, not once per run.

What data is stored, and how do I delete it?

Three things. Your results live in the run's dataset, like any Actor. Your confirmed changes are appended to a named dataset — tech-stack-changes unless you rename it — which persists across runs and holds nothing but the change records themselves; delete it from Storage → Datasets whenever you like. Your scan history lives in a named key-value store on your own Apify account — tech-stack-snapshots unless you rename it — holding, per domain and per tier, the technology names and confidences, a hash, the TLS issuer and a DNS fingerprint. A domain you scan both ways therefore has two entries, one for its Tier-1 history and one for its rendered history, in the same bucket record. No page content is stored. Retention is 180 days. To delete it, run once with purgeSnapshots: true — which clears both histories — or delete the store from Storage in Apify Console. The Actor runs under your account and writes only to your own storages.

Why does a site show 0 technologies?

Usually one of four reasons. The site returned no HTTP response at all, in which case the row carries an error code and bills as domain_failed. It served a challenge or block page to a datacenter IP — the row is still a result, and DNS and TLS still produce detections. It is a genuinely plain site with no recognisable fingerprints. Or its stack is entirely client-rendered, which is what renderJs: true is for. The error field and the status code tell you which.

When should I turn rendering on?

When you need client-injected tools — see the section above for measured before/after counts. For platform, CDN, hosting, email and ATS attribution, Tier 1 already has the answer and a render only adds cost.

Do I need a proxy?

No, not for normal use. This Actor fetches the domains you already do business with, and there is nothing anti-bot to defeat. If a specific target blocks Apify's datacenter addresses, switch on the default datacenter proxy in Proxy configuration, or supply your own proxyUrls — either way the bandwidth is covered by the per-event price and is never billed to you on top of it.

Residential proxies are not supported. A rendered page is a few megabytes and residential transfer is roughly $8/GB, which is more than the domain_rendered event itself earns — so the price on this Actor cannot honestly include it, and charging you twice for the same run would be worse. A run configured with the RESIDENTIAL group stops in its first second with that explanation and scans nothing, so you are never billed for it. A 403 is not a dead end anyway: headers, DNS and TLS still produce a usable row.

Known limitations

  • A failed render is not retried. The row degrades to Tier 1, is stamped renderSkipped: "render_failed", and no domain_rendered is charged. Heavy pages can exceed the default 25-second renderTimeoutSecs; set it to 40 for those runs.
  • A render that finds nothing does not bill. If a render completes but returns no JavaScript global, no matched DOM selector, no XHR host and no stylesheet text — a challenge page, a hard block, an app shell that never booted — the row stays Tier 1 with renderSkipped: "no_signals" and no domain_rendered is charged, because the row would be identical to the Tier 1 one you already paid for.
  • A very heavy page can be read only partly. The in-page probe has its own budget inside renderTimeoutSecs; when it runs out the row is stamped renderTruncated: true and some of the 5,594 JavaScript paths and 1,787 DOM selectors were never evaluated. Raise renderTimeoutSecs for pages that hit it.
  • A confirmed change is one scan cycle late. A move has to appear in two consecutive scans before it is reported and billed, so on a daily schedule you hear about a real migration a day after the first scan that saw it. changes.pending shows it in the meantime; changeConfirmScans: 1 trades the delay back for the noise.
  • Applicant tracking systems are rarely visible from a homepage — pass a /careers path when you want them.
  • Version numbers are only reported when the page exposes them. Many modern stacks do not.

Fingerprint data and licensing

Technology detection uses the community fingerprint dataset published by enthec/webappanalyzer, which is licensed under GPL-3.0 and pinned to an exact commit. It is data only — the detection engine here is independent work under the MIT licence, and no GPL-licensed code is copied, linked or adapted. The dataset is fetched when the image is built and is not redistributed. Running this Actor as a hosted service is use, not distribution, so GPL-3.0's distribution obligations are not triggered. Detection results are facts about third-party websites and are yours to use freely. Full statement: NOTICE.md.