Every public API request carries a client key. A key belongs to one knowledge base and only works for that knowledge base.
Key typeLooks likeUse it inOrigin allowlistCAPTCHA
Public keykl_pk_…Browsers and mobile appsOptionalRequired when enabled
Secret pairkl_ci_… (ID) + kl_cs_… (secret)Your servers onlyNot checkedNever

Create a key

1

Open the knowledge base

Go to the knowledge base and open the Integrations tab.
2

Create the key

Under Client Keys, click Create key. Enter a Key name and pick Public key or Secret pair.
3

Set the options

Set the options below, then click Create key.
4

Copy the credentials

A secret is shown once, right after you create it. Store it somewhere safe.
OptionWhat it does
Allowed originsPublic keys only. Comma-separated list of sites that may use the key. Empty allows every origin
Allow MCP accessLets the key connect AI tools through the MCP server. On by default
Rate limitRequests per minute. Empty uses the default of 600
To revoke a key, click Revoke on its row in Client Keys, then Revoke key. Apps using it stop working at once. This cannot be undone. An internal knowledge base has no public API, so it cannot have client keys.

Send the key

CredentialHeaders
Public keyX-Client-Key: kl_pk_…
Secret pairX-Client-Id: kl_ci_… and X-Client-Secret: kl_cs_…
WebSocketQuery string: ?client_key=kl_pk_… or ?client_id=kl_ci_…&client_secret=kl_cs_…
From a server:
curl https://app.kelu.dev/api/v1/public/knowledge-bases/KB_ID/search \
  -H "X-Client-Id: kl_ci_..." \
  -H "X-Client-Secret: kl_cs_..." \
  -H "Content-Type: application/json" \
  -d '{"query": "rate limits"}'
From a browser, send X-Client-Key: kl_pk_… instead, plus a CAPTCHA token (below).
Never put a secret (kl_cs_…) in browser or mobile code. Anyone can read it there.

Allowed origins

  • Each entry is a full origin, such as https://docs.example.com.
  • https://*.example.com matches one level of subdomain, such as https://docs.example.com. It does not match https://example.com itself.
  • Once a public key has an allowlist, requests with no Origin header are refused with 403. That includes curl and server code. Use a secret pair there.

CAPTCHA

When Kelu has reCAPTCHA v3 turned on, requests made with a public key must carry a reCAPTCHA token. The widget does this for you.
  • Gated endpoints: chat, thread chat, search, feedback (POST) and form-deflect.
  • Not gated: widget-config, suggested questions, related articles, events, and withdrawing a rating.
  • HTTP: send the token as X-Captcha-Token: <token>.
  • WebSocket: send it as captcha_token on each chat frame.
  • A missing token returns 403 with CAPTCHA_REQUIRED. A token that fails returns 403 with CAPTCHA_FAILED.
To find out whether a key needs a token, call widget-config. It returns captcha_required and the recaptcha_site_key to mint tokens with.
Got CAPTCHA_REQUIRED from curl or a backend? You are using a public key. There is no way to mint a reCAPTCHA v3 token outside a browser, so switch to a secret pair. It is never CAPTCHA-gated.
Building your own browser client on @kelu/sdk? Pass getCaptchaToken and the SDK sends the header on every request:
const kelu = new Kelu({
  knowledgeBaseId: "KB_ID",
  clientKey: "kl_pk_...",
  getCaptchaToken: () =>
    new Promise((resolve) =>
      grecaptcha.ready(() =>
        grecaptcha.execute(SITE_KEY, { action: "chat" }).then(resolve),
      ),
    ),
});

Rate limits

  • Each key has its own limit per minute. The default is 600. You can set up to 100,000 when you create the key. Empty or 0 uses the default.
  • Every request with the key counts. Requests refused for a missing or failed CAPTCHA do not.
  • Over the limit you get 429 with code: "RATE_LIMITED" and a Retry-After header (seconds).
  • Responses carry X-RateLimit-Limit and X-RateLimit-Remaining.
  • On WebSocket, each question counts once. Over the limit you get an error frame with code: "rate_limited".