Contentful: a 2026 scout of the headless-CMS incumbent — real curl, real 404, real €300 wall

Contentful is the headless CMS that "headless CMS" used to mean. It is older than Strapi, older than Sanity, older than Hygraph; the API shape it published in 2013 is still the API shape every newer competitor is benchmarked against. We spent an evening on 2026-05-07 doing the basic-evaluation walk on it — pricing, free tier, a real curl against cdn.contentful.com — the way a developer with five tabs open would. The interesting part isn't a bug; it's where the friction lives in 2026 for a product that was first to market.

Honest about what this is

We're the team behind SimpleReview, a Chrome extension that drafts code-fix PRs on whatever element you click on a broken admin or storefront. We are not affiliated with Contentful, not partners, not customers. This page is a scouting note from one real evaluation session on 2026-05-07: public pricing page, public CDA reference, the actual cdn.contentful.com endpoint with a real curl. We did not buy a Lite seat, we did not sit through a demo call. If we got something wrong, open a GitHub issue and we'll fix it.

Friction 1 — the gap between Free and the next tier

Open contentful.com/pricing and the page actually does its job: there is a tier table with prices, unlike some of its enterprise-sales competitors. The friction is not "where are the numbers"; it's the shape of the numbers themselves.

Contentful pricing page on 2026-05-07: 'Products and plans built to grow your business' headline, sub-heading 'Compare plans for every project, business, and team', captured from www.contentful.com/pricing
Captured again on 2026-05-08 from https://www.contentful.com/pricing/ via headless Chrome at 1440×950. Above-the-fold is intentionally aspirational; the tier table itself sits below.

Pulling the numbers out of the page on the day we tested:

TierPriceUsersLocalesAPI calls / moCDN bandwidth
Free €0 forever 10 2 100K (no overage) 50 GB
Lite €300 / mo 20 3 1M 100 GB
Premium contact sales custom custom unlimited custom

