Plan limits
Every limit below comes from the plan of the account that owns the project, not from the plan of whoever is using it.
Account and API
Free | Pro | Ultra | |
|---|---|---|---|
Price a month | $0 | $29 | $99 |
Rate limit per project | 300 req/min | 1,000 req/min | 5,000 req/min |
Monthly requests | 100,000 | 2,000,000 | 10,000,000 |
Projects per account | 1 | 5 | 25 |
Max file size | 25 MB | 250 MB | 500 MB |
Max files | 1,000 | 10,000 | 50,000 |
Total storage | 1 GB | 10 GB | 50 GB |
AI answers/month | 50 | 450 | 1,650 |
AI embedding tokens/month | 2,000,000 | 20,000,000 | 50,000,000 |
Import pages/month | 50 | 1,000 | 5,000 |
Monthly limits reset on the 1st of each month at 00:00 UTC. Upgrade from your account settings. How import pages are counted is in Import files into your help center.
Monthly requests and AI answers are shared by every project the account owns; the per-minute rate limit applies to each project on its own. Prices exclude tax: Paddle, which handles payments, adds any tax due at checkout. Pro and Ultra also leave the "powered by kookee.dev" link out of your portal's footer.
An AI answer is a reply from the assistant that links at least one of your articles. Greetings, replies that found nothing and errors are free. Behind the answers sits a monthly AI cost ceiling: the chat stops at whichever runs out first, answering 400 with MONTHLY_AI_ANSWER_LIMIT_EXCEEDED or MONTHLY_AI_CHAT_LIMIT_EXCEEDED, and the site widget tells visitors the assistant is unavailable. Embedding tokens are spent indexing your articles for AI search. Running out of them does not stop the chat, but articles published or changed afterwards are not indexed until you save them again after the 1st.
Status reads, GET /v1/status and GET /v1/status/incidents/{id}, do not count toward monthly requests. They have a per-minute limit of their own, ten times the plan's rate limit and apart from the rest of the API, and one IP address can make up to 600 of them a minute. Past either limit they answer 429 with RATE_LIMIT_EXCEEDED. See Show your status page on your own site.
What one project can hold
Free | Pro | Ultra | |
|---|---|---|---|
People, including pending invitations | 3 | 10 | 100 |
Entries | 2,000 | 50,000 | 500,000 |
Feedback posts | 1,000 | 25,000 | 250,000 |
API keys | 10 | 50 | 200 |
Categories | 100 | 500 | 2,000 |
Tags | 500 | 2,500 | 10,000 |
Entry types | 20 | 100 | 500 |
Config values | 50 | 250 | 1,000 |
Comments per entry | 500 | 2,000 | 10,000 |
Custom fields per entry type | — | 5 | 25 |
Options per select field | — | 200 | 1,000 |
Webhook endpoints | 2 | 10 | 50 |
Integrations | — | One per service | One per service |
Status page components | 10 | 50 | 200 |
Incidents and maintenance | 1,000 | 25,000 | 250,000 |
Uptime monitors | 3 | 20 | 100 |
Feedback posts have a limit of their own and do not count toward entries.
An incident holds up to 200 updates on every plan. The shortest check interval is 5 minutes on Free and 1 minute on Pro and Ultra, and an account can have up to 10 monitors for the same host name across its projects. See Set up a status page and Monitor your site with uptime checks.
Webhooks and integrations also have hourly limits: 100 events an hour across the account on Free and 1,000 on Pro and Ultra, and 300 an hour for each endpoint or integration. Events over them are dropped. See Send events to your site with webhooks and Connect Slack, Discord, Telegram, GitHub, Jira, Asana or Freshdesk.
Every project shows what it is using against these numbers under Plan and usage in its settings, and each list warns as it fills rather than after.

