Skip to content

API keys ​

7 endpoints. Every path below is relative to the API base URL and already includes the /api/v1 prefix — see conventions for authentication, pagination, errors and idempotency.

Endpoint
GET/api-keysList API keys newest first, filterable by type and active state; private key material is never included
POST/api-keysIssue an API key and return its plaintext once — this is the only response that ever contains it
GET/api-keys/{apiKey}Retrieve one API key by id; a private key's material is reduced to its prefix and last four
PUT/api-keys/{apiKey}Update a key's name, scopes, restrictions or expiry. Key material is immutable
DELETE/api-keys/{apiKey}Revoke a key immediately and return the revoked key object
POST/api-keys/{apiKey}/rotateMint a replacement key, return its plaintext, and expire the old key after an overlap window
GET/api-keys/scopesList the scope vocabulary an API key may be granted, grouped by permission area

Endpoints ​

List API keys newest first, filterable by type and active state; private key material is never included ​

GET/api-keys
  • Requires api_key.read.
  • Query filters are type and active_only; per_page is clamped to 100.

Query parameters

NameTypeRequiredNotes
active_onlyboolean—default false
bash
curl -X GET "$CALLAGENT_API_URL/api-keys?active_only=false" \
  -H "Authorization: Bearer $CALLAGENT_API_KEY" \
  -H "Accept: application/json"
js
const response = await fetch(`${process.env.CALLAGENT_API_URL}/api-keys?active_only=false`, {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.CALLAGENT_API_KEY}`,
    Accept: 'application/json',
  },
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.type}: ${error.message}`);
}

const { data } = await response.json();
php
<?php

$handle = curl_init(getenv('CALLAGENT_API_URL') . '/api-keys?active_only=false');

curl_setopt_array($handle, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('CALLAGENT_API_KEY'),
        'Accept: application/json',
    ],
]);

$body = json_decode(curl_exec($handle), true);
$status = curl_getinfo($handle, CURLINFO_HTTP_CODE);
curl_close($handle);

if ($status >= 400) {
    throw new RuntimeException("{$body['error']['type']}: {$body['error']['message']}");
}

$data = $body['data'];
python
import os, requests

response = requests.get(
    f"{os.environ['CALLAGENT_API_URL']}/api-keys?active_only=false",
    headers={"Authorization": f"Bearer {os.environ['CALLAGENT_API_KEY']}"},
    timeout=30,
)
response.raise_for_status()
data = response.json()["data"]

Responses

StatusBody
200Array of ApiKeyResource

Errors

StatusWhen it happens
403The presenting API key does not carry the api_key.read scope

Example response, generated from the schema:

json
{
  "data": [
    {
      "id": "01JQ0000000000000000000000",
      "object": "string",
      "name": "Support Line",
      "type": "string",
      "display": "string",
      "public_key": "string",
      "scopes": {},
      "allowed_origins": {},
      "allowed_assistant_ids": {},
      "allowed_ips": {},
      "expires_at": "2026-01-01T12:00:00+00:00",
      "last_used_at": "2026-01-01T12:00:00+00:00",
      "last_used_ip": "string",
      "revoked_at": "2026-01-01T12:00:00+00:00",
      "rotated_from_id": "01JQ0000000000000000000000",
      "usable": true,
      "created_at": "2026-01-01T12:00:00+00:00"
    }
  ]
}

Issue an API key and return its plaintext once — this is the only response that ever contains it ​

POST/api-keys

A public key must carry at least one allowed origin — it ships inside a browser bundle, so origin restriction is the only thing protecting it.

  • Requires api_key.write.
  • type=public is refused unless allowed_origins is non-empty. Scopes accept '' and 'group.' wildcards; anything else must exist in the vocabulary.

Request body

FieldTypeRequiredNotes
namestringyesmax 255 chars
typeprivate · public · service_account · null | null—
scopesstring[] | null—
allowed_originsstring[] | null—
allowed_assistant_idsstring[] | null—
allowed_ipsstring[] | null—
expires_atstring | null—date-time
bash
curl -X POST "$CALLAGENT_API_URL/api-keys" \
  -H "Authorization: Bearer $CALLAGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
       "name": "Support Line",
       "type": "private",
       "expires_at": "2026-01-01T12:00:00+00:00"
     }'
