ClouisleClouisle

API Keys API

Create and manage user-owned API keys for Agents, workflows, and uploads

API keys let server-side integrations, scripts, and iframe embeds call Clouisle without a user session. A key belongs to the user who created it — it is not a team resource: its reach is the combination of that user's roles, team memberships, and the key's own bindings. The base path is /api/v1/api-keys.

Key Shape and What Actually Gates Access

The server generates keys in this shape:

clou_a1b2c3d4e5f67890a1b2c3d4e5f67890a1b2c3d4e5f67890a1b2c3d4e5f67890
└──┬─┘└───────────────────────────┬───────────────────────────┘
 clou_ prefix             32 random bytes as 64 hex characters
PropertyValue
Full length69 characters (clou_ + 64 hex)
Displayed prefixThe first 12 characters (clou_ + 8 hex), used for identification and search
StorageOnly a hash plus the prefix are stored; the full key appears once, in the create response
OwnerBound to the creating user at creation time (user_id); it cannot be transferred

Send it in the Authorization header, using the same Bearer form as a JWT:

Authorization: Bearer clou_a1b2c3d4e5f6...

API key authentication checks exactly four things, and failing any of them rejects the request:

  1. Active state — a key with is_active = false no longer matches, and the request fails with HTTP 401 / 2001 (INVALID_TOKEN).
  2. Expiry — a non-null expires_at in the past fails with HTTP 401 / 2002 (TOKEN_EXPIRED); leaving it empty means the key never expires.
  3. Owner account state — if the owning user is deactivated or pending approval, requests fail with HTTP 401 / 2004 (INACTIVE_USER).
  4. Resource bindings — when a key is bound to a list of Agents or Workflows it can only reach those; anything outside the list fails with HTTP 403 / 3000.

scopes and rate_limit are stored and echoed back, but the current version does not enforce either: there is no scope middleware and no per-key throttling. The four checks above are what actually applies. Never treat scopes as a security boundary, and do not rely on rate_limit for cost control.

Authentication and Permissions

Managing API keys requires a JWT user session. An API key cannot create, modify, or delete API keys.

PermissionAllowed operations
apikey:readList keys, view one key, read statistics
apikey:createCreate a key
apikey:updateUpdate, activate, deactivate a key
apikey:deleteDelete a key

Beyond the permission code there is an ownership check: a non-superuser can only operate on keys they created, otherwise the request fails with HTTP 403 / 3000 (PERMISSION_DENIED). Superusers can see and operate on everyone's keys and bypass the permission-code check.

Endpoints

MethodPathPurposePermission
GET/api/v1/api-keysList API keys with paginationapikey:read
GET/api/v1/api-keys/statsAggregate statisticsapikey:read
POST/api/v1/api-keysCreate an API keyapikey:create
GET/api/v1/api-keys/{api_key_id}Get one keyapikey:read
PUT/api/v1/api-keys/{api_key_id}Update a keyapikey:update
DELETE/api/v1/api-keys/{api_key_id}Delete (revoke) a keyapikey:delete
POST/api/v1/api-keys/{api_key_id}/activateActivate a keyapikey:update
POST/api/v1/api-keys/{api_key_id}/deactivateDeactivate a keyapikey:update

Route order matters: /stats is a fixed path and /{api_key_id} expects a UUID, so /api/v1/api-keys/stats is never parsed as an ID.

List API Keys

GET /api/v1/api-keys
curl -X GET "https://your-domain.com/api/v1/api-keys?page=1&page_size=20" \
  -H "Authorization: Bearer YOUR_TOKEN"
ParameterTypeDefaultDescription
pageinteger1Page number, 1-based
page_sizeinteger20Items per page
statusstring[]—Filter by active, inactive, or expired (repeatable; multiple values are OR-ed)
user_idUUID[]—Filter by owner; superuser only — ignored for regular users
searchstring—Case-insensitive match on name or key prefix

The status definitions overlap:

  • active: is_active = true and (expires_at is null or in the future).
  • inactive: is_active = false (expired or not).
  • expired: expires_at is in the past, even when is_active is still true.

A deactivated key that has also expired therefore appears under both inactive and expired, so the counts can add up to more than the total.

Non-superusers always see only their own keys. Results are ordered by creation time, newest first.

Response (200 OK):

