API Keys API
API keys are role-based machine identities for external systems (server-to-server). An API key authenticates via the X-API-Key header and receives the permissions of its assigned role — it runs through the same RBAC matrix as a logged-in user (unified actor). Keys are managed under /api/api-keys; this is an admin-only function and only possible with a logged-in user.
Authentication & Permissions
There are two separate layers: MANAGING keys (/api/api-keys) and USING a key against the normal API routes.
| Action | Permission |
|---|---|
| All management routes (read/create/update/delete/activate) | settings.manageRoles |
| Use a key (against API routes) | permissions of the assigned role |
User context required (no API key): All /api/api-keys routes require a logged-in user AND settings.manageRoles. An X-API-Key is rejected here with 403 — so an API key cannot manage itself or other keys.
Unified actor: On use, the permissions of the key's role apply, checked exactly as for signed-in users. If the key has no active role assigned, every request is rejected with 403 API_KEY_NO_ROLE. Details: Permissions & RBAC.
Endpoints Overview
All routes: settings.manageRoles + logged-in user.
| Method | Endpoint | Description |
|---|---|---|
GET | /api/api-keys | List all keys (without plaintext key, newest first) |
POST | /api/api-keys | Create key (201; plaintext key visible ONLY here) |
PUT | /api/api-keys/:id | Update key |
DELETE | /api/api-keys/:id | Permanently delete key |
POST | /api/api-keys/:id/deactivate | Deactivate key (isActive=false, reversible) |
POST | /api/api-keys/:id/reactivate | Reactivate key |
Deactivate via POST /:id/deactivate; alternatively isActive can be set via PUT /:id.
Fields
| Field | Type | Description |
|---|---|---|
name | String (1–100) | Display name, unique (duplicate → 409) |
description | String? (≤500) | Optional description |
roleId | String? | Assigned role — determines the key's permissions. If the role is deleted, the field is set to null. |
allowedIPs | String[] | IP whitelist (single IPv4/IPv6 or CIDR, e.g. 10.0.0.0/24). Empty = usable from anywhere. |
rateLimit | Int? (1–10000) | Max requests per minute. null = unlimited. |
expiresAt | DateTime? (ISO 8601) | Expiry date. Expired keys are rejected with 403 on use. |
isActive | Boolean | Active status. Always true on create; changeable only via update / (de)activate. |
key | String | Response only. Stored as a SHA-256 hash; the plaintext (prefix apk_) is returned exclusively on creation. |
keyPreview | String | First 12 chars for display (e.g. apk_Ab12Cd34…) |
roleName | String? | Response only: display name of the role (resolved from roleId) |
lastUsedAt | DateTime? | Response only: last usage timestamp (updated on every request) |
Create Key
POST /api/api-keys
{
"name": "SAP Integration",
"description": "API key for SAP ticket sync",
"roleId": "clx-integration-role",
"rateLimit": 1000,
"allowedIPs": ["192.168.1.100", "10.0.0.0/24"],
"expiresAt": "2027-12-31T23:59:59Z"
}
Response (201 Created)
{
"success": true,
"message": "API key created successfully. Save this key - it will not be shown again!",
"apiKey": {
"id": "clx...",
"name": "SAP Integration",
"key": "apk_8sJ2...full-plaintext-key-only-shown-once...",
"description": "API key for SAP ticket sync",
"isActive": true,
"rateLimit": 1000,
"createdAt": "2026-06-18T10:00:00Z",
"expiresAt": "2027-12-31T23:59:59Z",
"roleId": "clx-integration-role",
"roleName": "Integration",
"allowedIPs": ["192.168.1.100", "10.0.0.0/24"]
}
}
IMPORTANT: The full plaintext key (key) is returned ONLY ONCE on creation. After that all endpoints only return keyPreview (first 12 chars). Store the key securely right away!
Using a Key
The key is sent in the X-API-Key header (not as a Bearer token):
curl https://your-instance.com/api/tickets \
-H "X-API-Key: apk_8sJ2...your-key..."
Runtime checks (in this order)
| Check | On failure |
|---|---|
| X-API-Key header present | 401 API_KEY_REQUIRED |
| Key exists (SHA-256 lookup) | 401 INVALID_API_KEY |
isActive | 403 API_KEY_DISABLED |
| Not expired (expiresAt) | 403 API_KEY_EXPIRED |
| Client IP in allowedIPs (if set) | 403 IP_NOT_ALLOWED |
| Rate limit not exceeded (if set) | 429 RATE_LIMIT_EXCEEDED (+ Retry-After) |
The rate limit counts in a 60-second window. If the counter store (Redis) is unreachable, key requests are rejected with 503 SERVICE_UNAVAILABLE (+ Retry-After) so that the limit is never silently lifted. The IP check understands CIDR ranges and normalizes IPv4-mapped IPv6 (::ffff:…).
Error Codes
| HTTP | Error Code | Description |
|---|---|---|
| 400 | — | Validation error (e.g. invalid IP/CIDR, invalid date format, rateLimit outside 1–10000) |
| 403 | FORBIDDEN | Missing settings.manageRoles or no user context (X-API-Key) |
| 404 | API_KEY_NOT_FOUND | Key does not exist |
| 409 | API_KEY_NAME_EXISTS | A key with this name already exists |
The 401/403/429/503 codes in the "Runtime checks" table apply to USING a key; the codes above apply to MANAGING keys.
Security & Best Practices
- Use only for server-to-server integrations, never in the browser/frontend.
- Choose the role by least privilege — the key gets exactly its permissions.
- Set an IP whitelist and expiry date; configure a rate limit against abuse.
- Rotate keys (deactivate old, create new) and store them securely (secret store).
- All key actions (create/update/delete/(de)activate) are recorded in the audit log (SECURITY domain).