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. KLA creates a service-account client for your tenant on request; you give it the minimum roles it needs and exchange its client_id and client_secret for an access token using the client-credentials grant. An agent that requests decisions or proposes actions needs no service account: a tenant administrator creates a scoped API key for it (see API keys below), and the governance SDKs accept that key.
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, such as an agent runtime, in the tenant that created the key. A tenant administrator creates a key in Settings → API keys or with the Secrets API or CLI, then stores the returned value in a server-side secret manager. KLA returns the full key once. Set an expiry; the Console proposes 90 days.
kla api secrets createApiKey \
--input '{"name":"kyc-onboarding-agent","permissions":["actions:propose","policy:evaluate"],"expiresAt":"2027-01-07"}'
permissions accepts only the scopes in the table below. Any other value, including the older read, write, and delete, is rejected with 400 BAD_REQUEST and the list of valid scopes. The creation response lists, under effectiveAccess, every operation the key can call.
| API-key scope | Operations the key can call | What it is for |
|---|---|---|
actions:propose |
POST /v1/agent-actions, POST /v1/agent-actions/bindings/resolve, GET /v1/agent-actions/{proposalId}, POST /v1/agent-actions/{proposalId}/resume, POST /v1/agent-actions/{proposalId}/cancel |
Submit agent actions for a policy decision and read, resume, or cancel the proposals this key created. The active mandate and the policy decision still decide what runs. |
policy:evaluate |
POST /v1/guardrails/evaluate, POST /v1/policy/evaluate |
Request a policy decision (allow, warn, require_approval, or block) for an agent step. |
decisions:read |
GET /v1/approvals.getPending |
List pending approval requests. The key cannot approve or deny. |
A key reaches only the operations its scopes name. Every other request with that key, including another tRPC procedure or a batched call, returns 403 with {"error":"API key scope does not allow this operation","code":"api_key_scope_denied"}. A key whose stored scopes are not in the table can call nothing; Settings → API keys marks those scopes no access. API keys cannot create or revoke API keys.
Send the key in the x-kla-api-key header. API-key requests need no CSRF token.
curl -sG "https://api.kla.digital/v1/approvals.getPending" \
-H "x-kla-api-key: $KLA_API_KEY" \
--data-urlencode 'input={"json":{"limit":1}}'
An agent with actions:propose posts the same proposal body the governance SDKs send:
curl -s -X POST "https://api.kla.digital/v1/agent-actions" \
-H "x-kla-api-key: $KLA_API_KEY" \
-H "Content-Type: application/json" \
-d @proposal.json
The governance SDKs send the same header when you pass the key as apiKey (Node.js) or api_key (Python). They also accept a client-credentials bearer token as credential; a client takes one of the two.
An agent with policy:evaluate that runs its own tools asks for a decision before each tool call. KLA records the decision and runs nothing; the agent acts on the result. The tool must be an active entry in the tenant's Tool Catalog:
curl -s -X POST "https://api.kla.digital/v1/policy/evaluate" \
-H "x-kla-api-key: $KLA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"context": {
"occurredAt": "2026-10-10T09:00:00Z",
"identifiers": {
"workspaceId": "<tenant external ID>",
"runId": "onboarding-case-1042",
"gateId": "open-account-1",
"agentId": "<agent ID>",
"environment": "production"
},
"action": {
"actionType": "tool_call",
"toolName": "open_customer_account",
"toolArgs": { "risk_score": 15, "pep_match": false, "sanctions_hit": false }
}
},
"policyReference": { "policyId": "kyc-onboarding-gate" }
}'
| Field | Required | Meaning |
|---|---|---|
context.occurredAt |
yes | When the agent proposed the action (ISO 8601). |
context.identifiers.workspaceId |
yes | Your tenant external ID. |
context.identifiers.runId, gateId |
yes | Your identifiers for the run and this decision point. Resending the same runId and gateId with the same body returns the recorded decision; a changed body returns 409. |
context.identifiers.agentId, environment |
when the policy is scoped to them | Must match the policy's scope.agentIds and scope.environments. |
context.action.actionType |
yes | tool_call before the tool runs, tool_result for its output. |
context.action.toolName, toolArgs |
yes | The Tool Catalog key and the arguments the policy rules read. |
policyReference.policyId |
no | Evaluate this published policy. Without it, KLA resolves the policy whose scope matches the context. |
KLA derives the caller identity and tool entitlements from the key and the Tool Catalog, so the body carries no entitlements object. The response decision.decision is allow, warn, require_approval, or block. A require_approval decision opens a Decision Request in the Decision Desk. A 400 response lists each invalid field by its full path under issues.paths.
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 agent runtime needs
actions:propose, andpolicy:evaluateif it asks for decisions before acting. It needs no agent deployment rights. - A queue monitor needs
decisions:readand nothing else. - 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.
