Target audience: Contributors and operators who need to understand the complete integration API key lifecycle — creation, listing, rotation, and revocation.
The Credence Backend provides a programmatic API for managing integration API keys. Every mutating operation is recorded in the audit trail and every response is subject to ownership and authorization checks.
All keys follow the format:
cr_<64 lowercase hex characters>
Total length: 67 characters. Example:
cr_a3f2b1c0d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1
The first 8 hex characters after the cr_ prefix serve as a lookup prefix
for fast database index scans without full-hash comparisons on every request.
- Raw keys are never persisted. Only the SHA-256 hash is stored in the
api_keystable (hashed_keycolumn,varchar(64), unique). - The raw key is returned exactly once: in the response body of a successful
POST(create) orPOST .../rotate(rotate) call. After that the service cannot recover it. - Validation uses constant-time hash comparison to mitigate timing attacks.
- The
api_keystable includes a composite index on(owner_id, active)for efficient per-owner queries.
See src/migrations/006_add_api_keys_table.ts for the full schema.
All endpoints are mounted at /api/integrations/keys and require user
authentication via JWT (requireUserAuth middleware). API-key-based auth is
not accepted for key management operations.
| Method | Path | Description |
|---|---|---|
POST |
/api/integrations/keys |
Issue a new integration API key |
GET |
/api/integrations/keys |
List keys for the authenticated user |
POST |
/api/integrations/keys/:id/rotate |
Rotate a key (safe invalidation) |
DELETE |
/api/integrations/keys/:id |
Permanently revoke a key |
Issue a new API key with an optional scope and tier.
Request:
POST /api/integrations/keys
Content-Type: application/json
Authorization: Bearer <jwt_token>
{
"scope": "read",
"tier": "free"
}| Field | Type | Default | Description |
|---|---|---|---|
scope |
string | "read" |
Access scope — "read" or "full". |
tier |
string | "free" |
Subscription tier controlling rate limits — "free", "pro", or "enterprise". |
Success response (201):
{
"success": true,
"data": {
"id": "a1b2c3d4",
"key": "cr_a3f2b1c0d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1",
"prefix": "a3f2b1c0",
"scope": "read",
"scopes": ["read"],
"tier": "free",
"createdAt": "2026-07-29T12:00:00.000Z"
}
}Store the
keyvalue securely. It is the last time the raw key will be visible.
Validation errors (400):
{ "error": "Invalid scope. Allowed values: read, full" }{ "error": "Invalid tier. Allowed values: free, pro, enterprise" }Return all keys belonging to the authenticated user. The raw key and its hash are never included in the response.
Request:
GET /api/integrations/keys
Authorization: Bearer <jwt_token>Success response (200):
{
"success": true,
"data": [
{
"id": "a1b2c3d4",
"prefix": "a3f2b1c0",
"scope": "read",
"scopes": ["read"],
"tier": "free",
"ownerId": "user_abc",
"createdAt": "2026-07-29T12:00:00.000Z",
"lastUsedAt": null,
"active": true
},
{
"id": "e5f6a7b8",
"prefix": "c9d0e1f2",
"scope": "full",
"scopes": ["full"],
"tier": "pro",
"ownerId": "user_abc",
"createdAt": "2026-07-28T10:00:00.000Z",
"lastUsedAt": "2026-07-29T11:30:00.000Z",
"active": false
}
]
}Keys are returned in descending order by createdAt. Both active and
revoked keys are listed so operators can audit historical key assignments.
Atomically revoke the existing key and issue a replacement that inherits the same owner, scope, and tier. This is the canonical "safe invalidation" path.
Request:
POST /api/integrations/keys/a1b2c3d4/rotate
Authorization: Bearer <jwt_token>Success response (200):
{
"success": true,
"message": "API key rotated. Store the new key securely — it will not be shown again.",
"data": {
"id": "f9e8d7c6",
"key": "cr_b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5",
"prefix": "b4c5d6e7",
"scope": "read",
"scopes": ["read"],
"tier": "free",
"createdAt": "2026-07-29T12:05:00.000Z"
}
}Error responses:
| Status | Condition | Body |
|---|---|---|
| 404 | Key ID not found | { "error": "API key", "message": "API key 'bad-id' not found" } |
| 409 | Key already revoked | { "error": "Conflict", "message": "API key is already revoked and cannot be rotated" } |
| 403 | Non-owner, non-admin caller | { "error": "Forbidden", "message": "You do not have permission to rotate this API key" } |
Ownership rules:
- A user can rotate their own keys.
- Admin and super-admin users can rotate any key.
- Non-admin, non-owner callers receive
403 Forbidden.
Permanently deactivate an API key. Subsequent requests using the revoked key
receive 401 Unauthorized.
Request:
DELETE /api/integrations/keys/a1b2c3d4
Authorization: Bearer <jwt_token>Success response (200):
{
"success": true,
"message": "API key revoked successfully"
}Error responses:
| Status | Condition | Body |
|---|---|---|
| 404 | Key ID not found | { "error": "API key", "message": "API key 'bad-id' not found" } |
| 403 | Non-owner, non-admin caller | { "error": "Forbidden", "message": "You do not have permission to revoke this API key" } |
The same ownership rules as rotation apply.
Every lifecycle operation is written to the chain-backed audit log:
| Operation | Audit action | Status "success" includes |
Status "failure" includes |
|---|---|---|---|
| Create | CREATE_API_KEY |
key ID, prefix, scope, tier | — (creation cannot fail at service layer) |
| Rotate | ROTATE_API_KEY |
revoked key ID, new key ID, new key prefix, scope, tier, owner ID | reason: "key_not_found" or "key_already_revoked" |
| Revoke | REVOKE_API_KEY |
key ID, owner ID, prefix | — (failure logged with "API key not found") |
All entries include:
| Field | Description |
|---|---|
tenantId |
Resolved from the ambient request context (ALS). |
actorId |
The authenticated user's ID. |
actorEmail |
The authenticated user's email. |
resourceId |
The API key's opaque ID. |
resourceType |
Always "api_key". |
ipAddress |
The originating IP, when available. |
See src/services/apiKeyRotationService.ts for the implementation of every
audit record.
| Operation | Authenticated as | Authorization check |
|---|---|---|
| Create | Any authenticated user | No additional check (user creates for self) |
| List | Any authenticated user | Scoped to caller's ownerId |
| Rotate | Owner or admin | Owner match or admin/super-admin role |
| Revoke | Owner or admin | Owner match or admin/super-admin role |
Authentication is enforced by the requireUserAuth middleware (JWT bearer
token). API keys themselves cannot manage other API keys — this prevents a
compromised integration key from escalating privileges.
| Scope | Description |
|---|---|
read |
Read-only access to the integration's permitted resources. |
full |
Full read/write access within the integration's permitted resource scope. |
Scope validation is strict: any value other than "read" or "full" is
rejected with a 400 error.
| Tier | Rate limit (requests/min) | Intended use case |
|---|---|---|
free |
100 | Development / low-volume |
pro |
1 000 | Production / moderate-volume |
enterprise |
10 000 | High-volume / dedicated |
See docs/RATE_LIMITING_DESIGN.md for burst allowances and reset windows.
┌──────────────┐ HTTP ┌───────────────────┐
│ Client │ ──────────▶ │ createApiKeyRouter│
│ (JWT-auth) │ ◀────────── │ (routes/apiKeys) │
└──────────────┘ └────────┬──────────┘
│
┌────────────┴────────────┐
│ ApiKeyRotationService │
│ (business logic + audit)│
└────────┬────────────────┘
│
┌──────────────┴──────────────┐
│ ApiKeyRepository │
│ (InMemoryApiKeyRepository │
│ or future DB adapter) │
└──────────────┬──────────────┘
│
┌───────┴───────┐
│ apiKeys.ts │
│ (key gen, │
│ hash, store) │
└───────────────┘
src/routes/apiKeys.ts— Express router with four endpoints.src/services/apiKeyRotationService.ts— Business logic with audit logging.src/repositories/apiKeyRepository.ts—ApiKeyRepositoryinterface andInMemoryApiKeyRepositoryimplementation.src/services/apiKeys.ts— Low-level key generation, hashing, and in-memory store functions.src/middleware/auth.ts—requireUserAuthJWT middleware.src/db/repositories/apiKeysRepository.ts— PostgreSQL-backed repository (ready for DB wiring).
- Hash-only storage: Raw keys are SHA-256 hashed before storage. The raw value exists only in the HTTP response and the caller's secure store.
- Constant-time comparison: Validation uses the full SHA-256 hash, not string prefix matching, to prevent timing side channels.
- No key-for-key management: You cannot use an API key to manage other API keys. Only JWT-authenticated sessions may create, list, rotate, or revoke keys.
- Audit trail: Every mutation is logged with the actor, resource, and outcome. Rotation failures (not found, already revoked) are also recorded so suspicious probes are visible.
- Ownership scoping: By default a user can only act on their own keys. Admins may act on any key — this is audited through the same mechanism.
No environment variables are required specifically for the API key lifecycle. The feature is self-contained and uses defaults throughout.
| Variable (if wired) | Default | Purpose |
|---|---|---|
DATABASE_URL |
— | When set, enables DB-backed key repository (future) |
| Document | Covers |
|---|---|
docs/api-keys.md |
API key format, granular scopes, subscription tiers, error response reference |
docs/SECRETS.md |
Secret types, rotation cadence, and blast radius for all credential types |
docs/SECURITY.md |
Security architecture overview |
docs/JWT_CLAIMS.md |
JWT claims, headers, and consumer middleware |
docs/RATE_LIMITING_DESIGN.md |
Rate limit tiers, burst allowances, reset windows |
src/routes/apiKeys.ts |
Route handler implementation |
src/services/apiKeyRotationService.ts |
Audit-logged rotation service implementation |
tests/routes/apiKeys.test.ts |
Route-level test suite (all four operations, auth guards, audit verification) |