js
const response = await fetch(`${process.env.CALLAGENT_API_URL}/api-keys`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CALLAGENT_API_KEY}`,
    Accept: 'application/json',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "name": "Support Line",
    "type": "private",
    "expires_at": "2026-01-01T12:00:00+00:00"
  }),
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.type}: ${error.message}`);
}

const { data } = await response.json();
php
<?php

$handle = curl_init(getenv('CALLAGENT_API_URL') . '/api-keys');

curl_setopt_array($handle, [
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('CALLAGENT_API_KEY'),
        'Accept: application/json',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
    'name' => 'Support Line',
    'type' => 'private',
    'expires_at' => '2026-01-01T12:00:00+00:00',
]),
]);

$body = json_decode(curl_exec($handle), true);
$status = curl_getinfo($handle, CURLINFO_HTTP_CODE);
curl_close($handle);

if ($status >= 400) {
    throw new RuntimeException("{$body['error']['type']}: {$body['error']['message']}");
}

$data = $body['data'];
python
import os, requests

response = requests.post(
    f"{os.environ['CALLAGENT_API_URL']}/api-keys",
    headers={"Authorization": f"Bearer {os.environ['CALLAGENT_API_KEY']}"},
    json={
      "name": "Support Line",
      "type": "private",
      "expires_at": "2026-01-01T12:00:00+00:00"
    },
    timeout=30,
)
response.raise_for_status()
data = response.json()["data"]

Responses

StatusBody
201data-wrapped object

Errors

StatusWhen it happens
403The presenting API key does not carry the api_key.write scope
422Validation failed, an unknown scope was requested, or a public key had no allowed_origins

Example response, generated from the schema:

json
{
  "data": {
    "id": "01JQ0000000000000000000000",
    "name": "Support Line",
    "type": "string",
    "key": "string",
    "key_prefix": "string",
    "last_four": "string",
    "scopes": {},
    "expires_at": "2026-01-01T12:00:00+00:00",
    "notice": "This is a public key. It is safe to ship in a browser bundle, but restrict it by origin and assistant."
  }
}

Retrieve one API key by id; a private key's material is reduced to its prefix and last four ​

GET/api-keys/{apiKey}
  • Requires api_key.read.

Path parameters

NameTypeRequiredNotes
apiKeystringyesThe api key ID
bash
curl -X GET "$CALLAGENT_API_URL/api-keys/01JQ0000000000000000000000" \
  -H "Authorization: Bearer $CALLAGENT_API_KEY" \
  -H "Accept: application/json"
js
const response = await fetch(`${process.env.CALLAGENT_API_URL}/api-keys/01JQ0000000000000000000000`, {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.CALLAGENT_API_KEY}`,
    Accept: 'application/json',
  },
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.type}: ${error.message}`);
}

const { data } = await response.json();
php
<?php

$handle = curl_init(getenv('CALLAGENT_API_URL') . '/api-keys/01JQ0000000000000000000000');

curl_setopt_array($handle, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('CALLAGENT_API_KEY'),
        'Accept: application/json',
    ],
]);

$body = json_decode(curl_exec($handle), true);
$status = curl_getinfo($handle, CURLINFO_HTTP_CODE);
curl_close($handle);

if ($status >= 400) {
    throw new RuntimeException("{$body['error']['type']}: {$body['error']['message']}");
}

$data = $body['data'];
python
import os, requests

response = requests.get(
    f"{os.environ['CALLAGENT_API_URL']}/api-keys/01JQ0000000000000000000000",
    headers={"Authorization": f"Bearer {os.environ['CALLAGENT_API_KEY']}"},
    timeout=30,
)
response.raise_for_status()
data = response.json()["data"]

Responses

StatusBody
200ApiKeyResource

Errors

