Skip to content

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.

Authorization: Bearer <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

POST /api/tenants/{id}/health-check

Response:
{
  ...same as GET /health, but freshly checked
}

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

GET /api/ports/next-available

Response:
{
  "frontend_port": 32310,
  "backend_port": 32311
}

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