Storyblok scout: signup, demo API, and the bridge that fights localStorage

Storyblok is a hosted headless CMS. There is no self-host build, no community Docker image, no on-premise SKU outside an enterprise contract — if you want it, you sign up and use their cloud. We did the next-best scouting test: we walked through the signup page, hit the public Content Delivery API with the token Storyblok publishes in their own docs, read the response headers, and pulled the GitHub issues new users actually file on their first day. This is what we found, with screenshots taken on the date stamped above.

Honest about what this is

We're the team behind SimpleReview, a Chrome extension that drafts code-fix PRs on whatever site you click. We are not affiliated with Storyblok, we're not a partner, we don't get paid to recommend anything. This page is a scout's notebook from one Linux box on one date. Because Storyblok is SaaS-only, we did not deploy a stack — we tested the parts you can test from outside: the public website, the public API, and the public issue tracker. If we got a number wrong, open an issue and we'll correct it.

What "SaaS-only" means here

You will see Storyblok compared with Strapi, Directus, Payload, and other open-source headless CMSes. The shape is similar — a content tree, a delivery API, a Visual Editor, a webhook system — but the operating model is not. Storyblok runs on AWS in five regions, the source is closed, and the only artifacts you can self-manage are the SDKs and the field-type plugin code on GitHub. Self-hosting Storyblok is not a question of "find the right Helm chart"; it isn't an option. If that's a hard constraint for your project (data sovereignty, air-gapped networks, no third-party DPAs), stop here and look at Strapi or Directus.

Friction 1 — the signup is gated by company name

The signup page asks for the things you'd expect (first name, last name, email, password) plus two that surprise a solo developer: Company Name *, with the placeholder "Where do you work?", and Job Title *. Both fields are required. There is a "Sign Up With GitHub" button next to the email form, but the company-name and job-title fields still load in the email form below it. Below the fold there are role and use-case dropdowns.

Storyblok signup page captured 2026-05-08 at 1440 by 950: Sign Up With GitHub, first name, last name, required company name, required job title, email, password, and create-account button
Storyblok signup page, app.storyblok.com — captured again 2026-05-08 at 1440×950. The company-name and job-title fields are marked required.

This is a small thing, but it telegraphs the audience. Storyblok's free tier exists, but the funnel is built for people who can answer "Where do you work?" without inventing something. Indie devs and weekend-project builders type a placeholder and move on; teams skim past it. Worth knowing before you reach the page expecting Sanity-style instant onboarding.

Friction 2 — the pricing page leads with the trial, not the free plan

Storyblok pricing page captured 2026-05-08 at 1440 by 915: headline says 'Free to go live. Then pay for the features you want.' with a 'Start 45-Day Free Trial' button and four plan cards visible
storyblok.com/pricing — captured again 2026-05-08 at 1440×915. The big yellow CTA starts the Growth Plus trial, not the free Starter plan.

The hero says "Free to go live. Then pay for the features you want." The button below it is "Start 45-Day Free Trial" — that trial is the Growth Plus plan, not the free tier. To actually land on Starter you scroll past the hero, click "Compare All Plans", and pick Starter from the comparison grid. The free plan is real, just not where the funnel points first.

