Skip to content

Backend Profile Management API Reference

REST API documentation for managing backend profiles (video processing configurations).

Base URL

http://localhost:8000/admin

All endpoints are under the /admin prefix.

Authentication

Currently no authentication required. Future versions will require API keys or OAuth tokens.

Common Headers

Content-Type: application/json
Accept: application/json

Error Responses

All endpoints return standard HTTP error responses:

{
  "detail": "Error message describing what went wrong"
}

Or for business-rule validation errors raised by ProfileValidator (e.g. duplicate profile name, invalid profile name characters, unknown embedding type, missing schema template):

{
  "detail": {
    "message": "Profile validation failed",
    "errors": [
      "Profile 'video_test' already exists for tenant 'acme_corp'",
      "Invalid embedding type 'wrong_type'. Must be one of: ['multi_vector', 'single_vector']"
    ]
  }
}

Or for request-schema validation errors (missing/malformed required field, caught by FastAPI/Pydantic before the route body runs):

{
  "detail": [
    {
      "type": "missing",
      "loc": ["body", "tenant_id"],
      "msg": "Field required",
      "input": {"profile_name": "test", "...": "..."}
    }
  ]
}

Common status codes:

  • 400: Bad request (business-rule validation error — e.g. duplicate profile name, invalid profile name, unknown embedding type, missing schema template)

  • 404: Resource not found

  • 409: Conflict — only returned by Delete Profile when delete_schema=true and another profile still references the same schema

  • 422: Unprocessable entity (request body missing a required field or failing Pydantic type validation, e.g. tenant_id omitted entirely)

  • 500: Internal server error


Endpoints

1. Create Profile

Create a new backend profile for a tenant.

Endpoint: POST /admin/profiles

Request Body:

{
  "profile_name": "string (required)",
  "tenant_id": "string (required)",
  "type": "string (optional, default: 'video')",
  "schema_name": "string (required)",
  "embedding_model": "string (required)",
  "embedding_type": "string (required, enum: multi_vector|single_vector)",
  "description": "string (optional, default: '')",
  "strategies": {
    "segmentation": {
      "class": "string",
      "params": {}
    },
    "embedding": {
      "class": "string",
      "params": {}
    }
  },
  "pipeline_config": {
    "frame_extraction": {
      "fps": 1,
      "max_frames": 100
    }
  },
  "model_specific": {
    "quantization": "int8"
  },
  "schema_config": {
    "embedding_dim": 128,
    "num_patches": 1024
  },
  "model_loader": "string (required for embedded types, enum: colbert|colpali|colqwen|xclip)",
  "process_type": "string (optional, enum: direct_video|frame_based|video_chunks)",
  "extra_config": {
    "inference_services": {"embedding": "vllm_colpali"}
  },
  "deploy_schema": "boolean (optional, default: false)"
}

Example Request:

curl -X POST http://localhost:8000/admin/profiles \
  -H "Content-Type: application/json" \
  -d '{
    "profile_name": "video_colpali_mv_frame",
    "tenant_id": "acme_corp",
    "type": "video",
    "schema_name": "video_colpali_smol500_mv_frame",
    "embedding_model": "TomoroAI/tomoro-colqwen3-embed-4b",
    "embedding_type": "multi_vector",
    "model_loader": "colpali",
    "extra_config": {"inference_services": {"embedding": "vllm_colpali"}},
    "description": "ColPali model with frame-based embedding for video search",
    "strategies": {
      "segmentation": {
        "class": "FrameSegmentationStrategy",
        "params": {"fps": 0.5, "max_frames": 100}
      },
      "embedding": {
        "class": "MultiVectorEmbeddingStrategy",
        "params": {}
      }
    },
    "pipeline_config": {
      "frame_extraction": {
        "fps": 1,
        "max_frames": 100
      }
    }
  }'

Response: 201 Created

{
  "profile_name": "video_colpali_mv_frame",
  "tenant_id": "acme_corp",
  "schema_deployed": false,
  "tenant_schema_name": null,
  "schema_deploy_error": null,
  "created_at": "2024-01-15T10:00:00.000Z",
  "version": 1
}

