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
- Click your profile icon in the top-right corner.
- Select API Keys in the user menu.
- 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
/app/api-keys) and click Create Key.
| Field | Required | Description |
|---|---|---|
| Name | Yes | Descriptive name, 1–100 characters |
| Rate Limit | No | Requests per minute; 0 means unlimited. The form starts at 0 — see Rate Limits |
| Expiration | No | Date picker; leave empty for a key that never expires |
| Allowed Agents | No | Checkbox list that loads published Agents only, up to 100; empty = all Agents allowed |
| Allowed Workflows | No | Checkbox 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:
| Field | Content | Length |
|---|---|---|
key_prefix | First 12 characters of the full key (plaintext, used for lookup and display) | 12 |
key_hash | bcrypt 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:
- The prefix must resolve to a record whose hash verifies, otherwise
2001(invalid_api_key). - The record must be active — the lookup filters on
is_active=True, so a deactivated key is indistinguishable from a missing one. - It must not be expired — a null
expires_atmeans "never"; an expired key returns2002(api_key_expired). - The owner must still be valid — a deactivated owner or one pending approval returns
2004(inactive_user/pending_approval_user). - 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
3000withapi_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,
scopesaccepts 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 (
PUTsemantics); 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 than7days 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
| Code | Meaning | Typical msg_key |
|---|---|---|
2001 | Invalid key (hash mismatch) | invalid_api_key |
2002 | Key expired | api_key_expired |
2004 | Owner deactivated / pending approval | inactive_user / pending_approval_user |
3000 | Insufficient permission (owner lacks rights, or binding exceeded) | permission_denied / api_key_no_agent_access / api_key_no_workflow_access |
4000 | Key record not found | api_key_not_found |
1002 | Duplicate activate/deactivate | api_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_atand 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
scopesorrate_limitas a security boundary.
Troubleshooting
Key Does Not Work (401)
- Confirm the key is Active: deactivated keys are filtered out during authentication and behave exactly like a nonexistent key, returning
2001. - Check whether
expires_athas passed (2002). - Check the header format:
Bearer clou_...with no line breaks or extra spaces. - Compare the
key_prefix(first 12 characters) in the list against the key you are using to rule out a wrong-environment key. - If the full key is lost, create a new one — it cannot be recovered.
Key Is Valid but the Request Is Denied (403)
- The key may be bound to specific Agents / workflows: confirm the target is in the binding list, or clear the bindings and retry.
- The owner's role permissions may be insufficient: verify the same operation works while signed in as the owner.
- 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
- Deactivate the key from the list first (faster than deleting and it keeps the audit trail).
- Create a replacement, update the callers, and delete the old key once no traffic remains.
- Cross-check creation, updates, and deletions in the audit log.
Related Documentation
- API Keys API — endpoints, request bodies, and response fields
- API Keys reference — field and default quick reference
- Agent Management — an Agent's publish state determines whether it can be bound
- Workflow Orchestration — publishing workflows and the webhook trigger
- Audit Logs — audit records for key operations
How is this guide?