DEV Community

MikeL
MikeL

Posted on Originally published at detectzestack.com

Find Companies Using Contentful CMS via API (2026 Guide)

Contentful is the headless CMS that shows up underneath a specific kind of company: one that decided its marketing site needed its own front-end team. It is not a starter-plan product and it is not something a freelancer installs on a Friday. Finding the companies running it gives you a segment that has already paid for a content platform, already staffed a front end, and already committed to a build pipeline — which is exactly the profile that buys developer tooling, personalization, search, localization, and migration services.

The catch is that Contentful is genuinely harder to detect than WordPress, and for a structural reason rather than an accidental one. This post covers why, which fingerprints survive a static build, and how to turn those into a list of companies using Contentful through the DetectZeStack API. Every response and every number below was captured on the day this post was written.

Why Companies Using Contentful Are a Distinct Prospect Segment

A CMS detection is usually a weak signal because the population is enormous and undifferentiated. Contentful is the opposite. Adopting it means a team explicitly chose to separate content from presentation, which implies three things you can sell against:

  • There is a front-end team. Contentful returns JSON, not pages. Somebody has to build the rendering layer, which means an in-house or agency front-end practice exists and has a budget.
  • There is a build pipeline. Content changes trigger rebuilds or revalidation. That means CI, a hosting platform, preview environments, and all the tooling those drag along.
  • The site is not the product. Contentful usually sits under a marketing or docs site in front of a real product. The buyer you want is often in growth or marketing engineering, not IT.

Across the DetectZeStack scan corpus as of September 16, 2026, 106 domains carry a Contentful detection. That population skews hard toward B2B SaaS and consumer tech: dovetail.com, temporal.io, www.samsara.com, www.onepeloton.com, www.docusign.com, waymo.com, and www.atlassian.com are all in it. That is a narrow, high-value list — not the long tail you get from a WordPress query.

Why Contentful Is Harder to Detect Than WordPress

Headless means no generator meta tag and no CMS-rendered HTML

WordPress detection is easy because WordPress renders the page. It stamps <meta name="generator" content="WordPress 6.x"> into the head, serves assets from /wp-content/, and exposes /wp-json/. Every one of those is the CMS talking about itself in its own output.

Contentful never renders anything. Its Content Delivery API returns JSON; a framework consumes that JSON and produces the HTML. By the time a page reaches a browser, the markup was generated by Next.js, Gatsby, Nuxt, or a hand-rolled renderer — and those frameworks stamp themselves, not the CMS. There is no generator tag, no admin path, no CMS-owned URL prefix. If you are building a detector from first principles, this is the point where the WordPress playbook stops working. Our broader guide to detecting what CMS a website uses walks through that split in more detail.

The signals that do survive the build: ctfassets.net and the Contentful API host

One thing cannot be compiled away. Images, videos, and downloads uploaded to Contentful are served from Contentful's own asset CDN, and those absolute URLs end up in the rendered HTML because that is the whole point of a hosted asset pipeline. The fingerprint is the asset hostname:

Signal Where it appears
Image assets images.ctfassets.net
Video assets videos.ctfassets.net
Generic assets assets.ctfassets.net
Downloads downloads.ctfassets.net
Response header x-contentful-request-id

You can confirm the asset signal on any site with one line, no API key and no browser:

$ curl -sL https://temporal.io \
| grep -aoE '[a-z0-9.-]+\.(ctfassets\.net|contentful\.com)' \
| sort | uniq -c | sort -rn
230 images.ctfassets.net
2 videos.ctfassets.net
Enter fullscreen mode Exit fullscreen mode

230 image references on a single page. That is not an edge case — it is what a Contentful-backed marketing site looks like from the outside, and it is why the signal is reliable despite being indirect. A lower-volume site still trips it; dovetail.com shows four of each on its homepage.

The header signal, x-contentful-request-id, is a different thing and worth understanding so you do not go looking for it in the wrong place. It is set by Contentful's own API hosts, not by customer websites:

$ curl -sI "https://cdn.contentful.com/spaces/foo/entries" | grep -i contentful
server: Contentful
x-contentful-request-id: 0679d4f1-a3c1-403b-b078-6a0806ae1efb
Enter fullscreen mode Exit fullscreen mode