With deploy_schema, a deploy that fails after the profile is stored still answers 201: schema_deployed is false and schema_deploy_error names the schema and the failure, for a deploy from the profile.

Validation Rules:

  • profile_name: Must be unique within tenant, max 100 chars, alphanumeric + underscore + hyphen only
  • tenant_id: Required, non-empty — identifies the tenant owning the profile
  • type: Optional (defaults to "video"), must be a type of a shipped profile in configs/config.json backend.profiles ("video", "image", "audio", "document", "code", "wiki")
  • schema_name: Must have a matching template file {schema_name}_schema.json in the configured schema templates directory (defaults to configs/schemas/); the template must contain top-level name and document.fields
  • embedding_model: Format org/model or model-name
  • embedding_type: Must be multi_vector or single_vector
  • strategies: Optional (defaults to empty dict); each entry must be an object with a class key naming an importable strategy class
  • pipeline_config: Optional (defaults to empty dict), must be valid JSON object
  • model_specific: Optional (defaults to null), must be valid JSON object
  • schema_config: Optional (defaults to empty dict); if it includes embedding_dim, the value must be an integer between 1 and 100000
  • model_loader: The loader ingestion embeds with: colbert, colpali, colqwen or xclip (EMBEDDING_MODEL_LOADERS). Required when every shipped profile of the profile's type names one (today every type but wiki); a profile without it could be deployed but not ingested into
  • process_type: Optional (defaults to null, inferred by ingestion); one of direct_video, frame_based, video_chunks (PROCESS_TYPES)
  • extra_config: Optional (defaults to empty dict); further profile keys stored beside the named fields, such as inference_services, model_config, result_granularity or semantic_model. A key named like a profile field is refused
  • deploy_schema: Optional (defaults to false), boolean flag

2. List Profile Templates

The shipped profiles a tenant's new profile can start from, each with its whole configuration as ingestion reads it. A shipped name the tenant has created its own profile under is left out.

Endpoint: GET /admin/profile-templates

Query Parameters:

  • tenant_id (required): Tenant identifier

Response: 200 OK

{
  "tenant_id": "acme_corp",
  "templates": [
    {
      "profile_name": "document_text_semantic",
      "config": {
        "type": "document",
        "schema_name": "document_text",
        "embedding_model": "lightonai/LateOn",
        "embedding_type": "multi_vector",
        "model_loader": "colbert",
        "inference_services": {"embedding": "colbert_pylate"},
        "result_granularity": "source",
        "...": "..."
      }
    }
  ],
  "profile_types": ["video", "image", "audio", "document", "code", "wiki"],
  "embedding_types": ["multi_vector", "single_vector"],
  "model_loaders": ["colbert", "colpali", "colqwen", "xclip"],
  "process_types": ["direct_video", "frame_based", "video_chunks"]
}

profile_types, embedding_types, model_loaders and process_types are the values a new profile's choice fields take.

A template becomes a create request by moving its named keys to the fields of the same name and every other key into extra_config.

Error Response: 500 with error: "profile_templates_failed" when the config store cannot be read.


3. List Profiles

Get all backend profiles for a tenant.

Endpoint: GET /admin/profiles

Query Parameters:

  • tenant_id (required): Tenant identifier

Example Request:

curl "http://localhost:8000/admin/profiles?tenant_id=acme_corp"

Response: 200 OK

{
  "profiles": [
    {
      "profile_name": "video_colpali_mv_frame",
      "type": "video",
      "schema_name": "video_colpali_smol500_mv_frame",
      "embedding_model": "TomoroAI/tomoro-colqwen3-embed-4b",
      "description": "ColPali model with frame-based embedding",
      "schema_deployed": true,
      "created_at": "2024-01-15T10:00:00.000Z"
    },
    {
      "profile_name": "video_xclip_global",
      "type": "video",
      "schema_name": "video_xclip_sv_chunk_6s",
      "embedding_model": "microsoft/xclip-large-patch14",
      "description": "X-CLIP global embeddings",
      "schema_deployed": false,
      "created_at": "2024-01-15T10:00:00.000Z"
    }
  ],
  "total_count": 2,
  "tenant_id": "acme_corp"
}

