Tech stack detector for any list of websites: find the CMS, ecommerce platform, JavaScript framework, analytics, ad pixels, marketing tools, CDN and hosting each site uses. 136 technologies in 38 categories, with version and evidence. CSV or JSON export. Sites that fail are free.
Version stays "1.0" in .actor/actor.json: Apify accepts only MAJOR.MINOR there and assigns the patch (build) number at the next build, which is the patch bump for this release.
Unreleased
The sample site is now https://www.python.org/ (was https://example.com): SAMPLE_URL in my_actor/main.py, the urls input schema default and prefill, and a new actor.json exampleRunInput all hold this one value, so {}, the prefill and the example input are all sample runs (15 s deadline, free warning row if the site cannot be read). A sample run checks 1 site ($0.002 + start fee). Only the exact URL list ["https://www.python.org/"] is a sample run; other spellings (for example without the trailing slash) are normal runs. README Default column updated. New test tests/test_sample_1102v.py.
2026-11-02 (actor.json version unchanged: 1.0): private-address (SSRF) guard
On the Apify platform, every request (the URL itself, each redirect hop, and robots.txt) is refused when the host
resolves to a private, loopback, link-local, carrier-grade NAT, reserved, multicast or unspecified address
(including IPv4-mapped IPv6), or when any of the host's addresses is one. Same rules as Bulk HTTP Request.
Such a URL gets a free error row (it is dropped like other error rows when includeFailedUrls is off):
error: "not fetched: private, loopback and link-local addresses are not allowed". It counts as failed in
RUN_SUMMARY. When includeFailedUrls is off and no row is output at all, the dataset gets one free
status row instead: recordType: "status", message: "Not charged: N URL(s) were not fetched (...)".
A host the guard cannot resolve fails as a DNS error, as before (error now reads
"DNS or connection error: DNS lookup failed for "); the row stays free.
A robots.txt that redirects to a private address counts as unavailable (no robots.txt rules), like a robots.txt
that cannot be fetched.
Local runs (APIFY_IS_AT_HOME not set) are unchanged.
Files: my_actor/common.py (ip_blocked, host_problem, check_public, guard_request httpx request hook,
guard_site, Blocked, DnsLookupFailed, blocked_status_row; Http._robots_for checks the site before fetching robots.txt),
my_actor/main.py (hook on the httpx client, Blocked handled), tests/test_ssrf_guard_1102t.py (new),
README Limits (one line).
Unreleased (next 1.0.x build): error rows are free (round 1030b)
Charges (lower): rows with an error are no longer charged: HTTP 400 or higher, DNS or connection errors, timeouts, and websites robots.txt disallows (directly or through a redirect). Before, only rows for a redirect that robots.txt disallows were free. Rows for websites that returned a page below HTTP 400 are charged as before.
Unchanged: the rows output, their order and content, the Include failed URLs filter, and Max results (free rows still count toward it). Tested by comparing the rows of the previous code with the new code on the same inputs.
Under a Maximum cost per run, error rows no longer use up the budget, so a run can save more paid rows before it stops.
BREAKING (input editor): the urls field uses the plain URL list editor (stringList); the default and prefill are now ["https://example.com"]. Plain strings and {"url": ...} objects both work, so existing API inputs keep working. Before, Apify Console and apify-cli rejected plain strings. startUrls is accepted as an alias for urls.
BREAKING (output values):checkedAt is now the time each website was fetched, not the run start time.
BREAKING (fewer requests and charges): redirects are followed one hop at a time (at most 10) and every target is checked against its own robots.txt. A website that redirects to a disallowed page gets a free error row (not fetched: redirects to <url>, which robots.txt disallows) when Include failed URLs is on.
The Include failed URLs description now says what happens: failed rows are charged like other rows (except the redirect case above), and turning it off skips them.
Empty input and the prefilled sample: 15 second limit; if the sample site is down or slow the run succeeds with one free row with a warning field.