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.

6 min read1290 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. 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 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 approvals automation needs decisions:read and decisions: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.

⚠️ 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