Two status codes cover nearly all of these, and they mean different things.
401 Unauthorized — the key is missing, malformed, revoked, or is the wrong kind of key.
403 Forbidden — the request was understood, but it is not allowed. Almost always one of two causes: the origin is not in the project's Allowed Origins, or the request carried only a project id while the hosted portal is off.
Check the key reaches the SDK
The most common cause is not a wrong key but an undefined one. An environment variable that did not load produces a request with no usable key.
console.log('Key present:', Boolean(process.env.KOOKEE_API_KEY));
console.log('Key length:', process.env.KOOKEE_API_KEY?.length);
Never log the key itself — logs get shipped, shared and pasted into issues.
Usual reasons a variable is undefined:
The
.envfile is not in the directory the process starts fromThe framework needs a specific prefix (
VITE_,NEXT_PUBLIC_) for client-side accessThe dev server was not restarted after the file changed
Production has no environment variables configured at all — a very common first deploy failure
Verify the key itself
curl -i -H "Kookee-API-Key: YOUR_API_KEY" \
https://api.kookee.dev/v1/health
A success confirms the key is valid. A 401 means it is not — create a new one on Connect → API keys.
Paste carefully. A trailing newline, a truncated copy, or an inline comment accidentally included after the value (KEY="kk_…" # local) all produce exactly this failure.
401 with a key that looks right: is it an MCP token?
Kookee has two kinds of credential. The API key (Connect → API keys, starts with kk_) is for the SDK and REST API. The MCP token (Connect → MCP server, starts with kkm_) is only for AI assistants talking to the MCP server.
The public API rejects MCP tokens with 401 API_KEY_INVALID. If the value in your app starts with kkm_, that is the problem — create an API key instead.
Errors from the MCP server
An assistant that cannot connect usually reports one of these:
Code | What it means | What to do |
|---|---|---|
| The client sent an API key, or a shared MCP key from before v1.12.0 | Create a personal token on Connect → MCP server and reconnect with it |
| The token was revoked or mistyped, or it no longer belongs to an active member of the project | Create a new token |
| Too many requests in the last minute | Wait a minute and retry |
| The project owner's monthly request quota is used up | Upgrade, or wait for the next month |
MCP_TOKEN_INVALID deliberately does not say which of those it was. If the token still appears on the MCP server page, check that you copied all of it.
An assistant that says a tool does not exist, such as create_entry, is usually connected with a read-only token: those are not offered write tools. Create a token with Read and write access if it needs to change anything.
Errors when connecting by signing in
Apps that connect by signing in (Claude, ChatGPT, VS Code and Claude Code) first send you to a Kookee page. It can show:
This sign-in link has expired — the page was open for too long. Start connecting again from the app.
This link is not valid — the address was changed or cut short. Start again from the app.
Guest accounts cannot connect apps — create an account, or sign in to one, and try again.
Once connected, an assistant may report one of these codes. It usually corrects itself:
Code | What it means | What to do |
|---|---|---|
| You belong to several projects and the request did not say which | Name the project in your request |
| None of your projects has that slug | Use one of the projects the error lists |
| The account is not a member of any project | Create a project, or accept an invitation |
An app that used to work and now asks you to connect again was disconnected: under Security → Connected apps in your account settings, or by a password change or reset. Connect it again.
403: allowed origins
If your project has allowed origins set, browser requests from any domain not on the list are refused with 403 ORIGIN_NOT_ALLOWED. Typical triggers:
A new domain or subdomain (
www.versus the bare domain, a preview deployment,localhost)The request works from
curlor from your server but fails in the browser — server requests carry noOriginheader and skip the check entirely
Add the domain in project settings, or clear the list while developing.
The chat widget is rejected on a new site
The AI chat widget runs in the browser, so it is subject to the same origin check. When your project has Allowed Origins set and you install the widget on a new website, add that website's domain to the project's Allowed Origins in project settings, or its chat requests are rejected.
This is the usual reason a widget that works on one site shows an error on another.
403: portal not enabled
A request that carries only a project id — new Kookee({ projectId }), or the Kookee-Project-Id header — works only while the project's hosted portal is enabled. With the portal off, it is refused with 403 PORTAL_NOT_ENABLED. Enable the portal, or switch to an API key (apiKey) and restrict it to your site's domains with the project's Allowed Origins.
The same code comes back for a project id that does not exist (a value that is not a UUID at all gets 400 VALIDATION_ERROR), so if the portal is on, compare the id with the one in your project settings first.
An empty apiKey throws immediately
const kookee = new Kookee({ apiKey: '' }); // throws
The constructor rejects an empty key rather than failing later on the first request. If you see this at startup, the variable is empty rather than merely wrong.
Errors in the browser
A 401 from client-side code, when the server works, usually means the key was never bundled. Check the prefix requirement — VITE_ for Vite, NEXT_PUBLIC_ for Next.js — and rebuild. Environment variables are inlined at build time, so a variable added after the build is not in the bundle.
Handling it in code
import { KookeeApiError } from '@kookee/sdk';
try {
return await kookee.blog.list();
} catch (error) {
if (!(error instanceof KookeeApiError)) throw error;
if (error.status === 401) {
// Configuration problem — fail loudly, do not degrade silently
throw new Error('Kookee API key is invalid or missing');
}
if (error.status === 403) {
// ORIGIN_NOT_ALLOWED or PORTAL_NOT_ENABLED — see the two 403 sections above
throw new Error(`Kookee refused this request: ${error.code}`);
}
throw error;
}
Do not fall back to cached or empty content on a 401. A rate limit or a 500 is worth degrading around; an auth failure is a deploy problem, and hiding it means discovering weeks later that a page has been silently empty.
After a leak
Revoke the key on Connect → API keys and create a replacement. Revocation is immediate, so deploy the new key first if you want to avoid downtime.
A leaked API key is low severity — it only reaches already-public content — but rotate it so usage stays attributed correctly. A leaked MCP token should be revoked at once on Connect → MCP server; it reads your drafts, and a token with write access can publish content.
Still stuck?
Email admin@kookee.dev with your project slug, the status code, the code from the error, and whether the curl health check above succeeds.
Do not send your API key — not in an email, a screenshot or a log paste. We never need it and will never ask for it. If a key has already been exposed somewhere, revoke it first, then get in touch.