Keys and CORS
Publishable and secret API keys, the origin rule, rotation and revocation, how CORS follows your site's domains, and the monitor-then-enforce rollout.
Every public call may carry an API key in X-Api-Key. Keys are per site and
are created, rotated and revoked in Settings → Websites → Connect your
website.
Two kinds of key
Publishable pk_… | Secret sk_… | |
|---|---|---|
| Where it runs | The browser | Your own server (SSR, a backend job) |
| Secret? | No — it is inlined into your bundle by design | Yes — stored hashed, shown once at creation |
| Origin checked | Yes: only your site's own domains | No — a server call has no Origin |
| Rate limits | Standard | Five times higher |
| Bot check | Required on the five protected writes | Skipped |
| Visible in the CRM | Full value, any time | Prefix only, after creation |
A publishable key is not a password. Its value is elsewhere: you can revoke it, it is rate-limited on its own, and it tells the CRM which integration a call came from. Anyone can read it out of your JavaScript — and it will not work for them, because it is refused from any origin that is not one of your domains.
A secret key is a password. It skips the origin check precisely because a
server-to-server call has no origin to check, so treat it as a credential: keep
it in a server-only environment variable (never one prefixed NEXT_PUBLIC_),
never commit it, and rotate it if it ever appears in a browser bundle, a log or
a screenshot.
The origin rule
A pk_ call is accepted when its Origin is one of the site's domains. Outside
production http://localhost and http://127.0.0.1 are also accepted, so local
development works with the real key. Anything else is
403 SITE_KEY_ORIGIN_MISMATCH, with the same code when a key names a different
site than the request resolved to.
That is why the Domains tab matters twice: it decides which sites a host resolves, and it decides which origins a publishable key works from.
Rotation and revocation
- Rotate creates a new key and revokes the old one in a single step. Deploy the new value, then confirm nothing still uses the old one.
- Revoke kills a key immediately. A call with it answers
401 SITE_KEY_REVOKED. - A secret key's value is shown once, in the dialog that creates it. There is no way to read it again; if it is lost, rotate.
- Keys record a "last used" time, so you can tell a live key from a forgotten one before revoking it.
Rotating a publishable key is a deployment: the key lives in your build, so the new value only reaches visitors when the site is rebuilt. Rotate the secret key first if you are doing both.
CORS
CORS follows your site's domains, and nothing else. /api/public/* reflects an
Origin only when it is:
- a domain added to a site, or
- one of the platform's own configured front-ends, or
localhost, outside production.
Every other origin is refused before the handler runs — the browser reports a
CORS failure and there is no response body to read. If your fetch fails in the
browser but the same call works from curl, the origin is the thing to check,
not the key.
Request headers are allow-listed explicitly: Content-Type, Authorization,
X-Subdomain, X-Site, X-Api-Key, X-Turnstile-Token, X-Organization-Id,
X-Simulated-Organization-Id. A header outside that list fails the preflight,
so the browser never sends the request at all — which reads as a CORS bug rather
than a rejected header. Do not invent your own.
The rollout: monitor, then enforce
Keys are being switched on in two stages, so no existing website breaks at a deploy.
- Monitor (where the platform is now). A write with no key, an unknown key or a wrong origin is served, and the refusal is written to the server log instead. Nothing you build fails for lack of a key today.
- Enforce. The same request is refused:
401 SITE_KEY_REQUIRED,401 SITE_KEY_REVOKEDor403 SITE_KEY_ORIGIN_MISMATCH.
The switch is announced in the changelog before it happens. Send your keys now: in monitor mode a correct integration is indistinguishable from an enforced one, and on the day of the switch nothing changes for you.
Next
- Bot protection — the extra token on five writes.
- Rate limits — what "five times higher" means in numbers.
Identify your site
How a public request is matched to one website — verified domain, X-Site, and the deprecated X-Subdomain — and what a draft or paused site answers.
Bot protection
Cloudflare Turnstile on the five public writes bots abuse — when a token is required, how to send it, and what the refusals mean.