A customer's marketing site proxies nothing, so that header will not be on its response. It fires when you scan a Contentful API endpoint directly. In practice, essentially every real-world detection comes from the asset hostname.

The asset host has to be in the rendered HTML to count. A site that proxies Contentful images through its own domain — Next.js image optimization pointed at a custom loader, or a Cloudflare Worker rewriting asset URLs — strips the fingerprint entirely. Those sites run Contentful and will come back negative. This is the main false-negative source, and there is no HTTP-layer workaround for it.

The frontend stack sitting in front of Contentful

Because Contentful cannot render, every Contentful detection arrives with a rendering framework attached — and that co-detection is where the qualification value lives. Counting distinct domains across the 106 Contentful sites in the corpus:

Co-detected technology Domains
Amazon Web Services 54
React 52
Webpack 46
Next.js 32
Cloudflare 28
Vercel 24
Gatsby 15
Netlify 12
Vue.js 6
Nuxt.js 5

Roughly half of Contentful sites in the corpus run React, and Next.js alone covers 32 of 106. Gatsby is still present on 15, which is a meaningful sub-segment in its own right given Gatsby's trajectory — those are modernization conversations waiting to happen. On the hosting side, Vercel and Netlify together account for 36 of the 106, while AWS shows up on 54.

Check One Domain for Contentful Free (GET /demo)

The public /demo endpoint runs the full detector against any URL with no authentication and no signup, rate limited to 20 requests per hour per IP:

$ curl -s "https://detectzestack.com/demo?url=temporal.io" | python3 -m json.tool
Enter fullscreen mode Exit fullscreen mode

The real response, captured September 16, 2026:

{
"url": "https://temporal.io",
"domain": "temporal.io",
"technologies": [
{
"name": "Contentful",
"categories": ["CMS"],
"confidence": 100,
"description": "Contentful is an API-first content management platform to create, manage and publish content on any digital channel.",
"website": "https://www.contentful.com",
"icon": "Contentful.svg",
"source": "http"
},
{
"name": "HSTS",
"categories": ["Security"],
"confidence": 100,
"description": "HTTP Strict Transport Security (HSTS) informs browsers that the site should only be accessed using HTTPS.",
"website": "https://www.rfc-editor.org/rfc/rfc6797#section-6.1",
"source": "http"
},
{
"name": "Let's Encrypt",
"categories": ["SSL/TLS certificate authority"],
"confidence": 70,
"source": "tls"
},
{
"name": "Vercel",
"categories": ["PaaS"],
"confidence": 100,
"description": "Vercel is a cloud platform for static frontends and serverless functions.",
"website": "https://vercel.com",
"icon": "vercel.svg",
"source": "http"
}
],
"categories": {
"CMS": ["Contentful"],
"PaaS": ["Vercel"],
"SSL/TLS certificate authority": ["Let's Encrypt"],
"Security": ["HSTS"]
},
"meta": { "status_code": 200, "tech_count": 4, "scan_depth": "full" },
"cached": false,
"response_ms": 1901
}
Enter fullscreen mode Exit fullscreen mode

Three fields to read carefully. source: "http" means the Contentful match came from the HTML body rather than DNS or TLS — which is why meta.scan_depth must be "full" for a Contentful result to mean anything. A "partial" scan means the HTTP fetch was blocked or timed out and only the DNS and TLS layers ran, so the absence of Contentful in a partial result is unknown, not negative. And cached and response_ms are top-level fields, not part of meta.

API Examples: Detecting Contentful Four Ways

GET /check?tech=Contentful — a yes/no answer with confidence

When you want a boolean rather than the whole stack, /check runs the same scan and returns a compact object. The technology name is matched case-insensitively:

$ curl -s "https://detectzestack.p.rapidapi.com/check?url=dovetail.com&tech=Contentful" \
-H "X-RapidAPI-Key: YOUR_KEY" \
-H "X-RapidAPI-Host: detectzestack.p.rapidapi.com"
{
"domain": "dovetail.com",
"technology": "Contentful",
"detected": true,
"confidence": 100,
"version": "",
"categories": ["CMS"],
"response_ms": 4744,
"cached": false
}
Enter fullscreen mode Exit fullscreen mode

