Secret Management API
The Secret Management API provides a secure, hierarchical system for managing encrypted configuration secrets across your sites and applications. It features automatic encryption, schema validation, JSON schema validation, and a 2-tier inheritance model.
Overview
The Secret Management system enables you to:
- Classify Secret Types: Distinguish between system-provided and custom secret types
- Define Schemas: Create secret types with JSON Schema validation
- 2-Tier Inheritance: Site → App-level secrets with automatic fallback
- Automatic Encryption: Private and sensitive secrets are encrypted at rest
- Version History: Track all changes with full audit trails (excludes actual values for security)
- Optional Authentication: Public secrets accessible without authentication
- Caching: High-performance Redis caching for secret retrieval
- Flexible Tags: Tag-based system for categorizing and filtering secrets
Architecture
graph TD
A[Site Secrets] -->|inherited by| B[App Secrets]
B -->|resolves to| C[Secret Value]
D[Secret Type] -->|validates| A
D -->|validates| B
E[Tags] -->|categorize| A
E[Tags] -->|categorize| B
F[Type Classification] -->|system/custom| D
Key Concepts
Secret Hierarchy
Secrets follow a 2-tier inheritance model:
- Site Level: Scoped to a specific tenant/site (shared across all apps)
- App Level: Scoped to a specific application within a site
When retrieving a secret, the system searches: App → Site (first match wins)
Sensitivity Levels
Each secret type has an immutable sensitivity level:
-
public: Plaintext storage, accessible without authentication- Use for: Non-sensitive configuration (feature flags, public API endpoints)
-
private: Encrypted storage, requires authentication- Use for: API keys, service credentials
-
sensitive: Encrypted storage with additional security- Use for: Database passwords, signing keys, tokens
Sensitivity level is immutable after creation and inherited from the secret type.
Secret Types
Secret types define the structure and validation rules for secrets with a classification system:
- Type: Classification -
systemorcustom(extensible for future types)system: Built-in types that cannot be deleted (protected)custom: User-defined types that can be managed freely
- Name: Unique identifier (e.g.,
database_config,jwt_signing_key) - Schema: JSON Schema for validation
- Sensitivity Level: Determines encryption and access control
- Description: Human-readable documentation
Example Secret Type:
{
"type": "custom",
"name": "database_config",
"slug": "database-config",
"description": "Database connection configuration",
"sensitivity_level": "private",
"schema": {
"type": "object",
"properties": {
"host": {"type": "string"},
"port": {"type": "integer"},
"database": {"type": "string"},
"username": {"type": "string"},
"password": {"type": "string"}
},
"required": ["host", "port", "database"]
}
}
Type Classification
Secret types are classified into two categories:
-
System Types (
"type": "system"):- Pre-defined by the platform
- Cannot be deleted (protected)
- Can be updated (name, description, schema)
- Type classification is immutable after creation
-
Custom Types (
"type": "custom"):- User-defined
- Can be fully managed (created, updated, deleted when not in use)
- Type classification is immutable after creation
Both sensitivity_level and type classification are immutable after creation. Plan your secret type structure carefully.
Tags
Tags provide a flexible way to categorize and filter secrets:
- Name: Human-readable label (e.g., "production", "staging", "critical")
- Slug: Auto-generated URL-friendly identifier
- Color: Visual identification color code in UI
- Many-to-Many: Each secret can have multiple tags
Tags help you:
- Categorize secrets by environment (production, staging, development)
- Group by service type (database, api, authentication)
- Mark priority levels (critical, optional)
- Filter secrets in the admin interface and API queries
Base URL Structure
Site-Level Secrets (Tenant-specific)
/api/secrets/ # Site-level secrets
/api/secrets/?app={app_slug} # Filter by app
/api/secrets/?key={key} # Filter by key
Secret Types & Tags
# Site-level
/api/secret-types/
/api/tags/
Authentication
All endpoints require authentication except:
- GET
/api/secrets/{key}/- Returns only public secrets when unauthenticated
Authentication Methods
JWT Bearer Token (Recommended for API clients)
Authorization: Bearer YOUR_ACCESS_TOKEN
Django Session (Web browsers)
Cookie: sessionid=YOUR_SESSION_ID
API Endpoints
Secret Types
List Secret Types
Request:
GET /api/secret-types/
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
search | string | Search by name or description (partial, case-insensitive) |
type | string | Filter by type classification: system or custom |
sensitivity_level | string | Filter by sensitivity: public, private, or sensitive |
ordering | string | Sort field (prefix with - for descending). Default: name |
page | int | Page number (default: 1) |
page_size | int | Items per page (default: 20, max: 100) |
Available Ordering Fields:
name- Sort by secret type name (default)type- Sort by classification (system/custom)sensitivity_level- Sort by sensitivity levelcreated_at- Sort by creation timestampmodified_at- Sort by last modification timestamp
Response:
{
"items": [
{
"type": "custom",
"name": "database_config",
"slug": "database-config",
"description": "Database connection configuration",
"sensitivity_level": "private",
"schema": { /* JSON Schema */ }
},
{
"type": "system",
"name": "jwt_signing_key",
"slug": "jwt-signing-key",
"description": "JWT token signing key",
"sensitivity_level": "sensitive"
}
],
"count": 25,
"next": "http://your-tenant-domain.com/api/secret-types/?page=2",
"previous": null
}
Filter & Search Examples:
# Search by name or description
GET /api/secret-types/?search=database
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN
# Filter by type classification
GET /api/secret-types/?type=system
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN
# Filter by sensitivity level
GET /api/secret-types/?sensitivity_level=private
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN
# Combine search with filters
GET /api/secret-types/?search=api&type=custom&sensitivity_level=private
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN
Sorting Examples:
# Sort by name ascending (default)
GET /api/secret-types/?ordering=name
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN
# Sort by creation date descending (newest first)
GET /api/secret-types/?ordering=-created_at
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN
# Sort by sensitivity level ascending
GET /api/secret-types/?ordering=sensitivity_level
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN
# Combine filters, search, sorting and pagination
GET /api/secret-types/?search=config&type=custom&ordering=-created_at&page=1&page_size=10
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN
Create Secret Type (Custom)
POST /api/secret-types/
Host: your-tenant-domain.com
Content-Type: application/json
Authorization: Bearer YOUR_ACCESS_TOKEN
{
"type": "custom",
"name": "api_credentials",
"description": "API credentials for external services",
"sensitivity_level": "private",
"schema": {
"type": "object",
"properties": {
"api_key": {"type": "string"},
"api_secret": {"type": "string"}
},
"required": ["api_key"]
}
}
Response: 201 Created
{
"type": "custom",
"name": "api_credentials",
"slug": "api-credentials",
"sensitivity_level": "private"
}
Create Secret Type (System)
POST /api/secret-types/
Host: your-tenant-domain.com
Content-Type: application/json
Authorization: Bearer YOUR_ACCESS_TOKEN
{
"type": "system",
"name": "jwt_signing_key",
"description": "JWT token signing key",
"sensitivity_level": "sensitive"
}
Get Secret Type
GET /api/secret-types/{slug}/
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN
Response:
{
"type": "custom",
"name": "api_credentials",
"slug": "api-credentials",
"description": "API credentials for external services",
"sensitivity_level": "private"
}
Update Secret Type (PUT)
PUT /api/secret-types/{slug}/
Host: your-tenant-domain.com
Content-Type: application/json
Authorization: Bearer YOUR_ACCESS_TOKEN
{
"name": "api_credentials",
"description": "Updated: API credentials for integrations",
"schema": {
"type": "object",
"properties": {
"api_key": {"type": "string"},
"api_secret": {"type": "string"},
"environment": {"type": "string"}
},
"required": ["api_key"]
}
}
sensitivity_levelis immutable and cannot be updatedtypeclassification is immutable and cannot be changednamecan be updated if no naming conflicts existdescriptionandschemacan be freely updated- All fields in the payload are optional (partial updates supported)
Delete Secret Type
DELETE /api/secret-types/{slug}/
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN
Response: 200 OK
{
"message": "Secret type deleted successfully",
"detail": "Deleted secret type: api_credentials"
}
Error Response (System Type): 400 Bad Request
{
"error": "System secret types cannot be deleted."
}
Error Response (Type in Use): 400 Bad Request
{
"error": "This secret type is used by 5 secret(s) and cannot be deleted."
}
Secrets
List All Secrets
GET /api/secrets/
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
search | string | Search by key (partial, case-insensitive match) |
key | string | Filter by exact secret key |
app | string | Filter by app slug |
tags | string | Comma-separated tag slugs (OR logic) |
secret_type | string | Filter by secret type slug |
created_by | string | Filter by creator username |
ordering | string | Sort field (prefix with - for descending). Default: -created_at |
page | int | Page number (default: 1) |
page_size | int | Items per page (default: 20, max: 100) |
Available Ordering Fields:
key- Sort by secret keycreated_at- Sort by creation timestamp (default:-created_at)modified_at- Sort by last modification timestamptype__name- Sort by secret type namecreated_by__username- Sort by creator username
Response:
{
"items": [
{
"key": "stripe_api_key",
"tags": ["production", "payment"],
"secret_type": "api_credentials",
"value": {
"api_key": "sk_live_12345",
"api_secret": "secret_67890"
}
}
],
"count": 50,
"next": "http://your-tenant-domain.com/api/secrets/?page=2",
"previous": null
}
Search & Filter Examples:
# Search by key (partial match)
GET /api/secrets/?search=stripe
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN
# Filter by exact key
GET /api/secrets/?key=database_url
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN
# Filter by app slug
GET /api/secrets/?app=mobile-app
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN
# Filter by tags (any of the provided tags)
GET /api/secrets/?tags=production,database
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN
# Filter by secret type
GET /api/secrets/?secret_type=api-credentials
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN
# Filter by creator
GET /api/secrets/?created_by=admin
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN
# Combine multiple filters
GET /api/secrets/?app=mobile-app&tags=production&secret_type=api-credentials
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN
Sorting Examples:
# Sort by key ascending
GET /api/secrets/?ordering=key
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN
# Sort by creation date descending (newest first, default)
GET /api/secrets/?ordering=-created_at
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN
# Sort by secret type name
GET /api/secrets/?ordering=type__name
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN
# Combine search, filters, sorting and pagination
GET /api/secrets/?search=api&tags=production&ordering=-created_at&page=1&page_size=20
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN
Get Secret History
Retrieve the audit history for a specific secret. History tracking captures who made changes, when, and what action was performed, but excludes actual secret values for security.
Actions Tracked:
+= Created~= Changed/Updated-= Deleted
What is tracked:
- Key, app, type, tags
- Who made the change (user)
- When it happened (timestamp)
- What action was performed
What is NOT tracked (security):
- ❌ Actual secret values (plaintext or encrypted)
Site-Level Secret:
GET /api/secrets/{key}/history/
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN
App-Level Secret:
GET /api/secrets/{key}/history/?app={app_slug}
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN
With Pagination:
# Get first 5 history records
GET /api/secrets/{key}/history/?limit=5&offset=0
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN
# Get next 5 records
GET /api/secrets/{key}/history/?limit=5&offset=5
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN
Response: 200 OK
{
"key": "database_url",
"app": null,
"results": [
{
"action": "~",
"user": "admin@example.com",
"timestamp": "2025-11-22T04:30:15.123Z",
"key": "database_url",
"changes": {
"tags": ["production", "database"]
}
},
{
"action": "+",
"user": "admin@example.com",
"timestamp": "2025-11-21T10:15:00.000Z",
"key": "database_url",
"changes": {}
}
],
"total_count": 2,
"limit": 10,
"offset": 0,
"has_next": false,
"has_previous": false
}
Query Parameters:
app- App slug for app-level secretslimit- Number of records (default: 10, max: 100)offset- Records to skip for pagination (default: 0)
Get Secret by Key
Site-Level (with 2-tier inheritance):
# Authenticated - returns any secret based on permissions
GET /api/secrets/{key}/
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN
# Unauthenticated - returns only public secrets
GET /api/secrets/{key}/
Host: your-tenant-domain.com
App-Level:
GET /api/secrets/{key}/?app={app_slug}
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN
With Tag Validation:
# Get secret only if it has "production" OR "staging" tag
GET /api/secrets/{key}/?tags=production,staging
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN
# Get app-level secret with tag validation
GET /api/secrets/{key}/?app={app_slug}&tags=production
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN
Query Parameters:
app- Filter by app slug (optional)tags- Comma-separated tag names - returns 404 if secret doesn't have ANY of the specified tags (optional)
Response:
{
"key": "stripe_api_key",
"tags": ["production"],
"secret_type": "api_credentials",
"value": {
"api_key": "sk_live_12345",
"api_secret": "secret_67890"
}
}
Error Response (Unauthenticated, Private Secret): 401 Unauthorized
{
"error": "Authentication required",
"detail": "Authentication credentials were not provided. Please provide a valid JWT token or session to access this secret."
}
Error Response (Tag Mismatch): 404 Not Found
{
"error": "Secret not found",
"detail": "Secret with key 'stripe_api_key' does not have any of the required tags: production, critical"
}
Create Secret (Site-Level)
POST /api/secrets/
Host: your-tenant-domain.com
Content-Type: application/json
Authorization: Bearer YOUR_ACCESS_TOKEN
{
"secret_type": "api-credentials",
"key": "stripe_credentials",
"value": {
"api_key": "sk_live_abc123",
"api_secret": "secret_xyz789"
},
"tags": ["production", "payment"],
"app": null
}
Response: 201 Created
{
"key": "stripe_credentials",
"tags": ["production", "payment"],
"secret_type": "api_credentials",
"value": {
"api_key": "sk_live_abc123",
"api_secret": "secret_xyz789"
}
}
Create Secret (App-Level)
POST /api/secrets/
Host: your-tenant-domain.com
Content-Type: application/json
Authorization: Bearer YOUR_ACCESS_TOKEN
{
"secret_type": "api-credentials",
"key": "app_api_key",
"value": {
"api_key": "sk_test_mobile_123"
},
"tags": ["mobile", "staging"],
"app": "mobile-app"
}
Update Secret
PUT /api/secrets/{key}/
Host: your-tenant-domain.com
Content-Type: application/json
Authorization: Bearer YOUR_ACCESS_TOKEN
{
"secret_type": "api-credentials",
"key": "stripe_credentials",
"value": {
"api_key": "sk_live_updated_999",
"api_secret": "new_secret_888"
},
"tags": ["production"]
}
App-Level:
PUT /api/secrets/{key}/?app={app_slug}
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN
Response: 200 OK
{
"key": "stripe_credentials",
"tags": ["production"],
"secret_type": "api_credentials",
"value": {
"api_key": "sk_live_updated_999",
"api_secret": "new_secret_888"
}
}
Delete Secret
DELETE /api/secrets/{key}/
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN
App-Level:
DELETE /api/secrets/{key}/?app={app_slug}
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN
Response: 200 OK
{
"message": "Secret deleted successfully",
"detail": "Deleted secret with key: stripe_credentials"
}
Common Use Cases
1. Storing Database Credentials (Private)
Step 1: Create Custom Secret Type
POST /api/secret-types/
{
"type": "custom",
"name": "database_credentials",
"description": "Database connection credentials",
"sensitivity_level": "private",
"schema": {
"type": "object",
"properties": {
"host": {"type": "string"},
"port": {"type": "integer"},
"username": {"type": "string"},
"password": {"type": "string"}
},
"required": ["host", "username", "password"]
}
}
Step 2: Create Secret
POST /api/secrets/
{
"secret_type": "database-credentials",
"key": "postgres_main",
"value": {
"host": "db.example.com",
"port": 5432,
"username": "appuser",
"password": "secure_password_123"
},
"tags": ["database", "production"]
}
2. Public Configuration (Feature Flags)
Step 1: Create Public Secret Type
POST /api/secret-types/
{
"type": "custom",
"name": "feature_flags",
"description": "Application feature flags",
"sensitivity_level": "public",
"schema": {
"type": "object",
"properties": {
"dark_mode": {"type": "boolean"},
"new_dashboard": {"type": "boolean"}
}
}
}
Step 2: Create Public Secret
POST /api/secrets/
{
"secret_type": "feature-flags",
"key": "app_features",
"value": {
"dark_mode": true,
"new_dashboard": false
},
"tags": ["configuration", "public"]
}
Step 3: Access Without Authentication
# Anyone can access this
GET /api/secrets/app_features/
Host: your-tenant-domain.com
Response:
{
"key": "app_features",
"tags": ["configuration", "public"],
"secret_type": "feature_flags",
"value": {
"dark_mode": true,
"new_dashboard": false
}
}
3. App-Specific API Keys
# Each app gets its own API key
POST /api/secrets/
{
"secret_type": "api-credentials",
"key": "stripe_api_key",
"value": {"api_key": "sk_live_mobile_abc123"},
"tags": ["payment", "mobile"],
"app": "mobile-app"
}
POST /api/secrets/
{
"secret_type": "api-credentials",
"key": "stripe_api_key",
"value": {"api_key": "sk_live_web_xyz789"},
"tags": ["payment", "web"],
"app": "web-app"
}
# Retrieval follows inheritance
GET /api/secrets/stripe_api_key/?app=mobile-app
# Returns: sk_live_mobile_abc123
GET /api/secrets/stripe_api_key/?app=web-app
# Returns: sk_live_web_xyz789
4. System Secret Types (Protected)
# Create a system type that cannot be deleted
POST /api/secret-types/
{
"type": "system",
"name": "jwt_signing_key",
"description": "JWT token signing keys - DO NOT DELETE",
"sensitivity_level": "sensitive"
}
# Try to delete it - will fail
DELETE /api/secret-types/jwt-signing-key/
# Response: 400 Bad Request
# {"error": "System secret types cannot be deleted."}
# You can update name, description, and schema
PUT /api/secret-types/jwt-signing-key/
{
"description": "Updated description for JWT keys",
"schema": {
"type": "object",
"properties": {
"key": {"type": "string"},
"algorithm": {"type": "string", "enum": ["HS256", "RS256"]}
}
}
}
# Response: 200 OK
# Trying to change type classification will fail
PUT /api/secret-types/jwt-signing-key/
{
"type": "custom"
}
# Response: 400 Bad Request
# {"error": "Type classification cannot be changed after creation."}
5. Filtering Secrets by Tags
# Create secrets with tags
POST /api/secrets/
{
"secret_type": "database-credentials",
"key": "postgres_prod",
"value": {"host": "prod-db.example.com"},
"tags": ["production", "database", "critical"]
}
POST /api/secrets/
{
"secret_type": "api-credentials",
"key": "stripe_prod",
"value": {"api_key": "sk_live_123"},
"tags": ["production", "payment"]
}
POST /api/secrets/
{
"secret_type": "database-credentials",
"key": "postgres_staging",
"value": {"host": "staging-db.example.com"},
"tags": ["staging", "database"]
}
# Get all production secrets
GET /api/secrets/?tags=production
# Returns: postgres_prod, stripe_prod
# Get all database secrets (any environment)
GET /api/secrets/?tags=database
# Returns: postgres_prod, postgres_staging
# Get production database secrets (combine with app filter)
GET /api/secrets/?tags=production,database&app=backend-api
# Returns: Only secrets tagged with production OR database in backend-api
# Get critical production secrets
GET /api/secrets/?tags=critical,production
# Returns: postgres_prod (has both tags)
Error Responses
400 Bad Request - Schema Validation
{
"error": "Schema validation failed",
"detail": "host: Field required; port: Field required"
}
400 Bad Request - Duplicate Entry
{
"message": "Secret type 'api_credentials' already exists",
"field": "name"
}
400 Bad Request - System Type Protection
{
"error": "System secret types cannot be deleted."
}
400 Bad Request - Type Classification Immutable
{
"error": "Type classification cannot be changed after creation.",
"detail": {
"field": "type",
"current": "custom",
"attempted": "system"
}
}
401 Unauthorized
{
"error": "Authentication required",
"detail": "This secret requires authentication. Please provide a valid JWT token or session to access private or sensitive secrets."
}
403 Forbidden
{
"error": "Permission denied",
"detail": "You do not have permission to access this secret"
}
404 Not Found
{
"error": "Secret not found",
"detail": "Secret with key 'unknown_key' not found in app, site level(s)"
}
Best Practices
1. Use Appropriate Sensitivity Levels
- Public: Only for truly non-sensitive data (feature flags, public endpoints)
- Private: Default for most secrets (API keys, service credentials)
- Sensitive: Critical secrets (database passwords, signing keys)
2. Leverage Inheritance
- Store shared secrets at site level
- Store app-specific configs at app level
- App-level secrets override site-level ones
3. Use System Types for Critical Infrastructure
- Mark important secret types as
"type": "system"to prevent accidental deletion - Use for: JWT keys, database credentials, encryption keys
- System types can still be updated (name, description, schema), just not deleted
- Plan ahead:
typeclassification cannot be changed after creation
4. Use Tags for Categorization
Categorize secrets with meaningful tags for better management and filtering:
production,staging,development- Environment tagsdatabase,api,auth- Service type tagscritical,optional- Priority tagsmobile-app,web-app- Application-specific tags
5. Define Strict Schemas
Use comprehensive JSON schemas to:
- Validate secret structure
- Document required fields
- Prevent configuration errors
6. Rotate Secrets Regularly
Update sensitive secrets periodically:
PUT /api/secrets/api_key/
{
"secret_type": "api-credentials",
"key": "api_key",
"value": {"api_key": "new_rotated_key_xyz"},
"tags": ["production"]
}
Security Considerations
- Encryption: Private and sensitive secrets are automatically encrypted at rest using PostgreSQL's pgcrypto
- Authentication: All write operations require authentication
- Permission Checks: Access is controlled via site membership and permissions
- Audit Trail: All changes are tracked with timestamps and user information (excludes actual values)
- Schema Validation: JSON Schema validation prevents malformed secret values
- Immutable Fields: Both
sensitivity_levelandtypeclassification cannot be changed after creation - System Protection: System secret types cannot be deleted
- Delete Protection: Secret types cannot be deleted if secrets are using them
Tags API
Tags are used to organize and categorize secrets. They can be applied to multiple secrets and used for filtering.
Tag Properties
| Field | Type | Description |
|---|---|---|
name | string | Tag name (unique, alphanumeric with hyphens/underscores) |
slug | string | Auto-generated URL-friendly identifier (read-only) |
description | string | Optional description of what this tag represents |
color | string | Hex color code for UI display (default: #6B7280) |
Tag names must match the pattern ^[a-zA-Z0-9_-]+$ (letters, numbers, hyphens, underscores only).
List Tags
GET /api/tags/
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN
Response: 200 OK
[
{
"name": "production",
"slug": "production",
"description": "Production environment resources",
"color": "#EF4444"
},
{
"name": "database",
"slug": "database",
"description": "Database-related secrets",
"color": "#3B82F6"
}
]
Create Tag
POST /api/tags/
Host: your-tenant-domain.com
Content-Type: application/json
Authorization: Bearer YOUR_ACCESS_TOKEN
{
"name": "production",
"description": "Production environment resources",
"color": "#EF4444"
}
Response: 201 Created
{
"name": "production",
"slug": "production",
"description": "Production environment resources",
"color": "#EF4444"
}
Get Tag
GET /api/tags/{slug}/
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN
Update Tag (PUT)
PUT /api/tags/{slug}/
Host: your-tenant-domain.com
Content-Type: application/json
Authorization: Bearer YOUR_ACCESS_TOKEN
{
"name": "prod",
"description": "Updated description",
"color": "#DC2626"
}
Partial Update Tag (PATCH)
PATCH /api/tags/{slug}/
Host: your-tenant-domain.com
Content-Type: application/json
Authorization: Bearer YOUR_ACCESS_TOKEN
{
"color": "#10B981"
}
Delete Tag
DELETE /api/tags/{slug}/
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN
Response: 200 OK
{
"message": "Tag deleted successfully",
"detail": "Deleted tag: production"
}
Deleting a tag will remove the tag association from all secrets that use it. The secrets themselves will remain unchanged.
Programmatic Access (SDK)
For backend services, Celery tasks, or other server-side code, use the SecretService class instead of HTTP API calls.
Import
from cloud_site.secrets.services import SecretService
Get Single Secret
# Get a site-level secret
database_url = SecretService.get_secret(site, 'database_url')
# Get an app-level secret (with fallback to site-level)
api_key = SecretService.get_secret(site, 'stripe_api_key', app_slug='billing-app')
# Get secret with default value (no exception if not found)
optional_key = SecretService.get_secret(site, 'optional_feature', default='disabled')
# Disable JSON parsing (returns raw string)
raw_value = SecretService.get_secret(site, 'config', parse_json=False)
Get Secret or None
# Returns None instead of raising exception if not found
value = SecretService.get_secret_or_none(site, 'optional_key')
if value:
# Use the secret
pass
List All Secrets
# Get all site-level secrets
secrets = SecretService.list_secrets(site)
# Returns: {'database_url': 'postgresql://...', 'api_key': '...'}
# Get secrets including app-level (with inheritance applied)
secrets = SecretService.list_secrets(site, app_slug='billing-app')
# Include metadata (tags and secret type)
secrets = SecretService.list_secrets(site, include_metadata=True)
# Returns: {
# 'database_url': {
# 'value': 'postgresql://...',
# 'tags': ['database', 'production'],
# 'secret_type': 'database_credentials'
# }
# }
Get Secret History
from cloud_site.secrets.models import SiteSecret
# Get secret instance
secret = SiteSecret.objects.get(key='api_key', app__isnull=True)
# Get audit history with pagination
history = SecretService.get_secret_history(secret, limit=10, offset=0)
print(f"Total records: {history['total_count']}")
for record in history['results']:
print(f"{record['action']} by {record['user']} at {record['timestamp']}")
Caching
The SecretService automatically caches secrets using Redis:
- Private secrets: 5 minutes TTL (configurable via
SECRETS_CACHE_TTL) - Public secrets: 10 minutes TTL (configurable via
SECRETS_PUBLIC_CACHE_TTL) - Automatic invalidation: Cache is cleared on create/update/delete
Naming Constraints
Secret Type Names
Secret type names must follow this pattern:
^[a-zA-Z0-9_-]+$
Valid examples:
database_configjwt-signing-keyapi_credentials_v2
Invalid examples:
database config(spaces not allowed)api.credentials(dots not allowed)config@prod(special characters not allowed)
Tag Names
Tag names follow the same pattern:
^[a-zA-Z0-9_-]+$
Valid examples:
productionstaging-envcritical_alert
Interactive API Documentation
Explore the full API interactively:
- Swagger UI:
/ninja-docs/- Interactive API explorer - OpenAPI Schema:
/ninja-openapi.json- Machine-readable specification
Example URLs
# Production
https://tenant.yourdomain.com/ninja-docs/
# Development
http://localhost:8000/ninja-docs/
Additional Resources
- API Overview - General API authentication and usage
- Sites API - Site/tenant management
- User Management API - User and permissions