{"openapi":"3.1.0","info":{"title":"Vector Storage","description":"High-performance vector embedding storage and search microservice with a built-in\nRAG pipeline, hybrid search (FAISS + BM25), multi-provider embeddings, and LLM\nmetadata enrichment.\n\n**[Full Documentation](/docs)** — Architecture, API reference, data providers, deployment, and more.\n| **[Integration Guide](/integration-guide)** — Quick start, code examples, RAG pattern.\n| **[OpenAPI JSON](/openapi.json)** — Machine-readable schema.\n\n## Authentication\n\nAll endpoints (except `/v1/health`) require a Bearer token in the `Authorization`\nheader:\n\n```\nAuthorization: Bearer <api_key>\n```\n\nAPI keys are issued via the Admin Panel. Use the **Authorize** button at the top\nof this page to set your token once for all interactive requests.\n\n## Concepts\n\n- **Namespace** (`{ns}`) — top-level isolation unit. Each namespace has its own\n  FAISS + BM25 indexes, document storage, and stats.\n- **Client ID** (`{client_id}`) — secondary isolation key inside a namespace,\n  supplied in the URL path: `/v1/client/{client_id}/ns/{ns}/...`. Internally\n  composed as `{ns}__{client_id}` for separate FAISS + BM25 indexes per tenant.\n  Endpoints that don't need tenant scoping (health, warmup, rebuild, provider\n  tools) stay at `/v1/ns/{ns}/...`.\n- **Document ID** (`doc_id`) — logical document inside a client. One document\n  produces N chunks (vectors). Deleting a document removes all its chunks.\n- **Ingest job** (`ingest_id`) — async background pipeline that pulls data from\n  an external provider (e.g. LiveHelpNow), aggregates, and indexes it.\n- **Crawl job** (`crawl_id`) — async website crawl that turns each fetched page\n  into a regular document.\n\n## Error format\n\nEvery non-2xx response uses a single shape:\n\n```json\n{\n  \"error\": \"snake_case_code\",\n  \"message\": \"Human readable message\",\n  \"details\": { \"...optional context...\" }\n}\n```\n\nThe `error` field is the canonical machine-readable code (`not_found`,\n`invalid_request`, `validation_error`, `rate_limited`, `upstream_error`, etc.).\n","version":"0.7.0"},"paths":{"/v1/ns/{ns}/warmup":{"post":{"tags":["management"],"summary":"Warmup namespace","description":"Load a namespace's FAISS + BM25 indexes into the in-memory cache so that subsequent queries skip the cold-load latency.\n\n**When to use:** call after deploy or before a known traffic burst on a namespace that hasn't been queried recently.","operationId":"warmup_v1_ns__ns__warmup_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WarmupResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/ns/{ns}/rebuild":{"post":{"tags":["management"],"summary":"Rebuild namespace index","description":"Trigger an **asynchronous** rebuild of a namespace's FAISS index. Compacts the WAL, removes tombstoned vectors, and reselects the optimal index type (Flat / IVF / IVFPQ) for the current vector count.\n\n**Side-effects:** locks the namespace from new upserts during the rebuild (usually seconds to minutes). Safe to call on a healthy namespace.\n\n**Rate-limited:** 5/minute.","operationId":"rebuild_v1_ns__ns__rebuild_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}}],"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RebuildResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/ns/{ns}/stats":{"get":{"tags":["management"],"summary":"Namespace statistics","description":"Detailed statistics for a namespace (without client_id scoping). Use `/client/{client_id}/ns/{ns}/stats` for tenant-isolated view.","operationId":"stats_v1_ns__ns__stats_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatsResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/stats":{"get":{"tags":["management"],"summary":"Namespace statistics","description":"Detailed statistics for a namespace in three independent views:\n\n**1. FAISS view** (top-level scalar fields) — live numbers from the in-process index: `vector_count`, `vector_dimensions`, `index_type`, `index_size_bytes`, `wal_segments`, `last_rebuild_at`, `cached`. When `client_id` is provided in the path, reads the composite-namespace view `{ns}__{client_id}` — FAISS numbers then reflect only that tenant's index. Without `client_id`, numbers come from the naked namespace and are zero for any tenant that uses client_id isolation.\n\n**2. Document view** (`documents`) — aggregated document counts from StatsStore (ready/processing/error, chunks, sources breakdown). Filtered by `client_id` when provided; otherwise summed across every client of the namespace. Null if StatsStore is not enabled.\n\n**3. Pipeline view** (`pipelines`) — array with one entry per registered data provider that has a `data.sqlite` archive in S3 for this namespace. Every entry has the same universal shape: archive presence, raw and aggregated document counts grouped by `doc_type`, top aggregation categories, top keywords weighted by cluster size, optional per-entity date ranges, and the last ingest job summary. Providers without an archive for this namespace are filtered out; the list is empty when no providers are configured. This view is independent of `client_id` — archives are keyed by namespace, not composite namespace.","operationId":"stats_v1_client__client_id__ns__ns__stats_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatsResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/ns/{ns}":{"delete":{"tags":["management"],"summary":"Delete entire namespace","description":"**Destructive.** Permanently delete a namespace and all its data: vectors, documents, metadata, WAL segments, and crawl/ingest job history from S3.\n\n**Cannot be undone.** Returns `vectors_deleted` count for verification.","operationId":"delete_namespace_v1_ns__ns__delete","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteNamespaceResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/health":{"get":{"tags":["health"],"summary":"Health check","description":"Check service health including S3 connectivity, cache status, and uptime.","operationId":"health_v1_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}}}}},"/v1/ns/{ns}/documents/providers/{provider}/validate":{"post":{"tags":["ingest"],"summary":"Validate provider credentials","description":"Test provider API credentials **without** starting ingestion. Returns per-data-type availability counts and access status. Use this from a UI credentials form to give the user immediate feedback before persisting.","operationId":"validate_credentials_v1_ns__ns__documents_providers__provider__validate_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateCredentialsRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateCredentialsResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"400":{"description":"Unknown provider","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/documents/ingest":{"post":{"tags":["ingest"],"summary":"Start data ingestion from external provider","description":"Queue an asynchronous ingestion job that fetches data from an external provider (e.g. LiveHelpNow), aggregates similar items into clusters, and indexes them as documents in this namespace.\n\n**Pipeline phases:** fetching → aggregating → indexing → finalizing.\n\n**Side-effects:** writes raw data to S3 (`{ns}/providers/{provider}/data.sqlite`) for incremental syncs and stores credentials in the admin config store on first successful run. Webhook events: `ingest.progress`, `ingest.done`, `ingest.error`.\n\n**Follow-up:** poll `GET /ingest/{ingest_id}/status` or watch the webhook.","operationId":"start_ingest_v1_client__client_id__ns__ns__documents_ingest_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestRequest"}}}},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"400":{"description":"Unknown provider, invalid credentials, or no credentials configured","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/ns/{ns}/documents/ingest/{ingest_id}/status":{"get":{"tags":["ingest"],"summary":"Get ingestion job status","description":"Get the current status, phase, and progress of an ingestion job. Same response shape as items in `GET /ingest/history`.","operationId":"get_ingest_status_v1_ns__ns__documents_ingest__ingest_id__status_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"ingest_id","in":"path","required":true,"schema":{"type":"string","title":"Ingest Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestStatusResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Ingest job not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/ns/{ns}/documents/ingest/{ingest_id}/stop":{"post":{"tags":["ingest"],"summary":"Stop an active ingestion job","description":"Cancel a pending or running ingest job. Already-indexed documents remain. Status transitions to `cancelled`.","operationId":"stop_ingest_v1_ns__ns__documents_ingest__ingest_id__stop_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"ingest_id","in":"path","required":true,"schema":{"type":"string","title":"Ingest Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Ingest job not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"400":{"description":"Job is not in a cancellable state","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/ns/{ns}/documents/ingest/{ingest_id}":{"delete":{"tags":["ingest"],"summary":"Delete ingestion job from history","description":"Remove a completed, failed, or cancelled ingest job from the history table. Running or pending jobs must be stopped first via `POST .../stop`. Already-indexed documents are **not** removed by this call.","operationId":"delete_ingest_v1_ns__ns__documents_ingest__ingest_id__delete","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"ingest_id","in":"path","required":true,"schema":{"type":"string","title":"Ingest Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Ingest job not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"400":{"description":"Job is still running or pending","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/ns/{ns}/documents/ingest/history":{"get":{"tags":["ingest"],"summary":"List ingestion history for namespace","description":"List recent ingestion jobs for a namespace, newest first. Each item has the same shape as `GET /ingest/{ingest_id}/status`. Pagination via `limit` (default 50, max 200).","operationId":"ingest_history_v1_ns__ns__documents_ingest_history_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"description":"Maximum number of jobs to return","default":50,"title":"Limit"},"description":"Maximum number of jobs to return"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestHistoryResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/ns/{ns}/documents/providers":{"get":{"tags":["ingest"],"summary":"List available data providers","description":"List all data providers registered on this server, with their data types and MCP capability flag. Use to populate the provider picker in your UI.","operationId":"list_providers_v1_ns__ns__documents_providers_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProvidersListResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/ns/{ns}/documents/providers/{provider}/tools":{"get":{"tags":["ingest"],"summary":"Get MCP tools for a provider","description":"List the MCP tools exposed by a provider. Tools let an LLM call live provider APIs (e.g. fetch a fresh ticket) when search results indicate `has_mcp=true`.","operationId":"get_provider_tools_v1_ns__ns__documents_providers__provider__tools_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MCPToolsResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Provider not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"400":{"description":"Provider does not support MCP","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/ns/{ns}/documents/providers/{provider}/schedules":{"get":{"tags":["ingest"],"summary":"List recurring update schedules for a provider","description":"List all schedules for the given provider in this namespace (all tenants). Use `/client/{client_id}/ns/{ns}/...` to scope to one tenant.","operationId":"list_schedules_v1_ns__ns__documents_providers__provider__schedules_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}},{"name":"client_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleListResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Provider not registered","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/documents/providers/{provider}/schedules":{"get":{"tags":["ingest"],"summary":"List recurring update schedules for a provider","description":"List schedules for the given provider in this namespace, scoped to `client_id` from the path. Each entry has the full state (enabled, interval, last run, next run, last status).","operationId":"list_schedules_v1_client__client_id__ns__ns__documents_providers__provider__schedules_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}},{"name":"client_id","in":"path","required":true,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleListResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Provider not registered","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"put":{"tags":["ingest"],"summary":"Create or replace schedules for a provider's data_types","description":"Upsert recurring update schedules for one or more data_types of a provider. Existing schedules for the same `(provider, data_type)` are replaced atomically.\n\n**Validation:** the provider must support scheduling for each requested data_type, and `interval_hours` must respect the data_type's `min_interval_hours`. The first run is scheduled `interval_hours` from creation — call `POST .../run-now` if you want an immediate sync.","operationId":"upsert_schedules_v1_client__client_id__ns__ns__documents_providers__provider__schedules_put","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}},{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertSchedulesRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleListResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"400":{"description":"Provider does not support scheduling for the given data_type","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Provider not registered","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/documents/providers/{provider}/schedules/{data_type}":{"delete":{"tags":["ingest"],"summary":"Remove a single recurring schedule","description":"Delete one schedule by `(provider, data_type, client_id)`. The currently running ingest job (if any) is **not** cancelled — it will finish and report its outcome to the deleted schedule (which is then a no-op). Use `POST .../stop` on the ingest job to abort.","operationId":"delete_schedule_v1_client__client_id__ns__ns__documents_providers__provider__schedules__data_type__delete","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}},{"name":"data_type","in":"path","required":true,"schema":{"type":"string","title":"Data Type"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Schedule not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/documents/providers/{provider}/schedules/{data_type}/pause":{"post":{"tags":["ingest"],"summary":"Pause a recurring schedule","description":"Set `enabled=false` so the IngestionScheduler skips this schedule on its next tick. The next_run_at value is preserved — call `resume` to continue from where it left off.","operationId":"pause_schedule_v1_client__client_id__ns__ns__documents_providers__provider__schedules__data_type__pause_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}},{"name":"data_type","in":"path","required":true,"schema":{"type":"string","title":"Data Type"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Schedule not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/documents/providers/{provider}/schedules/{data_type}/resume":{"post":{"tags":["ingest"],"summary":"Resume a paused schedule","description":"Set `enabled=true`. The schedule fires on the next scheduler tick if `next_run_at <= now`, otherwise at `next_run_at`.","operationId":"resume_schedule_v1_client__client_id__ns__ns__documents_providers__provider__schedules__data_type__resume_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}},{"name":"data_type","in":"path","required":true,"schema":{"type":"string","title":"Data Type"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Schedule not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/documents/providers/{provider}/schedules/{data_type}/run-now":{"post":{"tags":["ingest"],"summary":"Trigger a scheduled update immediately","description":"Bypass the timer and enqueue an ingest job for this single data_type right now. The schedule's `last_run_at` is updated as if the timer had fired naturally; `next_run_at` is recomputed only after the job finishes (via the standard worker hook).\n\nReturns the same shape as `POST /ingest`.","operationId":"run_schedule_now_v1_client__client_id__ns__ns__documents_providers__provider__schedules__data_type__run_now_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}},{"name":"data_type","in":"path","required":true,"schema":{"type":"string","title":"Data Type"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Schedule not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"400":{"description":"No credentials configured for this provider","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/ns/{ns}/documents/providers/{provider}/call":{"post":{"tags":["ingest"],"summary":"Call an MCP tool on a provider","description":"Execute an MCP tool on a provider using the credentials stored for this namespace. Returns the raw tool output (shape is tool-specific). Rate-limited: 60/minute.","operationId":"call_provider_tool_v1_ns__ns__documents_providers__provider__call_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MCPCallRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MCPCallResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Provider not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"400":{"description":"Provider does not support MCP, no credentials, or unknown tool","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"502":{"description":"Upstream provider error during MCP call","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/documents/upload":{"post":{"tags":["documents"],"summary":"Upload document for processing","description":"Upload a file (PDF, DOCX, TXT, CSV, XLSX, HTML). The document is parsed, chunked, embedded, and indexed **asynchronously** — the response is returned as soon as the file is staged.\n\n**Side-effects:** spawns a background processing task. Original file and metadata are stored under `{namespace}/documents/{doc_id}/` in S3.\n\n**Follow-up:**\n- Poll `GET /v1/ns/{ns}/documents/{doc_id}` until `status` is `ready` or `error`\n- Or configure a webhook on the API key to receive `{doc_id, status, chunk_count, error, filename}`\n\n**Limits:** see `max_file_size_mb` (default 50 MB) and the `supported_extensions` setting.","operationId":"upload_document_v1_client__client_id__ns__ns__documents_upload_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_document_v1_client__client_id__ns__ns__documents_upload_post"}}}},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentUploadResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"413":{"description":"File exceeds the configured size limit","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"415":{"description":"Unsupported file extension","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/ns/{ns}/documents/{doc_id}":{"get":{"tags":["documents"],"summary":"Get document status","description":"Retrieve a single document's metadata and processing status.\n\n**Status values:** pending → processing → ready (or error / empty).\n\nUse this to poll for completion after `POST /upload`, `POST /fetch-url`, `POST /reprocess`, or `POST /chunks/upsert`.","operationId":"get_document_v1_ns__ns__documents__doc_id__get","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"doc_id","in":"path","required":true,"schema":{"type":"string","title":"Doc Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Document not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"tags":["documents"],"summary":"Delete document and its vectors","description":"Delete a document. Removes the original file and metadata from S3 and all associated chunks from FAISS + BM25 + WAL.\n\n**Side-effects:** decrements namespace document stats; safe to call on an already-empty document. Cannot be undone — re-upload to restore.","operationId":"delete_document_v1_ns__ns__documents__doc_id__delete","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"doc_id","in":"path","required":true,"schema":{"type":"string","title":"Doc Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentDeleteResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Document not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/ns/{ns}/documents":{"get":{"tags":["documents"],"summary":"List documents in namespace","description":"List documents in a namespace, sorted by creation date (newest first). Use `/client/{client_id}/ns/{ns}/documents` for tenant-scoped listing.\n\n**Pagination:** controlled via `limit` and `offset`. The response includes `total` (full count after filtering) plus the echoed `limit`/`offset`. Default page size is 100; max is 500.","operationId":"list_documents_v1_ns__ns__documents_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"description":"Page size (1-500)","default":100,"title":"Limit"},"description":"Page size (1-500)"},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Offset for pagination","default":0,"title":"Offset"},"description":"Offset for pagination"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentListResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/documents":{"get":{"tags":["documents"],"summary":"List documents in namespace","description":"List documents in a namespace for a specific client, sorted by creation date (newest first).\n\n**Pagination:** controlled via `limit` and `offset`. The response includes `total` (full count after filtering) plus the echoed `limit`/`offset`. Default page size is 100; max is 500.","operationId":"list_documents_v1_client__client_id__ns__ns__documents_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"description":"Page size (1-500)","default":100,"title":"Limit"},"description":"Page size (1-500)"},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Offset for pagination","default":0,"title":"Offset"},"description":"Offset for pagination"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentListResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/ns/{ns}/documents/{doc_id}/reprocess":{"post":{"tags":["documents"],"summary":"Reprocess document","description":"Delete old vectors and re-run the processing pipeline on the original file.\n\n**When to use:** after changing chunk size, embedding provider, enrichment prompts, or pipeline config — to bring an existing document into line with the new settings.\n\n**Side-effects:** removes old chunks from FAISS, runs parse → chunk → enrich → embed → upsert pipeline asynchronously. Returns immediately.\n\n**Follow-up:** poll `GET /documents/{doc_id}` or use the webhook callback.","operationId":"reprocess_document_v1_ns__ns__documents__doc_id__reprocess_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"doc_id","in":"path","required":true,"schema":{"type":"string","title":"Doc Id"}}],"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentUploadResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Document not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/documents/fetch-url":{"post":{"tags":["documents"],"summary":"Fetch and process a web page","description":"Download a web page by URL, extract content via trafilatura, chunk, embed, and index it. The page becomes a regular document accessible via `GET /documents/{doc_id}` and `DELETE /documents/{doc_id}`.\n\n**SSRF protection:** private/loopback IPs and non-http(s) schemes are rejected.\n\n**Side-effects:** spawns a background processing task; webhook callback sent on completion.","operationId":"fetch_url_v1_client__client_id__ns__ns__documents_fetch_url_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FetchUrlRequest"}}}},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FetchUrlResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Invalid URL or fetch failed (HTTP error / non-HTML content / size limit)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/documents/crawl":{"post":{"tags":["documents","crawl"],"summary":"Start website crawl","description":"Crawl a website starting from the given URL. Pages are fetched via BFS with configurable depth, page limits, URL filters, robots.txt and sitemap support. Each fetched page becomes a regular document.\n\n**Webhook callbacks** (sent to the webhook URL configured for the API key):\n- Per-page: `{event: 'crawl.page', crawl_id, doc_id, url, status, chunk_count, filename, error}`\n- Final: `{event: 'crawl.done', crawl_id, status, pages_found, pages_processed, pages_error, total_chunks, finished_at}`\n\n**Crawl statuses**: pending, running, done, stopped, error.\n**Page statuses**: pending, processing, ready, error, skipped, empty.\n\n**Follow-up:** `GET /documents/crawl/{crawl_id}` for status, `POST .../stop` to abort, `POST .../reprocess` to re-run with the same params.","operationId":"start_crawl_v1_client__client_id__ns__ns__documents_crawl_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CrawlRequest"}}}},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CrawlResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Invalid start_url or regex pattern","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/ns/{ns}/documents/crawl/{crawl_id}":{"get":{"tags":["documents","crawl"],"summary":"Get crawl job status","description":"Get the current status and progress of a crawl job, including per-page results (URL, depth, doc_id, status, chunk count). Use this for progress UIs while the job is running.","operationId":"get_crawl_status_v1_ns__ns__documents_crawl__crawl_id__get","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"crawl_id","in":"path","required":true,"schema":{"type":"string","title":"Crawl Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CrawlStatusResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Crawl job not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/ns/{ns}/documents/crawl/{crawl_id}/stop":{"post":{"tags":["documents","crawl"],"summary":"Stop a running crawl","description":"Stop an active crawl job. Already-processed pages remain in the index. Status transitions to `stopped`.","operationId":"stop_crawl_v1_ns__ns__documents_crawl__crawl_id__stop_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"crawl_id","in":"path","required":true,"schema":{"type":"string","title":"Crawl Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CrawlStopResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Active crawl job not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/ns/{ns}/documents/crawl/{crawl_id}/reprocess":{"post":{"tags":["documents","crawl"],"summary":"Re-crawl a completed crawl job","description":"Delete all documents/chunks from the old crawl and re-run with the same parameters (start URL, max_depth, max_pages, filters). The `crawl_id` is preserved.\n\n**Constraints:** only works for completed crawls (done/stopped/error). Active crawls must be stopped first.","operationId":"reprocess_crawl_v1_ns__ns__documents_crawl__crawl_id__reprocess_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"crawl_id","in":"path","required":true,"schema":{"type":"string","title":"Crawl Id"}}],"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CrawlReprocessResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Crawl job not found, still active, or missing saved config","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/chunks/upsert":{"post":{"tags":["chunks"],"summary":"Upsert pre-chunked text","description":"Accept pre-chunked text with caller-supplied IDs and metadata. Vector Storage generates embeddings via the active provider and indexes the chunks in FAISS + BM25 + WAL. Existing `chunk_id`s are overwritten (returned with `status='updated'`); new IDs return `status='created'`.\n\n**Grouping:** chunks are grouped under a logical `doc_id`. Delete all chunks for a document via `DELETE /documents/{doc_id}`.\n\n**Reserved metadata keys** set by Vector Storage on every chunk and never to be supplied by callers: `chunk_text`, `doc_id`, `client_id`, `doc_type`, `source`.\n\n**Limits:** 1-500 chunks per request, text 1-10 000 chars, metadata 10 KB.","operationId":"upsert_chunks_v1_client__client_id__ns__ns__chunks_upsert_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChunkUpsertRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChunkUpsertResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"No embedding provider configured / provider error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/pipeline/{config_name}":{"put":{"tags":["pipeline"],"summary":"Create or update pipeline config","description":"Create or update a named pipeline configuration for a client. Configs control LLM enrichment prompts, JSON output schema, vector strategy (single/multi), and chunking parameters.\n\n**Multi-vector strategy:** when `vector_strategy='multi'`, each chunk is expanded into N vectors via `vector_templates` (Jinja2 templates rendered from enrichment data). Useful for SOPs where one chunk contributes trigger/steps/keywords vectors.","operationId":"put_pipeline_config_v1_client__client_id__ns__ns__pipeline__config_name__put","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"config_name","in":"path","required":true,"schema":{"type":"string","title":"Config Name"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PipelineConfigRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PipelineConfigResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Invalid pipeline config (validation errors)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"get":{"tags":["pipeline"],"summary":"Get pipeline config by name","description":"Fetch a single pipeline config by `client_id` (in path) + `config_name`.","operationId":"get_pipeline_config_v1_client__client_id__ns__ns__pipeline__config_name__get","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"config_name","in":"path","required":true,"schema":{"type":"string","title":"Config Name"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PipelineConfigResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Pipeline config not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"tags":["pipeline"],"summary":"Delete pipeline config","description":"Delete a named pipeline config. Documents that referenced this config are not affected — they keep their already-indexed vectors. Re-process them to switch to the default pipeline.","operationId":"delete_pipeline_config_v1_client__client_id__ns__ns__pipeline__config_name__delete","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}},{"name":"config_name","in":"path","required":true,"schema":{"type":"string","title":"Config Name"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PipelineConfigDeleteResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Pipeline config not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/{client_id}/ns/{ns}/pipeline":{"get":{"tags":["pipeline"],"summary":"List pipeline configs for a client","description":"List all named pipeline configs registered for a given `client_id` (in path).","operationId":"list_pipeline_configs_v1_client__client_id__ns__ns__pipeline_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PipelineConfigListResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/admin/pipeline/configs":{"get":{"tags":["pipeline"],"summary":"List all pipeline configs (admin)","description":"Admin-only: list all pipeline configs across all clients in the system.","operationId":"admin_list_configs_v1_admin_pipeline_configs_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClientConfigsListResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"BearerAuth":[]}]}},"/v1/client/{client_id}/ns/{ns}/query":{"post":{"tags":["query"],"summary":"Text query (embed + search)","description":"Send a text query. The service embeds it via the active embedding provider and performs hybrid search (FAISS semantic + BM25 keyword) across the client-isolated namespace.\n\n**How it works:**\n1. Text is embedded using the active embedding provider's query model\n2. FAISS finds semantically similar vectors (dense search)\n3. BM25 finds keyword matches in chunk text (sparse search)\n4. Results are combined via Reciprocal Rank Fusion (RRF)\n5. Metadata filters are applied as post-filtering\n6. Top-k results are returned with chunk text and metadata\n\n**Metadata filters** support exact match, IN, gte/lte, contains, and boolean operators. All filters are AND-combined. See the `filters` field description for details.\n\n**Chunk metadata structure:**\nEach result includes metadata with fields: doc_id, doc_type, category, priority, heading, chunk_index, content_type, has_phone, has_price, has_datetime, has_address, source, source_url. When LLM enrichment is enabled: llm_category, llm_language, llm_entities, llm_topics, llm_content_kind.","operationId":"query_v1_client__client_id__ns__ns__query_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryResponse"}}}},"401":{"description":"Invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"API key lacks the required role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"No embedding provider configured","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/ns/{ns}/images/{signed_token}":{"get":{"tags":["images"],"summary":"Get image by signed token","description":"Serve an image from S3 using a signed token. No auth header required. Token includes doc_id, image_id, and expiry. Generated by the query endpoint. Image is streamed directly from S3 without buffering in memory.","operationId":"get_image_v1_ns__ns__images__signed_token__get","parameters":[{"name":"ns","in":"path","required":true,"schema":{"type":"string","title":"Ns"}},{"name":"signed_token","in":"path","required":true,"schema":{"type":"string","title":"Signed Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}},"image/jpeg":{},"image/png":{}}},"403":{"description":"Invalid or expired token"},"404":{"description":"Image not found"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"webhooks":{"doc.processing.done":{"post":{"summary":"Document processing finished","description":"Delivered after a document upload, fetch-url, or reprocess job reaches a terminal state. Sent to the webhook URL configured on the API key. Header `X-Webhook-Signature: sha256=<hmac>` is present when a webhook secret is configured.","operationId":"doc_processing_donedoc_processing_done_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentProcessedWebhook"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"crawl.page":{"post":{"summary":"Per-page crawl event","description":"Delivered once for each crawled page after its processing is finished.","operationId":"crawl_pagecrawl_page_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CrawlPageWebhook"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"crawl.done":{"post":{"summary":"Crawl job finished","description":"Delivered once when a crawl job reaches a terminal status.","operationId":"crawl_donecrawl_done_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CrawlDoneWebhook"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"ingest.progress":{"post":{"summary":"Ingest job progress","description":"Delivered at each phase boundary of a running ingest job.","operationId":"ingest_progressingest_progress_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestProgressWebhook"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"ingest.done":{"post":{"summary":"Ingest job completed","description":"Delivered once when an ingest job completes successfully.","operationId":"ingest_doneingest_done_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestDoneWebhook"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"ingest.error":{"post":{"summary":"Ingest job failed","description":"Delivered once if an ingest job fails.","operationId":"ingest_erroringest_error_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestErrorWebhook"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"Body_upload_document_v1_client__client_id__ns__ns__documents_upload_post":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"},"doc_type":{"type":"string","title":"Doc Type","default":"general"},"category":{"type":"string","title":"Category","default":""},"priority":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Priority","description":"Document priority 1-10. Omit to use tier default for this doc_type."},"analyze_images":{"type":"boolean","title":"Analyze Images","description":"Extract and analyze images via Vision LLM","default":false},"pipeline":{"type":"string","title":"Pipeline","description":"Named pipeline config (enrichment/vector strategy)","default":""}},"type":"object","required":["file"],"title":"Body_upload_document_v1_client__client_id__ns__ns__documents_upload_post"},"ChunkItem":{"properties":{"chunk_id":{"type":"string","maxLength":128,"minLength":1,"title":"Chunk Id","description":"Caller-supplied unique chunk identifier (logical chunk-level ID, not vector ID)."},"text":{"type":"string","maxLength":10000,"minLength":1,"title":"Text","description":"Text to embed (1-10000 chars)"},"metadata":{"additionalProperties":true,"type":"object","title":"Metadata","description":"Arbitrary user metadata (strings, numbers, booleans, lists). Reserved keys set by Vector Storage and **never** to be supplied here: `chunk_text`, `doc_id`, `client_id`, `doc_type`, `source`. Max 10 KB serialized."}},"additionalProperties":true,"type":"object","required":["chunk_id","text"],"title":"ChunkItem","description":"A single chunk in an upsert request.\n\nThe canonical field is ``chunk_id``. The legacy ``id`` alias is still\naccepted on input for backwards compatibility but is not emitted on\noutput."},"ChunkStatusItem":{"properties":{"chunk_id":{"type":"string","title":"Chunk Id","description":"Caller-supplied chunk ID"},"status":{"type":"string","enum":["created","updated"],"title":"Status","description":"Outcome for this chunk: 'created' (new) or 'updated' (overwrote existing)."}},"type":"object","required":["chunk_id","status"],"title":"ChunkStatusItem"},"ChunkUpsertRequest":{"properties":{"doc_id":{"type":"string","maxLength":128,"minLength":1,"title":"Doc Id","description":"Logical document ID for grouping chunks"},"doc_type":{"type":"string","maxLength":64,"title":"Doc Type","description":"Document type (sop, kb, shopify, etc.)","default":"custom"},"pipeline":{"type":"string","maxLength":64,"title":"Pipeline","description":"Named pipeline config (enrichment/vector strategy)","default":""},"priority":{"anyOf":[{"type":"integer","maximum":10.0,"minimum":1.0},{"type":"null"}],"title":"Priority","description":"Document priority 1-10 used by Source Priority Engine for ranking boost. When omitted, the tier default for this doc_type is used (see Admin Panel → Source Priority)."},"chunks":{"items":{"$ref":"#/components/schemas/ChunkItem"},"type":"array","maxItems":500,"minItems":1,"title":"Chunks","description":"Chunks to upsert (1-500)"}},"type":"object","required":["doc_id","chunks"],"title":"ChunkUpsertRequest","examples":[{"chunks":[{"chunk_id":"sop-trigger-001","metadata":{"keywords":["tracking","order","shipping"],"sop_group":"Orders & Shipping","source_type":"sop","vector_type":"trigger"},"text":"Customer asks about order tracking, shipping status, where is my order"},{"chunk_id":"sop-steps-001","metadata":{"sop_group":"Orders & Shipping","source_type":"sop","vector_type":"steps"},"text":"Step 1: Check order status in Shopify admin. Step 2: Provide tracking number."}],"doc_id":"sop-doc-550e8400-e29b","doc_type":"sop"}]},"ChunkUpsertResponse":{"properties":{"status":{"type":"string","title":"Status","default":"ok"},"doc_id":{"type":"string","title":"Doc Id"},"upserted":{"type":"integer","title":"Upserted"},"index_total":{"type":"integer","title":"Index Total"},"chunks":{"items":{"$ref":"#/components/schemas/ChunkStatusItem"},"type":"array","title":"Chunks"}},"type":"object","required":["doc_id","upserted","index_total","chunks"],"title":"ChunkUpsertResponse","examples":[{"chunks":[{"chunk_id":"sop-trigger-001","status":"created"},{"chunk_id":"sop-steps-001","status":"created"}],"doc_id":"sop-doc-550e8400-e29b","index_total":4521,"status":"ok","upserted":2}]},"ClientConfigsListResponse":{"properties":{"clients":{"items":{"$ref":"#/components/schemas/PipelineConfigSummary"},"type":"array","title":"Clients"}},"type":"object","required":["clients"],"title":"ClientConfigsListResponse"},"CrawlDoneWebhook":{"properties":{"event":{"type":"string","const":"crawl.done","title":"Event","default":"crawl.done"},"crawl_id":{"type":"string","title":"Crawl Id"},"status":{"type":"string","enum":["done","stopped","error"],"title":"Status"},"pages_found":{"type":"integer","title":"Pages Found"},"pages_processed":{"type":"integer","title":"Pages Processed"},"pages_error":{"type":"integer","title":"Pages Error"},"total_chunks":{"type":"integer","title":"Total Chunks"},"finished_at":{"type":"string","title":"Finished At","description":"ISO-8601 UTC timestamp"}},"type":"object","required":["crawl_id","status","pages_found","pages_processed","pages_error","total_chunks","finished_at"],"title":"CrawlDoneWebhook","description":"Sent once when a crawl job reaches a terminal state."},"CrawlPageInfo":{"properties":{"url":{"type":"string","title":"Url"},"doc_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Doc Id"},"status":{"type":"string","enum":["pending","processing","ready","error","skipped","empty"],"title":"Status","description":"Per-page status. One of: pending, processing, ready, error, skipped, empty."},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"},"depth":{"type":"integer","title":"Depth"},"title":{"type":"string","title":"Title","default":""},"chunk_count":{"type":"integer","title":"Chunk Count","default":0},"extracted_text_length":{"type":"integer","title":"Extracted Text Length","default":0}},"type":"object","required":["url","status","depth"],"title":"CrawlPageInfo"},"CrawlPageWebhook":{"properties":{"event":{"type":"string","const":"crawl.page","title":"Event","default":"crawl.page"},"crawl_id":{"type":"string","title":"Crawl Id"},"doc_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Doc Id","description":"Document ID, null if the page was skipped/errored before indexing"},"url":{"type":"string","title":"Url"},"status":{"type":"string","enum":["pending","processing","ready","error","skipped","empty"],"title":"Status"},"chunk_count":{"type":"integer","title":"Chunk Count","default":0},"filename":{"type":"string","title":"Filename","default":""},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"}},"type":"object","required":["crawl_id","url","status"],"title":"CrawlPageWebhook","description":"Sent for each crawled page once its processing finishes."},"CrawlReprocessResponse":{"properties":{"crawl_id":{"type":"string","title":"Crawl Id"},"status":{"$ref":"#/components/schemas/CrawlStatus"},"message":{"type":"string","title":"Message"}},"type":"object","required":["crawl_id","status","message"],"title":"CrawlReprocessResponse"},"CrawlRequest":{"properties":{"start_url":{"type":"string","maxLength":2048,"minLength":10,"title":"Start Url"},"max_depth":{"type":"integer","maximum":10.0,"minimum":1.0,"title":"Max Depth","default":3},"max_pages":{"type":"integer","maximum":500.0,"minimum":1.0,"title":"Max Pages","default":50},"crawl_delay":{"type":"number","maximum":10.0,"minimum":0.5,"title":"Crawl Delay","default":1.0},"url_pattern":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Url Pattern"},"exclude_pattern":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Exclude Pattern"},"follow_sitemap":{"type":"boolean","title":"Follow Sitemap","default":true},"respect_robots":{"type":"boolean","title":"Respect Robots","default":true},"strip_query_params":{"type":"boolean","title":"Strip Query Params","default":true},"doc_type":{"type":"string","title":"Doc Type","default":"webpage"},"category":{"type":"string","title":"Category","default":""},"priority":{"anyOf":[{"type":"integer","maximum":10.0,"minimum":1.0},{"type":"null"}],"title":"Priority","description":"Document priority 1-10. When omitted, the tier default for this doc_type is used."},"analyze_images":{"type":"boolean","title":"Analyze Images","description":"Extract and analyze images from crawled pages via Vision LLM","default":false}},"type":"object","required":["start_url"],"title":"CrawlRequest"},"CrawlResponse":{"properties":{"crawl_id":{"type":"string","title":"Crawl Id"},"start_url":{"type":"string","title":"Start Url"},"max_pages":{"type":"integer","title":"Max Pages"},"status":{"$ref":"#/components/schemas/CrawlStatus","description":"Initial crawl status. One of: pending, running, done, stopped, error."}},"type":"object","required":["crawl_id","start_url","max_pages","status"],"title":"CrawlResponse"},"CrawlStatus":{"type":"string","enum":["pending","running","done","stopped","error"],"title":"CrawlStatus"},"CrawlStatusResponse":{"properties":{"crawl_id":{"type":"string","title":"Crawl Id"},"status":{"$ref":"#/components/schemas/CrawlStatus"},"start_url":{"type":"string","title":"Start Url"},"pages_found":{"type":"integer","title":"Pages Found"},"pages_processed":{"type":"integer","title":"Pages Processed"},"pages_error":{"type":"integer","title":"Pages Error"},"total_chunks":{"type":"integer","title":"Total Chunks","default":0},"pages":{"items":{"$ref":"#/components/schemas/CrawlPageInfo"},"type":"array","title":"Pages"},"created_at":{"type":"string","title":"Created At"},"finished_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Finished At"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"}},"type":"object","required":["crawl_id","status","start_url","pages_found","pages_processed","pages_error","pages","created_at"],"title":"CrawlStatusResponse"},"CrawlStopResponse":{"properties":{"message":{"type":"string","title":"Message"}},"type":"object","required":["message"],"title":"CrawlStopResponse"},"DataTypeInfo":{"properties":{"name":{"type":"string","title":"Name","description":"Programmatic name of the data type"},"label":{"type":"string","title":"Label","description":"Human-readable label","default":""},"description":{"type":"string","title":"Description","description":"Short description for UI","default":""},"supports_scheduling":{"type":"boolean","title":"Supports Scheduling","description":"Whether this data_type can be polled on a recurring schedule. When false, the Schedule API rejects creation of schedules for it.","default":false},"default_interval_hours":{"anyOf":[{"type":"integer","maximum":8760.0,"minimum":1.0},{"type":"null"}],"title":"Default Interval Hours","description":"Recommended polling interval in hours. Used as the default when a client creates a schedule without specifying one. Null when supports_scheduling=false."},"min_interval_hours":{"anyOf":[{"type":"integer","maximum":8760.0,"minimum":1.0},{"type":"null"}],"title":"Min Interval Hours","description":"Hard lower bound on polling interval (rate-limit / API courtesy). The Schedule API rejects intervals below this value. Null = no enforcement."}},"type":"object","required":["name"],"title":"DataTypeInfo","description":"Single data type exposed by a provider (e.g. 'tickets', 'chats').\n\nBeyond identification fields, declares whether this data_type can be\npolled on a recurring schedule via the Pull Ingestion Scheduler. Each\ndata_type may have its own polling cadence — e.g. tickets every 6h,\ndepartments once a week."},"DeleteNamespaceResponse":{"properties":{"namespace":{"type":"string","title":"Namespace"},"deleted":{"type":"boolean","title":"Deleted"},"vectors_deleted":{"type":"integer","title":"Vectors Deleted"}},"type":"object","required":["namespace","deleted","vectors_deleted"],"title":"DeleteNamespaceResponse"},"DocumentDeleteResponse":{"properties":{"doc_id":{"type":"string","title":"Doc Id"},"deleted_vectors":{"type":"integer","title":"Deleted Vectors"}},"type":"object","required":["doc_id","deleted_vectors"],"title":"DocumentDeleteResponse"},"DocumentListResponse":{"properties":{"documents":{"items":{"$ref":"#/components/schemas/DocumentResponse"},"type":"array","title":"Documents"},"total":{"type":"integer","title":"Total","description":"Total number of documents matching the filter (across all pages)"},"limit":{"type":"integer","title":"Limit","description":"Page size used for this response"},"offset":{"type":"integer","title":"Offset","description":"Offset used for this response"}},"type":"object","required":["documents","total","limit","offset"],"title":"DocumentListResponse"},"DocumentProcessedWebhook":{"properties":{"doc_id":{"type":"string","title":"Doc Id","description":"Document identifier"},"status":{"type":"string","enum":["pending","processing","ready","empty","error"],"title":"Status","description":"Terminal document status: ready / empty / error"},"chunk_count":{"type":"integer","title":"Chunk Count","description":"Number of chunks indexed (0 on error)"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error","description":"Error message if status is 'error'; otherwise null"},"filename":{"type":"string","title":"Filename","description":"Original filename or generated synthetic name"}},"type":"object","required":["doc_id","status","chunk_count","filename"],"title":"DocumentProcessedWebhook","description":"Sent when a document upload / fetch-url / reprocess job finishes\n(successfully or with an error)."},"DocumentResponse":{"properties":{"doc_id":{"type":"string","title":"Doc Id"},"client_id":{"type":"string","title":"Client Id","default":""},"filename":{"type":"string","title":"Filename"},"file_size":{"type":"integer","title":"File Size"},"doc_type":{"type":"string","title":"Doc Type"},"category":{"type":"string","title":"Category","default":""},"priority":{"type":"integer","title":"Priority","default":5},"status":{"type":"string","enum":["pending","processing","ready","empty","error"],"title":"Status","description":"Current document status. One of: pending, processing, ready, empty, error."},"chunk_count":{"type":"integer","title":"Chunk Count","default":0},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"},"created_at":{"type":"string","title":"Created At","default":""},"processed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Processed At"},"source":{"type":"string","title":"Source","default":"upload"},"source_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Url"},"crawl_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Crawl Id"}},"type":"object","required":["doc_id","filename","file_size","doc_type","status"],"title":"DocumentResponse"},"DocumentStats":{"properties":{"documents_total":{"type":"integer","title":"Documents Total","description":"Total documents in the namespace"},"documents_ready":{"type":"integer","title":"Documents Ready","description":"Documents in 'ready' state"},"documents_processing":{"type":"integer","title":"Documents Processing","description":"Documents currently being processed"},"documents_error":{"type":"integer","title":"Documents Error","description":"Documents that failed processing"},"chunks_total":{"type":"integer","title":"Chunks Total","description":"Total chunks across all documents"},"clients_total":{"type":"integer","title":"Clients Total","description":"Number of distinct client_ids in the namespace"},"sources":{"anyOf":[{"additionalProperties":{"type":"integer"},"type":"object"},{"type":"null"}],"title":"Sources","description":"Document count grouped by source (e.g. {'upload': 12, 'crawl': 5, 'ingest': 30})."}},"type":"object","required":["documents_total","documents_ready","documents_processing","documents_error","chunks_total","clients_total"],"title":"DocumentStats","description":"Aggregated document counters for a namespace.\n\nAll counters are always returned (server-side initialised to 0). The\n``sources`` map gives a per-source breakdown of document count."},"DocumentUploadResponse":{"properties":{"doc_id":{"type":"string","title":"Doc Id","description":"Generated document identifier"},"client_id":{"type":"string","title":"Client Id"},"filename":{"type":"string","title":"Filename"},"file_size":{"type":"integer","title":"File Size"},"status":{"type":"string","enum":["pending","processing","ready","empty","error"],"title":"Status","description":"Initial status. Typically 'processing' immediately after upload; watch GET /documents/{doc_id} or the webhook callback for the terminal state."},"doc_type":{"type":"string","title":"Doc Type"}},"type":"object","required":["doc_id","client_id","filename","file_size","status","doc_type"],"title":"DocumentUploadResponse"},"ErrorResponse":{"properties":{"error":{"type":"string","title":"Error","description":"Machine-readable snake_case error code (e.g. 'not_found', 'invalid_request', 'rate_limited')."},"message":{"type":"string","title":"Message","description":"Human-readable error message suitable for logging or end-user display."},"details":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Details","description":"Optional structured context: field-level validation errors, upstream provider response, etc. Shape depends on error type."}},"type":"object","required":["error","message"],"title":"ErrorResponse","description":"Canonical error envelope for every non-2xx response.","examples":[{"error":"not_found","message":"Document not found"},{"details":{"errors":[{"loc":["body","client_id"],"msg":"field required","type":"missing"}]},"error":"validation_error","message":"Request body failed validation"}]},"FetchUrlRequest":{"properties":{"url":{"type":"string","maxLength":2048,"minLength":10,"title":"Url"},"doc_type":{"type":"string","title":"Doc Type","default":"webpage"},"category":{"type":"string","title":"Category","default":""},"priority":{"anyOf":[{"type":"integer","maximum":10.0,"minimum":1.0},{"type":"null"}],"title":"Priority","description":"Document priority 1-10. When omitted, the tier default for this doc_type is used (see Admin Panel → Source Priority)."},"analyze_images":{"type":"boolean","title":"Analyze Images","description":"Extract and analyze images from the page via Vision LLM","default":false}},"type":"object","required":["url"],"title":"FetchUrlRequest"},"FetchUrlResponse":{"properties":{"doc_id":{"type":"string","title":"Doc Id"},"source_url":{"type":"string","title":"Source Url"},"status":{"type":"string","title":"Status"}},"type":"object","required":["doc_id","source_url","status"],"title":"FetchUrlResponse"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"HealthResponse":{"properties":{"status":{"type":"string","title":"Status"},"s3_connected":{"type":"boolean","title":"S3 Connected"},"cache_namespaces":{"type":"integer","title":"Cache Namespaces"},"cache_memory_mb":{"type":"number","title":"Cache Memory Mb"},"cache_memory_limit_mb":{"type":"integer","title":"Cache Memory Limit Mb"},"uptime_seconds":{"type":"integer","title":"Uptime Seconds"}},"type":"object","required":["status","s3_connected","cache_namespaces","cache_memory_mb","cache_memory_limit_mb","uptime_seconds"],"title":"HealthResponse"},"IngestDoneWebhook":{"properties":{"event":{"type":"string","const":"ingest.done","title":"Event","default":"ingest.done"},"ingest_id":{"type":"string","title":"Ingest Id"},"provider":{"type":"string","title":"Provider"},"namespace":{"type":"string","title":"Namespace"},"status":{"type":"string","enum":["pending","running","completed","failed","cancelled"],"title":"Status"},"stats":{"$ref":"#/components/schemas/IngestProgress","description":"Final counters (raw_fetched, documents_aggregated, documents_indexed, chunks_created)"},"timestamp":{"type":"string","title":"Timestamp"}},"type":"object","required":["ingest_id","provider","namespace","status","stats","timestamp"],"title":"IngestDoneWebhook","description":"Sent once when an ingest job completes successfully."},"IngestErrorWebhook":{"properties":{"event":{"type":"string","const":"ingest.error","title":"Event","default":"ingest.error"},"ingest_id":{"type":"string","title":"Ingest Id"},"provider":{"type":"string","title":"Provider"},"namespace":{"type":"string","title":"Namespace"},"status":{"type":"string","const":"failed","title":"Status"},"error":{"additionalProperties":true,"type":"object","title":"Error","description":"Error envelope: {code: snake_case, message: human readable}"},"timestamp":{"type":"string","title":"Timestamp"}},"type":"object","required":["ingest_id","provider","namespace","status","error","timestamp"],"title":"IngestErrorWebhook","description":"Sent if an ingest job fails."},"IngestHistoryResponse":{"properties":{"syncs":{"items":{"$ref":"#/components/schemas/IngestStatusResponse"},"type":"array","title":"Syncs","description":"Most recent ingest jobs for the namespace, newest first."}},"type":"object","required":["syncs"],"title":"IngestHistoryResponse"},"IngestProgress":{"properties":{"items_fetched":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Items Fetched","description":"Number of raw items fetched from the provider so far"},"items_total":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Items Total","description":"Total raw items the provider expects to return, if known"},"raw_fetched":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Raw Fetched","description":"Total raw records fetched (final count after fetching phase)"},"documents_aggregated":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Documents Aggregated","description":"Number of aggregated documents produced from raw records"},"documents_indexed":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Documents Indexed","description":"Number of documents successfully indexed into the vector store"},"chunks_created":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Chunks Created","description":"Total chunks created across all indexed documents"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status","description":"Free-form transient status hint (e.g. 'starting')"}},"additionalProperties":true,"type":"object","title":"IngestProgress","description":"Progress counters reported by an ingest job.\n\nAll fields are optional and may appear depending on the current\nphase. The shape is stable: consumers can read any subset."},"IngestProgressWebhook":{"properties":{"event":{"type":"string","const":"ingest.progress","title":"Event","default":"ingest.progress"},"ingest_id":{"type":"string","title":"Ingest Id"},"provider":{"type":"string","title":"Provider"},"namespace":{"type":"string","title":"Namespace"},"phase":{"type":"string","enum":["fetching","aggregating","indexing","finalizing"],"title":"Phase"},"progress":{"$ref":"#/components/schemas/IngestProgress"},"timestamp":{"type":"string","title":"Timestamp","description":"ISO-8601 UTC timestamp"}},"type":"object","required":["ingest_id","provider","namespace","phase","progress","timestamp"],"title":"IngestProgressWebhook","description":"Sent at each phase boundary while an ingest job is running."},"IngestRequest":{"properties":{"provider":{"type":"string","maxLength":64,"minLength":1,"title":"Provider"},"credentials":{"additionalProperties":true,"type":"object","title":"Credentials","description":"Provider-specific credentials. Shape varies per provider — see GET /providers/{provider}/credential_fields. Optional override; if omitted, credentials configured via Admin Panel for this namespace are used."},"config":{"additionalProperties":true,"type":"object","title":"Config","description":"Provider-specific configuration (e.g. data_types, date ranges, concurrency). Shape varies per provider."},"pipeline":{"type":"string","maxLength":64,"title":"Pipeline","description":"Named pipeline config (enrichment/vector strategy)","default":""},"webhook_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Webhook Url","description":"Override webhook URL"},"phases":{"anyOf":[{"items":{"type":"string","enum":["fetch","aggregate","index"]},"type":"array"},{"type":"null"}],"title":"Phases","description":"Which pipeline phases to run. Default (omit or null) = all three: ['fetch','aggregate','index']. Use partial subsets for advanced scenarios:\n\n- `['fetch']` — refresh data.sqlite from the source API only, do not re-aggregate or re-index.\n- `['aggregate','index']` — re-process existing data.sqlite without dialing the source API. Useful when API is down or when clustering parameters changed.\n- `['index']` — re-index already-aggregated documents from data.sqlite. Useful after changing embedding model or pipeline config without re-running expensive fetch/aggregate phases.\n\nThe index phase is **resumable** — on crash mid-run, the next execution picks up the first row whose `indexed_at` is still NULL. Combine with `force_full=true` to re-index everything from scratch (clears all `indexed_at` flags before indexing)."},"force_full":{"type":"boolean","title":"Force Full","description":"If true, the aggregate phase clears all `indexed_at` flags in the persistent aggregated_documents store before writing, forcing the index phase to re-process every document instead of only the unindexed ones. Use after changing embedding model, pipeline config, or chunking strategy.","default":false},"schedule":{"anyOf":[{"items":{"$ref":"#/components/schemas/ScheduleConfig"},"type":"array"},{"type":"null"}],"title":"Schedule","description":"Optional. After running the initial ingest, also create recurring schedules for the listed data_types. Each entry validates against the provider's scheduling rules (supports_scheduling, min_interval_hours). Omit to run a one-shot ingest with no recurring follow-ups."}},"type":"object","required":["provider"],"title":"IngestRequest","examples":[{"config":{"data_types":["tickets","chats","kb","departments"]},"credentials":{"client_id":"246D36D5-...","client_secret":"73285AB8..."},"provider":"livehelpnow"}]},"IngestResponse":{"properties":{"ingest_id":{"type":"string","title":"Ingest Id","description":"Canonical ingest job identifier"},"provider":{"type":"string","title":"Provider"},"status":{"type":"string","enum":["pending","running","completed","failed","cancelled"],"title":"Status","description":"Initial job status, typically 'pending' immediately after creation"},"data_available":{"additionalProperties":{"anyOf":[{"type":"integer"},{"type":"string"}]},"type":"object","title":"Data Available","description":"Per-data-type availability returned by the provider's credential validation step. Keys are provider-specific data type names (e.g. 'tickets', 'kb_articles', 'departments'). Values are typically item counts (int); providers that cannot count exactly may return 'available' as a string sentinel."}},"type":"object","required":["ingest_id","provider","status"],"title":"IngestResponse"},"IngestStatusResponse":{"properties":{"ingest_id":{"type":"string","title":"Ingest Id","description":"Canonical ingest job identifier"},"provider":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Provider","description":"Data provider name (e.g. 'livehelpnow')"},"status":{"type":"string","enum":["pending","running","completed","failed","cancelled"],"title":"Status","description":"Job status. One of: pending, running, completed, failed, cancelled."},"phase":{"anyOf":[{"type":"string","enum":["fetching","aggregating","indexing","finalizing"]},{"type":"null"}],"title":"Phase","description":"Current pipeline phase. One of: fetching, aggregating, indexing, finalizing. Null when the job has not started or is finished."},"progress":{"$ref":"#/components/schemas/IngestProgress","description":"Progress counters (see IngestProgress)."},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error","description":"Error message if status is 'failed'"},"started_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Started At","description":"ISO-8601 UTC timestamp when the job entered 'running'"},"finished_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Finished At","description":"ISO-8601 UTC timestamp when the job reached a terminal status"}},"type":"object","required":["ingest_id","status"],"title":"IngestStatusResponse","description":"Current state of a single ingest job. Same shape is used by\n/status and (per-item) by /history."},"MCPCallRequest":{"properties":{"tool":{"type":"string","title":"Tool","description":"MCP tool name (see /tools)"},"params":{"additionalProperties":true,"type":"object","title":"Params","description":"Tool-specific parameters as defined by MCPToolDefinitionModel.parameters"}},"type":"object","required":["tool"],"title":"MCPCallRequest"},"MCPCallResponse":{"properties":{"tool":{"type":"string","title":"Tool"},"result":{"anyOf":[{"additionalProperties":true,"type":"object"},{"items":{},"type":"array"}],"title":"Result","description":"Tool result. Shape is tool-specific — see the tool definition's 'returns' field on the provider documentation."}},"type":"object","required":["tool","result"],"title":"MCPCallResponse"},"MCPToolDefinitionModel":{"properties":{"name":{"type":"string","title":"Name","description":"Tool name (passed as 'tool' in /call)"},"description":{"type":"string","title":"Description","description":"Human-readable tool description","default":""},"parameters":{"additionalProperties":true,"type":"object","title":"Parameters","description":"JSON Schema describing accepted parameters"}},"type":"object","required":["name"],"title":"MCPToolDefinitionModel","description":"OpenAPI-friendly view of an MCP tool definition."},"MCPToolsResponse":{"properties":{"provider":{"type":"string","title":"Provider"},"tools":{"items":{"$ref":"#/components/schemas/MCPToolDefinitionModel"},"type":"array","title":"Tools"}},"type":"object","required":["provider","tools"],"title":"MCPToolsResponse"},"PipelineAggregationCategory":{"properties":{"category":{"type":"string","title":"Category","description":"Category label as emitted by the provider's aggregator"},"cluster_count":{"type":"integer","title":"Cluster Count","description":"Number of aggregated cluster rows whose `category` metadata field equals this value."},"total_docs":{"type":"integer","title":"Total Docs","description":"Sum of `cluster_size` across those clusters — how many raw source documents the category covers in total."}},"type":"object","required":["category","cluster_count","total_docs"],"title":"PipelineAggregationCategory","description":"One category (or department) that drove cluster formation.\n\nFilled from `metadata_json.category` on aggregated cluster rows.\n`cluster_count` is how many clusters exist under this category;\n`total_docs` sums `metadata_json.cluster_size` across those\nclusters, answering \"how many raw docs does this category cover\"."},"PipelineArchivePresence":{"properties":{"s3_key":{"type":"string","title":"S3 Key","description":"S3 key of the archive file, always `{namespace}/providers/{provider}/data.sqlite` by convention."},"size_bytes":{"type":"integer","title":"Size Bytes","description":"Size of the locally cached archive file in bytes"},"s3_last_modified":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"S3 Last Modified","description":"S3 `LastModified` at the time of the most recent download into the local cache. Used by the archive cache TTL check to detect out-of-band changes on S3."},"cached":{"type":"boolean","title":"Cached","description":"Whether the archive is currently held in the on-disk provider archive cache. True right after a stats fetch because collecting stats itself hydrates the cache."}},"type":"object","required":["s3_key","size_bytes","cached"],"title":"PipelineArchivePresence","description":"Presence metadata for the provider's `data.sqlite` snapshot."},"PipelineConfigDeleteResponse":{"properties":{"deleted":{"type":"boolean","title":"Deleted"}},"type":"object","required":["deleted"],"title":"PipelineConfigDeleteResponse"},"PipelineConfigListResponse":{"properties":{"configs":{"items":{"$ref":"#/components/schemas/PipelineConfigSummary"},"type":"array","title":"Configs"}},"type":"object","required":["configs"],"title":"PipelineConfigListResponse"},"PipelineConfigRequest":{"properties":{"enrichment_enabled":{"type":"boolean","title":"Enrichment Enabled","default":true},"enrichment_prompt":{"type":"string","maxLength":10000,"title":"Enrichment Prompt","description":"Custom LLM prompt template for enrichment","default":""},"enrichment_output_schema":{"additionalProperties":true,"type":"object","title":"Enrichment Output Schema","description":"Opaque user-supplied JSON Schema that constrains the LLM enrichment output. Vector Storage does not interpret this — it is forwarded as-is to the enrichment provider for structured-output enforcement."},"enrichment_max_text":{"type":"integer","maximum":50000.0,"minimum":100.0,"title":"Enrichment Max Text","description":"Max chars of text passed to the LLM per chunk","default":3000},"vector_strategy":{"type":"string","pattern":"^(single|multi)$","title":"Vector Strategy","description":"'single' = one vector per chunk; 'multi' = one vector per template","default":"single"},"vector_templates":{"items":{"$ref":"#/components/schemas/VectorTemplateSchema"},"type":"array","title":"Vector Templates"},"chunk_size_tokens":{"type":"integer","maximum":4000.0,"minimum":50.0,"title":"Chunk Size Tokens","default":400},"chunk_overlap_tokens":{"type":"integer","maximum":500.0,"minimum":0.0,"title":"Chunk Overlap Tokens","default":50}},"type":"object","title":"PipelineConfigRequest","examples":[{"chunk_overlap_tokens":50,"chunk_size_tokens":400,"enrichment_enabled":true,"enrichment_output_schema":{"properties":{"title":{"type":"string"},"summary":{"type":"string"},"tags":{"items":{"type":"string"},"type":"array"}},"required":["title","summary"],"type":"object"},"enrichment_prompt":"Extract: title, summary, tags","vector_strategy":"single"}]},"PipelineConfigResponse":{"properties":{"client_id":{"type":"string","title":"Client Id"},"config_name":{"type":"string","title":"Config Name"},"enrichment_enabled":{"type":"boolean","title":"Enrichment Enabled"},"enrichment_prompt":{"type":"string","title":"Enrichment Prompt"},"enrichment_output_schema":{"additionalProperties":true,"type":"object","title":"Enrichment Output Schema","description":"Opaque user-supplied JSON Schema for LLM output validation"},"enrichment_max_text":{"type":"integer","title":"Enrichment Max Text"},"vector_strategy":{"type":"string","title":"Vector Strategy"},"vector_templates":{"items":{"$ref":"#/components/schemas/VectorTemplateSchema"},"type":"array","title":"Vector Templates"},"chunk_size_tokens":{"type":"integer","title":"Chunk Size Tokens"},"chunk_overlap_tokens":{"type":"integer","title":"Chunk Overlap Tokens"},"created_at":{"type":"string","title":"Created At"},"updated_at":{"type":"string","title":"Updated At"}},"type":"object","required":["client_id","config_name","enrichment_enabled","enrichment_prompt","enrichment_output_schema","enrichment_max_text","vector_strategy","vector_templates","chunk_size_tokens","chunk_overlap_tokens","created_at","updated_at"],"title":"PipelineConfigResponse"},"PipelineConfigSummary":{"properties":{"client_id":{"type":"string","title":"Client Id"},"config_name":{"type":"string","title":"Config Name"},"enrichment_enabled":{"type":"boolean","title":"Enrichment Enabled"},"vector_strategy":{"type":"string","title":"Vector Strategy"},"vector_templates_count":{"type":"integer","title":"Vector Templates Count"},"updated_at":{"type":"string","title":"Updated At"}},"type":"object","required":["client_id","config_name","enrichment_enabled","vector_strategy","vector_templates_count","updated_at"],"title":"PipelineConfigSummary"},"PipelineDateRange":{"properties":{"label":{"type":"string","title":"Label","description":"Entity label, e.g. 'chats' or 'tickets'"},"earliest":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Earliest","description":"Earliest ISO timestamp found, or null if empty"},"latest":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Latest","description":"Latest ISO timestamp found, or null if empty"}},"type":"object","required":["label"],"title":"PipelineDateRange","description":"Min/max timestamp for one logical entity in a provider archive.\n\n`label` is the caller-chosen name ('chats', 'tickets', ...); the\ncollector fills it from a provider-supplied spec so every provider\ncan pick its own labels without changing the response shape."},"PipelineLastSync":{"properties":{"job_id":{"type":"string","title":"Job Id","description":"Ingest job identifier"},"finished_at":{"type":"string","title":"Finished At","description":"ISO-8601 timestamp when the job entered the done state"},"raw_fetched":{"type":"integer","title":"Raw Fetched","description":"Number of raw records pulled from the upstream API during the FETCH phase of this job (incremental or force-full)."},"documents_aggregated":{"type":"integer","title":"Documents Aggregated","description":"Number of aggregated documents written to the `aggregated_documents` table during the AGGREGATE phase."},"documents_indexed":{"type":"integer","title":"Documents Indexed","description":"Number of aggregated documents that actually went through the INDEX phase — i.e. new or changed since the previous run. Thanks to deterministic doc_id, unchanged documents are skipped, so this can be much lower than `documents_aggregated` on incremental runs."},"chunks_created":{"type":"integer","title":"Chunks Created","description":"Number of FAISS chunks written during the INDEX phase. Matches the added `vector_count` delta for this job."},"errors":{"type":"integer","title":"Errors","description":"Non-fatal errors encountered during the job"},"phases":{"items":{"type":"string"},"type":"array","title":"Phases","description":"Sorted list of phases this job ran. Normal runs include all three (fetch, aggregate, index); partial runs may execute any subset."},"force_full":{"type":"boolean","title":"Force Full","description":"Whether this job was requested as a full re-index rather than an incremental update."}},"type":"object","required":["job_id","finished_at","raw_fetched","documents_aggregated","documents_indexed","chunks_created","errors","phases","force_full"],"title":"PipelineLastSync","description":"Summary of the last completed ingest job for a provider.\n\nRead from `{namespace}/providers/{provider}/last_sync.json`, written\nby `ingest_worker.finalize` after a successful job. Null if no\ningest has ever run for this namespace/provider pair."},"PipelineTopKeyword":{"properties":{"keyword":{"type":"string","title":"Keyword","description":"Keyword string as emitted by the provider's aggregator"},"weight":{"type":"integer","title":"Weight","description":"Sum of `cluster_size` across clusters that contain this keyword — approximates how many raw documents the keyword effectively represents. Sort key of the list."},"clusters":{"type":"integer","title":"Clusters","description":"Number of distinct clusters mentioning this keyword"}},"type":"object","required":["keyword","weight","clusters"],"title":"PipelineTopKeyword","description":"One keyword that showed up in the aggregated clusters.\n\nExtracted from `metadata_json.keywords` (JSON array) via\n`json_each`. `weight` is the sum of `cluster_size` of the clusters\nthat contain the keyword — i.e. how many raw documents the keyword\neffectively represents. `clusters` is the raw count of clusters."},"ProviderInfo":{"properties":{"name":{"type":"string","title":"Name"},"has_mcp":{"type":"boolean","title":"Has Mcp","description":"Whether the provider exposes MCP tools"},"data_types":{"items":{"$ref":"#/components/schemas/DataTypeInfo"},"type":"array","title":"Data Types"}},"type":"object","required":["name","has_mcp"],"title":"ProviderInfo"},"ProviderPipelineStats":{"properties":{"name":{"type":"string","title":"Name","description":"Data provider name"},"has_archive":{"type":"boolean","title":"Has Archive","description":"Whether this provider has a data.sqlite snapshot in S3 for this namespace"},"archive":{"anyOf":[{"$ref":"#/components/schemas/PipelineArchivePresence"},{"type":"null"}],"description":"Presence metadata for the `data.sqlite` snapshot. Null when the provider has no archive in S3 for this namespace — though such entries are already filtered out of the `/stats` response."},"raw_documents_total":{"type":"integer","title":"Raw Documents Total","description":"Total rows in the raw `documents` table of the archive, i.e. `sum(raw_documents_by_type.values())`.","default":0},"raw_documents_by_type":{"additionalProperties":{"type":"integer"},"type":"object","title":"Raw Documents By Type","description":"Row count of the raw `documents` table grouped by doc_type. Keys are provider-specific strings."},"aggregated_documents_total":{"type":"integer","title":"Aggregated Documents Total","description":"Total rows in the `aggregated_documents` table — sum of clusters and pass-through rows across all doc_types.","default":0},"aggregated_documents_by_type":{"additionalProperties":{"type":"integer"},"type":"object","title":"Aggregated Documents By Type","description":"Row count of the pre-computed aggregated layer grouped by doc_type."},"aggregation_categories":{"items":{"$ref":"#/components/schemas/PipelineAggregationCategory"},"type":"array","title":"Aggregation Categories","description":"Categories (or departments) that were used as the grouping key when the aggregator built clusters, sorted by total raw documents descending."},"top_keywords":{"items":{"$ref":"#/components/schemas/PipelineTopKeyword"},"type":"array","title":"Top Keywords","description":"Top keywords extracted from aggregated cluster metadata, weighted by cluster_size so categories covering more raw docs dominate the tail."},"date_ranges":{"items":{"$ref":"#/components/schemas/PipelineDateRange"},"type":"array","title":"Date Ranges","description":"Earliest/latest timestamps for provider-specified entity labels. Empty for providers that don't expose date fields."},"last_sync":{"anyOf":[{"$ref":"#/components/schemas/PipelineLastSync"},{"type":"null"}],"description":"Summary of the most recent ingest job for this provider/namespace pair, read from `last_sync.json` in S3. Null if no ingest has ever completed, or if the file is missing or corrupt."}},"type":"object","required":["name","has_archive"],"title":"ProviderPipelineStats","description":"Universal pipeline stats for one data provider in a namespace.\n\nSame shape for every provider — counts are dicts keyed by\nprovider-specific `doc_type` strings, so future providers can add\nnew types without changing the schema. Only surfaces in the\n/stats response when `has_archive=True`."},"ProvidersListResponse":{"properties":{"providers":{"items":{"$ref":"#/components/schemas/ProviderInfo"},"type":"array","title":"Providers"}},"type":"object","required":["providers"],"title":"ProvidersListResponse"},"QueryRequest":{"properties":{"text":{"type":"string","maxLength":5000,"minLength":1,"title":"Text"},"top_k":{"type":"integer","maximum":100.0,"minimum":1.0,"title":"Top K","default":5},"filters":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Filters","description":"Metadata filters applied as post-filtering after vector search. Filters match against ANY metadata field stored with vectors.\n\n**Standard fields:** doc_id, client_id, doc_type, category, priority, heading, chunk_index, content_type, source, source_url, crawl_id, has_phone, has_price, has_datetime, has_address.\n\n**LLM-enriched fields** (when LLM enrichment is enabled): llm_category, llm_language, llm_entities, llm_topics, llm_content_kind.\n\n**Supported operators:**\n- Exact match: `{\"doc_type\": \"faq\"}`\n- IN (one of list): `{\"doc_type\": [\"faq\", \"price_list\"]}`\n- Greater/equal: `{\"priority_gte\": 5}`\n- Less/equal: `{\"priority_lte\": 8}`\n- Array contains: `{\"llm_topics_contains\": \"pricing\"}`\n- Boolean: `{\"has_price\": true}`\n\nAll filters are combined with AND logic. OR between values of the same field is supported via list syntax."},"score_threshold":{"anyOf":[{"type":"number","maximum":1.0,"minimum":0.0},{"type":"null"}],"title":"Score Threshold","description":"Minimum similarity score (0.0-1.0). Results below this threshold are excluded."},"search_mode":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Search Mode","description":"Search mode:\n- `hybrid` (default) — FAISS semantic + BM25 keyword search combined via RRF fusion\n- `dense` — FAISS only (semantic similarity)\n- `sparse` — BM25 only (keyword matching)"},"dense_weight":{"anyOf":[{"type":"number","maximum":1.0,"minimum":0.0},{"type":"null"}],"title":"Dense Weight","description":"Weight of FAISS (dense) results in hybrid fusion (0.0-1.0). BM25 weight = 1 - dense_weight. Default: 0.6."}},"type":"object","required":["text"],"title":"QueryRequest","examples":[{"dense_weight":0.6,"filters":{"has_price":true,"llm_category":"price_list","llm_topics_contains":"pricing"},"score_threshold":0.5,"search_mode":"hybrid","text":"subscription pricing plans","top_k":5}]},"QueryResponse":{"properties":{"results":{"items":{"$ref":"#/components/schemas/QueryResultItem"},"type":"array","title":"Results","description":"Ranked list of matching chunks"},"search_time_ms":{"type":"number","title":"Search Time Ms","description":"Search time in milliseconds (excluding embedding)"},"embedding_time_ms":{"type":"number","title":"Embedding Time Ms","description":"Query embedding time in milliseconds"},"total_vectors":{"type":"integer","title":"Total Vectors","description":"Total vectors in the searched namespace"}},"type":"object","required":["results","search_time_ms","embedding_time_ms","total_vectors"],"title":"QueryResponse"},"QueryResultItem":{"properties":{"chunk_id":{"type":"string","title":"Chunk Id","description":"Vector / chunk identifier. For documents indexed via the document pipeline this has the format `{doc_id}_chunk_{index}`. For chunks pushed via `POST /chunks/upsert` this is the caller-supplied chunk_id verbatim."},"score":{"type":"number","title":"Score","description":"Final relevance score. In hybrid mode this is the RRF fusion score **after** the Source Priority boost has been applied (see `base_score` for the pre-boost value). In dense mode it is the cosine similarity; in sparse mode the BM25 score."},"base_score":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Base Score","description":"Pre-boost RRF score, only set in hybrid mode when a non-trivial priority boost was applied to this result. Use `score / base_score` to read back the multiplier; or read `priority_boost_applied` directly."},"priority_boost_applied":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Priority Boost Applied","description":"Multiplier applied to the RRF score by the Source Priority Engine. Values >1.0 mean the chunk was boosted (high priority); values <1.0 mean it was penalised. Null when no boost was applied (default-priority chunks, dense or sparse mode, or boost weight = 0)."},"chunk_text":{"type":"string","title":"Chunk Text","description":"Original chunk text content","default":""},"metadata":{"additionalProperties":true,"type":"object","title":"Metadata","description":"Chunk metadata. Standard fields: doc_id, client_id, doc_type, category, priority, heading, chunk_index, content_type, has_phone, has_price, has_datetime, has_address, source, source_url, crawl_id. LLM-enriched fields (when enrichment is enabled): llm_category, llm_language, llm_entities, llm_topics, llm_content_kind. Image chunks may include image_key, image_url, image_url_expires_in."}},"type":"object","required":["chunk_id","score"],"title":"QueryResultItem"},"RebuildResponse":{"properties":{"namespace":{"type":"string","title":"Namespace"},"status":{"type":"string","title":"Status"}},"type":"object","required":["namespace","status"],"title":"RebuildResponse"},"ScheduleConfig":{"properties":{"data_type":{"type":"string","title":"Data Type","description":"Provider data_type to schedule (must have supports_scheduling=true)."},"interval_hours":{"type":"integer","maximum":8760.0,"minimum":1.0,"title":"Interval Hours","description":"Polling interval in hours. Must respect the data_type's min_interval_hours when set."},"enabled":{"type":"boolean","title":"Enabled","description":"Set to false to create the schedule paused.","default":true}},"type":"object","required":["data_type","interval_hours"],"title":"ScheduleConfig","description":"Single recurring update entry for one provider data_type.\n\nUsed both inside ``IngestRequest.schedule`` (when creating schedules\nalongside the initial ingest) and inside ``UpsertSchedulesRequest``\n(standalone schedule management API)."},"ScheduleListResponse":{"properties":{"schedules":{"items":{"$ref":"#/components/schemas/ScheduleResponse"},"type":"array","title":"Schedules"}},"type":"object","required":["schedules"],"title":"ScheduleListResponse"},"ScheduleResponse":{"properties":{"schedule_id":{"type":"string","title":"Schedule Id","description":"Internal identifier"},"namespace":{"type":"string","title":"Namespace"},"client_id":{"type":"string","title":"Client Id"},"provider":{"type":"string","title":"Provider"},"data_type":{"type":"string","title":"Data Type"},"interval_hours":{"type":"integer","title":"Interval Hours"},"enabled":{"type":"boolean","title":"Enabled"},"last_run_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Run At","description":"ISO-8601 UTC timestamp of the last run start, or null if the schedule has never fired."},"last_job_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Job Id","description":"ingest_id of the last (or current) ingest job triggered by this schedule."},"last_status":{"anyOf":[{"type":"string","enum":["pending","running","completed","failed","cancelled"]},{"type":"null"}],"title":"Last Status","description":"Status of the last run. One of: pending, running, completed, failed, cancelled."},"last_error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Error","description":"Error message from the last failed run, if any."},"next_run_at":{"type":"string","title":"Next Run At","description":"ISO-8601 UTC timestamp of the next planned run."},"created_at":{"type":"string","title":"Created At"},"updated_at":{"type":"string","title":"Updated At"}},"type":"object","required":["schedule_id","namespace","client_id","provider","data_type","interval_hours","enabled","next_run_at","created_at","updated_at"],"title":"ScheduleResponse","description":"Single schedule with its full state."},"StatsResponse":{"properties":{"namespace":{"type":"string","title":"Namespace","description":"Base namespace the caller asked about. Stays unchanged even when `?client_id=` scopes the FAISS view to a composite internal key."},"vector_count":{"type":"integer","title":"Vector Count","description":"Number of FAISS vectors in the (composite or naked) namespace. Read from the cache entry when loaded, otherwise from the persisted manifest."},"vector_dimensions":{"type":"integer","title":"Vector Dimensions","description":"Embedding dimensionality. When the namespace is empty this falls back to the active embedding provider's default so the field is never misleadingly stale."},"index_type":{"type":"string","title":"Index Type","description":"FAISS index type in use — `Flat`, `IVF`, `IVFPQ`, or `unknown` for an empty/unloaded namespace."},"index_size_bytes":{"type":"integer","title":"Index Size Bytes","description":"Memory footprint of the FAISS index in bytes"},"wal_segments":{"type":"integer","title":"Wal Segments","description":"Number of pending WAL segments waiting to be compacted into the main index via rebuild."},"last_rebuild_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Rebuild At","description":"ISO-8601 timestamp of the most recent rebuild, or null if the namespace has never been rebuilt."},"cached":{"type":"boolean","title":"Cached","description":"Whether the namespace is currently loaded in the in-memory FAISS cache."},"documents":{"anyOf":[{"$ref":"#/components/schemas/DocumentStats"},{"type":"null"}],"description":"Aggregated document counts from StatsStore. Filtered by `client_id` when the query parameter is supplied, otherwise summed across every client of this namespace. Null when StatsStore is not enabled for this instance."},"pipelines":{"items":{"$ref":"#/components/schemas/ProviderPipelineStats"},"type":"array","title":"Pipelines","description":"Per-data-provider pipeline statistics — one element per registered provider that has an archive in S3 for this namespace. Providers with no archive are filtered out. This list is independent of `client_id` because the archive is namespace-scoped, not composite-scoped."}},"type":"object","required":["namespace","vector_count","vector_dimensions","index_type","index_size_bytes","wal_segments","cached"],"title":"StatsResponse","description":"Full `/v1/ns/{ns}/stats` payload.\n\nThe response combines three views of a namespace:\n\n  * **FAISS view** (top-level scalar fields) — live numbers from\n    the in-process index manager. When `?client_id=` is supplied\n    they reflect the composite `{ns}__{client_id}` internal key;\n    otherwise they reflect the naked namespace (which is zero\n    for any tenant using client_id isolation).\n  * **Document view** (`documents`) — aggregated counts from\n    `StatsStore`, filtered by `client_id` if provided.\n  * **Pipeline view** (`pipelines`) — per-data-provider snapshot\n    of the `data.sqlite` archive each provider persists to S3."},"UpsertSchedulesRequest":{"properties":{"schedules":{"items":{"$ref":"#/components/schemas/ScheduleConfig"},"type":"array","maxItems":20,"minItems":1,"title":"Schedules","description":"Schedules to upsert. Existing entries with the same (provider, data_type) tuple are replaced atomically."}},"type":"object","required":["schedules"],"title":"UpsertSchedulesRequest"},"ValidateCredentialsRequest":{"properties":{"credentials":{"additionalProperties":true,"type":"object","title":"Credentials","description":"Provider-specific credentials to validate. Shape varies per provider."},"data_types":{"items":{"type":"string"},"type":"array","title":"Data Types","description":"Data types to check access for (empty = all data types the provider supports)"}},"type":"object","required":["credentials"],"title":"ValidateCredentialsRequest"},"ValidateCredentialsResponse":{"properties":{"ok":{"type":"boolean","title":"Ok","description":"True if credentials are valid for all requested data types"},"data_available":{"additionalProperties":{"anyOf":[{"type":"integer"},{"type":"string"}]},"type":"object","title":"Data Available","description":"Per-data-type availability. Keys are provider-specific data type names. Values are typically item counts (int); some providers return 'available' as a string sentinel when an exact count is unavailable."},"details":{"additionalProperties":{"type":"string"},"type":"object","title":"Details","description":"Per-check status messages. Each value either starts with 'ok' (success) or describes the failure (e.g. 'failed: invalid token')."}},"type":"object","required":["ok"],"title":"ValidateCredentialsResponse"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"VectorTemplateSchema":{"properties":{"id_suffix":{"type":"string","maxLength":32,"minLength":1,"title":"Id Suffix","description":"Vector ID suffix appended to {doc_id}_chunk_{idx}_<suffix>"},"text_template":{"type":"string","minLength":1,"title":"Text Template","description":"Jinja2 template rendered with enrichment data to produce the vector text"},"metadata_include":{"items":{"type":"string"},"type":"array","title":"Metadata Include","description":"Whitelist of LLM-extracted fields to copy into chunk metadata. Empty = include all."}},"type":"object","required":["id_suffix","text_template"],"title":"VectorTemplateSchema"},"WarmupResponse":{"properties":{"namespace":{"type":"string","title":"Namespace"},"status":{"type":"string","title":"Status"},"load_time_ms":{"type":"number","title":"Load Time Ms"},"vector_count":{"type":"integer","title":"Vector Count"},"cache_size_mb":{"type":"number","title":"Cache Size Mb"}},"type":"object","required":["namespace","status","load_time_ms","vector_count","cache_size_mb"],"title":"WarmupResponse"}},"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"API key","description":"Bearer token issued via the Admin Panel. Set once via the Authorize button and reused for all secured requests."}}},"tags":[{"name":"health","description":"Health and monitoring"},{"name":"management","description":"Namespace lifecycle (warmup, rebuild, stats, delete)"},{"name":"documents","description":"Document upload, fetch, and management"},{"name":"crawl","description":"Website crawling — multi-page document ingestion"},{"name":"chunks","description":"Pre-chunked text upsert (caller-supplied IDs + metadata → embedding + indexing)"},{"name":"pipeline","description":"Named pipeline configs (enrichment prompts, vector strategies)"},{"name":"ingest","description":"Data provider ingestion and MCP tools"},{"name":"query","description":"Text-based semantic search (embed + hybrid search)"},{"name":"admin","description":"Admin panel API (requires admin role)"},{"name":"vectors","description":"Low-level vector CRUD and search"}],"security":[{"BearerAuth":[]}]}