ClouisleClouisle

API Keys

Create, bind, and revoke API keys and learn what scopes and rate limits actually enforce

API keys call the Clouisle API from contexts without a user session. Every key authenticates as its owning user, so the owner's role permissions and team memberships apply to every request; the key can additionally narrow access to specific Agents or workflows.

Prerequisites: your role needs apikey:read / apikey:create / apikey:update / apikey:delete (Super Admin has all of them by default). A key can only be bound to Agents and workflows you can already access — otherwise creation/update is rejected.

Accessing API Keys

  1. Click your profile icon in the top-right corner.
  2. Select API Keys in the user menu.
  3. Or navigate directly to /app/api-keys.

Each row shows the name, a status badge (Active / Inactive / Expired), the key_prefix (first 12 characters), rate limit, expiration, last-used time, and creation time. The page only offers a search box, which filters client-side by name or prefix (the backend also supports filtering by status and by user, but this page exposes no controls for them).

Visibility is decided by the backend: only Super Admins see and manage everyone's keys. Every other user — even with apikey:read — sees only their own keys, and editing or deleting someone else's key returns a 3000 permission error.

Creating a Key

### Open the creation form
Go to API Keys (/app/api-keys) and click Create Key.
### Fill in the configuration

FieldRequiredDescription
NameYesDescriptive name, 1–100 characters
Rate LimitNoRequests per minute; 0 means unlimited. The form starts at 0 — see Rate Limits
ExpirationNoDate picker; leave empty for a key that never expires
Allowed AgentsNoCheckbox list that loads published Agents only, up to 100; empty = all Agents allowed
Allowed WorkflowsNoCheckbox list that loads published workflows only, up to 100; empty = all workflows allowed
### Save and copy the key
Click Create (Save when editing). The API Key Created dialog then shows the full key — the only time it is ever displayed. Use the copy button and store it in a password manager or environment variable.

The full key is shown exactly once. Lists and detail endpoints return only key_prefix (the first 12 characters) and the stored value is a bcrypt hash, so it cannot be recovered. A lost key can only be replaced by a new one.

scopes is not settable in the UI (the form never submits it), so keys created from the interface always carry the server default ["chat"]; see Scopes.

Key Format and Storage

clou_<64 hexadecimal characters>

The server generates 32 random bytes with secrets.token_hex(32) (64 hex characters) and prefixes them with clou_. Only two things are persisted:

FieldContentLength
key_prefixFirst 12 characters of the full key (plaintext, used for lookup and display)12
key_hashbcrypt hash of the full key≤ 255

Authentication takes the first 12 characters as an index lookup and then verifies the hash against each candidate, so a prefix collision costs a few extra hash computations rather than causing wrong results.

Editing a Key

PUT /api/v1/api-keys/{id} can change the name, scopes, rate limit, expiration, active state (is_active, the switch that only appears while editing), and the Agent / workflow bindings. Omitting a binding field (null) leaves the current binding untouched; sending an empty array [] clears it and opens the key back up to "everything accessible".

Editing and deleting both require the owner; the key itself cannot be edited — there is no rotation endpoint. Rotate by creating a new key, migrating callers, then deactivating the old one.

The date picker submits the expiration as 00:00:00Z on the chosen day. Choosing 2026-01-01 means the key stops working at midnight UTC on 2026-01-01, not at the end of that day.

Deactivating / Reactivating a Key

Deactivation makes the key fail authentication immediately while keeping the record and its audit history:

  • Deactivate: POST /api/v1/api-keys/{id}/deactivate
  • Reactivate: POST /api/v1/api-keys/{id}/activate

Repeating an operation errors out: deactivating an already inactive key returns api_key_already_inactive, activating an active key returns api_key_already_active (both 1002 bad-request errors).

Deleting a Key

DELETE /api/v1/api-keys/{id} permanently removes the record. You do not need to deactivate first, and deletion cannot be undone. An audit log entry is written before the delete, so the actor, timestamp, and deleted key_prefix remain discoverable.

Using API Keys

Put the full key in the Authorization header — the same format as JWT:

curl -X GET "https://your-domain.com/api/v1/agents" \
  -H "Authorization: Bearer clou_your_api_key_here"
import requests

api_key = "clou_your_api_key_here"

response = requests.get(
    "https://your-domain.com/api/v1/agents",
    headers={"Authorization": f"Bearer {api_key}"},
)

Authentication and Preconditions

The backend routes by prefix: values starting with clou_ take the API key path, everything else is parsed as a JWT. A key must satisfy all of the following:

  1. The prefix must resolve to a record whose hash verifies, otherwise 2001 (invalid_api_key).
  2. The record must be active — the lookup filters on is_active=True, so a deactivated key is indistinguishable from a missing one.
  3. It must not be expired — a null expires_at means "never"; an expired key returns 2002 (api_key_expired).
  4. The owner must still be valid — a deactivated owner or one pending approval returns 2004 (inactive_user / pending_approval_user).
  5. The binding must allow this call — a key bound to Agents may only call those Agents; a key bound to workflows may only trigger those workflows. Empty bindings mean no restriction. A blocked call returns 3000 with api_key_no_agent_access / api_key_no_workflow_access.