Plan limits per the public pricing page on the test date (we don't restate these for SEO; we restate them because the comparison row below makes more sense with them in front of you):

PlanListed priceSeats incl.Traffic / moAPI requests / moLocales
Starter$01 (max 2)100 GB100k2
Growth$99/mo5 (max 10)400 GB → 1 TB1M → 5M2 → unlimited
Growth Plus$349/mo15 (max 20)1 TB → 2 TB4M → 15M10 → unlimited
Premium / EliteCustom

Two numbers worth flagging. The Starter plan caps you at 2 locales — meaning your free CMS supports two languages, full stop. If you need EN + DE + FR you're paying $99/mo (Growth) plus $20 per extra locale on top. The 100k API requests / month cap on Starter works out to roughly 3,300 requests/day, which is fine for a brochure site behind aggressive caching but is the first thing that will trip a JAMstack build that revalidates every commit.

First-hand artifact — the public Content Delivery API

Storyblok publishes a working demo space token in their own docs (wANpEQEsMYGOwLxwXQ76Ggtt). It's a real space, it really responds, and you can test the API shape without signing up. Here is what we got back at 15:22 UTC on 2026-05-07 from a Hetzner box:

$ curl -sSL -D - -o /dev/null \
    "https://api.storyblok.com/v2/cdn/stories?token=wANpEQEsMYGOwLxwXQ76Ggtt&per_page=1"

HTTP/2 301
location: https://api.storyblok.com/v2/cdn/stories?cv=1540140368&per_page=1&token=...
sb-be-version: 5.822.3
x-cache: Miss from cloudfront
x-amz-cf-pop: HEL51-P7

HTTP/2 200
content-type: application/json; charset=utf-8
content-length: 774
cache-control: max-age=0, public, s-maxage=604800, stale-if-error=3600
total: 5
per-page: 1
etag: W/"e48d26077d2a2f30514426f0426c70c9"
sb-be-version: 5.822.3
x-runtime: 0.046216

Three observations from the wire:

  • Every first request 301-redirects to a URL with a cv=<version> query parameter appended. cv is Storyblok's cache-version stamp; the redirect is how they invalidate edge caches when content changes. If your HTTP client doesn't follow redirects by default, your first call to a fresh space looks broken. Set --location on curl, redirect: 'follow' on fetch.
  • The CDN is CloudFront, edge POP is Helsinki for our test box. Storyblok's "EU" endpoint is geo-routed; your latency depends on the nearest CloudFront PoP, not on where the origin sits.
  • s-maxage=604800 — a week. Edge cache is aggressive, which is the right default for content that changes via webhook-driven invalidations but matters if you expect "publish" to be instant.

The body itself is a small but real payload — 774 bytes, two stories, both authored years ago:

{
  "stories": [
    {"name":"health","slug":"health","created_at":"2018-10-21T08:41:21.044Z", ...},
    {"name":"demo","slug":"demo","content":{"body":[{"text":"This content is coming from Storyblok","component":"headline"}], ...}}
  ],
  "cv": 1540140368, "rels": [], "links": []
}

And the space metadata, fetched via the same token, deserves to be quoted verbatim because it's accidentally honest:

$ curl -sSL "https://api.storyblok.com/v2/cdn/spaces/me?token=wANpEQEsMYGOwLxwXQ76Ggtt"
{"space":{"id":39985,"name":"Demos (DONT DELETE)","domain":"http://localhost:4000/", ...}}

Yes, the public demo space is named "Demos (DONT DELETE)". Someone put that note in the name field years ago and it ships in every quickstart. We promise we did not edit the response.

Friction 3 — region routing fails closed, and that's the right behavior

Storyblok runs five regional clusters: EU (api.storyblok.com), US (api-us.storyblok.com), CA (api-ca.storyblok.com), AP (api-ap.storyblok.com), and a separate China deployment at app.storyblokchina.cn. The token belongs to one cluster. Hit the wrong one and:

$ curl -sSL -D - -o /dev/null \
    "https://api-us.storyblok.com/v2/cdn/stories?token=wANpEQEsMYGOwLxwXQ76Ggtt&per_page=1"

HTTP/2 401
content-length: 24
sb-be-version: 5.822.3
Right call by Storyblok

An invalid-region token returns 401, not a cross-region proxy. That means data residency is enforced at the cluster boundary — if you tell Storyblok your space is EU, your delivery requests to the US endpoint are rejected, period. Good for GDPR posture, slightly annoying when you copy-paste a quickstart and forget to switch the SDK base URL.

The official SDKs (@storyblok/js, storyblok-nuxt, @storyblok/react) accept a region option that maps to the right hostname. Most copy-paste tutorials don't show it, because most copy-paste tutorials assume EU. If you signed up after picking US in the region selector and your storyblok-nuxt config doesn't set region: 'us', you'll get the 401 above and "wrong token" is the wrong diagnosis.

Friction 4 — the Visual Editor's iframe needs a localStorage that exists

The Visual Editor is the feature Storyblok markets hardest, and it's a real differentiator: you point it at your site's URL, the page loads inside their CMS in an iframe, and clicking on a heading in the iframe selects it in the side panel. To make it work, you set Location (default environment) in the space settings to a URL your site is reachable on, then load it.

This is where a new user's day-1 hits the rocks, and the GitHub issue storyblok/storyblok#646 documents the exact failure mode. Quoting the reporter:

Real user report — issue #646

"I went into the Settings and in the General tab to change the value of the Location (default environment) to http://localhost:3000/. After that, when I click 'home' content I get the nuxt page error which disappears quickly and the browser open a blank page with a chrome debug console error: bridge:24 Uncaught DOMException: Failed to read the 'localStorage' property from 'Window': Access is denied for this document."

Translation: the Storyblok preview bridge.js reads localStorage from inside the iframe; modern browsers block third-party-context localStorage when the iframe origin doesn't match the top-frame cookie policy. With strict cookie blocking enabled (Chrome with third-party cookies blocked, Safari, Brave at default), the bridge throws and the editor goes blank. The issue is closed because the cause is browser policy, not Storyblok bug — but the answer is "configure your browser" or "use a proper HTTPS preview URL, not localhost", which is the kind of friction that bounces a free-tier user back to Sanity Studio.

Other real day-1 reports on the same tracker before it was archived: autosave breaking undo (#520), the input event missing the field name (#429), preview toolbar resizing on mousewheel scroll (#486). None of these are deal-breakers; they're the texture of a Visual Editor that does a lot and therefore has surface area.

Honest comparison — one row each

We are not going to write a 4,000-word "Storyblok vs everyone" page. One row, picked for the dimension that actually moves the decision:

ToolFree tier headlineThe thing it's better atThe thing it's worse at
Storyblok 1 seat, 100k API req, 2 locales Visual Editor — clicking on the live page to edit it is genuinely smooth when it works Schema/component editing happens in the dashboard, not in your repo. No git-as-source-of-truth.
Sanity 3 users, 10k docs, generous bandwidth Studio is a React app that lives in your repo — schema is code, version-controlled. No native Visual Editor. Their Presentation tool exists but you wire it up.
Contentful 5 users, 25k records, 2M API calls Battle-tested at enterprise scale, broadest SDK coverage. Pricing scales hard above the free tier — content types and locales are billed.

If your team has a designer who hates dashboards and wants to click on a paragraph to edit it, Storyblok wins. If your team is engineer-heavy and wants the schema in a PR, Sanity wins. If you're the IT department of a Fortune 500 picking on procurement criteria, Contentful's incumbency is a feature.

Things we'd change in the docs

  1. Document the 301 on first request in the quickstart, not just in the API reference. New users on fetch-without-redirects panic at "broken" responses.
  2. Put the region selector in the SDK quickstart's first code block. "Forgot to set region" is a 401 we shouldn't have to debug.
  3. Add a "Visual Editor in a private-browsing context" troubleshooter. The localStorage failure mode is browser policy, but the docs can say so up front instead of leaving it to a closed GitHub issue.
  4. Surface the 2-locale Starter cap on the marketing pages, not just the comparison grid. It's the first wall a multilingual project hits on the free tier.

Where this fits

One short, honest write-up per CMS we evaluate. Adjacent: Ghost 5.x first-deploy notes, Strapi 5 self-host scout, Directus on a single VPS, Sanity Studio on day one. SimpleReview is the Chrome extension that turns whatever element you click on a broken admin into a draft code-fix PR — including the schema/blok views inside Storyblok's dashboard, once you're past signup.

Demo: SimpleReview on a Storyblok issue

~/$ curl https://api-us.storyblok.com/v2/cdn/stories
SimpleReview
$ curl -D - "https://api-us.storyblok.com/v2/cdn/stories?token=$EU_TOKEN"
 
< HTTP/2 401
< sb-be-version: 5.822.3
< via: 1.1 cloudfront
⨯ HTTP 401 Unauthorized
{"error":"Unauthorized"}
cause: token issued in EU space, hitting api-us endpoint
region-boundary enforced — same token works against api.storyblok.com
✓ HTTP 200 · 1 story · CDN edge HEL51-P7
$ curl -D - "https://api.storyblok.com/v2/cdn/stories?token=$EU_TOKEN"
 
< HTTP/2 301 → /v2/cdn/stories?token=...
< HTTP/2 200
< sb-be-version: 5.822.3
< x-amz-cf-pop: HEL51-P7
{"stories":[{"name":"Demos (DONT DELETE)","slug":"home"}]}
Comment×
EU token, US endpoint|
Fix it ✓ Done
waiting for selection…
Detected
Status401
Causeregion mismatch
Fix plan
EU tokens hit api.storyblok.com; US tokens hit api-us.storyblok.com. Match host to space region.
Result
200 with story payload. Edge cache via CloudFront.
✓ Fix ready
fix(storyblok): pick host by region
1 line · client.ts
Click SimpleReview → select the 401 response → Fix it → host matches space region