Detect Drupal Website: Manual Checks + API Guide (2026)
Drupal is the CMS of governments, universities, media companies, and large enterprises — the segment of the web where WordPress stops and structured content, editorial workflow, and access control begin. That makes "does this site run Drupal?" a question with real money and real risk attached: Drupal shops sell into it, security teams patch it on a deadline, and migration consultants quote against it. The good news is that a default Drupal installation announces itself in several places at once, and even a hardened one usually leaks a signal or two.
This guide covers both approaches: the manual checks you can run against a single site with curl and a view-source, and the DetectZeStack API for confirming Drupal programmatically or sweeping a thousand-domain prospect list. Every code example is copy-pasteable and uses fields that actually exist in the API response.
Why Detect Drupal? Sales, Security, and Migration Use Cases
Three audiences ask this question daily, for different reasons:
- Sales and agency teams. If you sell Drupal development, hosting (Acquia-style managed platforms), module work, or support retainers, your addressable market is precisely "organizations currently running Drupal." A verified technographic list beats guessing from industry alone, and because Drupal skews toward government, education, and enterprise, a Drupal hit also tells you something about deal size and procurement style.
- Security and asset-inventory teams. Drupal has a history of severe, wormable vulnerabilities — "Drupalgeddon" (SA-CORE-2014-005) and "Drupalgeddon 2" (SA-CORE-2018-002) were both exploited at internet scale within days of disclosure. When the next core advisory drops, the first question is "which of our domains, subsidiaries, and vendors run Drupal?" You want that inventory before the advisory, not after.
- Migration and replatforming consultants. Drupal 7 reached end of life in January 2025, and a long tail of sites still runs it. Identifying Drupal sites — and ideally the major version — is how you find replatforming prospects and scope the work.
All three need the same answer in code: is this site Drupal, and if so, which major version?
Manual Ways to Detect a Drupal Website
Drupal emits more fingerprints than most CMSes. Here they are in descending order of reliability.
Check the X-Generator Header and Meta Generator Tag
Modern Drupal (8 and later) sends an X-Generator response header on every page by default. One curl -I answers the question:
$ curl -sI https://www.example.gov | grep -iE "x-generator|x-drupal"
x-generator: Drupal 10 (https://www.drupal.org)
x-drupal-cache: HIT
Two signals in one shot:
X-Generator: Drupal 10 (https://www.drupal.org)— the clearest Drupal tell on the web. It names the CMS, states the major version, and links to drupal.org. If you see this, you are done.X-Drupal-Cache(and on newer sitesX-Drupal-Dynamic-Cache) — Drupal's internal page-cache headers, reportingHITorMISS. These survive on many sites that strip the generator header, because ops teams rely on them for cache debugging.
There is also a stranger, older tell: Drupal's page cache has historically sent Expires: Sun, 19 Nov 1978 05:00:00 GMT — a deliberately ancient date (Drupal founder Dries Buytaert's birthday) used to mark content as always-stale to intermediaries. Seeing 19 November 1978 in an Expires header is a strong Drupal hint all by itself.
The same generator string usually appears in the HTML head as a meta tag, which survives some proxy configurations that strip custom headers:
$ curl -s https://www.example.gov | grep -io '<meta name="generator"[^>]*>'
<meta name="generator" content="Drupal 10 (https://www.drupal.org)">
Look for /sites/default/ Asset Paths and Drupal Settings JSON
When headers and meta tags have been sanitized, the page body still betrays Drupal's very distinctive file layout. Drupal serves uploaded files, theme assets, and aggregated CSS/JS from /sites/default/files/ (or /sites/all/ on older multisite installs), and no other mainstream CMS uses those paths:
$ curl -s https://www.example.gov | grep -o '/sites/\(default\|all\)/[^"]*' | head -5
/sites/default/files/css/css_AbC123.css
/sites/default/files/js/js_XyZ789.js
/sites/default/files/logo.svg
Drupal 8+ also injects its front-end configuration as a JSON blob with an unmistakable attribute:
<script type="application/json" data-drupal-selector="drupal-settings-json">
{"path":{"baseUrl":"\/","currentPath":"node\/1",...},"user":{"uid":0}}
</script>
Anything containing data-drupal-selector, a drupalSettings JSON payload (or the legacy Drupal.settings object on Drupal 7), or a drupal.js script reference is a body-level confirmation. Sites running Drupal's JavaScript also expose a global Drupal object you can check in the browser console.
Probe Known Drupal Paths (CHANGELOG.txt, /user/login, /core/)
A third manual technique is to request paths that exist on Drupal and almost nothing else:
/user/login— Drupal's login route. A themed login form at exactly this path (rather than/wp-login.phpor/admin) is characteristic of Drupal./core/CHANGELOG.txtand/core/assets — Drupal 8+ keeps core files under/core/. On poorly hardened sites,CHANGELOG.txtis world-readable and states the exact version. On Drupal 7 the file lived at/CHANGELOG.txtin the web root./core/misc/drupal.js— a core JavaScript file at a predictable path; an HTTP 200 here is a strong signal.
$ curl -s -o /dev/null -w "%{http_code}\n" https://www.example.gov/core/misc/drupal.js
200
A caution: well-run Drupal sites block the txt files precisely because they leak the patch level, and probing paths generates 404 noise in the target's logs. Use path probes as a tiebreaker, not an opener — and only against sites you have a legitimate reason to assess.
Why Manual Checks Fail on Hardened or Cached Drupal Sites
Each signal above can be individually suppressed. Security-conscious Drupal shops remove the generator header and meta tag (a one-line change or a common hardening module), a CDN or Varnish layer in front of the origin can strip X-Drupal-Cache and rewrite Expires, and an aggressive edge cache can serve pages where aggregation has rewritten asset URLs. A single curl against the home page then comes back clean even though the site is Drupal to the core.
Reliable detection therefore means checking all the signals — headers, meta tags, HTML body patterns, script paths, and the JavaScript layer — and combining them, which is exactly what you do not want to hand-script across a list of 1,000 domains. That is the job of an API.
Detect Drupal Instantly with the DetectZeStack API
DetectZeStack runs the full fingerprint set in one call: the X-Generator and X-Drupal-Cache headers, the 1978 Expires tell, the meta generator tag, /sites/default/ and /sites/all/ asset paths, drupal.js script references, and the Drupal JavaScript global. Any of them firing returns Drupal in the CMS category — and because Drupal implies PHP, a PHP entry usually rides along in the same response.
curl Example with GET /analyze and the JSON Response
You can smoke-test the response shape right now against the public /demo endpoint, which runs the same detector pipeline with no API key (IP rate-limited):
curl -s "https://detectzestack.com/demo?url=https://www.drupal.org" | python3 -m json.tool
The response is a single JSON object. Trimmed to the relevant entries, a Drupal hit looks like this:
{
"url": "https://www.drupal.org",
"domain": "drupal.org",
"technologies": [
{
"name": "Drupal",
"categories": ["CMS"],
"confidence": 100,
"description": "Drupal is a free and open-source web content management framework.",
"website": "https://www.drupal.org/",
"icon": "Drupal.svg",
"source": "http",
"version": "10",
"cpe": "cpe:2.3:a:drupal:drupal:*:*:*:*:*:*:*:*"
},
{
"name": "PHP",
"categories": ["Programming languages"],
"confidence": 100,
"description": "PHP is a general-purpose scripting language.",
"website": "https://www.php.net",
"icon": "PHP.svg",
"source": "http",
"version": "",
"cpe": "cpe:2.3:a:php:php:*:*:*:*:*:*:*:*"
}
],
"categories": { "CMS": ["Drupal"], "Programming languages": ["PHP"] },
"meta": { "status_code": 200, "tech_count": 12, "scan_depth": "full" },
"cached": false,
"response_ms": 1764
}
A few fields worth understanding before you build on them:
version— populated from the generator signal, so it is typically the major version only ("10"). Drupal deliberately omits the minor and patch level from its generator string, and a site that strips the generator entirely returns an empty string here. Empty means "version not externally visible," not "not Drupal."cpe— the CPE identifier security teams map to vulnerability databases. See CPE identifiers explained for turning this into CVE matches.source— where the signal came from;"http"for Drupal, since the fingerprints live in the response headers and body.response_msandcached— top-level fields (not insidemeta) reporting scan duration and cache status. Themetaobject carries onlystatus_code,tech_count, andscan_depth.
For production, sign up on RapidAPI and call /analyze with your key — same shape, no demo rate limit:
curl -s "https://detectzestack.p.rapidapi.com/analyze?url=https://www.drupal.org" \
-H "x-rapidapi-key: $RAPIDAPI_KEY" \
-H "x-rapidapi-host: detectzestack.p.rapidapi.com" \
| jq '.technologies[] | select(.name == "Drupal")'
If Drupal is present, that pipe prints exactly the matching object; if not, jq prints nothing and exits cleanly — easy to wire into a shell filter. When Drupal is the only technology you care about, the /check endpoint answers the yes/no question directly with a ?tech= parameter (case-insensitive), so you skip the client-side filtering entirely.
Checking Drupal Across 1,000 Domains with POST /analyze/batch
For lists, POST /analyze/batch scans up to 10 URLs concurrently per call. A 1,000-domain sweep is therefore 100 batch calls — each URL still counts against your quota, but the concurrency and error handling are the API's problem instead of yours:
curl -s -X POST "https://detectzestack.p.rapidapi.com/analyze/batch" \
-H "x-rapidapi-key: $RAPIDAPI_KEY" \
-H "x-rapidapi-host: detectzestack.p.rapidapi.com" \
-H "Content-Type: application/json" \
-d '{"urls": ["www.drupal.org", "www.example.com", "example.gov"]}'
The response wraps one full /analyze result per URL, plus per-item errors so one dead domain does not sink the batch:
{
"results": [
{ "url": "www.drupal.org", "result": { "...full /analyze response..." : "" } },
{ "url": "bad-domain.example", "error": "failed to fetch URL" }
],
"total_ms": 4210,
"successful": 2,
"failed": 1
}
Chunking a file of domains into batches of 10 and extracting the Drupal hits is a few lines of shell:
split -l 10 domains.txt chunk_
for f in chunk_*; do
urls=$(jq -R -s 'split("\n") | map(select(length > 0)) | {urls: .}' "$f")
curl -s -X POST "https://detectzestack.p.rapidapi.com/analyze/batch" \
-H "x-rapidapi-key: $RAPIDAPI_KEY" \
-H "x-rapidapi-host: detectzestack.p.rapidapi.com" \
-H "Content-Type: application/json" \
-d "$urls" \
| jq -r '.results[] | select(.result.technologies[]?.name == "Drupal") | .url'
done
That prints every Drupal domain in the list, one per line — a prospect list or a patch-day inventory, depending on which hat you are wearing. For throughput, quota math, and connection-reuse patterns at larger scale, see Batch Scan 1,000 Websites for Tech Stack.
Drupal Detection Signals Explained (Headers, HTML, DNS)
For reference, here is the full signal set the detector evaluates, with rough reliability:
| Signal | Where | Reliability |
|---|---|---|
| X-Generator: Drupal 10 (...) | HTTP header | Definitive; includes major version |
| X-Drupal-Cache / X-Drupal-Dynamic-Cache | HTTP header | Definitive when present |
| Expires: 19 Nov 1978 | HTTP header | Strong; Drupal's trademark stale date |
| <meta name="generator" content="Drupal..."> | HTML head | Definitive; includes major version |
| /sites/default/ or /sites/all/ asset paths | HTML body | Strong; unique file layout |
| drupal.js script reference | HTML body | Strong |
| Drupal JavaScript global | JS runtime | Strong |
Note what is not on the list: DNS. Drupal is application code, so DNS-layer detection cannot name it directly — but DNS still adds context. Managed Drupal platforms sit behind recognizable CDN and hosting infrastructure, so the same /analyze call that confirms Drupal from HTTP signals also reports the CDN and hosting layer from DNS, giving you "Drupal on a managed platform" versus "Drupal on a bare VPS" in one response. That distinction matters for both sales (budget signal) and security (who applies the patches).
Comparing Two Sites' Stacks with POST /compare
A common follow-up: you know one target runs Drupal and want to see how a competitor's stack differs, or check which of two university sites carries more legacy. POST /compare takes 2–10 URLs and returns each site's technologies plus the shared and unique sets:
curl -s -X POST "https://detectzestack.p.rapidapi.com/compare" \
-H "x-rapidapi-key: $RAPIDAPI_KEY" \
-H "x-rapidapi-host: detectzestack.p.rapidapi.com" \
-H "Content-Type: application/json" \
-d '{"urls": ["www.drupal.org", "wordpress.org"]}'
The response groups the delta for you:
{
"domains": [
{
"url": "www.drupal.org",
"domain": "drupal.org",
"technologies": [ { "name": "Drupal", "categories": ["CMS"], "confidence": 100, "...": "" } ],
"unique": ["Drupal"]
},
{
"url": "wordpress.org",
"domain": "wordpress.org",
"technologies": [ { "name": "WordPress", "categories": ["CMS"], "confidence": 100, "...": "" } ],
"unique": ["WordPress"]
}
],
"shared": ["PHP", "MySQL"],
"total_ms": 3892
}
The unique array on each domain and the top-level shared array answer the "what does one run that the other does not" question without any client-side set math. For an agency pitching a Drupal-to-anything migration, one /compare of the prospect against a reference build is a slide in your deck.
Get Your API Key and Start Detecting
Everything above the batch section works with zero setup via the /demo endpoint. For real volume, get a key on RapidAPI — the free tier includes 100 requests per month, no credit card, and every endpoint shown here: /analyze, /check, /analyze/batch, and /compare. Paid tiers scale from 1,000 to 50,000 requests per month, and the same key also unlocks certificate checks, historical snapshots, and tech-change tracking for the Drupal sites you monitor.
Patch-day tip: the Drupal entry's cpe field is the bridge from "we detected Drupal" to "which advisories apply." Store the CPE alongside the domain when you scan, and the next core security release becomes a lookup instead of a fire drill.
Conclusion
Detecting Drupal on a single site is usually one command: curl -sI and look for X-Generator: Drupal, X-Drupal-Cache, or the 1978 Expires date. When headers are stripped, the meta generator tag, /sites/default/ asset paths, the drupalSettings JSON blob, and predictable core paths like /core/misc/drupal.js fill the gap. No other CMS shares this exact fingerprint set.
Detecting it across a portfolio is one API call per site — /analyze for the full stack, /check for the yes/no, /analyze/batch for lists, and /compare for head-to-head deltas — each returning Drupal in the CMS category with the major version when the site exposes it and a CPE for security work. The same response hands you the rest of the stack for free: PHP, database, CDN, analytics, all in one quota-counted request.
Related Reading
- Detect What CMS a Website Uses — the full CMS detection guide: WordPress, Drupal, Joomla, and friends
- Check if a Website Uses WordPress — the same playbook for Drupal's biggest neighbor
- How to Detect if a Website Uses Laravel — framework detection on the custom-build side of the PHP web
- CPE Identifiers Explained for Security Teams — turn the Drupal cpe field into CVE matches
- Batch Scan 1,000 Websites for Tech Stack — concurrency, quota, and connection-reuse patterns for large lists
- Website Technology Checker API — full endpoint reference and integration guide
Detect Drupal and Every Other Technology in One API Call
One HTTP request returns every CMS, language, database, CDN, and analytics tag on a page — Drupal included, with its CPE for security work. 100 requests per month free. No credit card.
Get your free API key