Skip to content

Authentication ​

Three credentials, three audiences ​

CredentialWho holds itLifetimeScope
Private API keyYour backend, CI, the CLIUntil revokedEverything its scopes allow
Public keyA browser bundleUntil revokedExchange for a client token, nothing else
Client JWTA browser, for one sessionMinutesOne organization, one assistant, one session

The control plane issues ca_sk_ and ca_pk_. Those are the prefixes you will actually see.

The Web SDK is more permissive on the way in, because it also has to reject keys pasted from elsewhere: it treats sk_ and ca_sk_ as private and refuses them, and accepts pk_, ca_pk_ and pub_ as public. A wider public prefix list is a design proposal, not something the platform mints.

Private keys ​

Either header is accepted, so pick whichever your HTTP client makes easy:

http
Authorization: Bearer <key>
http
X-CallAgent-Key: <key>

Stored as sha256(key) in api_keys.key_hash, so a database leak yields nothing replayable — and a lost key is rotated, never recovered. A key can be restricted by scope, by origin, by allowed assistant and by IP allowlist; all four are checked on every request by App\Http\Middleware\AuthenticateApiKey.

The organization is derived from the key. There is no organization_id parameter anywhere in the API, so there is nothing to tamper with; a resource in another organization returns 404, identical to one that never existed.

Requests are rate limited per key — 300 per minute by default, overridden by your plan's api_rate_limit_per_minute where one is set, and 60 per minute per IP when unauthenticated. Note the consequence: an organization with three keys has three separate buckets. See Rate limits.

GET /api-keys/scopes returns the full scope vocabulary. Scopes are <resource>.<verb> — call.read, recording.delete, assistant.publish — and <resource>.* grants the group.

Never put a private key in a browser. The Web SDK refuses one on sight:

ts
new CallAgent({ publicKey: 'sk_…' });
// CallAgentError: private_key_rejected — "That looks like a private/secret key…"

Public keys and client tokens ​

Browsers get a public key, which is useless on its own. The SDK exchanges it for a short-lived JWT:

http
POST /api/v1/client-tokens
Authorization: Bearer <public key>
Origin: https://example.com

{ "public_key": "<public key>", "assistant_id": "01JQ…", "ttl": 900 }

201 {
  "data": {
    "token": "eyJ…",
    "token_type": "Bearer",
    "expires_at": "2026-01-01T12:15:00+00:00",
    "expires_in": 900,
    "organization_id": "01J…",
    "session_id": "01J…"
  }
}

The Origin header is checked against the key's allowed origins: a valid key presented from a site it does not name is refused. Tokens are signed HS256 with a key derived from CREDENTIAL_ENCRYPTION_KEY, so they survive an APP_KEY rotation and a leaked APP_KEY cannot mint them. Default lifetime 900 s, maximum 3600 s.

POST /api/v1/client-tokens/verify checks one, which is useful when developing an SDK.

The token's claims bound what the page can do:

organization   which tenant
assistant      which assistant may be called
user           your opaque end-user id
session        this session only
origins        which sites may present it
exp            when it dies

The SDK refreshes 60 seconds before exp rather than waiting for a 401, and re-sends the current token on every reconnect. If you prefer to mint tokens on your own backend, pass { token } instead of { publicKey } — the SDK then never calls the exchange.

Endpoint status

The token exchange is live. Starting a browser call with that token (POST /api/v1/client/calls) is implemented in the SDK but not published by the control plane. See The browser and What is not built.

CLI credentials ​

bash
callagent login            # prompts, echo suppressed
echo "$KEY" | callagent login --stdin
callagent whoami
callagent logout

The first key cannot come from here. POST /api-keys needs a key to call it, so the first one is created in the dashboard — see Quickstart step 1.

Resolution order:

  1. CALLAGENT_API_KEY — wins over everything, never touches disk. Use this in CI.
  2. The OS keychain (macOS Keychain, libsecret on Linux).
  3. ~/.callagent/credentials.json, created mode 0600 inside a 0700 directory.

The key is never passed as a command-line argument, so it does not reach shell history or a ps listing.

Webhook authentication ​

Outbound webhooks are signed, not authenticated with a key. See Webhooks.

Documented against the code. Anything not yet implemented is labelled, never described as working.