This is a real pricing page — we want to be fair about that. But the shape is unusual. The Free tier is generous on API calls (100K is enough for a small marketing site that doesn't redeploy on every save) and brutal on locales (2 is "English plus one" — not even the EU floor). The next step up is €300 per month, and what you get for it is one extra locale, ten extra users, and 9× more API calls. There is no €30 / €50 / €100 step in between. For a freelancer or a six-person startup who's outgrown the Free tier on locales but not on traffic, the choice is "stay on Free with two locales" or "pay €3,600/year to add a third one." The middle of the curve is missing.

What gets gated

Roles cap at 2 on Free, 3 on Lite. If you want a fourth role — a separate "translator" identity, a "client reviewer" identity, a CI bot — you're moved into custom-pricing Premium. Same for environments past the default master setup, same for SSO, same for SLAs. Contentful's pricing isn't hidden — it's just bimodal: a hobby plan and an enterprise plan with no real prosumer rung in between.

Friction 2 — the delivery / preview / management token split

Where Contentful's API is hidden is in the access-token model. There are three different token classes and they live on three different hosts:

TokenHostWhat it readsCached
Content Delivery (CDA)cdn.contentful.comPublished entries onlyYes (CDN)
Content Preview (CPA)preview.contentful.comPublished + draft entriesNo
Content Management (CMA)api.contentful.comRead + write everythingNo

The first time you build a Next.js / Nuxt / Astro front-end against Contentful, the lesson everybody learns is the same one: you build it against the CDA, deploy it, then realise editors can't see their drafts on the staging URL because drafts only exist on the preview API on a different host with a different token. There's a long and well-documented trail of tickets and Stack Overflow answers about this. It's not a bug — it's an architectural choice that makes the cached delivery layer fast — but it's the single most-asked friction question Contentful gets, and a fresh evaluator usually doesn't notice it until they're already shipping. Worth knowing on the way in.

What the API actually does — a real curl

The pricing page can ungate, hide, or rename things. The production API cannot. cdn.contentful.com is public-facing; you can hit it with no account and observe the real shape (you'll get an error, but the error is documentation).

First, the cleanest possible test — ask for a space that doesn't exist:

$ curl -sD - "https://cdn.contentful.com/spaces/INVALID/entries"

HTTP/2 404
content-type: application/vnd.contentful.delivery.v1+json
x-content-type-options: nosniff
server: Contentful
x-contentful-region: us-east-1
contentful-api: cda
access-control-allow-origin: *
access-control-expose-headers: Etag,x-contentful-ratelimit-second-limit,x-contentful-ratelimit-reset
via: 1.1 varnish, 1.1 varnish
x-served-by: cache-ewr-kewr1740028-EWR, cache-bma-essb1270035-BMA
x-cache: MISS
x-contentful-request-id: aaeeb664-5507-496f-81a7-d0870f99bf61
content-length: 165

Body, verbatim:

{
  "sys": {
    "type": "Error",
    "id": "NotFound"
  },
  "message": "The resource could not be found.",
  "requestId": "aaeeb664-5507-496f-81a7-d0870f99bf61"
}

That is a clean shape. Contentful's error envelope has lived in this form for the better part of a decade: a sys.type: "Error" wrapper, a stable sys.id string you can switch on (NotFound, AccessTokenInvalid, RateLimitExceeded, BadRequest...), a human message, and a requestId that you'll be asked for if you ever escalate to support. The headers are honest about what's serving the request — server: Contentful, fronted by Varnish (not Cloudflare, unlike most of the newer headless CMSes), with cache POPs visible in x-served-by (kewr = Newark, essb = Stockholm). The x-cache: MISS tells you the validation hit origin — consistent with a 404 that has to be authoritative.

Second test — a syntactically-valid space ID (their public demo space cfexampleapi) with a deliberately bogus token:

$ curl -sD - "https://cdn.contentful.com/spaces/cfexampleapi/entries" \
    -H "Authorization: Bearer invalid_token_xyz"

HTTP/2 401
x-contentful-ratelimit-second-limit: 78
cf-space-id: cfexampleapi
cf-environment-id: master
x-contentful-route: /spaces/:space/entries
etag: "11677993936344935350"
contentful-api: cda
content-length: 174

{"sys":{"type":"Error","id":"AccessTokenInvalid"},"message":"The access token you sent could not be found or is invalid.","requestId":"c4dc9272-4de6-4c1c-94dc-e306d9c8ea30"}

This response is more interesting than the 404 because it's leaking useful diagnostics: cf-space-id echoes back the space they resolved (so we know cfexampleapi is a real space), cf-environment-id: master tells us they fell back to the default environment, x-contentful-route tells you which route handler matched, and x-contentful-ratelimit-second-limit: 78 tells you the per-second budget for an unauthenticated request before the token is validated. Their public docs cite the rate limit as "55 requests per second"; the live header reports 78 for this particular path. Two different numbers from the same vendor for the same dimension — the docs and the headers should reconcile, and on this date they don't quite.

Third test — the Content Management API at api.contentful.com, hit with no token at all:

$ curl -sD - "https://api.contentful.com/spaces"

HTTP/2 401
content-type: application/vnd.contentful.management.v1+json
server: Contentful
strict-transport-security: max-age=31536000
x-contentful-request-id: bce88fd6-367b-43a3-b5c4-d17f700b26ba

{"message":"An access token is required. Please send one through the HTTP Authorization header or as the query parameter \"access_token\".","requestId":"bce88fd6-367b-43a3-b5c4-d17f700b26ba","sys":{"id":"AccessTokenInvalid","type":"Error"}}

Note the access-control-allow-headers on the CMA host (we trimmed it from the snippet above) advertises a sprawling set of X-Contentful-* headers including X-Contentful-Async-Ai-Action, X-Contentful-Ai-Actions-Platform, X-Contentful-Personal-Intercept, and X-Contentful-Skip-Ui-Draft-Validation. The 2025-2026 AI-actions push is real and it's wired into the CMA — you can see the surface area without an account just by reading the CORS preflight allowlist.

The CDA reference page itself

Contentful Content Delivery API reference page captured 2026-05-07. Shows breadcrumb 'Documentation / API reference / Content Delivery API', section headings for Authentication, API rate limits, Common resource attributes, and a Fair Usage Policy note.
The CDA reference at contentful.com/developers/docs/references/content-delivery-api/, captured 2026-05-07. Note the explicit "Fair Usage Policy" note and the "API rate limits" section in the left-hand nav — both are downstream of having a paid model where overage matters.

Two things about the docs page worth flagging. First: there's an honest, prominent "Fair Usage Policy" link in the introduction box — the kind of thing a vendor adds after enough customers have bumped into the soft limits. Second: the EU data-residency host (cdn.eu.contentful.com) is a separate base URL, not a regional hint on the main host. If a German customer asks "where is my content stored", the answer is "the host you point your SDK at" — and the SDK config lives in your code, so this is something to land in the architecture decision, not in the deployment scripts.

Honestly, next to Strapi / Directus / Payload

We've done the same scouting walk on the open-source side — Strapi, Directus, Payload — and we've also scouted the closer SaaS competitors: Contentstack, DatoCMS, Hygraph, Sanity. Contentful's place on that map is "the one everyone benchmarks against." Concretely:

DimensionContentful (this scout)Strapi / Directus / Payload
Time to first curl against your own data Free-tier signup + space + token: ~10 minutes from zero. ~10 minutes from docker run to authenticated GET.
Public pricing Free / €300 Lite / Custom Premium. Numbers are real. Self-host: free. Cloud tiers: published, usually $0/$10/$50 rungs.
Locale gating Free = 2, Lite = 3, more = custom Premium. Unlimited locales out of the box on all three.
Token model Three classes (CDA / CPA / CMA), three hosts. Has friction. One token per role; same host. Less elegant edge caching, simpler mental model.
Multi-region delivery Two CDN hosts (US default + EU residency), Varnish-fronted. Whatever you put in front of the container. CDN is your job.
SDK ecosystem JS, TS, Ruby, PHP, .NET, Java, Python, Swift, Android — all maintained. JS/TS first-class; rest is community.
Vendor lock-in Medium-high. CMA export tooling exists; content model is theirs. Low. The DB is yours. Migration is a SQL dump.
Compliance posture SOC 2, ISO 27001, GDPR-ready EU residency. You inherit your own posture. Audit reports require legal work.

The right way to read this table is not as a verdict. Contentful is the right answer for an editorial team of fifteen-plus people who need a battle-tested CDN with an EU residency option, a mature SDK in whatever language their backend is in, and a vendor with a SOC 2 report a Fortune-500 procurement team will accept. The locale gating bites if you're a small team scaling into multi-language; the €300 Lite jump bites if you're prosumer-shaped. The open-source three are the right answer for a startup that wants to own the database, ship in a weekend, and not be priced into a tier change at month seven.

Things we'd change

  1. Add a €30-50 prosumer rung between Free and Lite. The current curve has a Grand-Canyon-shaped hole between 2 locales / 100K calls and 3 locales / 1M calls. A "Solo / Indie" tier at €30 with 4 locales, 250K calls, 5 users, would catch every freelancer and agency-shop that currently grows out of Free into a competitor.
  2. Reconcile the rate-limit numbers. Docs say 55 req/s. Live x-contentful-ratelimit-second-limit header on a 401 response says 78. Pick one and document the path-level differences if they're real.
  3. Put the CDA / CPA / CMA distinction front-and-center in the getting-started. It's the single biggest source of "why don't I see my drafts" support load. A two-sentence callout on the first auth page would save thousands of tickets a year.
  4. Document the AI-actions surface like the Delivery API. The CMA CORS allowlist already advertises a half-dozen X-Contentful-Async-Ai-Action headers. The reference page for that surface should exist before the headers do.

What we'd actually do

If we had a customer with a 30-person editorial team, four locales, and a Java backend that needs an SDK their security team has already approved, we'd recommend Contentful and not pretend otherwise — the API is clean, the regions are real, the SDKs are maintained, and the €300 Lite is a small line item next to a salary budget. If we had a five-person team building a localized product into 2027, we'd self-host one of Strapi, Directus, or Payload, accept the CDN-is-your-job tax, and not be priced out of a fourth locale on a quiet Tuesday. The mismatch between Contentful's tier curve and a small team's growth curve isn't a flaw — it's the segmentation. Worth knowing which side of it you're on before you pick.

Where this fits

Adjacent scouting notes from the same week: Contentstack — behind the "Contact us" wall, DatoCMS — the GraphQL-first SaaS, Hygraph — multi-region GraphQL, Sanity — the structured-content one, Strapi — the open-source default, Directus — SQL-first with a real admin, Payload — TypeScript-first headless, Ghost — the headless-blog wedge. SimpleReview is the Chrome extension that turns whatever element you click on a broken admin or storefront into a draft code-fix PR — it works on a Contentful-rendered front end the same way it works on a Strapi one, because by the time the page renders it's just HTML.

Demo: SimpleReview on a Contentful issue

~/$ curl https://cdn.contentful.com/spaces/...
SimpleReview
$ curl -D - \
   https://cdn.contentful.com/spaces/INVALID/entries
 
< HTTP/2 404
< x-served-by: cache-ewr-kewr1740028-EWR
< x-contentful-ratelimit-second-limit: 78
⨯ NotFound · sys.id = "NotFound"
{"sys":{"type":"Error","id":"NotFound"},
"message":"The resource could not be found.",
"requestId":"aaeeb664-5507-496f-81a7-d0870f99bf61"}
# docs claim 55 req/s, header shows 78
✓ HTTP 200 · entries returned · 78 req/s budget
$ curl -D - \
   -H "Authorization: Bearer $CDA_TOKEN" \
   https://cdn.contentful.com/spaces/cfexampleapi/entries
 
< HTTP/2 200
< x-contentful-request-id: 9f81…
{"sys":{"type":"Array"},"items":[{...}]}
Comment×
space-id is a 12-char hash|
Fix it ✓ Done
waiting for selection…
Detected
Status404
sys.idNotFound
Fix plan
Contentful space-id is a 12-char alphanumeric hash. Replace placeholder with env var.
Result
200 + entries. 78 req/s budget visible (docs say 55).
✓ Fix ready
fix(contentful): use $CONTENTFUL_SPACE_ID
1 line · client.ts
Click SimpleReview → select NotFoundFix it → real space-id wired in