confidence: 100 is what an unambiguous hostname fingerprint earns. version is empty and always will be — Contentful is hosted SaaS, so there is no customer-visible release number to report, and the hostname pattern captures no digits. Filter on name or detected, never on version.

GET /lookup?tech=Contentful — domains already known to run Contentful

/lookup is a reverse index over DetectZeStack's prior scans, not a crawl of the open web. Treat it as a way to seed a list rather than to enumerate every Contentful site that exists. Results paginate with limit and offset, clamped per tier: 2 rows on the free tier, 50 on Pro, 200 on Ultra, 800 on Mega.

$ curl -s "https://detectzestack.p.rapidapi.com/lookup?tech=Contentful&limit=50" \
-H "X-RapidAPI-Key: YOUR_KEY" \
-H "X-RapidAPI-Host: detectzestack.p.rapidapi.com"
{
"technology": "Contentful",
"total": 106,
"limit": 2,
"offset": 0,
"results": [
{
"domain": "dovetail.com",
"category": "CMS",
"confidence": 100,
"version": "",
"first_seen": "2026-05-11T04:09:35Z",
"last_seen": "2026-09-16T13:02:44Z"
},
{
"domain": "www.onepeloton.com",
"category": "CMS",
"confidence": 100,
"version": "",
"first_seen": "2026-05-27T13:39:21Z",
"last_seen": "2026-09-15T18:02:31Z"
}
],
"response_ms": 11
}
Enter fullscreen mode Exit fullscreen mode

That call was made on a free-tier key, which is why limit comes back as 2 even though the request asked for 50 — the tier ceiling overrides your parameter silently. total is not clamped, so you can always see how large the segment is before deciding whether the tier is worth upgrading.

The first_seen and last_seen pair is the most underused field in the response. dovetail.com first seen in May and last seen today has run Contentful continuously across every scan in between — a stable signal. A wide gap between the two means the detection is stale and worth re-scanning before the domain goes on an outreach list.

POST /analyze/batch — scan up to 10 prospect domains per request

For domains you bring yourself, /analyze/batch takes up to 10 URLs per request and scans them concurrently. Each item in results carries either a result object with the single-domain shape or an error string. This call mixes two Contentful sites with one that is not:

$ curl -s -X POST "https://detectzestack.p.rapidapi.com/analyze/batch" \
-H "X-RapidAPI-Key: YOUR_KEY" \
-H "X-RapidAPI-Host: detectzestack.p.rapidapi.com" \
-H "Content-Type: application/json" \
-d '{"urls": ["dovetail.com", "www.samsara.com", "getbootstrap.com"]}' \
| jq '{successful, failed, total_ms,
rows: [.results[] | {url,
contentful: ([.result.technologies[]? | select(.name == "Contentful")] | length > 0),
cms: .result.categories["CMS"],
depth: .result.meta.scan_depth,
count: .result.meta.tech_count}]}'
{
"successful": 3,
"failed": 0,
"total_ms": 3590,
"rows": [
{ "url": "dovetail.com",    "contentful": true,  "cms": ["Contentful"], "depth": "full", "count": 12 },
{ "url": "www.samsara.com", "contentful": true,  "cms": ["Contentful"], "depth": "full", "count": 8  },
{ "url": "getbootstrap.com","contentful": false, "cms": null,           "depth": "full", "count": 11 }
]
}
Enter fullscreen mode Exit fullscreen mode

getbootstrap.com is a deliberate negative: it has no CMS category at all. Note that a null category is not the same as a miss, and a partial scan is not the same as a negative — the filter has to check the exact name and the scan depth together.

POST /compare — diff a Contentful site against a WordPress competitor

/compare takes 2 to 10 URLs and returns what they share and what each one has alone. Running a headless site against a traditional one makes the architectural split obvious in a single response:

$ curl -s -X POST "https://detectzestack.p.rapidapi.com/compare" \
-H "X-RapidAPI-Key: YOUR_KEY" \
-H "X-RapidAPI-Host: detectzestack.p.rapidapi.com" \
-H "Content-Type: application/json" \
-d '{"urls": ["dovetail.com", "techcrunch.com"]}'
{
"shared": ["HSTS"],
"domains": [
{
"domain": "dovetail.com",
"unique": ["Netlify", "Next.js", "Node.js", "Algolia", "Linkedin Ads",
"React", "Webpack", "Amazon CloudFront", "Amazon S3",
"Amazon Web Services", "Contentful"]
},
{
"domain": "techcrunch.com",
"unique": ["Yoast SEO Premium", "reCAPTCHA", "Google Tag Manager",
"Let's Encrypt", "MySQL", "WordPress", "WordPress Block Editor",
"WordPress Site Editor", "WordPress VIP", "Nginx", "PHP",
"Sailthru", "Yoast SEO"]
}
],
"total_ms": 2547
}
Enter fullscreen mode Exit fullscreen mode

One shared technology out of 25. That is the headless split rendered as data: Contentful plus a JavaScript build chain plus a CDN on one side, WordPress plus PHP plus MySQL plus Nginx on the other. If you sell into one of those worlds, this response tells you in one call which conversation you are having.

Building a Contentful Prospect List End to End

A raw list of Contentful domains is a starting point, not a segment. The value comes from what else is in the same response — and because /analyze returns the entire stack in one call, you already have it without a second lookup.

Two real hits from the batch above sort into completely different buckets. dovetail.com comes back with JavaScript frameworks: ["Next.js", "React"], PaaS: ["Amazon Web Services", "Netlify"], Search engines: ["Algolia"], Advertising: ["Linkedin Ads"], and 12 technologies total — a mature Jamstack build with a search vendor and paid acquisition already in place. www.samsara.com comes back with JavaScript frameworks: ["Nuxt.js", "Vue.js"], A/B Testing: ["Intellimize"], CDN: ["Amazon CloudFront"], and 8 technologies — a Vue shop on AWS running personalization, with no search vendor detected.

Same CMS, opposite pitches. If you sell site search, dovetail already bought and samsara has not. If you sell React component libraries, the Vue site is disqualified before anyone writes an email. If you sell experimentation tooling, samsara has already proven it will pay for that category. Filtering Contentful hits on the co-detected framework and vendor set splits the list along exactly the line your product cares about.

Here is the whole thing as a shell pipeline. It reads domains.txt one domain per line, sends batches of 10, writes one CSV row per Contentful hit with the co-detected framework and hosting, and pushes failures and partial scans into a retry file instead of silently counting them as negatives:

#!/usr/bin/env bash
# find-contentful.sh - build a Contentful prospect list from domains.txt
KEY="YOUR_KEY"
HOST="detectzestack.p.rapidapi.com"

echo "requested,resolved,frameworks,hosting,tech_count" > contentful.csv
: > contentful_retry.txt

# 10 URLs per request is the /analyze/batch maximum
xargs -n 10 < domains.txt | while read -r batch; do
urls=$(printf '%s\n' $batch | jq -R . | jq -s '{urls: .}')
resp=$(curl -s -X POST "https://$HOST/analyze/batch" \
-H "X-RapidAPI-Key: $KEY" \
-H "X-RapidAPI-Host: $HOST" \
-H "Content-Type: application/json" \
-d "$urls")