{
  "code": 0,
  "data": {
    "items": [
      {
        "id": "8f14e45f-ceea-467a-9d0f-1a2b3c4d5e6f",
        "name": "Production API Key",
        "key_prefix": "clou_a1b2c3d",
        "user_id": "3f2a1b0c-9d8e-4f7a-8b6c-5d4e3f2a1b0c",
        "user": { "id": "3f2a1b0c-9d8e-4f7a-8b6c-5d4e3f2a1b0c", "username": "alice" },
        "scopes": ["chat"],
        "rate_limit": 1000,
        "is_active": true,
        "last_used_at": "2026-09-24T14:30:00Z",
        "expires_at": "2027-02-11T00:00:00Z",
        "agents": [{ "id": "0b1c2d3e-...", "name": "Customer Support", "icon": "🤖" }],
        "workflows": [],
        "created_at": "2026-02-11T10:00:00Z",
        "updated_at": "2026-02-11T10:00:00Z"
      }
    ],
    "total": 1,
    "page": 1,
    "page_size": 20
  },
  "msg": "success"
}

user is present only when the owner could be loaded. last_used_at is refreshed on every successful authenticated request.

Get an API Key

GET /api/v1/api-keys/{api_key_id}
Path parameterTypeRequiredDescription
api_key_idstring (UUID)YesThe API key UUID

The body matches a list item (never including key). A missing key returns HTTP 404 / 4000; a key owned by someone else returns HTTP 403 / 3000.

Create an API Key

POST /api/v1/api-keys
FieldTypeRequiredDefaultDescription
namestringYes—Name, 1–100 characters
scopesstring[]No["chat"]Permission scopes (stored only, never enforced)
rate_limitintegerNo1000Requests per minute, 0 means unlimited (stored only, never enforced); must be ≥ 0
expires_atstring (ISO 8601)NonullExpiry time; omit or pass null for a key that never expires
agent_idsUUID[]No[]Agents this key may access; an empty array means no restriction
workflow_idsUUID[]No[]Workflows this key may access; an empty array means no restriction
curl -X POST "https://your-domain.com/api/v1/api-keys" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Production API Key",
    "expires_at": "2027-02-11T00:00:00Z"
  }'

Bindings are validated at creation time: every entry in agent_ids / workflow_ids goes through the Agent/Workflow access check, so naming a resource that does not exist or that you cannot access fails immediately (HTTP 404 / 4000 or 403 / 3000). Authorization and binding are therefore the same step.

Response (200 OK):

{
  "code": 0,
  "data": {
    "id": "8f14e45f-ceea-467a-9d0f-1a2b3c4d5e6f",
    "name": "Production API Key",
    "key": "clou_a1b2c3d4e5f67890a1b2c3d4e5f67890a1b2c3d4e5f67890a1b2c3d4e5f67890",
    "key_prefix": "clou_a1b2c3d",
    "user_id": "3f2a1b0c-9d8e-4f7a-8b6c-5d4e3f2a1b0c",
    "scopes": ["chat"],
    "rate_limit": 1000,
    "is_active": true,
    "expires_at": "2027-02-11T00:00:00Z",
    "last_used_at": null,
    "agents": [],
    "workflows": [],
    "created_at": "2026-02-11T16:00:00Z",
    "updated_at": "2026-02-11T16:00:00Z"
  },
  "msg": "API key created successfully"
}

data.key appears in this response only. The server keeps just a hash, so every later read returns only key_prefix. Write it to a secret manager or environment variable immediately; if you lose it, delete the key and create a new one.

Update an API Key

PUT /api/v1/api-keys/{api_key_id}

Every field is optional; only the fields you send change, and omitted fields keep their current value.

FieldTypeDescription
namestringName, 1–100 characters
scopesstring[]Replaces the scope list
rate_limitintegerReplaces the rate-limit value; must be ≥ 0
expires_atstring (ISO 8601)Replaces the expiry time
is_activebooleanFlips the active state directly
agent_idsUUID[]Replaces the Agent bindings entirely; [] clears them (cleared means no restriction)
workflow_idsUUID[]Replaces the Workflow bindings entirely; [] clears them

agent_ids / workflow_ids are replacement semantics, not incremental appends. To add one binding, first read the current bindings (GET the key) and submit the full list, otherwise the remaining bindings are silently dropped. Only null or an omitted field means “leave unchanged”.

The response returns the full updated object with msg set to API key updated successfully.

Delete an API Key

DELETE /api/v1/api-keys/{api_key_id}

Deletion takes effect immediately: the key no longer authenticates (later requests return HTTP 401 / 2001). data in the response is the deleted key object (returned for audit echo), not null.

Deletion is irreversible — there is no soft delete or grace period. When rotating, create the replacement, confirm it works, then delete the old key.

Activate and Deactivate

POST /api/v1/api-keys/{api_key_id}/activate
POST /api/v1/api-keys/{api_key_id}/deactivate

Both return the updated key object. Calling either while already in the target state is an error, which also helps detect concurrent edits:

  • activate on an active key: HTTP 400 / 1002 (api_key_already_active).
  • deactivate on an inactive key: HTTP 400 / 1002 (api_key_already_inactive).

