Account and Operations

Rate limits, plans and error handling

Updated 109 views

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.

Plan and usage panel on the Free plan: team seats 2 of 3, API keys 0 of 10, entries 5 of 2,000, and meters for categories, tags, entry types and configs

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:

JSON
{ "code": "RATE_LIMIT_EXCEEDED" }

Monthly limit:

JSON
{ "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:

TypeScript
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 as ENTRY_NOT_FOUND or API_KEY_INVALID

  • message — a generic description built from the status

  • status — 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

TypeScript
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:

TypeScript
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.

Was this article helpful?