echo "$resp" | jq -r '.results[]
| select(.result != null)
| . as $item
| $item.result as $r
| select([$r.technologies[].name] | index("Contentful"))
| [
$item.url,
$r.domain,
(($r.categories["JavaScript frameworks"] // []) | join(";")),
(($r.categories["PaaS"] // []) | join(";")),
($r.meta.tech_count | tostring)
] | @csv' >> contentful.csv

# Fetch errors and DNS-only scans are unknown, not negative
echo "$resp" | jq -r '.results[]
| select(.error != null or .result.meta.scan_depth == "partial")
| .url' >> contentful_retry.txt

sleep 1
done

echo "Contentful hits: $(($(wc -l < contentful.csv) - 1))"
Enter fullscreen mode Exit fullscreen mode

A 1,000-domain list is 100 batch calls. Throughput, retries, and a Python version of this loop are covered in how to batch scan 1,000 websites. For wiring the resulting rows into a CRM alongside firmographic data, see building a lead enrichment pipeline with tech detection.

Tracking Migrations Onto and Off Contentful

A company mid-replatform is worth more than a company that settled three years ago, and the timing signal is in the change feed. GET /changes?domain=example.com returns technologies added and removed between consecutive scans of a domain, with history depth gated by tier: 7 days on Basic, 30 on Pro, 90 on Ultra, 365 on Mega. GET /history?domain=example.com&include_changes=true gives you the same movement attached to the underlying snapshots.

One honest warning before you build alerting on this. Over the change history for Contentful in the corpus, there are 33 added events and 35 removed events — and they are not 68 migrations. www.atlassian.com alone accounts for 13 adds and 13 removes, and www.chegg.com for 9 and 9. Those domains are flapping, not replatforming: their homepage sometimes renders with a ctfassets.net asset and sometimes does not, depending on which A/B or personalization variant the scanner happened to receive.

A single removed event is noise. A sustained absence is a migration. Before you treat a Contentful removal as a replatform, require the technology to be missing from several consecutive full scans, and check whether the same domain has flapped before. A domain with a one-directional change and no history of alternating events — monzo.com added it once, bombas.com removed it once — is the shape you actually want to alert on.

If you want this pushed to you rather than polled, tracking website tech changes via API covers the monitoring side, including the alerting patterns that filter out exactly this kind of flapping.

Rate Limits, Caching, and What a Contentful Sweep Costs

Scan results are cached per domain, and a cache hit is what makes a repeat sweep cheap. In the /check response above, cached: false with response_ms: 4744 is a cold fetch of a heavy marketing page; a warm repeat of the same domain returns in single-digit milliseconds. That matters for list hygiene: re-running last quarter's 1,000-domain sweep is dramatically faster than the first pass, because most of those domains are already warm.

Three cost notes worth planning around:

  • Batch requests are still per-URL against your quota. A 10-URL /analyze/batch call is convenient, not free — budget 1,000 domains as 1,000 requests, not 100.
  • /lookup is one request regardless of result count. Pulling 200 known Contentful domains on Ultra costs one request. If your goal is a seed list rather than verification of specific accounts, start here.
  • /demo costs nothing but is capped at 20 requests per hour per IP. Use it to build and debug your jq filter against the exact response shape before you spend a single paid request.

Accuracy Notes and Known Limits

Proxied assets are invisible. Covered above, and it is the single largest false-negative source. A site that rewrites Contentful asset URLs onto its own domain leaves no HTTP-layer trace of the CMS at all.

A partial scan cannot see Contentful. The fingerprint lives in the HTML body. If meta.scan_depth is "partial", only DNS and TLS signals are present and the result is "unknown." Treating it as "no" quietly deletes real prospects from your list, which is why the script above routes partials to a retry file.

Only the URL you give it is scanned. A company whose homepage is a hand-built hero but whose /blog or /resources section is Contentful-backed will come back negative on the apex domain. For Contentful specifically this is a common shape — headless CMSes are frequently adopted section by section. If your list matters, scan two or three representative paths per domain.

Contentful's own marketing site may rate-limit you. A scan of contentful.com returned "status_code": 429 and only two technologies. A non-200 status code in meta is a signal that the body you got was an error page, not the real site — check it before recording the result.

Get Your API Key and Start Finding Contentful Sites

Start with /demo — no key, no signup, and it returns the exact response shape shown above, so you can build your filter before spending a request. When you are ready to scan a list, the free tier on RapidAPI is 100 requests per month with no credit card. Paid plans start at $9/month for 1,000 requests and scale to 50,000. Every tier uses the same endpoints and the same response shape, so the pipeline you write against the free tier runs unchanged when you scale it up.

The same call that finds Contentful also names the rendering framework, the hosting platform, the CDN, the search vendor, the analytics tags, and every other library on the page. One quota, one response, the whole stack.

Related Reading

Top comments (0)