StatusWhen it happens
403The presenting API key does not carry the api_key.read scope
404No such key in this organization

Example response, generated from the schema:

json
{
  "data": {
    "id": "01JQ0000000000000000000000",
    "object": "string",
    "name": "Support Line",
    "type": "string",
    "display": "string",
    "public_key": "string",
    "scopes": {},
    "allowed_origins": {},
    "allowed_assistant_ids": {},
    "allowed_ips": {},
    "expires_at": "2026-01-01T12:00:00+00:00",
    "last_used_at": "2026-01-01T12:00:00+00:00",
    "last_used_ip": "string",
    "revoked_at": "2026-01-01T12:00:00+00:00",
    "rotated_from_id": "01JQ0000000000000000000000",
    "usable": true,
    "created_at": "2026-01-01T12:00:00+00:00"
  }
}

Update a key's name, scopes, restrictions or expiry. Key material is immutable ​

PUT/api-keys/{apiKey}
  • Requires api_key.write.
  • PATCH is accepted on the same path, with the same body.
  • type, key_hash, public_key, key_prefix, last_four and organization_id are silently dropped rather than rejected.

Path parameters

NameTypeRequiredNotes
apiKeystringyesThe api key ID

Request body

FieldTypeRequiredNotes
namestring—max 255 chars
scopesstring[]—
allowed_originsstring[]—
allowed_assistant_idsstring[]—
allowed_ipsstring[]—
expires_atstring | null—date-time
bash
curl -X PUT "$CALLAGENT_API_URL/api-keys/01JQ0000000000000000000000" \
  -H "Authorization: Bearer $CALLAGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
       "name": "Support Line",
       "expires_at": "2026-01-01T12:00:00+00:00"
     }'
js
const response = await fetch(`${process.env.CALLAGENT_API_URL}/api-keys/01JQ0000000000000000000000`, {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${process.env.CALLAGENT_API_KEY}`,
    Accept: 'application/json',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "name": "Support Line",
    "expires_at": "2026-01-01T12:00:00+00:00"
  }),
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.type}: ${error.message}`);
}

const { data } = await response.json();
php
<?php

$handle = curl_init(getenv('CALLAGENT_API_URL') . '/api-keys/01JQ0000000000000000000000');

curl_setopt_array($handle, [
    CURLOPT_CUSTOMREQUEST => 'PUT',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('CALLAGENT_API_KEY'),
        'Accept: application/json',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
    'name' => 'Support Line',
    'expires_at' => '2026-01-01T12:00:00+00:00',
]),
]);

$body = json_decode(curl_exec($handle), true);
$status = curl_getinfo($handle, CURLINFO_HTTP_CODE);
curl_close($handle);

if ($status >= 400) {
    throw new RuntimeException("{$body['error']['type']}: {$body['error']['message']}");
}

$data = $body['data'];
python
import os, requests

response = requests.put(
    f"{os.environ['CALLAGENT_API_URL']}/api-keys/01JQ0000000000000000000000",
    headers={"Authorization": f"Bearer {os.environ['CALLAGENT_API_KEY']}"},
    json={
      "name": "Support Line",
      "expires_at": "2026-01-01T12:00:00+00:00"
    },
    timeout=30,
)
response.raise_for_status()
data = response.json()["data"]

Responses

StatusBody
200ApiKeyResource

Errors

StatusWhen it happens
403The presenting API key does not carry the api_key.write scope
404No such key in this organization
422Validation failed or an unknown scope was requested

Example response, generated from the schema:

json
{
  "data": {
    "id": "01JQ0000000000000000000000",
    "object": "string",
    "name": "Support Line",
    "type": "string",
    "display": "string",
    "public_key": "string",
    "scopes": {},
    "allowed_origins": {},
    "allowed_assistant_ids": {},
    "allowed_ips": {},
    "expires_at": "2026-01-01T12:00:00+00:00",
    "last_used_at": "2026-01-01T12:00:00+00:00",
    "last_used_ip": "string",
    "revoked_at": "2026-01-01T12:00:00+00:00",
    "rotated_from_id": "01JQ0000000000000000000000",
    "usable": true,
    "created_at": "2026-01-01T12:00:00+00:00"
  }
}

