API Key Management
Eneo provides a comprehensive API key system for programmatic access. Keys support two ownership models (user and service), four scope levels (tenant, space, assistant, app), three permission tiers (read, write, admin), and layered security enforcement including rate limiting, IP/origin restrictions, and scope-based access control.
Key Types
Every API key has a type prefix that determines its intended use and security constraints.
| Aspect | sk_ (Secret Key) | pk_ (Public Key) |
|---|---|---|
| Use case | Server-to-server, backend integrations | Browser-based, frontend apps |
| Access restriction | IP allowlist (CIDR notation) | Origin allowlist (scheme required) |
| Max permission | admin | read |
| Resource permissions | Supported | Supported for none/read only (jobs and prompts are not available) |
| Origin restrictions | Not allowed | Required for creation (at least one) |
| IP restrictions | Optional | Not allowed |
The key prefix (sk_ or pk_) is embedded in the key string itself and validated on every request. A pk_ key that requests write or admin permission is rejected by policy validation.
Ownership Model
API keys have an ownership field that fundamentally changes how the key is authenticated and what happens when users are modified.
User Keys (ownership: "user")
User keys are bound to a specific user via owner_user_id. On every request, the system:
- Looks up the owner in the database
- Verifies the owner account is active (not deactivated or deleted)
- For tenant-scoped admin keys: verifies the owner still has admin permissions
- For space/assistant/app-scoped keys: verifies the owner is still a member of the target space
If any of these checks fail, the request is denied. This means user keys inherit the lifecycle of their owner: if a user is deactivated, suspended, or removed from a space, all their keys stop working.
Service Keys (ownership: "service")
Service keys are designed for integrations, automated systems, and machine-to-machine communication. They are not tied to any user’s identity or lifecycle.
When a service key is used, the system constructs a synthetic user instead of looking up a real one:
- Unique ID per key: The synthetic user’s id is set to the API key’s own UUID. The id never exists in the
userstable, but it gives audit logs, telemetry, and caches a stable per-key identifier that maps back to the key. - Always active: The synthetic user is always in
ACTIVEstate - No real roles: Space access is derived from the key’s permission level, not from membership
Only tenant administrators can create service keys. Service keys with write or admin permission require either an IP allowlist or an expiration date as a guardrail against misuse, regardless of scope.
What service keys cannot do
Service keys are automation principals, not service accounts. They can read, update, and delete supported existing resources within their scope, but they cannot create new user-owned resources. Endpoints that would insert a row keyed on user_id (assistants, apps, spaces, group chats, prompts, collections, websites, files, info blobs, roles, MCP servers, and the like) return 403 with code: "service_key_cannot_create_resources" when called by a service key. Resource creation belongs to humans; the service key can operate on supported endpoints once the resource exists.
Files are the important exception to “delete existing resources”: files are user-owned today. Service keys cannot upload files and cannot delete files. File upload returns 403 with code: "service_key_cannot_create_resources", and file delete returns 403 with code: "service_key_cannot_delete_files". Use a user key for workflows that upload runtime files and clean them up after a run.
Two related guardrails come from the same principle:
- Cannot use user-identity endpoints (
/me, legacy “my keys”, info-blob updates):403withcode: "user_identity_required". - Cannot manage other API keys: API-key mutations require a session login; any API key, including a tenant-admin service key, is rejected.
Why service keys exist: In production environments, integrations should not break when an employee leaves the organization. With user keys, deactivating a departing employee’s account would silently break every integration that used their key. Service keys solve this by decoupling key validity from any individual user’s lifecycle.
What happens when the creator is deleted
Service keys store created_by_user_id (who created the key) separately from ownership. The foreign key uses SET NULL on delete, so:
- The key remains fully functional after the creator is deleted
owner_user_idisNULLfor service keys (by design)created_by_user_idbecomesNULL(audit trail partially lost, but key works)
Authentication Flow
When a request arrives with an API key in the X-API-Key header, the following steps execute in order:
1. Key Resolution
The plain key is extracted from the header and hashed. The system supports two hash algorithms:
- HMAC-SHA256 (current standard) using a server-side secret
- SHA256 (legacy) as a fallback
The key hash is looked up against the api_keys_v2 table using the idx_api_keys_v2_key_hash index. Existing v2 keys found via the legacy SHA256 hash are upgraded to HMAC-SHA256 on use. Legacy v1 keys are first mirrored into v2 as SHA256-backed keys, then upgrade on the next v2 resolution.
2. State Validation
The key’s effective state is computed with the following priority:
revoked_at is set → REVOKED (permanent, highest priority)
expires_at < now → EXPIRED (time-based)
rotation grace passed → REVOKED (old rotated key)
suspended_at is set → SUSPENDED (temporary)
otherwise → ACTIVEOnly ACTIVE keys proceed. Revoked, expired, rotation-grace-expired, and suspended keys return 401 during API-key authentication.
3. Ownership Resolution
Based on the ownership field:
- User keys: The owner is fetched from the database. The system verifies the owner is active, belongs to the correct tenant, and still has the required permissions for the key’s scope.
- Service keys: A synthetic user is constructed. No database user lookup occurs.
4. Guardrail Checks
- IP validation (
sk_keys): Ifallowed_ipsis configured, the client IP must match at least one CIDR block - Origin validation (
pk_keys): The request must include anOriginheader. For new keys, the origin must match one of the key’s configuredallowed_origins.
5. Rate Limiting
Redis-based rate limiting with scope-aware defaults:
| Scope | Default Limit |
|---|---|
| Tenant | 10,000 req/hour |
| Space | 5,000 req/hour |
| Assistant | 1,000 req/hour |
| App | 1,000 req/hour |
Per-key overrides are possible via the rate_limit field. The tenant policy max_rate_limit_override acts as an absolute ceiling.
6. Permission Enforcement
Method-level (always enforced): HTTP methods are mapped to permission levels:
| HTTP Method | Required Permission |
|---|---|
GET, HEAD, OPTIONS | read |
POST, PUT, PATCH | write |
DELETE | admin |
Some endpoints override this mapping. For example, certain POST endpoints that are semantically read operations (like search) accept read permission.
Resource-level (feature-gated): If the key has resource_permissions configured, per-resource-type permissions are checked for assistants, apps, spaces, knowledge, conversations, files, jobs, and prompts. For sk_ keys, the top-level permission is derived from the highest configured resource permission when the key is created or updated. For pk_ keys, every resource permission is capped to none or read, and jobs/prompts are rejected.
7. Scope Enforcement
On scope-enforced routes, the system verifies that the key’s scope covers the requested resource. See Scope & Permissions for details.
8. Request Proceeds
The authenticated user (real or synthetic) is injected into the request context. For service keys, the active_api_key field on the synthetic user allows downstream code (like SpaceActor) to derive the appropriate role.
Scope & Permissions
Scope Types
Scopes define the boundary of what a key can access, from broadest to narrowest:
| Scope | Access | scope_id |
|---|---|---|
tenant | All resources in the tenant | NULL |
space | Single space and its contents | Space UUID |
assistant | Single assistant and its conversations | Assistant UUID |
app | Single app and its runs | App UUID |
Files are user-owned rather than space-owned. A scoped user key can access the file API when its files resource permission allows it, but file reads and deletes are still constrained to files owned by that key’s user.
Permission Levels
Permissions define what operations a key can perform within its scope:
| Permission | Space Role | User keys can do | Service keys can do |
|---|---|---|---|
read | VIEWER | Read resources, list items | Same |
write | EDITOR | Create and update resources | Update supported existing resources (creation is gated) |
admin | ADMIN | Delete resources, manage space settings | Delete supported existing resources, manage settings (creation is gated) |
For files, write allows a user key to upload files owned by that user. admin is required to delete files, and deletion is still limited to files owned by the key’s user. Service keys cannot upload or delete files.
How Service Keys Derive Space Roles
Service keys are not space members, so SpaceActor derives a synthetic role:
- Tenant-scoped: The key’s permission maps to a role that applies to every space in the tenant
- Space-scoped: The role applies only if
scope_idmatches the current space - Assistant/app-scoped: The role applies only if the scoped resource exists in the current space
This means a service key with scope: space, permission: write gets EDITOR access to exactly one space, and no access to any other space.
Scope Enforcement Details
Scope enforcement distinguishes between list endpoints (no resource ID in URL) and single-resource endpoints (resource ID in URL):
List endpoints:
- Tenant-scoped keys: always pass
- Space-scoped keys: pass (service layer filters by space)
- Assistant/app-scoped keys: pass only for their resource type (e.g., an assistant-scoped key can list conversations but not apps)
Single-resource endpoints:
- The system resolves the target resource’s parent space
- Compares the resolved space against the key’s
scope_id - Denies access if the resource is outside the key’s scope
Strict mode adds additional safety for list endpoints: if the endpoint cannot deterministically filter results to the key’s scope, the request is denied rather than returning potentially over-broad results.
Key Lifecycle
Creation
Keys are created via POST /api/v1/api-keys. The policy service validates:
- Scope and permission compatibility
- Service key guardrails (admin-only creation, IP/expiry for write/admin permission)
pk_key constraints (origins required, no admin, no IPs)- Resource permission shape and scope compatibility
sk_effective permission derivation from the highest resource permissionpk_resource permission caps (none/readonly)- Tenant policy limits (expiration requirements, rate limit ceilings)
The plain key secret is returned exactly once in the response. It is never stored or retrievable again.
Rotation
Rotation creates a new key while keeping the old one temporarily valid:
- A new key is generated with the same scope, permission, and restrictions
- The old key gets a
rotation_grace_untiltimestamp (default: 24 hours, configurable via tenant policy) - Both keys work during the grace period
- After the grace period, the old key stops working
This enables zero-downtime key rotation for integrations.
Suspension
Suspension is a temporary, reversible state change:
- Sets
suspended_atand optional reason code/text - Key immediately stops working
- Can be reactivated via the API, restoring
ACTIVEstate - Cannot suspend already-revoked or expired keys
Revocation
Revocation is permanent and irreversible:
- Sets
revoked_atand optional reason code/text - Key immediately and permanently stops working
- Cannot be reactivated
- Delegated-key cascade fields exist in policy for compatibility, but current key management is session-only and does not create or cascade delegated child keys.
Expiration
Keys with an expires_at timestamp are checked at runtime:
- State is computed dynamically: if
now > expires_at, the key isEXPIRED - Expired keys cannot be reactivated
- Tenant policy can require expiration on all keys (
require_expiration) and cap the maximum duration (max_expiration_days)
State Priority
When multiple state indicators are present, the most restrictive wins:
revoked_at > expires_at > rotation_grace_until > suspended_at > activeA key that is both suspended and expired is treated as expired. A key that is both expired and revoked is treated as revoked. A rotated key whose rotation_grace_until has passed is treated as revoked.
Tenant Policies
Tenant administrators can configure API key policies that apply to all keys in their tenant:
| Policy | Default | Description |
|---|---|---|
max_delegation_depth | 3 | Reserved for delegated-key support |
revocation_cascade_enabled | false | Reserved for delegated-key cascade support |
require_expiration | false | All new keys must have an expiry date |
max_expiration_days | null | Maximum days until expiry (null = unlimited) |
auto_expire_unused_days | null | Auto-revoke after N days of inactivity |
max_rate_limit_override | null | Absolute ceiling on per-key rate limits |
rotation_grace_hours | 24 | Grace period for old keys during rotation |
FAQ
Which key type should I use?
Use an sk_ key for backend systems, scheduled jobs, MCP servers, and other server-to-server integrations. sk_ keys can use read, write, or admin permission and may be restricted by client IP.
Use a pk_ key only for browser-based integrations. pk_ keys are read-only, must list allowed origins, cannot use IP restrictions, and can only use none or read resource permissions. jobs and prompts resource permissions are rejected for pk_ keys.
Should integrations use user keys or service keys?
Use a user key when the integration should follow a specific user’s lifecycle and memberships. If the user is deactivated, removed from a space, or loses required permissions, the key stops working.
Use a service key for production automation that must outlive an individual employee. Service keys are created by tenant administrators, resolve to a synthetic service identity, and are authorized by their key scope and permission instead of a real user’s memberships. The important caveat is that service keys cannot create user-owned resources; they are for operating on supported resources that already exist.
What new permissions do v2 keys have?
V2 keys add explicit permission levels, scopes, ownership, and optional per-resource permissions. The top-level permission is a method ceiling:
readallows read-style requests such asGETwriteallows create/update-style requests such asPOST,PUT, andPATCHadminallows delete-style requests such asDELETE
Routes can override the method mapping when a request is semantically read-only. If resource_permissions are set, the key must also have enough permission for that resource type. Resource permissions can be configured for assistants, apps, spaces, knowledge, conversations, files, jobs, and prompts. For sk_ keys, the top-level permission is derived from the highest configured resource permission. For pk_ keys, every resource permission is capped at read, and jobs/prompts are not available.
How do scopes affect access?
Scope limits where the key can operate:
tenantkeys can access tenant-wide resources, subject to permission checksspacekeys are limited to one spaceassistantkeys are limited to one assistant and its conversationsappkeys are limited to one app and its runs
Tenant-scoped admin user keys require the owner to remain a tenant administrator. Tenant-scoped read/write user keys inherit the owner’s live role permissions through endpoint-level guards. Space-, assistant-, and app-scoped user keys require the owner to remain a member of the target space.
Can API keys create or manage other API keys?
No. API-key management endpoints require a session login. API-key callers, including tenant-admin service keys, are rejected for API-key mutations.
Can service keys create resources?
No. This is the main caveat with service keys: they are automation principals, not resource owners. Service keys can operate on supported existing resources within their scope, but endpoints that create new rows owned by user_id reject service-key callers with code: "service_key_cannot_create_resources".
Create assistants, apps, spaces, group chats, prompts, collections, websites, files, info blobs, roles, and MCP servers with a user account first. Then use the service key for supported automation around those resources. Files are more restricted than most existing resources: service keys cannot upload files and cannot delete files because files are user-owned today.
What happens to our old keys?
Legacy keys still resolve through a compatibility path. When a legacy key is used, Eneo mirrors it into api_keys_v2 where possible:
- Legacy user keys become tenant-scoped
sk_keys tied to the original owner. They becomeadminonly if the owner currently has tenant admin permission; otherwise they becomewrite. - Legacy assistant keys become assistant-scoped
sk_keys with read permission. - Existing SHA256-backed v2 keys upgrade to HMAC-SHA256 when they resolve through the v2 lookup path.
This keeps existing integrations working while giving resolved keys the v2 lifecycle, audit, scope, and policy machinery. The original plain secret is not changed or shown again.
When do we need to migrate to the new keys?
Legacy keys are supported through the compatibility path, so existing integrations do not need to stop on release day. Migrate when you want narrower scopes, service-key ownership, explicit resource permissions, rotation, better audit visibility, or browser-safe pk_ origin restrictions.
The safest migration path is to create a new v2 key with the narrowest practical scope and permission, deploy it to the integration, confirm traffic and audit logs show the new key being used, then revoke the legacy key. For production automations owned by a team rather than a person, prefer a service sk_ key with an IP allowlist or expiration date.
Can I retrieve a key secret later?
No. The plain secret is shown exactly once when the key is created or rotated. If a secret is lost, rotate the key and update the integration with the new value.
Security Design
Storage
API key secrets are never stored in plaintext. They are hashed with HMAC-SHA256 using a server-side secret (api_key_hash_secret). Existing v2 SHA256-hashed keys are upgraded when they resolve through the v2 lookup path.
Guardrails
- Service keys with write/admin permission must have an IP allowlist or expiration date
- New
pk_keys cannot have admin permission and must specify at least one allowed origin - Each origin must include an
http://orhttps://scheme and a host (http://localhost:3000is valid)
Origin Trust Model
Allowed origins for pk_ keys are validated per key — there is no central tenant-wide allowlist that overrides or constrains what individual keys may list. This is a deliberate design choice:
- Authority: the user who creates the key chooses which origins it accepts. Any space editor with permission to create
pk_keys can list arbitrary external origins (e.g.https://my-integration.example.com). - Tenant admins do not gate per-key origin lists. Tenant-level policy controls whether a user may mint
pk_keys (via roles and quotas) and may enforce rate limits, but does not vet the origins on each key. - Implication: a leaked
pk_key is only as constrained as the origin list its creator chose. Treat key creation as a security-relevant action and review key listings in audit logs. - Fail-closed: a
pk_key withallowed_originsunset (NULL) or empty cannot authenticate at all — the origin check rejects every request rather than silently passing them through.
Audit Trail
Every key lifecycle event is logged to the audit system:
- Actions tracked: creation, rotation, update, suspension, reactivation, revocation, expiration
- Metadata captured: scope, permission, IP address, user agent, request ID, before/after field values, reason codes
Scope Enforcement
Scope enforcement and strict mode are always active. API keys are always restricted to resources within their configured scope.