Seats and invitations are covered in Inviting your team to a project.
Rate limit responses
Exceeding a limit returns 429 Too Many Requests.
Per-minute limit:
{ "code": "RATE_LIMIT_EXCEEDED" }
Monthly limit:
{ "code": "MONTHLY_API_LIMIT_EXCEEDED" }
The distinction matters when you handle these. RATE_LIMIT_EXCEEDED clears within a minute, so retrying makes sense. MONTHLY_API_LIMIT_EXCEEDED will not clear until the reset date — retrying is pointless, and you should fall back to cached content and alert someone.
When a capacity limit is full
Rate limits are about how often you ask. Capacity limits are about how much a project holds, and they are checked when something is created — in the dashboard, or by an AI assistant over MCP.
Going one over what the plan allows is refused with 403 Forbidden and a code naming what filled up: ENTRY_LIMIT_REACHED, API_KEY_LIMIT_REACHED, CATEGORY_LIMIT_REACHED, TAG_LIMIT_REACHED, ENTRY_TYPE_LIMIT_REACHED, CONFIG_LIMIT_REACHED, COMMENT_LIMIT_REACHED, FEEDBACK_POST_LIMIT_REACHED, FIELD_LIMIT_REACHED, FIELD_OPTION_LIMIT_REACHED, WEBHOOK_LIMIT_REACHED or PROJECT_LIMIT_REACHED. A file import that needs more pages than are left this month is refused the same way, with IMPORT_PAGE_LIMIT_REACHED. Custom fields come with Pro and Ultra, so adding one on Free is refused with ENTRY_FIELDS_NOT_IN_PLAN; see Custom fields for your content and feedback. Integrations work the same way: connecting one on Free is refused with INTEGRATION_NOT_AVAILABLE_ON_PLAN. The status page and uptime monitors answer 403 too, with STATUS_COMPONENT_LIMIT_REACHED, INCIDENT_LIMIT_REACHED, INCIDENT_UPDATE_LIMIT_REACHED or MONITOR_LIMIT_REACHED, and with MONITOR_HOST_LIMIT_REACHED for an eleventh monitor of one host name. A check interval shorter than the plan allows is refused with 400 and MONITOR_INTERVAL_NOT_ALLOWED.
Seats answer differently: inviting somebody into a project with no seat left, or admitting them through an invite link, is refused with 409 Conflict and MEMBER_LIMIT_REACHED.
None of these clear by waiting. Delete something, free a seat, or move to a larger plan.
Messages from the site widget's Feedback tab have limits of their own, the same on every plan: 30 an hour from one visitor address and 120 an hour per project, answered with 429, and 1,000 waiting for review per project, answered with 403 WIDGET_FEEDBACK_LIMIT_REACHED until your team reviews some on the Feedback page. Sending them does not count against monthly requests.
People with no role in a project, such as portal visitors, can post up to 10 feedback posts and 50 comments in it in any 24 hours, counting blog, changelog and feedback comments together, the same on every plan. Past that they get 429 with FEEDBACK_POST_DAILY_LIMIT_REACHED or COMMENT_DAILY_LIMIT_REACHED. Members of the project have no daily cap.
KookeeApiError
Failed requests throw a typed error:
import { KookeeApiError } from '@kookee/sdk';
try {
const post = await kookee.blog.getBySlug('non-existent');
} catch (error) {
if (error instanceof KookeeApiError) {
error.code; // 'ENTRY_NOT_FOUND'
error.message; // 'Request failed with status 404'
error.status; // 404
}
}
Properties:
code— machine-readable error code, such asENTRY_NOT_FOUNDorAPI_KEY_INVALIDmessage— a generic description built from the statusstatus— the HTTP status code
The API's error body carries only a code, never a message, so message is always the SDK's default, 'Request failed with status {status}'. If the response has no JSON body, code falls back to 'UNKNOWN_ERROR'.
kookee.status.getIncident(id) and GET /v1/status/incidents/{id} answer 404 with INCIDENT_NOT_FOUND for a draft or for an id that is not one of the project's incidents. The API does not check the portal's Status page module, so STATUS_PAGE_NOT_ENABLED comes only from Kookee's hosted status page while that module is off.
Handling by status
try {
return await kookee.blog.getBySlug(slug);
} catch (error) {
if (!(error instanceof KookeeApiError)) throw error;
switch (error.status) {
case 404:
return null; // render your not-found page
case 401:
throw error; // bad API key — a deploy problem, fail loudly
case 429:
return getCached(slug); // backed off, serve stale
default:
return getCached(slug); // 5xx — degrade rather than break the page
}
}
Re-throwing anything that is not a KookeeApiError matters. A bug in your own code should not be swallowed by a catch block meant for network failures.
Staying under the limits
Cache aggressively. Content changes when someone publishes, not on every request. Even a 60-second cache turns a busy page from thousands of requests per minute into one.
Fetch on the server. A server-rendered page makes one API call for all its visitors. The same page fetching client-side makes one call per visitor.
Batch what you can. kookee.config.list({ keys: [...] }) fetches many values in one request. Ten getByKey() calls are ten requests.
Debounce search inputs. Searching on every keystroke is the single fastest way to hit a rate limit. 200–300ms of debounce is plenty.
Request only what you need. limit defaults to 10 and maxes at 100 — asking for 100 items to display 5 wastes payload, and asking for 10 pages of 10 wastes requests.
Exponential backoff
For background jobs and scripts:
async function withRetry<T>(fn: () => Promise<T>, attempts = 3): Promise<T> {
for (let attempt = 0; attempt < attempts; attempt++) {
try {
return await fn();
} catch (error) {
const isRetryable =
error instanceof KookeeApiError &&
error.code === 'RATE_LIMIT_EXCEEDED' &&
attempt < attempts - 1;
if (!isRetryable) throw error;
await new Promise((resolve) => setTimeout(resolve, 2 ** attempt * 1000));
}
}
throw new Error('unreachable');
}
Waits of 1s, 2s then 4s. Do not apply this to MONTHLY_API_LIMIT_EXCEEDED — retrying a monthly limit just spends more of a budget you have already exhausted.