Authentication
Authenticate against the KLA Control Plane API with OAuth 2.0 sign-in, client-credentials service accounts, and scoped API keys for machine calls.
Every call to the KLA Control Plane API uses a short-lived OAuth 2.0 bearer access token or a scoped API key and is scoped to a single tenant. The KLA Control Plane is a govern-in-place runtime safety, audit, and governance layer for enterprise AI agents; its API is how you register agents, evaluate policies, route approvals, and pull evidence. This page explains the credential types, how to obtain a token, how to call the API, and how to keep credentials least-privilege and rotated.
OAuth credential types
KLA issues tokens through its identity provider, an OpenID Connect (OIDC) service. Which flow you use depends on who is calling.
| Interactive sign-in | Service account | |
|---|---|---|
| Caller | A human in the Console (the KLA web app) | A backend service, script, or CI job |
| Flow | OAuth 2.0 / OIDC with PKCE | OAuth 2.0 client credentials |
| Secret | None stored by the app | client_id + client_secret |
| Identity | A person, with their roles | A machine principal you create |
| Use for | Reviewing the Decision Desk, building policies | SDK and API calls, automation |
Interactive sign-in is what the Console uses. The browser runs the OAuth 2.0 Authorization Code flow with PKCE (Proof Key for Code Exchange), so no client secret ever lives in front-end code. You do not implement this yourself; it ships with the Console.
Service accounts are what your integrations use. You create a service-account client per integration, give it the minimum roles it needs, and exchange its client_id and client_secret for an access token using the client-credentials grant.
flowchart LR H["Human in Console"] -->|"PKCE sign-in"| IDP["KLA identity provider"] M["Backend service"] -->|"client credentials"| IDP IDP -->|"bearer token"| API["KLA Control Plane API"]
Getting a service-account token
Exchange your client credentials at the identity provider's token endpoint. The realm path is your tenant, so substitute your tenant slug for <tenant>.
curl -s -X POST \
"https://auth.kla.digital/realms/<tenant>/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=svc-claims-triage" \
-d "client_secret=$KLA_CLIENT_SECRET"
The response is a JSON object containing the bearer access token and its lifetime in seconds. The example below shows a dev-realm response; each environment configures its own lifetime, so read expires_in from every response:
{
"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6...",
"expires_in": 1800,
"token_type": "Bearer",
"scope": "agents:read decisions:write"
}
Capture the token for the calls that follow:
export KLA_ACCESS_TOKEN=$(curl -s -X POST \
"https://auth.kla.digital/realms/<tenant>/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=svc-claims-triage" \
-d "client_secret=$KLA_CLIENT_SECRET" | jq -r .access_token)
Calling the API
Send the token in the Authorization header. The verified tenant claim in the token selects the current tenant. The API host is https://api.kla.digital. Use this round-trip to confirm your token works:
curl -s https://api.kla.digital/v1/tenants.current \
-H "Authorization: Bearer $KLA_ACCESS_TOKEN"
A successful call returns the tenant the token is scoped to:
{
"result": {
"data": {
"json": {
"id": "123e4567-e89b-42d3-a456-426614174000",
"externalId": "acme-prod",
"name": "Acme Corp",
"displayName": "Acme Corp",
"branding": {
"displayName": "Acme Corp",
"logoUrl": null,
"logoMarkUrl": null,
"logoAlt": "Acme Corp",
"accentColor": null,
"coBranding": "kla-first"
}
}
}
}
}
Two optional headers carry different meanings:
| Header | Behavior |
|---|---|
x-kla-tenant-id |
Internal tenant UUID. Authentication and verified membership determine the tenant. |
x-kla-tenant-external-id |
Selects another tenant by external ID when the authenticated subject has an active membership in that tenant. |
A subject without an active membership receives 403 when it tries to select another tenant:
curl -s https://api.kla.digital/v1/tenants.current \
-H "Authorization: Bearer $KLA_ACCESS_TOKEN" \
-H "x-kla-tenant-external-id: another-tenant"
{ "error": "Tenant access denied" }
The sequence below shows the full path a machine client follows on each token cycle.
sequenceDiagram participant C as Client service participant IDP as KLA identity provider participant API as KLA Control Plane API C->>IDP: POST token endpoint with client credentials IDP-->>C: access_token plus expires_in C->>API: GET v1/tenants.current with bearer API-->>C: 200 tenant payload Note over C,IDP: On 401 expired, request a fresh token and retry
API keys
API keys authenticate a scoped machine caller in the tenant that created the key. Create a key with the Secrets API or CLI, then store the returned value in your server-side secret manager. KLA returns the full key once.
kla api secrets createApiKey \
--input '{"name":"approval-queue-reader","permissions":["decisions:read"]}'
Send the key in the x-kla-api-key header. This is the API-key header for the Control Plane.
curl -sG "https://api.kla.digital/v1/approvals.getPending" \
-H "x-kla-api-key: $KLA_API_KEY" \
--data-urlencode 'input={"json":{"limit":1}}'
API-key scopes expand only through the documented mapping below. Other stored scope strings grant no Control Plane permissions.
| API-key scope | Canonical permission | Control Plane access |
|---|---|---|
decisions:read |
approval:list |
Read the pending approval queue through approvals.getPending. |
API-key revocation is permanent. secrets.revoke accepts either identifier the creation response returns: apiKey.id or secretId. Both resolve inside the calling tenant; an unknown or another-tenant identifier returns NOT_FOUND. Store apiKey.id from the creation response in API_KEY_ID, then revoke it. Create a replacement key and update the integration after a key is revoked.
kla api secrets revoke --set id="$API_KEY_ID"
Token lifetime and refresh
Dev realms currently issue 30-minute access tokens (expires_in: 1800). Read expires_in from every token response because each environment can configure a different lifetime. Never pin or cache a token past its expiry.
- Service accounts simply request a new token from the token endpoint when the current one nears expiry. The client-credentials grant does not issue a refresh token; re-running the exchange is the refresh.
- Interactive Console sessions use a separate, longer-lived refresh token managed by the browser to obtain new access tokens silently.
- A
401 Unauthorizedfrom the API means the token is expired or invalid. Request a fresh token and retry once.
Least-privilege scopes and roles
Each token carries the roles of its principal, and the API authorizes every request against them. Grant a service account only the roles its job requires:
- An agent that emits telemetry needs write access to traces, not policy authoring.
- An approvals automation needs
decisions:readanddecisions:write, not agent deployment rights. - A read-only evidence exporter needs evidence read access and nothing else.
Create one service account per integration so you can revoke or re-scope a single caller without disrupting others. Roles are managed in the Agent Registry and tenant settings of the Console.
Rotating service-account secrets
A client_secret is a long-lived credential: treat it like a database password. Rotate it on a schedule and immediately on any suspected exposure.
Store and rotate service-account secrets in the Secrets Vault, the Console surface for sensitive storage. Generate a new secret there, deploy it to your integration, confirm new tokens mint correctly, then retire the old secret. The Secrets Vault supports overlapping secrets during a rotation so you can roll forward with zero downtime.
Retired API-key records
API-key revocation is permanent. A revoked API-key backing secret has no restore or rotation operation. secrets.get with includeValue: true returns NOT_FOUND for a revoked, archived, expired, missing, or another-tenant record. Authorized metadata reads may return revoked or expired lifecycle data; they never return the secret value. Create a new API key and update the integration.
Never embed a long-lived client_secret, or any service-account credential, in browser code, mobile apps, public repositories, or client-side bundles. Anything shipped to a user's device or a public repo is compromised. Keep service-account secrets server-side only, inject them from the Secrets Vault or your secret manager at runtime, and use the Console's PKCE sign-in for anything a human touches.