Notes:

  • Returns empty array if no profiles exist for tenant

  • Profiles are tenant-isolated (cannot see other tenants' profiles)

  • The list reads the tenant's stored profiles, so a profile created, updated or deleted through any runtime worker or replica is listed as it is at once

  • created_at in this list response is NOT a persisted per-profile creation timestamp — the config store doesn't track it, so the route fills it in as the current time of the list request. Every profile in a given response therefore shows the same created_at. Use Get Profile for a real per-profile version/timestamp derived from the config store's version history.


4. Get Profile

Get a specific backend profile by name.

Endpoint: GET /admin/profiles/{profile_name}

Path Parameters:

  • profile_name: Profile identifier

Query Parameters:

  • tenant_id (required): Tenant identifier

Example Request:

curl "http://localhost:8000/admin/profiles/video_colpali_mv_frame?tenant_id=acme_corp"

Response: 200 OK

{
  "profile_name": "video_colpali_mv_frame",
  "tenant_id": "acme_corp",
  "type": "video",
  "schema_name": "video_colpali_smol500_mv_frame",
  "embedding_model": "TomoroAI/tomoro-colqwen3-embed-4b",
  "embedding_type": "multi_vector",
  "description": "ColPali model with frame-based embedding for video search",
  "strategies": {
    "segmentation": {
      "class": "FrameSegmentationStrategy",
      "params": {"fps": 0.5, "max_frames": 100}
    },
    "embedding": {
      "class": "MultiVectorEmbeddingStrategy",
      "params": {}
    }
  },
  "pipeline_config": {
    "frame_extraction": {
      "fps": 1,
      "max_frames": 100
    }
  },
  "schema_config": {},
  "model_specific": null,
  "model_loader": "colpali",
  "process_type": null,
  "extra_config": {"inference_services": {"embedding": "vllm_colpali"}},
  "schema_deployed": false,
  "tenant_schema_name": null,
  "created_at": "2024-01-15T10:00:00.000Z",
  "version": 1
}

Error Response: 404 Not Found

{
  "detail": "Profile 'video_colpali_mv_frame' not found for tenant 'acme_corp'"
}

Notes:

  • The profile, version and created_at come from one read of the tenant's stored backend config, so a write through any runtime worker or replica is answered at once, and the content and version are always from the same write

5. Update Profile

Update mutable fields of an existing profile.

Endpoint: PUT /admin/profiles/{profile_name}

Path Parameters:

  • profile_name: Profile identifier

Request Body:

{
  "tenant_id": "string (required)",
  "description": "string (optional)",
  "strategies": "object (optional)",
  "pipeline_config": "object (optional)",
  "model_specific": "object (optional)"
}

Mutable Fields:

  • description

  • strategies (Dict[str, Any])

  • pipeline_config (Dict[str, Any])

  • model_specific

Immutable Fields (cannot be updated, create new profile instead):

  • type

  • schema_name

  • embedding_model

  • schema_config

  • model_loader

ProfileUpdateRequest only declares tenant_id, description, strategies, pipeline_config, and model_specific, so any immutable field name sent in the request body (e.g. schema_name) is silently dropped by Pydantic before the route body runs — it never reaches the update logic. If that leaves no recognized fields to change, the request fails with 400 Bad Request: "No fields to update provided" (see below) rather than a "cannot update immutable field" error.

Example Request:

curl -X PUT http://localhost:8000/admin/profiles/video_colpali_mv_frame \
  -H "Content-Type: application/json" \
  -d '{
    "tenant_id": "acme_corp",
    "description": "Updated: ColPali with optimized frame extraction",
    "pipeline_config": {
      "frame_extraction": {
        "fps": 0.5,
        "max_frames": 50
      }
    }
  }'

Response: 200 OK

{
  "profile_name": "video_colpali_mv_frame",
  "tenant_id": "acme_corp",
  "updated_fields": ["description", "pipeline_config"],
  "version": 2
}

Error Response: 400 Bad Request (no mutable fields provided, e.g. body only contained an immutable field name)

{
  "detail": "No fields to update provided"
}

If strategies is one of the provided mutable fields, its value is re-validated with the same strategy-class checks used at creation time; a bad strategies block returns 400 with {"message": "Invalid update fields", "errors": [...]}.