Deactivation is a reversible pause: the key record and its bindings stay intact, and reactivating restores exactly the previous access.

Statistics

GET /api/v1/api-keys/stats

Regular users get statistics for their own keys; superusers get deployment-wide numbers.

{
  "code": 0,
  "data": { "total": 3, "active": 2, "inactive": 0, "expired": 1 },
  "msg": "success"
}

The definitions match the list status filter: active requires both an active flag and a non-expired key, inactive looks only at the flag, and expired looks only at the expiry time. Because the definitions overlap, the four numbers do not have to add up to total.

Error Codes

HTTPCodeScenario
4001001Body validation failed (empty name, negative rate_limit)
4001002The key is already in the requested state (duplicate activate/deactivate)
4012000No credentials provided
4012001API key invalid, deleted, or deactivated; JWT invalid or revoked
4012002API key expired
4012004The key's owner is deactivated or pending approval
4032003JWT cannot be decoded or has expired
4033000Missing an apikey:* permission, operating on someone else's key, or a binding mismatch
4044000api_key_id does not exist

Scope Values

scopes is a free-form string array: the server validates no enumeration and enforces nothing. To stay compatible with a future version, these are the values seen in the UI and community examples:

ValueMeaning
chatCall Agent chat endpoints (the creation default)
agent:readRead Agent information
agent:chatChat with an Agent
workflow:executeExecute a workflow
workflow:runTrigger a workflow run
kb:readRead knowledge base content

If you need least privilege today, the only reliable mechanism is resource binding: restrict agent_ids / workflow_ids to the resources you actually call, and deactivate keys you no longer need. Scope strings are documentation and audit trail only.

Best Practices

Security

  • Keep full keys in environment variables or a secret manager — never in version control, logs, or client-side code.
  • One key per integration so a single deactivation does not break every caller.
  • Set expires_at on production keys and allow an overlap window when rotating.
  • Use agent_ids / workflow_ids to limit a key to the resources it needs.
  • Review last_used_at regularly and delete keys that are no longer used.
  • Respond to a leak with DELETE (full revoke) or deactivate (temporary pause).

Lifecycle management

  • Name keys with their purpose and environment, for example prod-chatbot-gateway.
  • Rotation flow: create the new key → update the caller → watch last_used_at stop advancing → delete the old key.
  • Prefer deactivate for temporary investigations; reactivating is cheaper than redistributing a new secret.
  • Re-check keys after team membership changes: a key assumes its owner's identity, so an owner leaving a team changes what the key can reach.

Code Examples

Python

import os
import requests

BASE = "https://your-domain.com/api/v1"


def create_api_key(token: str, name: str, agent_ids: list[str] | None = None) -> dict:
    """Create an API key bound to specific Agents."""
    response = requests.post(
        f"{BASE}/api-keys",
        headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
        json={
            "name": name,
            "expires_at": "2027-02-11T00:00:00Z",
            "agent_ids": agent_ids or [],
        },
    )
    result = response.json()
    if result["code"] != 0:
        raise RuntimeError(f"create failed: {result['code']} {result['msg']}")
    # data["key"] is available only in this one response
    print(f"created {result['data']['key_prefix']}…")
    return result["data"]


def list_api_keys(token: str, status: str = "active") -> dict:
    """Page through your own API keys, filtered by status."""
    response = requests.get(
        f"{BASE}/api-keys",
        headers={"Authorization": f"Bearer {token}"},
        params={"page": 1, "page_size": 20, "status": status},
    )
    return response.json()["data"]


# Usage
token = os.environ["USER_TOKEN"]
new_key = create_api_key(token, "prod-chatbot")
print("store the full key in your secret manager:", new_key["key"])

JavaScript

const BASE = 'https://your-domain.com/api/v1';

async function createApiKey(token, name, agentIds = []) {
  const response = await fetch(`${BASE}/api-keys`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      name,
      expires_at: '2027-02-11T00:00:00Z',
      agent_ids: agentIds,
    }),
  });
  const result = await response.json();
  if (result.code !== 0) {
    throw new Error(`create failed: ${result.code} ${result.msg}`);
  }
  return result.data; // data.key is available only in this one response
}

async function deactivateApiKey(token, keyId) {
  const response = await fetch(`${BASE}/api-keys/${keyId}/deactivate`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${token}` },
  });
  const result = await response.json();
  if (result.code !== 0) {
    throw new Error(`deactivate failed: ${result.code} ${result.msg}`);
  }
  return result.data;
}

// Usage
const token = process.env.USER_TOKEN;
const created = await createApiKey(token, 'prod-chatbot');
console.log('the full key is only available in this response:', created.key);

How is this guide?

On this page