Revoke a key immediately and return the revoked key object ​

DELETE/api-keys/{apiKey}
  • Requires api_key.write.
  • DELETE does not delete: it stamps revoked_at and returns 200 with a body, not 204. An optional reason is recorded in the audit log.

Path parameters

NameTypeRequiredNotes
apiKeystringyesThe api key ID
bash
curl -X DELETE "$CALLAGENT_API_URL/api-keys/01JQ0000000000000000000000" \
  -H "Authorization: Bearer $CALLAGENT_API_KEY" \
  -H "Accept: application/json"
js
const response = await fetch(`${process.env.CALLAGENT_API_URL}/api-keys/01JQ0000000000000000000000`, {
  method: 'DELETE',
  headers: {
    Authorization: `Bearer ${process.env.CALLAGENT_API_KEY}`,
    Accept: 'application/json',
  },
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.type}: ${error.message}`);
}

const { data } = await response.json();
php
<?php

$handle = curl_init(getenv('CALLAGENT_API_URL') . '/api-keys/01JQ0000000000000000000000');

curl_setopt_array($handle, [
    CURLOPT_CUSTOMREQUEST => 'DELETE',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('CALLAGENT_API_KEY'),
        'Accept: application/json',
    ],
]);

$body = json_decode(curl_exec($handle), true);
$status = curl_getinfo($handle, CURLINFO_HTTP_CODE);
curl_close($handle);

if ($status >= 400) {
    throw new RuntimeException("{$body['error']['type']}: {$body['error']['message']}");
}

$data = $body['data'];
python
import os, requests

response = requests.delete(
    f"{os.environ['CALLAGENT_API_URL']}/api-keys/01JQ0000000000000000000000",
    headers={"Authorization": f"Bearer {os.environ['CALLAGENT_API_KEY']}"},
    timeout=30,
)
response.raise_for_status()
data = response.json()["data"]

Responses

StatusBody
200data-wrapped object

Errors

StatusWhen it happens
403The presenting API key does not carry the api_key.write scope
404No such key in this organization

Example response, generated from the schema:

json
{
  "data": {
    "id": "01JQ0000000000000000000000",
    "object": "string",
    "name": "Support Line",
    "type": "string",
    "display": "string",
    "public_key": "string",
    "scopes": {},
    "allowed_origins": {},
    "allowed_assistant_ids": {},
    "allowed_ips": {},
    "expires_at": "2026-01-01T12:00:00+00:00",
    "last_used_at": "2026-01-01T12:00:00+00:00",
    "last_used_ip": "string",
    "revoked_at": "2026-01-01T12:00:00+00:00",
    "rotated_from_id": "01JQ0000000000000000000000",
    "usable": true,
    "created_at": "2026-01-01T12:00:00+00:00"
  }
}

Mint a replacement key, return its plaintext, and expire the old key after an overlap window ​

POST/api-keys/{apiKey}/rotate

Returns the replacement's plaintext. The old key keeps working for overlap_hours so a rolling deploy does not break; pass 0 after a leak.

  • Requires api_key.write.
  • The response has a sibling rotated_from next to data. overlap_hours defaults to 24; 0 revokes the old key immediately.

Path parameters

NameTypeRequiredNotes
apiKeystringyesThe api key ID

Request body

FieldTypeRequiredNotes
overlap_hoursinteger | null—min 0. max 720
bash
curl -X POST "$CALLAGENT_API_URL/api-keys/01JQ0000000000000000000000/rotate" \
  -H "Authorization: Bearer $CALLAGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
       "overlap_hours": 1
     }'
js
const response = await fetch(`${process.env.CALLAGENT_API_URL}/api-keys/01JQ0000000000000000000000/rotate`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CALLAGENT_API_KEY}`,
    Accept: 'application/json',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "overlap_hours": 1
  }),
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.type}: ${error.message}`);
}

const { data } = await response.json();
php
<?php

$handle = curl_init(getenv('CALLAGENT_API_URL') . '/api-keys/01JQ0000000000000000000000/rotate');

curl_setopt_array($handle, [
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('CALLAGENT_API_KEY'),
        'Accept: application/json',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
    'overlap_hours' => 1,
]),
]);

$body = json_decode(curl_exec($handle), true);
$status = curl_getinfo($handle, CURLINFO_HTTP_CODE);
curl_close($handle);

if ($status >= 400) {
    throw new RuntimeException("{$body['error']['type']}: {$body['error']['message']}");
}

$data = $body['data'];
python
import os, requests

response = requests.post(
    f"{os.environ['CALLAGENT_API_URL']}/api-keys/01JQ0000000000000000000000/rotate",
    headers={"Authorization": f"Bearer {os.environ['CALLAGENT_API_KEY']}"},
    json={
      "overlap_hours": 1
    },
    timeout=30,
)
response.raise_for_status()
data = response.json()["data"]

Responses

StatusBody
201data-wrapped object

Errors

StatusWhen it happens
403The presenting API key does not carry the api_key.write scope
404No such key in this organization
422overlap_hours is not an integer in 0..720

Example response, generated from the schema:

json
{
  "data": {
    "id": "01JQ0000000000000000000000",
    "name": "Support Line",
    "type": "string",
    "key": "string",
    "key_prefix": "string",
    "last_four": "string",
    "scopes": {},
    "expires_at": "2026-01-01T12:00:00+00:00",
    "notice": "This is a public key. It is safe to ship in a browser bundle, but restrict it by origin and assistant."
  },
  "rotated_from": {
    "id": "01JQ0000000000000000000000",
    "object": "string",
    "name": "Support Line",
    "type": "string",
    "display": "string",
    "public_key": "string",
    "scopes": {},
    "allowed_origins": {},
    "allowed_assistant_ids": {},
    "allowed_ips": {},
    "expires_at": "2026-01-01T12:00:00+00:00",
    "last_used_at": "2026-01-01T12:00:00+00:00",
    "last_used_ip": "string",
    "revoked_at": "2026-01-01T12:00:00+00:00",
    "rotated_from_id": "01JQ0000000000000000000000",
    "usable": true,
    "created_at": "2026-01-01T12:00:00+00:00"
  }
}

List the scope vocabulary an API key may be granted, grouped by permission area ​

GET/api-keys/scopes
  • No scope is enforced in the handler — any valid key may call it.
  • The only api-keys endpoint that enforces no scope at all — any authenticated key can read it.
bash
curl -X GET "$CALLAGENT_API_URL/api-keys/scopes" \
  -H "Authorization: Bearer $CALLAGENT_API_KEY" \
  -H "Accept: application/json"
js
const response = await fetch(`${process.env.CALLAGENT_API_URL}/api-keys/scopes`, {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.CALLAGENT_API_KEY}`,
    Accept: 'application/json',
  },
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.type}: ${error.message}`);
}

const { data } = await response.json();
php
<?php

$handle = curl_init(getenv('CALLAGENT_API_URL') . '/api-keys/scopes');

curl_setopt_array($handle, [
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('CALLAGENT_API_KEY'),
        'Accept: application/json',
    ],
]);

$body = json_decode(curl_exec($handle), true);
$status = curl_getinfo($handle, CURLINFO_HTTP_CODE);
curl_close($handle);

if ($status >= 400) {
    throw new RuntimeException("{$body['error']['type']}: {$body['error']['message']}");
}

$data = $body['data'];
python
import os, requests

response = requests.get(
    f"{os.environ['CALLAGENT_API_URL']}/api-keys/scopes",
    headers={"Authorization": f"Bearer {os.environ['CALLAGENT_API_KEY']}"},
    timeout=30,
)
response.raise_for_status()
data = response.json()["data"]

Responses

StatusBody
200data-wrapped array

Example response, generated from the schema:

json
{
  "data": [
    {
      "group": "string",
      "scopes": []
    }
  ]
}

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