Concurrency:

  • Updates are serialized via a single in-process lock on the shared ConfigManager instance — it guards backend-profile reads/writes for every tenant in that runtime process, not just the tenant being updated; it does not span multiple runtime processes

  • The config store versions every write — each successful update creates a new version and version in the response is the resulting version number


6. Delete Profile

Delete a backend profile and optionally its backend schema.

Endpoint: DELETE /admin/profiles/{profile_name}

Path Parameters:

  • profile_name: Profile identifier

Query Parameters:

  • tenant_id (required): Tenant identifier
  • delete_schema (optional, default=false): Also delete backend schema

Example Request (profile only):

curl -X DELETE "http://localhost:8000/admin/profiles/video_colpali_mv_frame?tenant_id=acme_corp"

Example Request (profile + schema):

curl -X DELETE "http://localhost:8000/admin/profiles/video_colpali_mv_frame?tenant_id=acme_corp&delete_schema=true"

Response: 200 OK

{
  "profile_name": "video_colpali_mv_frame",
  "tenant_id": "acme_corp",
  "schema_deleted": true,
  "deleted_at": "2024-01-15T10:30:00.000Z"
}

Error Response: 404 Not Found

{
  "detail": "Profile 'video_colpali_mv_frame' not found for tenant 'acme_corp'"
}

Error Response: 409 Conflict (only when delete_schema=true and another profile in the same tenant still references the schema)

{
  "detail": "Cannot delete schema 'video_colpali_smol500_mv_frame': other profiles using it: ['video_colpali_other_profile']"
}

Error Response: 500 (only when delete_schema=true and the redeploy would also drop deployed schemas the schema registry does not know — e.g. a schema deployed without registration). The delete is refused instead of silently destroying the sibling schemas; register them or remove them explicitly first (delete_tenant_schemas / POST /admin/reconcile-orphans), then retry.

{
  "detail": "Refusing to delete 'video_colpali_smol500_mv_frame_acme_acme': redeploying without it would also drop ['knowledge_graph_acme_acme'] — deployed schemas the registry does not know and cannot reconstruct. Register them or remove them explicitly first (delete_tenant_schemas / POST /admin/reconcile-orphans)."
}

Notes:

  • Deletion is permanent - no undo

  • If delete_schema=true but schema doesn't exist, operation still succeeds

  • Cannot delete other tenants' profiles


7. Deploy Schema

Deploy backend schema for a profile to the configured backend.

Endpoint: POST /admin/profiles/{profile_name}/deploy

Path Parameters:

  • profile_name: Profile identifier

Request Body:

{
  "tenant_id": "string (required)",
  "force": "boolean (optional, default=false)"
}

Parameters:

  • force: If true, redeploy even if already deployed

Example Request:

curl -X POST http://localhost:8000/admin/profiles/video_colpali_mv_frame/deploy \
  -H "Content-Type: application/json" \
  -d '{
    "tenant_id": "acme_corp",
    "force": false
  }'

Response: 200 OK

{
  "profile_name": "video_colpali_mv_frame",
  "tenant_id": "acme_corp",
  "schema_name": "video_colpali_smol500_mv_frame",
  "tenant_schema_name": "video_colpali_smol500_mv_frame_acme_corp_acme_corp",
  "deployment_status": "success",
  "deployed_at": "2024-01-15T10:30:00.000Z"
}

Error Response: 200 OK (with failed status)

{
  "profile_name": "video_colpali_mv_frame",
  "tenant_id": "acme_corp",
  "schema_name": "video_colpali_smol500_mv_frame",
  "tenant_schema_name": "",
  "deployment_status": "failed",
  "deployed_at": "2024-01-15T10:30:00.000Z",
  "error_message": "Connection to Vespa refused"
}