Every successful authentication refreshes last_used_at, which is what you check to tell whether a key is still being used.

Bindings are enforced at the call entry points: Agent conversations (the /api/v1/chat routes), iframe embedding (the Agent and workflow run routes under /api/v1/embed), and the workflow webhook trigger (POST /api/v1/workflows/webhook/{webhook_token}, which accepts only clou_ keys and requires a published workflow with the webhook trigger enabled).

Scopes

scopes is a JSON array of strings that is stored but never evaluated: the backend records and echoes whatever you send, and the authentication path never reads it.

  • There is no field for it in the UI, so keys created there are fixed at the default ["chat"].
  • Through the API, scopes accepts any array of strings (e.g. ["agent:read","kb:read"]) — there is no enumeration check and no wildcard semantics, and no dedicated scope-management endpoint.
  • Updates replace the whole array (PUT semantics); there is no "add-only" rule.
  • The data model's comment says an empty array means full access, but because nothing consumes the field that statement currently has no observable effect.

Do not use scopes for access control. What actually decides authorization is the owning user's role permissions, team memberships, and the key's Agent / workflow bindings.

Rate Limits

rate_limit is a per-key "requests per minute" value; 0 means unlimited and the model and request schema default to 1000.

Nothing in this version reads or enforces it: no middleware, dependency, or authentication step performs rate limiting, and responses carry no X-RateLimit-* headers. The 1000/min or Unlimited shown in the interface is purely informational.

There is also an easy-to-miss default mismatch: the request schema defaults to 1000, but the creation form starts at 0 (unlimited). Since the form always submits the field, keys created from the UI are effectively unlimited rather than 1000.

For real call constraints, use bindings (narrow the callable Agents / workflows), expiration dates, and role permissions — or enforce limits at your own gateway. The API Keys reference documents the field defaults, but that does not mean they are enforced.

Expiration Reminders

The backend runs tasks.check_api_key_expiration daily at 09:00 (on the default queue) and sends in-app notifications to key owners:

  • Expiring soon (apikey.expiring): active, not-yet-expired keys with fewer than 7 days left — sent only when the remaining days are exactly 7, 3, or 1, so it does not repeat daily.
  • Expired (apikey.expired): active keys that expired within the last 24 hours.

The notification does not change key state — an expired key still carries the "active" flag and is only stopped by expires_at during authentication. Deactivating a key before it expires stops these notifications.

Stats

GET /api/v1/api-keys/stats returns key statistics for the current user (total, active, inactive, expired); a Super Admin gets site-wide numbers. The definitions match the list filters: active requires the key to be enabled and unexpired, while expired only looks at whether expires_at is in the past (regardless of the active flag). The /app/api-keys page does not currently render these numbers.

Error Codes

CodeMeaningTypical msg_key
2001Invalid key (hash mismatch)invalid_api_key
2002Key expiredapi_key_expired
2004Owner deactivated / pending approvalinactive_user / pending_approval_user
3000Insufficient permission (owner lacks rights, or binding exceeded)permission_denied / api_key_no_agent_access / api_key_no_workflow_access
4000Key record not foundapi_key_not_found
1002Duplicate activate/deactivateapi_key_already_active / api_key_already_inactive

Best Practices

✅ Do

  • Create one key per purpose (production, CI, each partner) so it can be revoked alone.
  • Set an expiration and replace long-lived keys with time-boxed ones.
  • Bind keys to the Agents / workflows they actually call.
  • Keep them in a secret manager or environment variables, never in a repository.
  • Periodically review last_used_at and clean up keys that are no longer used.

❌ Don't

  • Use one person's key to carry several people's traffic (authorization follows that person).
  • Ship keys in frontend code, browser extensions, or public docs.
  • Treat scopes or rate_limit as a security boundary.

Troubleshooting

Key Does Not Work (401)

  1. Confirm the key is Active: deactivated keys are filtered out during authentication and behave exactly like a nonexistent key, returning 2001.
  2. Check whether expires_at has passed (2002).
  3. Check the header format: Bearer clou_... with no line breaks or extra spaces.
  4. Compare the key_prefix (first 12 characters) in the list against the key you are using to rule out a wrong-environment key.
  5. If the full key is lost, create a new one — it cannot be recovered.

Key Is Valid but the Request Is Denied (403)

  1. The key may be bound to specific Agents / workflows: confirm the target is in the binding list, or clear the bindings and retry.
  2. The owner's role permissions may be insufficient: verify the same operation works while signed in as the owner.
  3. The owner account may be deactivated or pending approval (this shows up as 2004).

Other Users' Keys Are Not Visible

Only Super Admins see every key. That is decided by the backend (is_superuser) and is unrelated to the apikey:read permission.

Suspected Key Leak

  1. Deactivate the key from the list first (faster than deleting and it keeps the audit trail).
  2. Create a replacement, update the callers, and delete the old key once no traffic remains.
  3. Cross-check creation, updates, and deletions in the audit log.

How is this guide?

On this page