Tenant Manager API Documentation¶
Base URL: http://localhost:32201/api or https://manager.nominate.ai/api
Authentication¶
All API endpoints require authentication via JWT token.
Login¶
POST /api/auth/login
Content-Type: application/json
{
"username": "admin",
"password": "password"
}
Response:
{
"access_token": "jwt_token_here",
"token_type": "bearer",
"user": {
"id": "uuid",
"username": "admin",
"email": "admin@example.com",
"role": "admin"
}
}
Tenants¶
List Tenants¶
GET /api/tenants
Query Parameters:
- status: filter by status (running, stopped, error)
- search: search by name, domain
- limit: pagination limit (default: 50)
- offset: pagination offset (default: 0)
Response:
{
"tenants": [
{
"id": "uuid",
"name": "Ed Gallrein for State Rep",
"slug": "gallrein-campaign",
"domain": "kentucky.nominate.ai",
"api_domain": "kentuckyapi.nominate.ai",
"status": "running",
"health_status": "healthy",
"frontend_port": 32300,
"backend_port": 32301,
"created_at": "2025-11-13T12:00:00Z",
"deployed_at": "2025-11-13T12:05:00Z"
}
],
"total": 1,
"limit": 50,
"offset": 0
}
Get Tenant¶
GET /api/tenants/{id}
Response:
{
"id": "uuid",
"name": "Ed Gallrein for State Rep",
"slug": "gallrein-campaign",
"domain": "kentucky.nominate.ai",
"api_domain": "kentuckyapi.nominate.ai",
"status": "running",
"health_status": "healthy",
"health_details": {
"frontend": "up",
"backend": "up",
"database": "healthy",
"last_check": "2025-11-13T13:00:00Z"
},
"frontend_port": 32300,
"backend_port": 32301,
"directory": "/home/bisenbek/projects/nominate/gallrein",
"github_repo": "git@github.com:org/gallrein.git",
"created_at": "2025-11-13T12:00:00Z",
"deployed_at": "2025-11-13T12:05:00Z",
"notes": "Initial deployment for Kentucky race"
}
Create Tenant¶
POST /api/tenants
Content-Type: application/json
{
"name": "John Smith for Senate",
"slug": "smith-campaign",
"domain": "smith.nominate.ai",
"api_domain": "smithapi.nominate.ai",
"frontend_port": 32310, # Optional - auto-allocated if not provided
"backend_port": 32311, # Optional - auto-allocated if not provided
"github_repo": "git@github.com:org/smith.git",
"notes": "Second campaign deployment"
}
Response:
{
"id": "uuid",
"name": "John Smith for Senate",
"slug": "smith-campaign",
"domain": "smith.nominate.ai",
"status": "pending",
"frontend_port": 32310,
"backend_port": 32311,
"created_at": "2025-11-14T10:00:00Z"
}
Update Tenant¶
PUT /api/tenants/{id}
Content-Type: application/json
{
"name": "John Smith for U.S. Senate",
"notes": "Updated campaign name"
}
Response:
{
"id": "uuid",
"name": "John Smith for U.S. Senate",
...updated fields
}
Delete Tenant¶
DELETE /api/tenants/{id}
Query Parameters:
- remove_files: boolean (default: false) - delete tenant directory
- remove_nginx: boolean (default: true) - remove NGINX configs
- remove_services: boolean (default: true) - remove systemd services
Response:
{
"message": "Tenant deleted successfully",
"removed": {
"files": true,
"nginx": true,
"services": true,
"ports_released": [32310, 32311]
}
}
Tenant Configuration¶
Get Configuration¶
GET /api/tenants/{id}/config
Response:
{
"id": "uuid",
"tenant_id": "uuid",
"app_name": "Ed Gallrein for State Rep",
"app_name_short": "Gallrein",
"project_name": "gallrein-campaign",
"admin_email": "admin@kentucky.nominate.ai",
"frontend_url": "https://kentucky.nominate.ai",
"backend_api_url": "http://localhost:32301/api",
"ws_base_url": "wss://kentucky.nominate.ai",
"ollama_embed_model": "nomic-embed-text:latest",
"ollama_llm_model": "gemma3:12b",
"ollama_base_url": "http://localhost:11434",
"custom_env": {
"FEATURE_FLAG_X": "true"
}
}
Update Configuration¶
PUT /api/tenants/{id}/config
Content-Type: application/json
{
"app_name": "Ed Gallrein for Kentucky State Rep",
"admin_email": "contact@gallrein.com",
"custom_env": {
"FEATURE_FLAG_X": "true",
"CUSTOM_SETTING": "value"
}
}
Response:
{
...updated configuration
}
Regenerate .env File¶
POST /api/tenants/{id}/config/regenerate-env
Response:
{
"message": ".env file regenerated successfully",
"path": "/home/bisenbek/projects/nominate/gallrein/.env",
"backup": "/home/bisenbek/projects/nominate/gallrein/.env.backup.20251114"
}
Tenant Theme¶
Get Theme¶
GET /api/tenants/{id}/theme
Response:
{
"id": "uuid",
"tenant_id": "uuid",
"primary_color": "#0e173e",
"secondary_color": "#f1c613",
"accent_color": "#f1c613",
"text_color": "#374151",
"sidebar_color": "#f8fafc",
"logo_full": "/assets/logos/logo-full.png",
"logo_cropped": "/assets/logos/logo-cropped.png",
"logo_icon": "/assets/logos/logo-icon.png",
"logo_name": "/assets/logos/logo-name.png",
"ai_generated": true,
"source_website": "https://gallrein.com",
"created_at": "2025-11-13T12:00:00Z"
}
Update Theme¶
PUT /api/tenants/{id}/theme
Content-Type: multipart/form-data
{
"primary_color": "#0e173e",
"secondary_color": "#f1c613",
"accent_color": "#f1c613",
"logo_full": <file upload>,
"logo_cropped": <file upload>,
"logo_icon": <file upload>,
"logo_name": <file upload>
}
Response:
{
...updated theme
"message": "Theme updated and applied to tenant"
}
Analyze Website (AI)¶
POST /api/tenants/{id}/theme/analyze
Content-Type: application/json
{
"website_url": "https://gallrein.com",
"capture_screenshot": true
}
Response:
{
"analysis_id": "uuid",
"status": "processing",
"message": "Analysis started. Check /api/tenants/{id}/theme/suggestions for results"
}
Get Theme Suggestions¶
GET /api/tenants/{id}/theme/suggestions
Query Parameters:
- analysis_id: specific analysis (optional, returns latest if not provided)
Response:
{
"analysis_id": "uuid",
"status": "complete",
"website_url": "https://gallrein.com",
"screenshot": "/path/to/screenshot.png",
"suggestions": [
{
"id": "uuid",
"suggestion_number": 1,
"name": "Professional Blue & Gold",
"description": "A classic, trustworthy palette that conveys stability and tradition",
"primary_color": "#0e173e",
"secondary_color": "#f1c613",
"accent_color": "#f1c613",
"text_color": "#1f2937",
"palette_json": {
"50": "#eff6ff",
"100": "#dbeafe",
...tailwind scale
},
"reasoning": "Based on the candidate's website, this palette emphasizes professionalism..."
},
{
"id": "uuid",
"suggestion_number": 2,
"name": "Energetic Campaign Red",
...
}
]
}
Select Theme Suggestion¶
POST /api/tenants/{id}/theme/apply-suggestion
Content-Type: application/json
{
"suggestion_id": "uuid"
}
Response:
{
"message": "Theme suggestion applied successfully",
"theme": {
...applied theme
}
}
Deployment¶
Deploy Tenant¶
POST /api/tenants/{id}/deploy
Content-Type: application/json
{
"skip_ssl": false,
"skip_nginx": false,
"skip_services": false
}
Response:
{
"deployment_id": "uuid",
"status": "running",
"started_at": "2025-11-14T10:00:00Z",
"estimated_duration": 300
}
Get Deployment Status¶
GET /api/deployments/{deployment_id}
Response:
{
"id": "uuid",
"tenant_id": "uuid",
"status": "running",
"current_step": "Configuring NGINX",
"steps": [
{"name": "Copy files", "status": "success", "duration": 5},
{"name": "Initialize database", "status": "success", "duration": 10},
{"name": "Create systemd services", "status": "success", "duration": 3},
{"name": "Configure NGINX", "status": "running", "started": "2025-11-14T10:00:18Z"},
{"name": "Setup SSL", "status": "pending"},
{"name": "Start services", "status": "pending"}
],
"logs": "Full deployment log...",
"started_at": "2025-11-14T10:00:00Z"
}
Get Deployment Logs¶
GET /api/deployments/{deployment_id}/logs
Query Parameters:
- stream: boolean (default: false) - server-sent events for real-time logs
Response (if not streaming):
{
"deployment_id": "uuid",
"logs": "Full deployment log text..."
}
Response (if streaming):
Server-Sent Events with log updates
Start Tenant Services¶
POST /api/tenants/{id}/start
Response:
{
"message": "Services started successfully",
"services": {
"frontend": "active",
"backend": "active"
}
}
Stop Tenant Services¶
POST /api/tenants/{id}/stop
Response:
{
"message": "Services stopped successfully",
"services": {
"frontend": "inactive",
"backend": "inactive"
}
}
Restart Tenant Services¶
POST /api/tenants/{id}/restart
Response:
{
"message": "Services restarted successfully",
"services": {
"frontend": "active",
"backend": "active"
}
}
Health & Monitoring¶
Get Tenant Health¶
GET /api/tenants/{id}/health
Response:
{
"tenant_id": "uuid",
"overall_status": "healthy",
"checks": {
"frontend": {
"status": "up",
"response_time_ms": 45,
"status_code": 200
},
"backend": {
"status": "up",
"response_time_ms": 23,
"status_code": 200
},
"database": {
"status": "healthy",
"size_mb": 245,
"connections": 2
},
"services": {
"frontend_service": "active",
"backend_service": "active"
}
},
"last_check": "2025-11-14T10:00:00Z"
}
Trigger Health Check¶
Get Service Logs¶
GET /api/tenants/{id}/logs
Query Parameters:
- service: frontend|backend
- lines: number of lines (default: 100)
- follow: boolean (default: false) - stream logs
Response:
{
"tenant_id": "uuid",
"service": "frontend",
"logs": "Log content here..."
}
Remote Tenant API¶
These endpoints call the tenant's own API to get stats and perform actions.
Get Tenant Stats¶
GET /api/tenants/{id}/remote/stats
Response:
{
"persons": 81232,
"events": 12,
"tags": 45,
"segments": 8,
"communications": 1523,
"database_size_mb": 245
}
Get Persons Count¶
GET /api/tenants/{id}/remote/persons/count
Response:
{
"total": 81232,
"by_status": {
"active": 79000,
"inactive": 2232
}
}
Trigger Tenant Backup¶
POST /api/tenants/{id}/remote/backup
Response:
{
"message": "Backup initiated",
"backup_path": "/backups/gallrein-20251114.db",
"size_mb": 245
}
NGINX Management¶
Reload NGINX¶
POST /api/nginx/reload
Response:
{
"message": "NGINX reloaded successfully",
"timestamp": "2025-11-14T10:00:00Z"
}
Test NGINX Configuration¶
POST /api/nginx/test
Response:
{
"valid": true,
"output": "nginx: the configuration file /etc/nginx/nginx.conf syntax is ok..."
}
List NGINX Configs¶
GET /api/nginx/configs
Response:
{
"configs": [
{
"domain": "kentucky.nominate.ai",
"path": "/etc/nginx/sites-enabled/kentucky.nominate.ai",
"type": "frontend",
"tenant_id": "uuid",
"enabled": true
},
{
"domain": "kentuckyapi.nominate.ai",
"path": "/etc/nginx/sites-enabled/kentuckyapi.nominate.ai",
"type": "api",
"tenant_id": "uuid",
"enabled": true
}
]
}
Port Management¶
List Port Allocations¶
GET /api/ports
Response:
{
"allocations": [
{
"frontend_port": 32300,
"backend_port": 32301,
"tenant_id": "uuid",
"tenant_name": "Ed Gallrein for State Rep",
"allocated_at": "2025-11-13T12:00:00Z",
"status": "in_use"
},
{
"frontend_port": 32310,
"backend_port": 32311,
"tenant_id": null,
"status": "available"
}
],
"available_count": 9,
"in_use_count": 1
}
Get Next Available Ports¶
Settings¶
Get Settings¶
GET /api/settings
Response:
{
"settings": [
{
"key": "anthropic_api_key",
"value": "***hidden***",
"description": "Anthropic API key for Claude",
"is_secret": true
},
{
"key": "health_check_interval",
"value": "300",
"value_type": "integer",
"description": "Health check interval in seconds"
}
]
}
Update Setting¶
PUT /api/settings/{key}
Content-Type: application/json
{
"value": "sk-ant-..."
}
Response:
{
"key": "anthropic_api_key",
"value": "***hidden***",
"updated_at": "2025-11-14T10:00:00Z"
}
Logs & Audit¶
Get Logs¶
GET /api/logs
Query Parameters:
- tenant_id: filter by tenant
- level: info|warning|error|critical
- category: deployment|api|health_check|system|audit
- limit: pagination limit (default: 100)
- offset: pagination offset (default: 0)
Response:
{
"logs": [
{
"id": "uuid",
"tenant_id": "uuid",
"level": "info",
"category": "deployment",
"message": "Deployment started",
"details": {"deployment_id": "uuid"},
"created_at": "2025-11-14T10:00:00Z"
}
],
"total": 1523,
"limit": 100,
"offset": 0
}
WebSocket Endpoints¶
Deployment Progress (WebSocket)¶
WS /ws/deployments/{deployment_id}
Messages:
{
"type": "progress",
"deployment_id": "uuid",
"status": "running",
"current_step": "Configuring NGINX",
"progress_percent": 60
}
{
"type": "log",
"message": "Creating NGINX configuration..."
}
{
"type": "complete",
"deployment_id": "uuid",
"status": "success",
"duration_seconds": 285
}
Tenant Health (WebSocket)¶
WS /ws/tenants/{id}/health
Subscribes to health check updates for a tenant.
Messages:
{
"type": "health_update",
"tenant_id": "uuid",
"overall_status": "healthy",
"checks": {...}
}
Error Responses¶
All error responses follow this format:
{
"error": "Error message",
"detail": "Detailed error description",
"code": "ERROR_CODE",
"timestamp": "2025-11-14T10:00:00Z"
}
Common HTTP status codes:
- 200 OK - Success
- 201 Created - Resource created
- 400 Bad Request - Invalid input
- 401 Unauthorized - Authentication required
- 403 Forbidden - Insufficient permissions
- 404 Not Found - Resource not found
- 409 Conflict - Resource conflict (e.g., duplicate slug)
- 500 Internal Server Error - Server error