Deployment Process:

  1. Check if schema already exists (skip if exists and force=false)
  2. Call backend.schema_registry.deploy_schema() with tenant_id, base_schema_name, optional config, and optional force parameter
  3. Generate tenant-specific schema name via backend.get_tenant_schema_name(tenant_id, base_schema_name): the tenant_id is first canonicalized to org:tenant form (a bare tenant like acme_corp canonicalizes to acme_corp:acme_corp; an explicit org:tenant value like acme:prod is used as-is), then the colon is replaced with an underscore and appended — so tenant_id="acme_corp" yields {base_schema_name}_acme_corp_acme_corp, and tenant_id="acme:prod" yields {base_schema_name}_acme_prod
  4. Return deployment status ("success", "failed", or "already_deployed")

Prerequisites:

  • Profile must exist
  • The profile resolves from the tenant's stored profiles first, otherwise from the tenant's merged catalog of shipped (config.json) profiles, which GET /admin/profiles and GET /admin/profiles/{profile_name} do not show

  • Schema template must exist in configured schema directory

  • Backend must be accessible

  • System config must have valid backend_url


Request/Response Schemas

ProfileCreateRequest

{
  profile_name: string,        // Required, unique within tenant
  tenant_id: string,           // Required: tenant identifier for isolation
  type?: string,               // Optional (default: "video")
  schema_name: string,         // Required, must exist in schema dir
  embedding_model: string,     // Required (e.g., "TomoroAI/tomoro-colqwen3-embed-4b")
  embedding_type: "multi_vector" | "single_vector",  // Required
  description?: string,        // Optional (default: "")
  strategies?: object,         // Optional (default: {}, Dict[str, Any])
  pipeline_config?: object,    // Optional (default: {}, Dict[str, Any])
  model_specific?: object,     // Optional (default: null, Dict[str, Any])
  schema_config?: object,      // Optional (default: {}, Dict[str, Any])
  model_loader?: string,       // colbert | colpali | colqwen | xclip; required for embedded types
  process_type?: string | null, // direct_video | frame_based | video_chunks (default: null)
  extra_config?: object,       // Optional (default: {}), keys stored beside the fields
  deploy_schema?: boolean      // Optional (default: false)
}

ProfileSummary

{
  profile_name: string,
  type: string,
  description: string,
  schema_name: string,
  embedding_model: string,
  schema_deployed: boolean,
  created_at: string               // ISO 8601 timestamp
}

ProfileListResponse

{
  profiles: ProfileSummary[],      // List of profile summaries
  total_count: number,
  tenant_id: string
}

ProfileDetail

{
  profile_name: string,
  tenant_id: string,
  type: string,
  schema_name: string,
  embedding_model: string,
  embedding_type: string,
  description: string,
  strategies: object,              // Dict[str, Any]
  pipeline_config: object,         // Dict[str, Any]
  schema_config: object,           // Dict[str, Any]
  model_specific: object | null,
  model_loader: string,
  process_type: string | null,
  extra_config: object,            // Dict[str, Any]
  schema_deployed: boolean,
  tenant_schema_name: string | null,
  created_at: string,              // ISO 8601 timestamp
  version: number
}

ProfileUpdateRequest

{
  tenant_id: string,           // Required
  description?: string,        // Optional
  strategies?: object,         // Optional (Dict[str, Any])
  pipeline_config?: object,    // Optional (Dict[str, Any])
  model_specific?: object      // Optional (Dict[str, Any])
}

ProfileUpdateResponse

{
  profile_name: string,
  tenant_id: string,
  updated_fields: string[],    // Fields that were changed
  version: number              // Incremented version
}

ProfileDeleteResponse

{
  profile_name: string,
  tenant_id: string,
  schema_deleted: boolean,
  deleted_at: string             // ISO 8601 timestamp
}

SchemaDeploymentRequest

{
  tenant_id: string,  // Required
  force: boolean      // Optional, default=false
}

SchemaDeploymentResponse

{
  profile_name: string,
  tenant_id: string,
  schema_name: string,
  tenant_schema_name: string,
  deployment_status: string,      // "success" | "failed" | "already_deployed"
  deployed_at: string,            // ISO 8601 timestamp
  error_message?: string          // Only present if deployment_status is "failed"
}

Complete Workflow Example

1. Create a new profile

