Auth & scopes
The platform API authenticates with a key from your org. It comes in two forms — same org, same scopes, different runtime:
| Form | Credential | Sent as |
|---|---|---|
| Server (backend) | kld_sk_live_… | Authorization: Bearer … or X-Api-Key |
| Publishable (browser) | kld_pk_live_… | exchanged by the SDK for a short-lived session; never sent as a raw bearer |
Authorization: Bearer kld_sk_live_…
# or
X-Api-Key: kld_sk_live_…
Server keys are the bearer you send from your backend on each request.
Publishable keys are for the browser: the SDK trades one for a short-lived,
origin-bound session token at runtime, so the publishable key itself is never a
standing credential in page source. A publishable key presented directly as a
bearer is rejected — use it through the SDK. A server key gets no CORS
grant, so a page can never read a response made with one — but CORS cannot
stop the request from leaving the browser, so a server key pasted into page
source has already leaked by the time the API refuses it. The SDK refuses
kld_sk_… keys at mount for exactly this reason: keep server keys
server-side, always.
Existing kld_live_… legacy keys keep working as a direct bearer in both places.
Scopes
A key carries capability scopes; each route family requires one:
| Scope | Route family | Used by |
|---|---|---|
ai | /inference-api/b2b/v1/chat/*, /retrieval/* | chat embed |
design | /inference-api/b2b/v1/design/* | editor embed |
maps | designed basemaps | the tile embed |
vendor | (modifier, not a route) | the chat embed, on your own data |
By default a Pro/Enterprise key is minted with ai, design and maps. Free-plan
keys are clamped to maps + the tile product (details).
vendor — request it deliberately, and only where you mean it
vendor is not a route gate. It does not admit or deny a request; it decides
whether the AI chat may consult your organization's uploaded vendor data when
it answers. Every other scope answers "may this key call this endpoint"; vendor
answers "may this key speak for our internal data".
It is therefore never included by default — ask for it explicitly at mint:
{ "name": "our-site-widget", "scopes": ["ai", "vendor"] }
Which key carries vendor is the entire security decision. The chat embed is a
browser widget: it exchanges a publishable key for a short-lived session, so a
vendor-enabled key necessarily sits in a public page. That is fine for data you
are happy to show every visitor of that site — a property list, opening hours,
public rates. It is not fine for anything you would not publish. The origin
allowlist that guards the exchange is a browser convention, not a confidentiality
boundary: a caller that sets its own Origin header is not stopped by it.
So:
- One key per surface. A marketing site, a demo page and your customer-facing
app should not share a key. Only the one that must answer from your data gets
vendor. - Never put a rate sheet, cost basis, or anything unreleased behind a publishable key. If the answer would embarrass you on a public page, the data does not belong in a vendor-scoped embed.
- Sessions are narrowed to their product, so a
tileorviewersession never carriesvendoreven when its parent key holds it.
To withdraw it, revoke the key rather than editing its scopes. Scopes are stamped into a session at mint and a session lives up to 30 minutes, so an edit takes effect only as live sessions expire; a revoke is immediate.
401 vs 403
These are distinct on purpose:
- 401 Unauthorized — missing / invalid / revoked / expired key. Re-check the key value and that it isn't revoked.
- 403 Forbidden (
insufficient_scope) — the key is valid but lacks the scope the route needs. Mint (or re-mint) a key with the right scope.
Both fail closed: a key with no scopes is denied everywhere.
See Errors for the full status table.