API reference¶
KDBL Context Lake (K-Lake) exposes a REST API. The UI and the kdbl-control CLI both use it, so anything you see in the console can be scripted.
Base URL¶
Everything except the health probes lives under /api:
Authentication¶
All /api/* endpoints require a bearer token. The two accepted token types:
- Personal access token (PAT) — minted from
/api/tokensor the UI. Format:kdblpat_<base64>. - OIDC bearer token — issued by your tenant's identity provider. It must
carry an issuer (
iss) and an audience (aud) claim that match the tenant's sign-in settings; a token that leaves either out is refused with401(auth.token_missing_issuer/auth.token_missing_audience), even when the identity provider signed it. Entra ID, Okta, Google and K-Lake's own sign-in server always include both; Keycloak needs an Audience mapper on the client.
Send the token in the Authorization header:
Unauthenticated endpoints (no token required):
GET /healthz— livenessGET /readyz— readinessGET /api/authn/discover?tenant=<slug>— returns the tenant's sign-in issuer, client ID, audience and the issuer'sauthorization_endpoint+token_endpoint(read from its discovery document). 404 when the tenant has no SSO configured; 503 when the issuer's metadata cannot be fetched
Conventions¶
- All identifiers in path segments are URL-encoded.
s3://my-bucketbecomess3%3A%2F%2Fmy-bucket. - Request and response bodies are JSON.
- Timestamps are ISO-8601 UTC.
- Listing endpoints accept
?limit=and?after=for cursor pagination (default 100 rows, hard ceiling 1000). The body stays a flat array; when there may be another page the response carries aLink: <...>; rel="next"header to follow. Treat the cursor as opaque. The Estate insights file drill-down (/api/discover/files) is the exception — it returnsnext_cursorin its body. - Errors return a JSON body with an appropriate 4xx or 5xx status — see Errors.
Errors¶
Every error answers with the same envelope:
{
"error": "bad request: issuer must be an http(s) URL",
"code": "sso.issuer_invalid",
"params": { "field": "issuer" }
}
error— the message in English. Show it as-is when you have nothing better.code— always present: a stable, machine-readable code, lowercase and dotted as<area>.<reason>. Branch oncode, never on the text oferror, which may be reworded.params— present when the code has values to show (a field name, a count, a limit): the values a translated message fills in.
A code, once published, is never renamed or reused. An error without a more specific code carries its kind's generic one:
| Code | Status |
|---|---|
bad_request |
400 |
unauthorized |
401 |
forbidden |
403 |
not_found |
404 |
conflict |
409 |
payload_too_large |
413 |
too_many_requests |
429 — params carries limit and retry_after_secs |
service_unavailable |
503 — try again later |
internal |
500 — never carries detail; it is logged server-side |
There are about 370 specific codes. They are grouped by area, so the part before the dot tells you what the request was about:
| Area | What it covers | Examples |
|---|---|---|
auth |
Who may call; token problems | auth.tenant_admin_required (403), auth.token_missing_audience (401) |
token |
Personal access tokens | token.scope_unknown (400), token.scope_insufficient (403) |
request, db |
Malformed or conflicting requests | request.invalid_json (400), request.invalid_cursor (400), request.field_required (400, field) |
source, principal, endpoint_policy |
Sources, their configuration, grants and schedules | source.not_found (404, source_id), source.id_conflict (409), source.endpoint_refused (400) |
file |
Files and their versions | file.not_found (404), file.revision_not_found (404) |
search |
Content search | search.query_empty (400), search.offset_unsupported (400) |
entity, ticker_alias |
The knowledge graph and ticker aliases | entity.not_found (404), ticker_alias.ticker_invalid (400) |
classification |
The classification scheme | classification.label_unknown (400) |
action |
Smart Actions | action.step_ambiguous (400) |
discover |
Estate insights | discover.drilldown_unsupported (400) |
dead_letter |
Failed work | dead_letter.state_invalid (400), dead_letter.cluster_scope_required (400) |
tenant, tenant_lifecycle, settings, download_links |
Tenants and their settings | tenant.not_found (404, slug), tenant.suspended (403), tenant.retention_above_default (400, field, max) |
user |
Users and roles | user.last_admin (409), user.role_invalid (400) |
sso, directory, entra |
Console sign-in and directory sync | sso.issuer_invalid (400), sso.shared_issuer_conflict (409), directory.ldap_config_incomplete (400) |
mcp |
The MCP endpoint, its sign-in, clients and terminology | mcp.token_scope_refused (403, scopes), mcp.redirect_uri_insecure (400), mcp.client_not_found (404) |
quota, licence, feature |
Quotas, the licence and licensed features | quota.sources_exceeded (403, used, max), feature.not_licensed (403, feature) |
usage |
Usage reports | usage.window_too_long (400) |
warehouse, warehouse_audit |
Data warehouses | warehouse.not_found (404), warehouse.slug_taken (409) |
ca_cert, console_tls, branding |
Certificates and branding | ca_cert.chain_refused (400), console_tls.no_pending_request (409) |
partner, delegation |
Hosting many customers | partner.not_found (404), delegation.tenant_admin_required (403) |
support, cloud_extraction, deployment |
Diagnostic packs, cloud extraction and the deployment | support.full_level_unacknowledged (400), deployment.master_key_missing (503) |
The console shows each code's message in the reader's language; an
integration can do the same by keying its own text on code and filling in
params.
Endpoints¶
Health¶
| Method | Path | Description |
|---|---|---|
GET |
/healthz |
Liveness probe. Always returns 200 if the process is up. |
GET |
/readyz |
Readiness probe. Returns 200 only if dependencies are reachable. |
Sources¶
| Method | Path | Description |
|---|---|---|
GET |
/api/sources |
List sources in the calling tenant |
POST |
/api/sources |
Create a source (see body below) |
GET |
/api/sources/:id |
Source detail |
DELETE |
/api/sources/:id |
Remove a source and its indexed files |
GET |
/api/sources/:id/stats |
File count, bytes, last indexed time |
GET |
/api/sources/:id/health |
Last successful crawl + last error |
GET |
/api/sources/:id/crawls |
Recent crawl runs and outcomes |
POST |
/api/sources/:id/crawl |
Trigger a crawl. Body (all optional): prefix, mode, label, force_reextract (re-extract unchanged files too), force_reextract_extensions (narrow a forced re-run to these file extensions, e.g. ["pdf"]; requires force_reextract, else 400) |
GET |
/api/sources/:id/files |
Paginated file listing |
GET |
/api/sources/:id/files/:key |
Single file detail |
GET |
/api/sources/:id/files/:key/access?user= |
Why a user can or cannot see this file: the trimming decision replayed with both sides' identities (tenant admin). See Why can't this user see this file? |
POST |
/api/sources/:id/enabled |
Toggle the enabled flag |
POST |
/api/sources/:id/bulk-ingest |
Toggle the bulk-ingest fast path |
POST |
/api/sources/:id/meta-caps |
Set which optional enrichments to gather |
POST |
/api/sources/:id/backfill-meta |
Enqueue enrichment for previously-indexed files |
GET |
/api/sources/:id/meta-coverage |
How many files have each enrichment populated |
POST |
/api/sources/:id/subtree |
Tune how large directory trees are split into parallel work units ({depth?, split_threshold?}) |
POST |
/api/sources/:id/security-trim |
Set the per-file trim policy ({mode, fail_closed?}) |
POST |
/api/sources/:id/multichannel |
SMB3 multi-channel (smbfs only) |
GET/PUT/DELETE |
/api/sources/:id/metadata-rules |
The source's metadata rules (document dates and keyword fields such as ticker, sector, analyst and note type), the fields they produce and the background re-read's progress. PUT replaces the rules and re-reads the source's existing files in the background; DELETE removes them |
POST |
/api/sources/:id/metadata-rules/backfill |
Re-read every file with the current rules |
POST |
/api/sources/:id/metadata-rules/preview |
Dry run of rules against sample paths or the source's first files. Writes nothing |
GET/POST |
/api/sources/:id/boilerplate |
Tenant admin. Boilerplate demotion for this source: the settings in force (enabled, min_files, min_share) and what has been found. POST takes any of the three settings; null restores a default |
POST |
/api/sources/:id/boilerplate/backfill / …/reset |
Tenant admin. Count any files not yet counted, or forget what was learned and recount |
GET |
/api/content/search |
Search the extracted content of every source the caller can see (?q=&limit=&mode=&group=&source_id=). q takes bare words (relevance-ranked, any-of), "quoted phrases", -exclusions, A OR B, and matches reference-like tokens such as INC-2026-0714 as a whole; once identifier-parts search is enabled, a word also matches part of an underscore-joined identifier (kozijn finds IFC_kozijn_90x114, ranked below the word on its own); file names and recovered original names are searched too. mode is hybrid (default) or lexical; hybrid degrades to lexical when the source or tenant has it off. group=file returns one hit per document (its best chunk, plus chunk_hits and up to three siblings). Each hit carries match_tier (phrase / all / partial — phrase hits are ranked first), name_match, mode, snippet, rank, a citation and a url. source_id narrows to one source. |
GET |
/api/sources/:id/content/search |
The same search scoped to one source; same parameters and hit shape (minus source_id). |
GET/POST |
/api/sources/:id/extract |
Read (GET) or set (POST) the content-extraction policy: enabled, extension allow/deny lists, size cap, path globs |
GET |
/api/sources/:id/extract/coverage |
Extraction status rollup |
GET |
/api/sources/:id/extract/progress |
Live extraction progress |
GET |
/api/sources/:id/crawl-progress |
Live crawl progress |
GET/POST |
/api/sources/:id/schedules |
List / add a recurring crawl or backfill |
PATCH/DELETE |
/api/sources/:id/schedules/:sid |
Update / remove a schedule |
POST |
/api/sources/:id/schedules/:sid/run |
Run a schedule now |
Create source body:
{
"source_id": "s3://my-bucket",
"protocol": "s3",
"config": { "bucket": "my-bucket", "region": "us-east-1" },
"secret": { "access_key_id": "AKIA...", "secret_access_key": "..." }
}
Set secret to null to use ambient credentials on the worker (S3 IRSA / instance profile, or Azure Entra ID Workload / Managed Identity for azblob).
The config and secret shapes vary per protocol — see Sources for fields per protocol. For example, an Azure Blob source uses "protocol": "azblob", "config": { "account": "...", "container": "..." }, and a tagged secret such as { "auth": "sas", "sas_token": "..." } or { "auth": "account_key", "account_key": "..." }.
Files & downloads (open the original)¶
K-Lake extracts text and doesn't store the original bytes, so opening a file
means re-fetching it from its source on demand. To let a browser open/verify a
citation, the API mints short-lived signed links (one per search hit) that
name the user they were minted for and one file. The download route verifies
the link, applies the tenant's citation link policy, re-checks access, then
streams the bytes via the extractor — the API never holds source credentials.
Every download, and every refused one, is audited (tool = files/download).
Each tenant chooses who may open a citation link: anyone_with_link (the link
is the credential), signed_in (the reader must be signed in to the tenant, and
sees the file only if they may see it anyway) or match_user (only the user the
link was minted for). A browser with no session is sent to sign in and returns
to the link. See Citation links for the policy, its default
and the sign-in lifetime.
| Method | Path | Description |
|---|---|---|
GET |
/api/files/download?t=<token> |
Stream the original file behind a signed link. No bearer — the signed link carries the credential. Whether the response is an inline preview or an attachment is fixed when the link is minted. 401 if the link is invalid or expired, or — under signed_in / match_user — the browser has no session (answered with a sign-in page or a 303 redirect to the identity provider); 403 under match_user when the signed-in user is not the one the link was minted for; 404 if the reader can no longer see the file. A Range request is honoured, so audio and video seek. |
POST |
/api/files/download-session |
Bearer. Give the caller's browser a download session for their tenant on this host (204) — how the console lets links open for a signed-in console user. Tenant users only: 403 for the cluster-admin token. |
DELETE |
/api/files/download-session |
Bearer. End the caller's download session on this host (the console's sign-out). |
GET / PUT / DELETE |
/api/tenants/:slug/token-pricing |
Read, set or remove the tenant's AI token price: {currency, per_million_tokens}, with currency one of USD, GBP or EUR and the price above 0 and at most 1000, kept to four decimal places. The dashboard's token-savings card values saved tokens at it. Tenant admin for their own tenant (me works), cluster admin for any. |
GET / PUT |
/api/tenants/:slug/download-links |
Read or set the tenant's citation link policy (mode: anyone_with_link | signed_in | match_user, or null for the default) and sign-in lifetime (session_ttl_secs). Tenant admin for their own tenant (me works), cluster admin for any. |
The links are produced for you on the responses that reference files, so you rarely build the URL yourself:
GET /api/sources/:id/content/search— each hit carries aurl(an inline preview link to the original), alongsidekey/seq/snippet/rank.GET /api/sources/:id/files/:key— the file detail carries bothpreview_url(inline) anddownload_url(attachment).
All three are omitted when signed downloads aren't configured
(KDBL_DOWNLOAD_SIGNING_SECRET / KDBL_API_PUBLIC_URL / KDBL_INTERNAL_FETCH_TOKEN
unset), so a client should treat them as optional. Links expire after one hour
by default (downloads.ttlSeconds in the chart, KDBL_DOWNLOAD_TTL_SECS).
Access control (per source)¶
| Method | Path | Description |
|---|---|---|
GET |
/api/sources/:id/acl |
List principals and roles |
POST |
/api/sources/:id/acl |
Grant a principal access |
DELETE |
/api/sources/:id/acl/:principal |
Revoke a principal |
Status¶
| Method | Path | Description |
|---|---|---|
GET |
/api/status |
Tenant queue depth and per-source rollup |
GET |
/api/status?include_cluster=true |
Cluster-wide rollup (requires cluster admin) |
Discover — data estate (tenant admin)¶
Data-estate analytics. All three require a tenant-admin token; include_cluster=true
additionally requires a cluster-admin token. See Estate insights.
| Method | Path | Description |
|---|---|---|
GET |
/api/discover/overview |
Estate totals (files, bytes, sources, last-indexed) + extraction/searchability coverage. ?source_id= to scope, ?include_cluster=true for cross-tenant. |
GET |
/api/discover/breakdown?dimension=<d> |
Top-N breakdown. dimension ∈ extension|mime|language|size|age|owner|storage_class|source|exposure; optional source_id, metric=files|bytes, limit (max 100). |
GET |
/api/discover/files?source_id=&dimension=&value= |
Files behind a bucket (keyset-paginated: limit max 1000, after=<cursor>). Drillable dimensions: extension|mime|owner|storage_class|language. |
Capacity — storage & query cost¶
| Method | Path | Description |
|---|---|---|
GET |
/api/capacity |
Storage split (base / lexical / hybrid), chunk + embedding counts, retained-vector bytes, and per-mode query latency for the tenant. ?include_cluster=true for cluster-wide (cluster admin). |
Entities — the knowledge graph¶
Every response is access-trimmed to the calling token. See Knowledge graph.
| Method | Path | Description |
|---|---|---|
GET |
/api/entities |
Search by name prefix and/or kind; with no filter, the most-mentioned. |
GET |
/api/entities/{id} |
One entity. visible_document_count is trimmed to the caller; mention_count is the tenant-wide total. |
GET |
/api/entities/{id}/related |
Asserted relations (degree-capped). |
GET |
/api/entities/{id}/co-occurring |
Entities appearing in the same documents — a weaker claim than related. |
GET |
/api/entities/{id}/documents |
Documents mentioning it — the citation path. |
GET |
/api/entities/documents |
Documents mentioning a set of entities (the intersection question). Merge-aware. |
GET |
/api/entities/by-file |
What a document is about — the reverse lookup. Includes the companies its ticker field names through the ticker aliases. |
GET |
/api/entities/aliases |
Ticker aliases: ?q= (a ticker prefix or part of the name), &status= (active default, rejected, all), &after=, &limit=. Any tenant user |
POST |
/api/entities/aliases |
Create or replace one: {ticker, name, exchange?, entity_id?, status?}. Tenant admin |
DELETE |
/api/entities/aliases/:ticker |
?exchange=. Tenant admin |
POST |
/api/entities/aliases/import |
{csv, replace?}, ticker,name[,exchange] per line, at most 20,000 rows. Tenant admin |
GET |
/api/entities/{id}/merged |
The entities folded into this one by a previous merge. |
POST |
/api/entities/{id}/merge |
Fold a duplicate into another entity. Reversible. |
POST |
/api/entities/{id}/unmerge |
Detach it again. |
TLS trust anchors (tenant admin)¶
| Method | Path | Description |
|---|---|---|
GET |
/api/ca-certs |
Installed trust anchors, with subject, issuer and validity parsed from each certificate. |
POST |
/api/ca-certs |
Install a PEM trust anchor. |
DELETE |
/api/ca-certs/{cert_id} |
Remove one. Sources stop trusting it on the next configuration refresh. |
GET |
/api/sources/{source_id}/tls |
A source's trust mode and effective bundle. |
POST |
/api/sources/{source_id}/tls |
Set the mode: inherit (default), custom (a named subset), or none (platform roots only). |
Support — diagnostic packs¶
The in-band half of a diagnostic pack. These endpoints stay reachable when an expired licence has blocked everything else.
| Method | Path | Description |
|---|---|---|
GET |
/api/support/preview |
What a pack would contain at a given level, collecting nothing. |
GET |
/api/support/diagnostics |
Collect the in-band half. ?level=metadata|operational|full; full needs a cluster-admin token and an explicit acknowledgement. |
GET |
/api/support/runtime-tests |
Exercise the dependencies and report what actually works — not just what is configured. |
GET |
/api/support/history |
Who generated packs, when, at what level, and each pack's digest. No copy of the pack itself is kept. |
Tokens (self-service)¶
| Method | Path | Description |
|---|---|---|
GET |
/api/tokens |
List the calling user's tokens (metadata only — no secrets) |
POST |
/api/tokens |
Mint a new PAT. The raw token is returned once. |
DELETE |
/api/tokens/:id |
Revoke a token. Takes effect on every API server within five seconds |
Lifetime. Every token minted from 0.1.50 expires. Set expires_at
(RFC 3339) in the request to choose when; without it, the token expires at
the deployment's ceiling, 365 days by default (chart value
mcp.patMaxTtlDays), and a later expires_at is brought down to the
ceiling. The response's expires_at is the expiry that was set. An
expires_at that is not in the future is refused (400). Tokens minted
before 0.1.50 are left as they are; re-mint any that should have an end date.
Scopes. POST /api/tokens {"name": "...", "scopes": [...]} narrows what a
token can do. No scopes means the token carries its owner's full authority
(every token minted before scopes existed keeps working on the REST API). A
scope never widens: the owner's role and access rules still apply on top.
| Scope | Admits under /api |
On the MCP endpoint |
|---|---|---|
read |
Any GET the owner could make (plus asking a warehouse a question, and validating, suggesting or importing a warehouse model) — nothing that changes anything |
Accepted |
search |
Search, file viewing and the entity explorer only, whatever the owner's role | Accepted |
mcp |
Nothing: for an MCP client | Accepted |
admin |
Full authority, stated explicitly | Refused on its own (403, scope_refused); accepted beside mcp, search or read |
kdbl.search, kdbl.read |
Legacy names (what the console's AI access (MCP) › Tokens tab mints): restrict nothing | Accepted |
| (none) | Full authority | Accepted for this release only, with each use logged — see Token scopes |
Unknown scopes are refused when minting, and a scoped token can only mint
tokens within its own scopes. Re-mint unscoped tokens used by MCP clients with
the mcp scope before the next release, which refuses them on MCP.
Users (tenant admin)¶
| Method | Path | Description |
|---|---|---|
GET |
/api/users |
List users in the tenant |
POST |
/api/users |
Create a user (returns an initial PAT, shown once). role defaults to operator |
GET |
/api/users/me |
Current caller, including role and tenant_slug — the one route here open to any authenticated user. For an administrator it also carries authority: who manages the tenant (managed_by: tenant = self-managed, partner, platform), whether the caller may change its sign-in, and whether they may manage the deployment (see who manages what) |
GET |
/api/users/:id |
User detail |
PATCH |
/api/users/:id |
Update email, display name, groups, admin flag, role |
DELETE |
/api/users/:id |
Remove a user |
Roles. Every tenant user has a role, reader or operator, alongside the separate is_tenant_admin flag. A reader can use search and explore and nothing else: GET /api/content/search, the source list and detail, file detail and revisions, everything under /api/entities, and their own PATs under /api/tokens; every other route, and every write, returns 403 operator role required. Security trimming applies to readers exactly as to everyone else. An operator can do everything a tenant user could before roles existed, and existing users are operators. SSO users provisioned on first sign-in get the tenant's default_role; membership of a group in the tenant's operator groups makes them an operator on every sign-in, and a role set by hand is never overruled by the directory. See Console sign-in and roles.
A warehouse admin (is_warehouse_admin, a flag beside the role like is_tenant_admin) administers the tenant's data warehouses — every /api/warehouses route — and may read, but not change, the admin views a warehouse team needs: the Estate insights aggregates (/api/discover/overview and /breakdown, not the file-level listing), the MCP activity and configuration, the principal search used to pick a warehouse's allowed groups, tenant usage (/api/usage), and the admin audit log narrowed to warehouse changes. Everything else follows their role, so a reader who is a warehouse admin is the warehouse-team account: they cannot touch sources, crawls, extraction, classification, actions, users, the identity provider, MCP settings or tokens, CA certificates or branding. Set it with PATCH /api/users/:id {"is_warehouse_admin": true} (tenant admin), kdbl-control users warehouse-admin --user <email>, or the tenant's warehouse-admin groups (warehouse_admin_groups in the sign-in config). See Data warehouses.
Tenants (cluster admin)¶
| Method | Path | Description |
|---|---|---|
GET |
/api/tenants |
List tenants |
POST |
/api/tenants |
Create a tenant |
GET |
/api/tenants/:slug |
Tenant detail |
PATCH |
/api/tenants/:slug |
Update tenant configuration |
DELETE |
/api/tenants/:slug |
Delete a tenant — asynchronous: 202; the tenant is locked out at once and purged in the background. GET /api/tenants/:slug reports status: deleting and purge progress until it returns 404. See Hosting many customers |
POST |
/api/tenants/:slug/suspend |
Suspend (optional {"reason": "…"}): every sign-in path refuses the tenant's users with 403 tenant suspended and its ingest stops; data is kept |
POST |
/api/tenants/:slug/resume |
Undo a suspension (409 while the tenant is being deleted) |
GET |
/api/tenants/:slug/export |
The offboarding export: an NDJSON stream of the tenant's configuration (no secrets), users (no token hashes), sources, file metadata, file text, entities, relations and classifications, ending in an end record. Cluster admin, or a tenant admin for their own tenant |
GET / PUT |
/api/tenants/:slug/quotas |
The tenant's quotas, current usage and whether each is ok / warn / reached. Tenant admin reads their own; only a cluster admin sets them ({} clears them) |
GET / PUT |
/api/tenants/:slug/source-policy |
What the tenant's sources may borrow from the deployment and which hosts they may reach. Tenant admin reads their own (me works); only a cluster admin changes it |
GET |
/api/tenants/:slug/retention |
Tenant retention override. A tenant admin may read their own tenant; a cluster admin reads any. |
PATCH |
/api/tenants/:slug/retention |
Set tenant retention override. A tenant admin may set their own, but only to a value at or below the cluster default. |
GET |
/api/tenants/:slug/sso |
Read the sign-in config: issuer, client_id, audience, groups_claim, admin/operator/warehouse-admin groups, JIT, default role, tenant_claim, editable (whether this caller may change it), managed_by (tenant | partner | platform) and customer_manages_sign_in. Never returns a client secret. Cluster admin and platform operator: any tenant. Partner staff acting in the tenant, and the tenant's own admin: that tenant only |
PATCH |
/api/tenants/:slug/sso |
Merge sign-in keys (absent = leave, ""/[] = clear; tenant_claim: {"claim","value"}, both empty = clear). Issuer must be an http(s) URL. It may be one other tenants use only when every tenant on it stays unambiguous: each has an API audience, and tenants sharing an audience all have a tenant_claim on the same claim with different values; otherwise 409 says which tenants and what to set. Judged only when issuer, audience or tenant_claim change; the same check applies to an oidc_config sent to POST/PATCH /api/tenants. Cluster admin, platform operator, partner staff acting in the tenant whose partner access is admin or admin_and_content, or the tenant's own admin when the tenant is self-managed or its provider allows it. A change that would lock administrators out is refused (see Sign-in lockout warnings). Changes are audited under the caller's name. See who can change sign-in |
PUT |
/api/tenants/:slug/sso/self-service |
{"customer_manages_sign_in": true\|false}: the provider's switch that lets a managed tenant's own admins change its sign-in (default off; no effect on a self-managed tenant). Cluster admin, platform operator, or partner staff acting in the tenant; never the tenant's own admin (403) |
POST |
/api/tenants/:slug/sso/shared-issuer |
Dry run for {issuer, audience, tenant_claim}: the other tenants on that issuer and the problem a save would be refused for, or null. A tenant the caller may not administer comes back with anonymous: true and no slug, its audience and claim value shown only when they equal the ones sent. Same callers as PATCH |
POST |
/api/tenants/:slug/sso/check |
Fetch the stored issuer's metadata with the tenant's trust anchors; reports endpoints or the failure, plus which required keys are unset. Same callers as GET |
GET |
/api/tenants/:slug/directory |
Read non-secret directory-correlation config + has_*_secret flags |
PATCH |
/api/tenants/:slug/directory |
Merge graph / ldap / principal_mappings config. Tenant admin for their own tenant |
PUT |
/api/tenants/:slug/directory/graph-secret |
{"client_secret"}: the Graph client secret, sealed on the server and never returned. Tenant admin for their own tenant |
PUT |
/api/tenants/:slug/directory/ldap-secret |
{"bind_password"}: the LDAP bind password, sealed the same way. Tenant admin for their own tenant |
GET |
/api/cluster/retention |
Cluster-wide retention default |
Sign-in lockout warnings¶
PATCH /api/tenants/:slug/sso, PUT /api/mcp/tenants/:slug and
DELETE /api/mcp/tenants/:slug change how a tenant's users sign in. When a
change would lock administrators out, it is refused and nothing is saved:
HTTP/1.1 409 Conflict
{
"error": "this change could lock administrators out: … Send `acknowledge_lockout: true` to save it anyway.",
"lockout": [
{ "kind": "session_ends", "message": "You signed in through https://… After this change …" }
]
}
kind is one of:
session_ends: the caller signed in through the current issuer, and the issuer or API audience changes, so their session ends.loses_admin: the caller is an administrator only through a directory group, and none of their groups is in the new admin groups.no_admin_can_sign_in: no administrator could sign in after the change.
Repeat the request with acknowledge_lockout: true in the body (for the
DELETE, ?acknowledge_lockout=true) to save anyway. The cluster-admin token
is never warned. A personal access token keeps working whatever the sign-in
settings say, so it is the way back in.
Console HTTPS certificate¶
The certificate the console is served with. Callers:
the deployment's administrator (a
cluster admin, or the tenant admin of a deployment whose only tenant manages
itself). Everyone else gets 403. No route returns a private key, and every
write is recorded in the admin audit log. See
Console HTTPS certificate.
| Method | Path | Description |
|---|---|---|
GET |
/api/console-tls |
Status: the installed certificate (subject, names, issuer, validity, serial, SHA-256 fingerprint, days_left, expiry = ok / warning (under 30 days) / critical (under 7) / expired), pending_csr, https_active, force_https, managed, and whether it is applied |
POST |
/api/console-tls/csr |
Generate a private key on the server and a certificate request: {common_name, organization, organizational_unit, locality, state, country, dns_names[], ip_addresses[], key_type} (ecdsa-p256 default, rsa-2048, rsa-3072). Returns csr_pem only. Replaces a pending request |
GET / DELETE |
/api/console-tls/csr |
Read or discard the pending request |
POST |
/api/console-tls/certificate |
Install the CA's answer to the pending request: {certificate} as PEM (leaf and intermediates, or a PKCS#7 block) or base64 of a DER certificate or .p7b. 409 with no pending request; 400 for a certificate for another key, an expired one, a CA certificate or a broken chain |
POST |
/api/console-tls/import |
Install a certificate made elsewhere: {certificate, private_key} (PEM, unencrypted key) or {pkcs12, password} (base64 .pfx). Same checks |
DELETE |
/api/console-tls |
Remove the certificate and return to plain HTTP (also turns force_https off) |
PUT |
/api/console-tls/redirect |
{force_https}. Turning it on needs an installed certificate and a request that arrived over HTTPS (409 otherwise), so nobody locks themselves out |
PATCH /api/tenants/:slug with {"features": [...]} narrows which licensed features a tenant has (null = every licensed feature, [] = none); only a cluster admin may set it. See Licensing.
Features, usage and audit¶
| Method | Path | Description |
|---|---|---|
GET |
/api/features |
Any signed-in user. Each licensed feature for the caller's tenant: licensed, enabled and its limits. The console shows and hides sections on it |
GET |
/api/usage?from=&to= |
Tenant admin: the tenant's own daily usage (JSON with daily rows and totals; &format=csv for CSV). A window may span up to 400 days; the default is the last 30 |
GET |
/api/usage/tenants?from=&to=&tenant= |
Cluster admin: every tenant's daily usage — the billing export (&format=csv for CSV) |
GET |
/api/audit/admin |
The admin audit log, newest first: one append-only row for every state-changing API request — including refused ones — with who made it (principal and email), how they held the authority, what (method and route), for which tenant, and the response status. No request body is stored. Tenant admins see their own tenant; cluster admins see every tenant (?tenant=<slug> narrows). ?principal=, ?limit= (default 100, max 1000), ?before=<unix time> to page, ?format=csv for an export |
Warehouses¶
Questions asked of data warehouses, and their administration. Available where the licence includes data warehouses. "Admin" means a tenant admin or a warehouse admin.
| Method | Path | Who | Purpose |
|---|---|---|---|
GET |
/api/warehouses |
Any user | List warehouses |
POST |
/api/warehouses |
Admin | Register a warehouse (/api/warehouses/test probes an unsaved one) |
GET PATCH DELETE |
/api/warehouses/:id |
Admin | Read, update, remove |
POST |
/api/warehouses/:id/clone |
Admin | Clone the connection, with its credential and groups, and optionally its published model or draft (into the clone's draft) |
GET |
/api/warehouses/:id/access?user= |
Admin, warehouse admin | Whether one person may ask the warehouse, which models they reach, and why |
GET / POST |
/api/warehouses/:id/models |
Any user / Admin | List the connection's models (non-admins see the published ones offered to them); create a model |
GET / PATCH / DELETE |
/api/warehouses/:id/models/:model |
Admin | Read, update or delete one model |
GET / PUT / POST |
/api/warehouses/:id/models/:model/… |
Admin | The model: save a draft, diff, validate, suggest from the catalog, import, suggested values, publish, approve / reject, review comments, changelog, check, sync, accuracy runs and golden cases |
POST |
/api/warehouses/:id/models/:model/assist |
Admin | AI suggestions for a draft. Off unless the licence has structured_sources_assist and a tenant admin has switched AI help on |
POST |
/api/warehouses/:id/models/:model/ask |
Admin | Ask in words; the AI builds and runs the query as the caller. Sends query results to your AI provider. Off unless a tenant admin allows Ask |
GET |
/api/warehouses/:id/models/:model/schema |
Any user the model admits | The AI-facing model, as the caller sees it |
POST |
/api/warehouses/:id/models/:model/query |
Any user the model admits | Ask: {question}, {query} or {sql}, plus limit |
GET |
/api/warehouses/:id/queries, /api/warehouses/queries/export |
Admin | The query log (JSON or CSV) |
GET / PUT |
/api/tenant/warehouse-assist |
Tenant admin | The AI help settings: switch, provider, write-only key, gateway, example values, Ask, daily token budget |
The routes from before 0.1.50 without a model in the path
(/api/warehouses/:id/model…, /schema, /query, /evals) still work for
this release only, acting on the connection's default model. Move to the
per-model routes before the next release. The full list is in the
Data warehouses API reference.
MCP¶
The Model Context Protocol surface. See MCP server and connecting MCP clients. The cluster-admin token is refused on /mcp (403), and a PAT needs the mcp, search or read scope there (see Tokens).
Per-tenant endpoints (cluster admin unless noted): GET /api/mcp/tenants lists every tenant with its endpoint, scoped flag and sign-in leg (federation: tenant | deployment | none); PUT /api/mcp/tenants/:slug ({scoped, take_over, acknowledge_lockout}) connects a tenant — the first gets the bare /mcp, later ones /t/<slug>/mcp; DELETE takes it off. GET/PUT/DELETE /api/mcp/tenants/:slug/federation sets the tenant's own upstream identity provider (a tenant's own admin may call it too; the secret is write-only). GET/POST /api/mcp/tenants/:slug/keys and DELETE …/keys/:kid manage the tenant's own signing keys. Every route below also exists under /t/<slug>/… for a tenant on its own endpoint, with its discovery documents at /.well-known/oauth-protected-resource/t/<slug>/mcp, /.well-known/oauth-authorization-server/t/<slug> and /t/<slug>/.well-known/jwks.json. See Multi-tenant deployments.
| Method | Path | Description |
|---|---|---|
GET |
/.well-known/oauth-protected-resource |
RFC 9728 Protected Resource Metadata (unauthenticated) |
GET |
/.well-known/oauth-protected-resource/mcp |
Path-scoped PRM (unauthenticated) |
POST |
/mcp |
Streamable-HTTP JSON-RPC endpoint (OAuth 2.1 bearer; aud = the resource URI) |
GET |
/api/mcp/audit |
Query the MCP audit trail (tenant-scoped; ?tool=&principal=&limit=) |
GET |
/api/mcp/usage/tools |
Tokens returned to assistants per MCP tool, for the last 30 days and all time: each tool's calls, characters, tokens and share of what was returned, largest first. The Tokens by tool card on the AI access page's Flow tab. Tenant-wide; any member of the tenant may read it, and a cluster admin gets every tenant summed |
Who may change what. The deployment-wide MCP settings (PUT /api/mcp/config, the remote-access tunnel token, clients at the bare /mcp endpoint) belong to the deployment's administrator: a cluster admin, or the tenant admin of a deployment whose only tenant manages itself. GET /api/mcp/config reports can_edit for the caller. A tenant's own admin may also connect or disconnect their own tenant (PUT/DELETE /api/mcp/tenants/:slug) and register or remove its clients (POST/DELETE /api/mcp/clients with tenant: "<their slug>") when they may manage its sign-in. On a deployment with other tenants they may only point it at its own /t/<slug>/mcp.
K-Lake runs its own OAuth 2.1 authorization server at the MCP origin: it issues and signs the access tokens the MCP endpoint accepts, and federates the actual sign-in to your identity provider. The endpoints below are served once the deployment has its token-signing keys mounted. Clients discover and use them themselves — you never call them by hand — but they are the endpoints to check when a connection fails. See OAuth and identity providers.
| Method | Path | Description |
|---|---|---|
GET |
/.well-known/oauth-authorization-server |
Authorization server metadata (RFC 8414, unauthenticated) |
GET |
/.well-known/oauth-authorization-server/mcp |
Path-scoped form of the same document, for clients that ask for it |
GET |
/.well-known/openid-configuration |
OIDC discovery for the same server |
GET |
/.well-known/jwks.json |
Public keys the access tokens are signed with |
POST |
/register |
Dynamic client registration (RFC 7591) — how an assistant registers itself |
GET |
/authorize |
Start sign-in; renders the K-Lake consent screen |
GET |
/oauth/callback |
Return leg from your IdP |
POST |
/token |
Exchange the code, and refresh (rotating refresh tokens) |
Example: end-to-end¶
Add a source, trigger a crawl, watch progress:
# Add
curl -X POST -H "Authorization: Bearer $KDBL_TOKEN" \
-H "Content-Type: application/json" \
"$KDBL_URL/api/sources" \
-d '{
"source_id": "s3://docs-bucket",
"protocol": "s3",
"config": { "bucket": "docs-bucket", "region": "us-east-1" },
"secret": { "access_key_id": "AKIA...", "secret_access_key": "..." }
}'
# Crawl
curl -X POST -H "Authorization: Bearer $KDBL_TOKEN" \
"$KDBL_URL/api/sources/s3%3A%2F%2Fdocs-bucket/crawl"
# Watch
curl -H "Authorization: Bearer $KDBL_TOKEN" \
"$KDBL_URL/api/sources/s3%3A%2F%2Fdocs-bucket/stats"
Rate limits and pagination¶
A caller over a limit is answered 429 Too Many Requests with a Retry-After header. Back off for that long and retry.
- MCP, on every deployment: each user is held to a request rate (10 per second, burst 40, by default), and failed sign-ins are throttled per address and per token. See Exposing MCP to the internet.
- Deployments hosting many customers: each tenant has its own request rate and a share of the query-path concurrency (MCP tool calls, search, reranking), so one customer cannot crowd out the others. See Hosting many customers.
- Warehouses: questions are throttled per warehouse and per user. See Data warehouses.
For bulk reads use the listing endpoints with ?limit= and follow the cursor rather than hammering point lookups.