curl -X POST http://localhost:8000/admin/profiles \
  -H "Content-Type: application/json" \
  -d '{
    "profile_name": "video_test_profile",
    "tenant_id": "test_tenant",
    "type": "video",
    "schema_name": "video_colpali_smol500_mv_frame",
    "embedding_model": "TomoroAI/tomoro-colqwen3-embed-4b",
    "embedding_type": "multi_vector",
    "description": "Test profile for development"
  }'

2. Deploy the schema

curl -X POST http://localhost:8000/admin/profiles/video_test_profile/deploy \
  -H "Content-Type: application/json" \
  -d '{
    "tenant_id": "test_tenant",
    "force": false
  }'

3. Check deployment status

curl "http://localhost:8000/admin/profiles/video_test_profile?tenant_id=test_tenant"

4. Update pipeline configuration

curl -X PUT http://localhost:8000/admin/profiles/video_test_profile \
  -H "Content-Type: application/json" \
  -d '{
    "tenant_id": "test_tenant",
    "pipeline_config": {
      "frame_extraction": {
        "fps": 2,
        "max_frames": 200
      }
    }
  }'

5. List all profiles for tenant

curl "http://localhost:8000/admin/profiles?tenant_id=test_tenant"

6. Delete profile and schema

curl -X DELETE "http://localhost:8000/admin/profiles/video_test_profile?tenant_id=test_tenant&delete_schema=true"

Rate Limiting

Currently no rate limiting. Future versions will implement:

  • Per-tenant request limits

  • Burst protection

  • Deployment throttling


Versioning

API version: v1 (implicit, no version prefix required)

Breaking changes will be introduced in new API versions (/v2/admin/profiles).


Client Libraries

Python

import requests

class ProfileClient:
    def __init__(self, base_url: str = "http://localhost:8000"):
        self.base_url = base_url
        self.session = requests.Session()
        self.session.headers.update({"Content-Type": "application/json"})

    def create_profile(self, profile: dict) -> dict:
        response = self.session.post(
            f"{self.base_url}/admin/profiles",
            json=profile,
            timeout=30.0
        )
        response.raise_for_status()
        return response.json()

    def list_profiles(self, tenant_id: str) -> list:
        response = self.session.get(
            f"{self.base_url}/admin/profiles",
            params={"tenant_id": tenant_id},
            timeout=30.0
        )
        response.raise_for_status()
        return response.json()["profiles"]

    def deploy_schema(self, profile_name: str, tenant_id: str, force: bool = False) -> dict:
        response = self.session.post(
            f"{self.base_url}/admin/profiles/{profile_name}/deploy",
            json={"tenant_id": tenant_id, "force": force},
            timeout=30.0
        )
        response.raise_for_status()
        return response.json()

# Usage
client = ProfileClient()
profile = client.create_profile({
    "profile_name": "video_test",
    "tenant_id": "my_tenant",
    "type": "video",
    "schema_name": "video_colpali_smol500_mv_frame",
    "embedding_model": "TomoroAI/tomoro-colqwen3-embed-4b",
    "embedding_type": "multi_vector"
})

JavaScript

class ProfileClient {
  constructor(baseUrl = "http://localhost:8000") {
    this.baseUrl = baseUrl;
  }

  async createProfile(profile) {
    const response = await fetch(`${this.baseUrl}/admin/profiles`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(profile)
    });
    if (!response.ok) throw new Error(await response.text());
    return response.json();
  }

  async listProfiles(tenantId) {
    const response = await fetch(
      `${this.baseUrl}/admin/profiles?tenant_id=${tenantId}`
    );
    if (!response.ok) throw new Error(await response.text());
    const data = await response.json();
    return data.profiles;
  }

  async deploySchema(profileName, tenantId, force = false) {
    const response = await fetch(
      `${this.baseUrl}/admin/profiles/${profileName}/deploy`,
      {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ tenant_id: tenantId, force })
      }
    );
    if (!response.ok) throw new Error(await response.text());
    return response.json();
  }
}

// Usage
const client = new ProfileClient();
const profile = await client.createProfile({
  profile_name: "video_test",
  tenant_id: "my_tenant",
  type: "video",
  schema_name: "video_colpali_smol500_mv_frame",
  embedding_model: "TomoroAI/tomoro-colqwen3-embed-4b",
  embedding_type: "multi_vector"
});

Next Steps