Getting Started

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.

8 min read1895 words

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 Unauthorized from 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, and policy:evaluate if it asks for decisions before acting. It needs no agent deployment rights.
  • A queue monitor needs decisions:read and 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.

⚠️ Warning

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.

Authentication | Developer Docs | KLA Control Plane