{"openapi":"3.1.0","info":{"title":"Mantle API","description":"Universal context layer for AI agents","version":"0.1.0"},"paths":{"/v1/health":{"get":{"tags":["health","health"],"summary":"Liveness probe","description":"Lightweight liveness probe — returns the service identity and version without touching any dependency. Intentionally unauthenticated and rate-unlimited so Azure Container Apps' health probe can call it at any frequency. For dependency health, use `GET /v1/ready` (operator) or `GET /v1/status/public` (public).","operationId":"health_check_v1_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Health Check V1 Health Get"}}}}}}},"/v1/ready":{"get":{"tags":["health","health"],"summary":"Readiness probe — exercises Postgres, Neo4j, Redis","description":"Returns 200 when every required dependency is reachable, 503 otherwise. Used by Azure Container Apps / K8s as the readiness gate before routing traffic to a new instance during rolling deploys.\n\nPublic, unauthenticated, rate-unlimited — platform probes call this constantly and must not be auth-gated. **Per-dependency labels are omitted** so the response can't be used by an external attacker to map our infrastructure or time an attack to a Postgres/Neo4j failover. Operators triaging a 503 should use the authenticated ``/v1/db-check``, ``/v1/graph-quality`` diagnostics for the granular dep state, or scrape ``/metrics``.","operationId":"readiness_check_v1_ready_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"503":{"description":"One or more dependencies unreachable."}}}},"/v1/graph-quality":{"get":{"tags":["health","health"],"summary":"Operator-only: graph-quality metrics across the platform","description":"Aggregate counts from the knowledge graph — total entities, connected vs orphan rate, relationship count, document count, community count, top entity and relationship types — surfaced **globally** (not tenant-scoped). Originally exposed under the tenant-admin gate; promoted to ``require_platform_operator`` after a pre-launch audit caught it returning cross-tenant aggregates that any customer admin could read.","operationId":"graph_quality_v1_graph_quality_get","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Graph Quality V1 Graph Quality Get"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"403":{"description":"Caller lacks the required role/scope. Session users need `admin` role; API keys need the `admin` scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}}}}},"/v1/db-check":{"get":{"tags":["health","health"],"summary":"Diagnostic: Postgres reachability","description":"Single Postgres `SELECT 1` round-trip with a 10-second connect timeout, returning the result value. Useful when triaging an alert to confirm whether it's the database or the app. Admin only.","operationId":"db_check_v1_db_check_get","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Db Check V1 Db Check Get"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"403":{"description":"Caller lacks the required role/scope. Session users need `admin` role; API keys need the `admin` scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}}}}},"/v1/sentry-test":{"post":{"tags":["health","health"],"summary":"Diagnostic: emit a test event to Sentry","description":"Deliberately raises a labeled exception so the Sentry integration can be smoke-tested end-to-end. The event lands in the project configured by ``SENTRY_DSN`` with ``service=backend``, ``environment=<MANTLE_ENV>``, and ``release=<SENTRY_RELEASE>``. Used once after wiring the DSN, and again whenever rotating it.\n\nReturns 500 when Sentry is initialized (the raised exception is what produces the Sentry event); returns 503 when Sentry is disabled (no DSN) so the caller knows the test was a no-op. Admin only — never expose this to customers.","operationId":"sentry_test_v1_sentry_test_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Sentry Test V1 Sentry Test Post"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"403":{"description":"Caller lacks the required role/scope. Session users need `admin` role; API keys need the `admin` scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}}}}},"/v1/graph-debug":{"get":{"tags":["health","health"],"summary":"Operator-only: detailed graph composition across the platform","description":"Operator deep-dive: entity types + counts, document types, doc↔entity relationship breakdowns, sample Slack-tagged entities, multi-source entities, doc-to-doc links — surfaced **globally** for staff triage. Promoted from tenant-admin to ``require_platform_operator`` after a pre-launch audit caught the response leaking entity NAMES + properties + document titles across tenants.","operationId":"graph_debug_v1_graph_debug_get","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Graph Debug V1 Graph Debug Get"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"403":{"description":"Caller lacks the required role/scope. Session users need `admin` role; API keys need the `admin` scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}}}}},"/v1/migrate":{"post":{"tags":["health","health"],"summary":"Diagnostic: ensure database tables exist","description":"Calls SQLAlchemy `Base.metadata.create_all` against the production engine. Idempotent: tables that already exist are untouched. Intended as a recovery hatch for first-time deploys; production schema changes go through Alembic via `alembic upgrade head` (see ops runbook). Admin only.","operationId":"run_migrations_v1_migrate_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Run Migrations V1 Migrate Post"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"403":{"description":"Caller lacks the required role/scope. Session users need `admin` role; API keys need the `admin` scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}}}}},"/v1/stats":{"get":{"tags":["health","health"],"summary":"Aggregate corpus stats for the active tenant","description":"Single-call summary used by the dashboard's home tile: counts of sources, documents, chunks, entities, relationships, and communities, plus the LLM and embedding models the platform is currently configured to use. Tenant-scoped for the counts; the model fields are operator-visible (same answer for every tenant until config changes). Returns zeroes (not an error) for any store that's transiently unavailable so the dashboard renders rather than blanks out.","operationId":"get_stats_v1_stats_get","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Stats V1 Stats Get"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/auth/login-url":{"get":{"tags":["auth","auth"],"summary":"Get the WorkOS authorization URL","description":"Generates the URL the frontend should redirect a signing-in user to. WorkOS handles the actual authentication (SSO, social, magic link) and redirects back to the configured callback. In dev mode with no WorkOS key configured, returns a `?code=dev` URL that exercises the dev-token path.","operationId":"login_url_v1_auth_login_url_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Login Url V1 Auth Login Url Get"}}}}}}},"/v1/auth/callback":{"post":{"tags":["auth","auth"],"summary":"Exchange a WorkOS auth code for tokens","description":"Completes the WorkOS authorization-code flow. On first login for a new WorkOS user, this also provisions the user's Organization, default Tenant, and admin Membership. Returns a JWT pair (`access_token` + optional `refresh_token`) the dashboard stores for subsequent API calls.","operationId":"auth_callback_v1_auth_callback_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthCallbackRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthResponse"}}}},"422":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}}}}},"/v1/auth/refresh":{"post":{"tags":["auth","auth"],"summary":"Refresh an expired access token","description":"Exchanges a previously-issued `refresh_token` for a new access token (and a rotated refresh token). Used by the dashboard's background token-refresh loop. On any failure (rate limit, WorkOS outage, expired token) the response is a generic 401 — the specific reason is logged server-side but never surfaced to the client to avoid helping attackers distinguish failure modes.","operationId":"refresh_token_v1_auth_refresh_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefreshRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthResponse"}}}},"422":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}},"401":{"description":"Refresh token invalid, expired, or provider-rejected."}}}},"/v1/auth/logout":{"post":{"tags":["auth","auth"],"summary":"Get a WorkOS-hosted logout URL to terminate the IdP session","description":"Returns a WorkOS-hosted logout URL the dashboard should redirect the browser to before clearing the local token. WorkOS terminates the AuthKit session cookie and 302s back to `return_to` (defaults to the frontend's `/auth/login`).\n\nWithout this round-trip, AuthKit's session cookie persists across our local token clear: the next 'Continue with SSO' click silently re-authenticates the previous user, and there's no way to switch accounts in the same browser. This endpoint is the IdP-side complement to local-token clearing.\n\nFor dev tokens (`access_token` starts with `dev:`) and any case where we can't extract a `sid` claim, returns `logout_url=null`. The frontend should treat null as 'no IdP session to terminate' and proceed with the local clear only.","operationId":"logout_v1_auth_logout_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogoutRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogoutResponse"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/auth/me/principals":{"get":{"tags":["auth","auth"],"summary":"Diagnostic: caller's resolved ACL principals + linked external identities","description":"Returns the principal set the ACL filter would compute for the current request, plus every ``UserExternalIdentity`` row tied to the caller's user_id. Diagnostic-only — useful when graph queries return empty unexpectedly. The principal set is what every ACL-filtered cypher query gets compared against; linked identities are the rows ``POST /v1/sources/{id}/claim-identity`` writes.","operationId":"me_principals_v1_auth_me_principals_get","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Me Principals V1 Auth Me Principals Get"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/auth/me":{"get":{"tags":["auth","auth"],"summary":"Get current authenticated user and tenant context","description":"Returns the authenticated user identity, the currently active tenant, and the organization that owns the tenant — all in a single round-trip. Use to populate the dashboard header, derive the role-based UI, and avoid a follow-up `GET /v1/tenants` call.\n\n**Pending-org users**: self-serve signups that haven't completed the org-setup step are returned with `needs_org_setup=true` and empty `org_id`/`tenant_id`/`role` fields. The frontend's dashboard layout uses this flag to redirect to `/onboarding/setup-org`. Once the user submits `POST /v1/auth/complete-signup`, subsequent `/v1/auth/me` calls return the full populated shape.\n\n`org_name` and `tenant_name` are display labels; for cross-tenant switching see `GET /v1/tenants`. The `role` field is one of `admin`, `member`, `viewer`, or `api_key` (when authenticated via an API key rather than a user session). `is_org_admin` is a platform-org-level flag — true for users who can create new workspaces and manage org-wide membership; orthogonal to the tenant-scoped `role`.","operationId":"me_v1_auth_me_get","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeResponse"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/auth/complete-signup":{"post":{"tags":["auth","auth"],"summary":"Provide org name to complete signup","description":"Provisions the caller's Organization, default Tenant, and admin membership. Used by self-serve signups (personal gmail / outlook accounts with no WorkOS organization) to pick an organization name after WorkOS authentication completes.\n\nIdempotent in spirit but strict in practice: if the caller already has `org_id` set, returns 409 with the existing org info — there is no upgrade path from one org to another via this endpoint.\n\nThe first user of an org is auto-promoted to `is_org_admin=true` since they are the org's creator. Subsequent invitees default to non-org-admin and become tenant-level admin only on workspaces they're invited to.","operationId":"complete_signup_v1_auth_complete_signup_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompleteSignupRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompleteSignupResponse"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"422":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}},"409":{"description":"Caller already has an organization. No-op; returns the existing org info."}}}},"/v1/tenants":{"get":{"tags":["tenants","tenants"],"summary":"List tenants in your organization","description":"Returns every tenant inside the caller's organization, with `is_active` flagging the one currently selected by the request's `X-Tenant-Id` header (or the dev token's tenant). Use this to populate a tenant switcher; pair with `GET /v1/auth/me` to know which one is the user's current context.","operationId":"list_tenants_v1_tenants_get","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/TenantResponse"},"title":"Response List Tenants V1 Tenants Get"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["tenants","tenants"],"summary":"Create a workspace inside the caller's organization","description":"Creates a new tenant (workspace) inside the caller's organization, with the caller auto-promoted to a tenant-admin membership row so they can immediately invite or rename. Restricted to org-admins to keep workspace sprawl under explicit control — tenant-admins of an existing workspace cannot mint sibling workspaces.\n\nSlugs are server-generated (URL-safe + 6-char random tail) and are NOT globally unique; only the org+id pair identifies a workspace. Returns the new workspace with `is_active=false` (switching is a client-side action via `X-Tenant-Id`).","operationId":"create_tenant_v1_tenants_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTenantBody"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TenantResponse"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"403":{"description":"Caller lacks the required role/scope. Session users need `admin` role; API keys need the `admin` scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}}}}},"/v1/tenants/members":{"get":{"tags":["tenants","tenants"],"summary":"List members of the active tenant","description":"Returns every user with a membership row on the caller's active tenant, with role and join timestamp. Used by the dashboard's Team page; safe to call from any read-scoped principal. Members from other tenants in the same organization are NOT included — memberships are tenant-scoped, not org-scoped.","operationId":"list_members_v1_tenants_members_get","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/MemberResponse"},"title":"Response List Members V1 Tenants Members Get"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/tenants/{tenant_id}":{"delete":{"tags":["tenants","tenants"],"summary":"Permanently delete a tenant (GDPR / closure)","description":"Wipes every trace of a tenant from both stores: Neo4j entities/documents/communities filtered by `tenant_id`, and the Postgres `tenants` row (which cascades to api_keys, usage_events, credit_ledger, data_sources, sync_events, context_objects, source_identity_mapping, relationship_types, usage_summary, and user_tenant_memberships). **Irreversible.**\n\nGuardrails: admin role/scope; the path tenant id must equal the caller's active tenant AND live inside the caller's org; the body must include `confirm` matching the tenant's slug exactly. Cross-tenant or cross-org attempts return 404 (not 403) so existence is not leaked. Rate-limited to 3/minute.\n\nIdempotent past first success — once the row is gone, retries 404.","operationId":"delete_tenant_v1_tenants__tenant_id__delete","parameters":[{"name":"tenant_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Tenant Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteTenantRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteTenantResponse"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"403":{"description":"Caller lacks the required role/scope. Session users need `admin` role; API keys need the `admin` scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}},"404":{"description":"Resource not found, or not visible to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"400":{"description":"Confirmation mismatch — `confirm` did not match the tenant's slug."}}},"patch":{"tags":["tenants","tenants"],"summary":"Rename a workspace","description":"Updates the workspace name. Slug is intentionally NOT regenerated — existing URLs / API-key audit metadata reference the slug, and renaming is a display-name change from the user's perspective. If you need a new slug, delete and re-create the workspace.\n\nAuth: tenant-admin of the workspace OR an org-admin of the owning org. Cross-org / cross-tenant attempts return 404 to avoid leaking workspace existence.","operationId":"rename_tenant_v1_tenants__tenant_id__patch","parameters":[{"name":"tenant_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Tenant Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenameTenantBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TenantResponse"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"403":{"description":"Caller lacks the required role/scope. Session users need `admin` role; API keys need the `admin` scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}},"404":{"description":"Resource not found, or not visible to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/tenants/{tenant_id}/members/{user_id}":{"delete":{"tags":["tenants","tenants"],"summary":"Remove a member from a workspace","description":"Removes the user's membership row from the workspace. The user account itself is not deleted — they remain a member of any other workspace they belong to, and continue to authenticate.\n\nLast-admin protection: removing the only tenant-admin of a workspace returns 400 `LAST_TENANT_ADMIN_PROTECTED` unless an org-admin in the same org could re-promote someone. The rule lives in `Tenant.can_remove_admin` and is shared with the role-change endpoint.\n\nOrg-admin protection: if the target is the only org-admin of the org and removing this membership would leave them without any membership in the org, returns 400 `LAST_ORG_ADMIN_PROTECTED`. (For now we approximate by checking if their only org-admin presence is via this org — see `Organization.can_remove_org_admin`.)","operationId":"remove_member_v1_tenants__tenant_id__members__user_id__delete","parameters":[{"name":"tenant_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Tenant Id"}},{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MembershipMutationResponse"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"403":{"description":"Caller lacks the required role/scope. Session users need `admin` role; API keys need the `admin` scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}},"404":{"description":"Resource not found, or not visible to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"400":{"description":"Last-admin protection or self-removal violation."}}},"patch":{"tags":["tenants","tenants"],"summary":"Change a member's role within a workspace","description":"Sets `UserTenantMembership.role` to the requested value (`admin` / `member` / `viewer`). Last-admin protection applies on demotions away from `admin` — see `Tenant.can_remove_admin` for the rule (shared with member removal). Idempotent: setting a role to its current value returns 200 with no audit write.","operationId":"change_member_role_v1_tenants__tenant_id__members__user_id__patch","parameters":[{"name":"tenant_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Tenant Id"}},{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangeMemberRoleBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MembershipMutationResponse"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"403":{"description":"Caller lacks the required role/scope. Session users need `admin` role; API keys need the `admin` scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}},"404":{"description":"Resource not found, or not visible to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"400":{"description":"Last-admin protection violation."}}}},"/v1/tenants/{tenant_id}/invitations":{"post":{"tags":["tenants","tenants"],"summary":"Invite a user to a workspace","description":"Creates a pending invitation for `email` to join the workspace with role `admin` / `member` / `viewer`. WorkOS-native delivery is used when the org has a `workos_org_id` (verified-domain email + revocable invitation id); personal-account orgs return the accept URL for the inviter to deliver manually (Phase A — transactional email is Phase B).\n\nIdempotency: a duplicate pending invite for the same `(tenant, email)` upserts the role rather than 409 — re-inviting a user with a corrected role just works. The DB-level partial unique index enforces `(tenant_id, lower(email)) WHERE accepted_at IS NULL AND revoked_at IS NULL` so concurrent creators serialize to one row.\n\nPre-flight checks: `ALREADY_MEMBER` (409) if the email already has a membership in this tenant; `USER_BELONGS_TO_OTHER_ORG` (409) when the email is a known user in a different org.","operationId":"create_tenant_invitation_v1_tenants__tenant_id__invitations_post","parameters":[{"name":"tenant_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Tenant Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInvitationBody"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvitationResponse"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"403":{"description":"Caller lacks the required role/scope. Session users need `admin` role; API keys need the `admin` scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}},"404":{"description":"Resource not found, or not visible to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"get":{"tags":["tenants","tenants"],"summary":"List pending invitations for a workspace","description":"Returns every invitation that is currently pending (`accepted_at IS NULL AND revoked_at IS NULL AND expires_at > now()`). Accepted, revoked, and expired invitations are filtered out — use the audit log for historical lookups.","operationId":"list_tenant_invitations_v1_tenants__tenant_id__invitations_get","parameters":[{"name":"tenant_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Tenant Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/InvitationResponse"},"title":"Response List Tenant Invitations V1 Tenants  Tenant Id  Invitations Get"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"403":{"description":"Caller lacks the required role/scope. Session users need `admin` role; API keys need the `admin` scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}},"404":{"description":"Resource not found, or not visible to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/tenants/{tenant_id}/invitations/{invitation_id}":{"delete":{"tags":["tenants","tenants"],"summary":"Revoke a pending invitation","description":"Marks the invitation `revoked_at = now()` and best-effort calls WorkOS `revoke_invitation` so the email link can't be re-clicked into action. Idempotent — revoking an already-revoked invitation returns 200 with `revoked: true`.","operationId":"revoke_tenant_invitation_v1_tenants__tenant_id__invitations__invitation_id__delete","parameters":[{"name":"tenant_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Tenant Id"}},{"name":"invitation_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Invitation Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Revoke Tenant Invitation V1 Tenants  Tenant Id  Invitations  Invitation Id  Delete"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"403":{"description":"Caller lacks the required role/scope. Session users need `admin` role; API keys need the `admin` scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}},"404":{"description":"Resource not found, or not visible to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/invitations/{token}":{"get":{"tags":["invitations","invitations"],"summary":"Public invitation preview","description":"Unauthenticated. Renders the workspace + org + inviter so the invitee can verify the invitation looks legitimate before signing in. Uniform 404 for missing / revoked / accepted / expired tokens to avoid enumeration leaks.","operationId":"preview_invitation_v1_invitations__token__get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvitationPreview"}}}},"404":{"description":"Resource not found, or not visible to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/invitations/{token}/accept":{"post":{"tags":["invitations","invitations"],"summary":"Accept an invitation","description":"Authenticated. The signed-in user's email must match the invitation email (case-insensitive). On success: a `UserTenantMembership` row is created with the invitation's role, the user's `org_id` is set to the invitation's org if it was previously NULL (pending self-serve user), and `accepted_at` is stamped.\n\nEdge cases:\n* `INVITE_EMAIL_MISMATCH` (403) — signed-in email differs.\n* `INVITE_EXPIRED` (410) — past `expires_at`.\n* `USER_BELONGS_TO_OTHER_ORG` (409) — caller is in a different   org than the invitation; cross-org migration is Phase B.\n* `ALREADY_MEMBER` (409) — caller is already a member of this   workspace (idempotent retry safety).","operationId":"accept_invitation_v1_invitations__token__accept_post","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Token"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AcceptResponse"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"404":{"description":"Resource not found, or not visible to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/billing/checkout/payg":{"post":{"tags":["billing","billing"],"summary":"Start a pay-as-you-go credit purchase","description":"Creates a Stripe Checkout Session for a one-shot credit top-up and returns the URL the user should be redirected to. Pricing is 1 credit = $0.01. After payment, Stripe redirects the user back to `STRIPE_SUCCESS_URL` and fires a `checkout.session.completed` webhook that grants the credits via `record_stripe_credit`.\n\nRestricted to org admins so a tenant-admin of one workspace cannot charge the company card. Rate-limited to 10/min per credential to discourage typo-driven repeated charges.","operationId":"start_payg_checkout_v1_billing_checkout_payg_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaygCheckoutRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutResponse"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"403":{"description":"Caller lacks the required role/scope. Session users need `admin` role; API keys need the `admin` scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}}}}},"/v1/billing/checkout/commitment":{"post":{"tags":["billing","billing"],"summary":"Start a 6-month commitment purchase","description":"Creates a Stripe Checkout Session for a fixed-price upfront commitment. Customer pays the full amount once, gets bonus credits, and any unused credits expire at the end of the term (180 days at launch). The webhook handler stamps `expires_at` on the resulting credit_ledger row using a server-side timestamp computed at checkout-creation time, so client clock skew can't shorten the term.\n\nRestricted to org admins. Rate-limited to 5/min — these are deliberate, larger transactions; a higher cadence almost certainly indicates a typo or attack.","operationId":"start_commitment_checkout_v1_billing_checkout_commitment_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommitmentCheckoutRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutResponse"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"403":{"description":"Caller lacks the required role/scope. Session users need `admin` role; API keys need the `admin` scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}}}}},"/v1/billing/portal":{"post":{"tags":["billing","billing"],"summary":"Open the Stripe-hosted billing portal","description":"Returns a one-shot URL the user can visit to manage their payment method, view past invoices, and download receipts. Implemented as a redirect to Stripe-hosted UI — we don't build any of this ourselves.\n\nReturns 400 `NO_PAYMENT_METHOD` when the org has never paid (no Stripe customer exists yet), so the FE can hide the 'Manage payments' link rather than showing a broken button.","operationId":"open_billing_portal_v1_billing_portal_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutResponse"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"403":{"description":"Caller lacks the required role/scope. Session users need `admin` role; API keys need the `admin` scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}}}}},"/v1/billing/info":{"get":{"tags":["billing","billing"],"summary":"Billing state for the dashboard","description":"Returns the data the FE needs to render the billing page chrome: whether the org has a Stripe customer (controls the 'Manage payments' link), and the list of active commitments with their expiry dates (drives the 'X credits expire on Y' banner above the balance).","operationId":"get_billing_info_v1_billing_info_get","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BillingInfoResponse"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/usage/balance":{"get":{"tags":["usage","usage"],"summary":"Get current credit balance","description":"Returns the tenant's current credit balance as a floating-point value. A balance ≤ 0 causes credit-consuming endpoints (search, extraction, embeddings) to return 402 Payment Required. Top up via [Dashboard → Billing](https://app.mantleai.dev/dashboard/billing).","operationId":"usage_balance_v1_usage_balance_get","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BalanceResponse"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/usage/events":{"get":{"tags":["usage","usage"],"summary":"List recent usage events","description":"Paginated history of credit-consuming operations: embeddings, extractions, completions. Each row carries the provider/model, input/output token counts, latency, and the credits debited. Use to surface a usage timeline in your billing UI or to reconcile spend per resource type. Newest-first ordering; default limit 50, max 200.","operationId":"usage_events_v1_usage_events_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UsageEventResponse"},"title":"Response Usage Events V1 Usage Events Get"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/connectors":{"get":{"tags":["sources","sources"],"summary":"List available connector types","description":"Returns the manifest for every connector the server supports — Google Drive, Gmail, Slack, Dropbox, GCS, S3, Postgres, etc. — including auth type and config schema. Used by the dashboard's 'Add source' wizard to render the catalog. The list is deployment-determined; no auth required because manifests describe capabilities, not data.","operationId":"list_connectors_v1_connectors_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/ConnectorManifestResponse"},"type":"array","title":"Response List Connectors V1 Connectors Get"}}}}}}},"/v1/sources":{"post":{"tags":["sources","sources"],"summary":"Create a data source","description":"Provisions a new connector instance for the active tenant. The returned source is in `pending` status until OAuth completes (for OAuth connectors) or credentials are stored (for service-account connectors). Use the connector's auth flow next:\n\n- OAuth: `GET /v1/sources/{id}/auth/url` → user-authorize →   `POST /v1/sources/auth/callback`\n- Service account: `POST /v1/sources/{id}/credentials`\n\nConnectors with `permission_model='none'` (GCS, S3, Postgres) are gated behind the `UNSAFE_ENABLE_NO_ACL_CONNECTORS` env var to prevent accidental tenant-wide data exposure.","operationId":"create_source_v1_sources_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSourceRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourceResponse"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"403":{"description":"API key lacks the `write` (or `admin`) scope required for mutations.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}}}},"get":{"tags":["sources","sources"],"summary":"List data sources for the active tenant","description":"Returns every connected source on the active tenant with current status, last-sync timestamp, document count, and any error message. Use to render the 'Connectors' page; pair with `GET /v1/sources/{id}/events` for live sync progress on a specific source.","operationId":"list_sources_v1_sources_get","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SourceResponse"},"title":"Response List Sources V1 Sources Get"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/sources/{source_id}":{"get":{"tags":["sources","sources"],"summary":"Get a single data source","description":"Returns detail for one source — same shape as `GET /v1/sources` but for a single id. Cross-tenant lookups return 404 (not 403) to avoid leaking the existence of sources outside the caller's tenant.","operationId":"get_source_v1_sources__source_id__get","parameters":[{"name":"source_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Source Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourceResponse"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"404":{"description":"Resource not found, or not visible to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["sources","sources"],"summary":"Disconnect a data source","description":"Removes the source row. With `purge_data=true`, also soft-deletes every ContextObject derived from this source AND retracts entity source_refs in Neo4j + removes Document nodes. Without it, the index entries remain (useful when reconnecting the same data under a new auth flow). Cross-tenant requests return 404.","operationId":"remove_source_v1_sources__source_id__delete","parameters":[{"name":"source_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Source Id"}},{"name":"purge_data","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Purge Data"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Remove Source V1 Sources  Source Id  Delete"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"403":{"description":"API key lacks the `write` (or `admin`) scope required for mutations.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}},"404":{"description":"Resource not found, or not visible to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"patch":{"tags":["sources","sources"],"summary":"Rename or reconfigure a data source","description":"Updates `name` and/or `config` on an existing source. When the config change adds folders/channels/labels/prefixes, the next sync rediscovers against the new selection (cursor cleared). When folders are removed, `cleanup_mode` controls whether the data they brought in is preserved (`unlink`, default) or soft-deleted (`purge`).\n\nSome connectors don't yet implement folder→file resolution; for those, `purge` falls back to `unlink_fallback` and the response signals so the UI can warn. Use `POST /cleanup/preview` for a non-mutating impact preview before sending the PATCH.","operationId":"update_source_v1_sources__source_id__patch","parameters":[{"name":"source_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Source Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSourceRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourceEditResponse"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"403":{"description":"API key lacks the `write` (or `admin`) scope required for mutations.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}},"404":{"description":"Resource not found, or not visible to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/sources/{source_id}/events":{"get":{"tags":["sources","sources"],"summary":"Live sync event feed for a source","description":"Streams the most recent extraction events for a source — file synced, file skipped, extraction started/finished, errors. The dashboard's Activity panel polls this every few seconds during an active sync. Filter by `sync_run_id` to scope to a specific run, or pass `since=<ISO timestamp>` for tail-style cursoring.","operationId":"get_source_events_v1_sources__source_id__events_get","parameters":[{"name":"source_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Source Id"}},{"name":"sync_run_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sync Run Id"}},{"name":"since","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Since"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"default":100,"title":"Limit"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SyncEventResponse"},"title":"Response Get Source Events V1 Sources  Source Id  Events Get"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"404":{"description":"Resource not found, or not visible to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/sources/{source_id}/failures":{"get":{"tags":["sources","sources"],"summary":"List unresolved extraction failures (DLQ)","description":"Returns the source's dead-letter queue: every chunk or file that failed extraction and has not been retried successfully. Each row carries `error_class`, `error_message`, and `attempts` so operators can triage. Pair with `POST /retry-failures` to re-enqueue.","operationId":"list_source_failures_v1_sources__source_id__failures_get","parameters":[{"name":"source_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Source Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourceFailuresResponse"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"404":{"description":"Resource not found, or not visible to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/sources/{source_id}/retry-failures":{"post":{"tags":["sources","sources"],"summary":"Re-enqueue extraction for failed files","description":"Triggers an async retry for every file that currently has an unresolved DLQ entry, or — if `file_ids` is provided — only those files. Returns the Celery task id and a count.\n\nIdempotent: extraction dedups via content-hash (Postgres) and MERGE (Neo4j), so the end state is correct even if the same file extracts twice. **Cost note:** if the original failure was a billing or quota issue, retries will burn tokens again — triage via `GET /failures` first.","operationId":"retry_source_failures_v1_sources__source_id__retry_failures_post","parameters":[{"name":"source_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Source Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RetryFailuresRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RetryFailuresResponse"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"403":{"description":"API key lacks the `write` (or `admin`) scope required for mutations.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}},"404":{"description":"Resource not found, or not visible to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/sources/{source_id}/health":{"get":{"tags":["sources","sources"],"summary":"Connector-level health + circuit-breaker state","description":"Returns durable failure state from the data_sources row + the open extraction-failure DLQ count. Used by the FE to render the status badge / 'Reconnect required' CTA / cooldown countdown.\n\n* `auth_status='needs_reauth'` → customer must reconnect;   retrying the existing token burns OAuth quota for nothing.\n* `circuit_breaker='open'` → next_retry_at is in the future;   the worker will skip dispatches until the cooldown expires.\n* `extraction_failure_count > 0` → there are unresolved   per-file failures; pair with `GET /failures` for triage.","operationId":"get_source_health_v1_sources__source_id__health_get","parameters":[{"name":"source_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Source Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourceHealthResponse"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"404":{"description":"Resource not found, or not visible to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/sources/{source_id}/health/reset":{"post":{"tags":["sources","sources"],"summary":"Operator escape hatch: clear connector health state","description":"Resets `consecutive_failures`, `next_retry_at`, `last_error_*`, and `auth_status` to a clean slate. The next dispatch will then exercise the source normally — if the underlying problem is still there, the failure machinery will re-classify it on the next failure and the cooldown kicks back in.\n\nUse cases:\n* You fixed an encryption-key env var; sources are stuck in   needs_reauth but the customer's token is fine.\n* The 24h backoff cap is too long for a recovered source.\n\nAudit-logged. Returns the cleared values so the audit log captures what was wiped.","operationId":"reset_source_health_v1_sources__source_id__health_reset_post","parameters":[{"name":"source_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Source Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourceHealthResetResponse"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"403":{"description":"API key lacks the `write` (or `admin`) scope required for mutations.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}},"404":{"description":"Resource not found, or not visible to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/sources/{source_id}/cleanup/preview":{"post":{"tags":["sources","sources"],"summary":"Preview the impact of a config change (non-mutating)","description":"Computes the added/removed folder diff and the count of files that would be affected if you sent this config in a `PATCH`. Used to populate the dashboard's confirmation dialog ('this will purge N files') before the user commits. Server-authoritative: the binding number for the confirm button must come from here.","operationId":"cleanup_preview_v1_sources__source_id__cleanup_preview_post","parameters":[{"name":"source_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Source Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CleanupPreviewRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CleanupPreviewResponse"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"403":{"description":"API key lacks the `write` (or `admin`) scope required for mutations.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}},"404":{"description":"Resource not found, or not visible to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/sources/{source_id}/files":{"get":{"tags":["sources","sources"],"summary":"List synced files for a source","description":"Returns the files the connector has surfaced — name, modified timestamp, size — excluding any whose chunks have been fully purged. Used by the dashboard's file browser. Reads authoritatively from `sync_state['files']`; cross-references ContextObject rows to hide explicitly-purged files.","operationId":"list_source_files_v1_sources__source_id__files_get","parameters":[{"name":"source_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Source Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourceFilesResponse"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"404":{"description":"Resource not found, or not visible to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/sources/{source_id}/files/purge":{"post":{"tags":["sources","sources"],"summary":"Purge specific synced files (file-level cleanup)","description":"Soft-deletes every ContextObject referencing the listed `file_ids`, plus retracts entity source_refs and removes Document nodes in Neo4j. Tenant + source scoped; foreign file_ids are silently ignored. Use this to cull a subset of files within an otherwise-kept source — complementary to folder-level (`PATCH`) and source-level (`DELETE`) purge.","operationId":"purge_files_v1_sources__source_id__files_purge_post","parameters":[{"name":"source_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Source Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FilesPurgeRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FilesPurgeResponse"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"403":{"description":"API key lacks the `write` (or `admin`) scope required for mutations.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}},"404":{"description":"Resource not found, or not visible to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/sources/{source_id}/browse":{"get":{"tags":["sources","sources"],"summary":"Browse folders on a connected source","description":"Calls the connector's `list_folders` against the source's stored credentials. Used by the 'Pick folders to sync' UI for connectors that support browsing (Google Drive, Dropbox). `parent_id` walks down the hierarchy. Connectors that don't implement `list_folders` (Slack, Gmail, GCS, S3, Postgres) return 400 — those use their native discovery semantics.","operationId":"browse_source_v1_sources__source_id__browse_get","parameters":[{"name":"source_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Source Id"}},{"name":"parent_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Parent Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","additionalProperties":true},"title":"Response Browse Source V1 Sources  Source Id  Browse Get"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"404":{"description":"Resource not found, or not visible to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}}}}},"/v1/sources/{source_id}/credentials":{"post":{"tags":["sources","sources"],"summary":"Store credentials for a non-OAuth source","description":"Encrypts and stores credentials for connectors that don't use OAuth — service account JSON for GCS, AWS access keys for S3, DSN for Postgres. Optionally accepts a ``config`` blob that's merged into the source row before validation; required for connectors whose manifest declares required config fields (GCS/S3 ``bucket_name``). The connector's `validate_credentials` is invoked immediately; if validation fails the credentials are still stored (so the user can fix and retry without re-typing) but `status` reflects the error.","operationId":"store_credentials_v1_sources__source_id__credentials_post","parameters":[{"name":"source_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Source Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CredentialBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Store Credentials V1 Sources  Source Id  Credentials Post"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"403":{"description":"API key lacks the `write` (or `admin`) scope required for mutations.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}},"404":{"description":"Resource not found, or not visible to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/sources/{source_id}/auth/url":{"get":{"tags":["sources","sources"],"summary":"Generate the OAuth authorization URL for a source","description":"Returns the connector-specific OAuth start URL the user should be redirected to. The state parameter is signed and bound to the source id; on the callback we verify it before storing tokens. Only valid for connectors with `auth_type='oauth2'` (Google Drive, Gmail, Slack, Dropbox).","operationId":"get_oauth_url_v1_sources__source_id__auth_url_get","parameters":[{"name":"source_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Source Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthUrlResponse"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"404":{"description":"Resource not found, or not visible to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}}}}},"/v1/sources/auth/callback":{"post":{"tags":["sources","sources"],"summary":"Complete the OAuth flow for a source","description":"Verifies the signed `state`, exchanges the OAuth `code` for access/refresh tokens via the connector, and stores the encrypted token bundle on the source. Status flips to `active` if the source already had a config (re-auth path) or `pending_config` if this is the first auth and folders/labels need to be picked next.","operationId":"oauth_callback_v1_sources_auth_callback_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthCallbackRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourceResponse"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"403":{"description":"API key lacks the `write` (or `admin`) scope required for mutations.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}},"404":{"description":"Resource not found, or not visible to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/sources/{source_id}/claim-identity":{"post":{"tags":["sources","sources"],"summary":"Capture / refresh the connecting user's identity in the source system","description":"Calls the source's userinfo endpoint with the stored OAuth credentials and writes a ``user_external_identities`` row for the calling user. Idempotent — re-running updates the existing row's ``verified_at``. Used to backfill identities for sources connected before this feature shipped, and as a recovery path when the OAuth-callback inline capture failed.\n\nReturns the captured identity. Returns 422 when the connector doesn't support end-user identity capture (e.g. service-account connectors like Postgres/S3/GCS).","operationId":"claim_external_identity_v1_sources__source_id__claim_identity_post","parameters":[{"name":"source_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Source Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Claim External Identity V1 Sources  Source Id  Claim Identity Post"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"404":{"description":"Resource not found, or not visible to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/sources/{source_id}/content/{file_id}":{"get":{"tags":["sources","sources"],"summary":"Fetch full document text from a source on demand","description":"Downloads the file from the original source (Drive, S3, Gmail, etc.), extracts text via the appropriate parser, and returns it truncated at 100 KB. Use when an agent or operator needs full content beyond the chunked semantic summaries already in the graph. The `file_id:path` syntax allows file ids that contain slashes (e.g. S3 keys).\n\n**ACL gate:** the caller must have access to at least one indexed chunk of this file under the per-source ACL — otherwise the response is 404 (not 403, to prevent enumeration of file ids the caller cannot see).","operationId":"get_document_content_v1_sources__source_id__content__file_id__get","parameters":[{"name":"source_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Source Id"}},{"name":"file_id","in":"path","required":true,"schema":{"type":"string","title":"File Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Document Content V1 Sources  Source Id  Content  File Id  Get"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"404":{"description":"Resource not found, or not visible to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Text extraction failed for the file's mime type."},"502":{"description":"Source returned an error fetching the file."}}}},"/v1/sources/{source_id}/query":{"post":{"tags":["sources","sources"],"summary":"Run a read-only SQL query against a connected database","description":"PostgreSQL-only today. The connector enforces a read-only transaction and parses the SQL to reject DDL/DML before execution, so the worst a malicious query can do is exhaust the limit/timeout on the connector. Result rows are coerced to JSON-safe types (datetimes, decimals, etc. become strings).\n\n`limit` defaults to 50 and is enforced server-side regardless of any LIMIT clause in the query.","operationId":"query_source_v1_sources__source_id__query_post","parameters":[{"name":"source_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Source Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","additionalProperties":true},"title":"Response Query Source V1 Sources  Source Id  Query Post"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"404":{"description":"Resource not found, or not visible to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}}}}},"/v1/sources/{source_id}/sync":{"post":{"tags":["sources","sources"],"summary":"Trigger an async sync for a source","description":"Enqueues a Celery extraction task for the source and flips `status` to `syncing`. Track progress via `GET /sources/{id}/events`. Returns immediately — the task runs out-of-band.\n\n`force=true` clears the connector's incremental sync cursor, forcing a full re-discover. Use when you suspect the cursor drifted (rare) or after a backfill of a source whose change stream missed events.","operationId":"trigger_sync_v1_sources__source_id__sync_post","parameters":[{"name":"source_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Source Id"}},{"name":"force","in":"query","required":false,"schema":{"type":"boolean","description":"Clear sync cursor to force full re-extraction","default":false,"title":"Force"},"description":"Clear sync cursor to force full re-extraction"},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourceResponse"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"403":{"description":"API key lacks the `write` (or `admin`) scope required for mutations.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}},"404":{"description":"Resource not found, or not visible to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/search":{"post":{"tags":["search","search"],"summary":"Semantic search over indexed content","description":"Vector similarity search across every indexed chunk visible to the caller. Embeds the query via Gemini, runs a pgvector similarity search, and returns the top `limit` hits with their score (cosine similarity, [0,1]). ACL-filtered at every row.\n\nCosts credits per call — refused with 402 when the tenant is out of balance, before any embedding work runs. Optional `source_id` narrows to a single connector; `min_score` filters low-relevance noise.","operationId":"semantic_search_v1_search_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SemanticSearchRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ChunkResult"},"title":"Response Semantic Search V1 Search Post"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"402":{"description":"Insufficient credits for the requested operation. Top up at [Dashboard → Billing](https://app.mantleai.dev/dashboard/billing).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientCreditsResponse"}}}},"422":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}}}}},"/v1/search/relationships":{"post":{"tags":["search","search"],"summary":"Fuzzy search over relationship types","description":"Embeds the query and finds the most similar relationship-type names in the knowledge graph. Use to map natural-language phrases (\"works with\", \"reports to\") onto the canonical rel_types your graph actually carries. Costs credits per call; refused at 402 when balance is empty.","operationId":"search_relationships_v1_search_relationships_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SemanticSearchRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RelationshipTypeResult"},"title":"Response Search Relationships V1 Search Relationships Post"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"402":{"description":"Insufficient credits for the requested operation. Top up at [Dashboard → Billing](https://app.mantleai.dev/dashboard/billing).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientCreditsResponse"}}}},"422":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}}}}},"/v1/search/entities":{"get":{"tags":["search","search"],"summary":"Search entities by name (substring match)","description":"Case-insensitive contains match against entity names in the knowledge graph. Optional `type` narrows by entity_type. ACL-filtered. Cheap (no embedding cost) — use when you need to resolve a known name to an entity_id before walking the graph.","operationId":"search_entities_endpoint_v1_search_entities_get","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string","minLength":1,"title":"Q"}},{"name":"type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Type"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"default":20,"title":"Limit"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/EntityResult"},"title":"Response Search Entities Endpoint V1 Search Entities Get"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/search/entities/{entity_id}":{"get":{"tags":["search","search"],"summary":"Get a single entity by id","description":"Returns the entity row from Neo4j: name, type, properties, source refs. Tenant-scoped and ACL-filtered. Cross-tenant ids and ids the caller can't see (ACL-restricted) both return 404 with the same shape — existence of restricted content is not leaked.","operationId":"get_entity_endpoint_v1_search_entities__entity_id__get","parameters":[{"name":"entity_id","in":"path","required":true,"schema":{"type":"string","title":"Entity Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Entity Endpoint V1 Search Entities  Entity Id  Get"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"404":{"description":"Resource not found, or not visible to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/search/entities/{entity_id}/facts":{"get":{"tags":["search","search"],"summary":"Fetch live structured facts about an entity from connected databases","description":"Federated query layer (Phase 1). Looks up the entity's schema_mappings, runs bounded read-only SELECTs against the customer's connected source databases (Postgres in Phase 1), and returns the matching rows annotated with provenance. Same auth + ACL contract as ``GET /entities/{entity_id}`` — 404 for invisible entities, no existence leak. Empty ``facts`` list is a normal outcome (no mappings or no row matches), not an error.","operationId":"get_entity_facts_endpoint_v1_search_entities__entity_id__facts_get","parameters":[{"name":"entity_id","in":"path","required":true,"schema":{"type":"string","title":"Entity Id"}},{"name":"max_rows_per_table","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"description":"Per-table row cap to bound response size.","default":25,"title":"Max Rows Per Table"},"description":"Per-table row cap to bound response size."},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Entity Facts Endpoint V1 Search Entities  Entity Id  Facts Get"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"404":{"description":"Resource not found, or not visible to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/search/entities/{entity_id}/links":{"get":{"tags":["search","search"],"summary":"List entity-row resolution links (manual + automated)","description":"Returns every ``entity_row_links`` row for the entity — one per (source_id, table_name) target. Includes resolved positives, negative-cache entries, and the strategy/evidence behind each. Used by the FE entity-detail drawer to render the resolution audit trail.","operationId":"list_entity_links_endpoint_v1_search_entities__entity_id__links_get","parameters":[{"name":"entity_id","in":"path","required":true,"schema":{"type":"string","title":"Entity Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Entity Links Endpoint V1 Search Entities  Entity Id  Links Get"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"404":{"description":"Resource not found, or not visible to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["search","search"],"summary":"Manually confirm or reject an entity-row link","description":"User / agent provides ground truth. Writes ``match_strategy='manual'``, the highest-trust strategy in the cascade — no future automated run (Layer 1-4) can overwrite a manual decision. Use ``action='confirm'`` with a ``row_pk_value`` to link, ``action='reject'`` (no row_pk_value) to assert no-match. Requires write scope.","operationId":"manual_entity_link_endpoint_v1_search_entities__entity_id__links_post","parameters":[{"name":"entity_id","in":"path","required":true,"schema":{"type":"string","title":"Entity Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_ManualLinkRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Manual Entity Link Endpoint V1 Search Entities  Entity Id  Links Post"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"404":{"description":"Resource not found, or not visible to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/search/entities/{entity_id}/links/candidates":{"get":{"tags":["search","search"],"summary":"List ambiguous resolution candidates awaiting user confirmation","description":"Walks the entity's ``entity_row_links`` rows for any with candidates stashed in ``resolution_evidence`` (negative cache from Layer 3 ambiguous, OR Layer 4 'unclear' decisions). Returns each candidate's row_pk_value + raw_value + similarity + LLM rationale (if any), grouped by (source_id, table_name). The FE drawer renders these as 'is this the right row?' questions; the agent can call ``confirm_entity_link`` on the user's pick.","operationId":"list_entity_link_candidates_endpoint_v1_search_entities__entity_id__links_candidates_get","parameters":[{"name":"entity_id","in":"path","required":true,"schema":{"type":"string","title":"Entity Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response List Entity Link Candidates Endpoint V1 Search Entities  Entity Id  Links Candidates Get"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"404":{"description":"Resource not found, or not visible to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/search/entities/{entity_id}/relationships/summary":{"get":{"tags":["search","search"],"summary":"Aggregate counts of an entity's relationships by type","description":"Single cheap query that groups an entity's relationships by rel_type with counts. Optional `top_k_examples` (0-10) attaches highest-confidence example edges per bucket — turns this into the agent-facing 'show me how X is connected, with samples' shape. Optional `max_buckets` caps the rel_type count list (`buckets_truncated` flags when the cap was hit).","operationId":"get_entity_relationships_summary_endpoint_v1_search_entities__entity_id__relationships_summary_get","parameters":[{"name":"entity_id","in":"path","required":true,"schema":{"type":"string","title":"Entity Id"}},{"name":"top_k_examples","in":"query","required":false,"schema":{"type":"integer","maximum":10,"minimum":0,"default":0,"title":"Top K Examples"}},{"name":"max_buckets","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","maximum":500,"minimum":1},{"type":"null"}],"title":"Max Buckets"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntityRelationshipsSummary"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/search/entities/degrees":{"post":{"tags":["search","search"],"summary":"Batch lookup of entity total-degree counts","description":"Returns a map of `entity_id → total relationships visible to this caller`. One ACL-filtered Cypher for the whole request — the graph canvas uses this to render \"+N more connections\" frontier hints without one round-trip per node. Capped at 200 ids per call.","operationId":"get_entity_degrees_endpoint_v1_search_entities_degrees_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntityDegreesRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntityDegreesResponse"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/search/entities/{entity_id}/relationships":{"get":{"tags":["search","search"],"summary":"Filterable, paginated list of an entity's relationships","description":"Edges flowing into or out of the entity, with rel_type, confidence, description, and the connected-entity stub. Filterable: by `direction` (both/outgoing/incoming), exact `rel_type`, substring `search` over connected-entity name, and a `min_confidence` floor. Sortable by confidence (default) or name. ACL-filtered. When `min_confidence` is set, both the edges AND the `total` reflect the filter — pagination stays self-consistent.","operationId":"get_entity_relationships_endpoint_v1_search_entities__entity_id__relationships_get","parameters":[{"name":"entity_id","in":"path","required":true,"schema":{"type":"string","title":"Entity Id"}},{"name":"direction","in":"query","required":false,"schema":{"type":"string","pattern":"^(both|outgoing|incoming)$","default":"both","title":"Direction"}},{"name":"rel_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Rel Type"}},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Search"}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","pattern":"^(confidence_desc|name_asc)$","default":"confidence_desc","title":"Sort"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":20,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"min_confidence","in":"query","required":false,"schema":{"anyOf":[{"type":"number","maximum":1.0,"minimum":0.0},{"type":"null"}],"title":"Min Confidence"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntityRelationshipsPage"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/search/entities/{from_id}/path":{"get":{"tags":["search","search"],"summary":"Shortest path between two entities, with evidence","description":"Finds the shortest RELATES_TO chain between `from_id` and `to` within `max_hops`, where every node passes ACL and every edge meets the `min_confidence` floor. Each returned node carries resolved sources for citation; each edge carries rel_type, confidence, and the description (evidence quote).\n\nReturns `found=False` with empty lists when no qualifying path exists — agents should retry with lower confidence or higher max_hops, or conclude no grounded connection is recorded. Backs the `trace_connection` MCP tool.","operationId":"get_entity_path_endpoint_v1_search_entities__from_id__path_get","parameters":[{"name":"from_id","in":"path","required":true,"schema":{"type":"string","title":"From Id"}},{"name":"to","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":200,"title":"To"}},{"name":"max_hops","in":"query","required":false,"schema":{"type":"integer","maximum":6,"minimum":1,"default":4,"title":"Max Hops"}},{"name":"min_confidence","in":"query","required":false,"schema":{"type":"number","maximum":1.0,"minimum":0.0,"default":0.0,"title":"Min Confidence"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntityPath"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/search/entities/{entity_id}/graph":{"get":{"tags":["search","search"],"summary":"Traverse the local subgraph from an entity","description":"Returns up to `limit` edges within `depth` hops of the entity, ACL-filtered at every node. Each edge carries flattened from/to nodes plus resolved sources. Use to render a graph canvas around an entity, or to power topology-level reasoning in agents.\n\n**Choose `get_relationships` over this when** you only need an entity's neighbors (depth=1) — that endpoint is cheaper and has richer filters. `traverse_graph` is the right call only for true multi-hop questions.","operationId":"traverse_graph_endpoint_v1_search_entities__entity_id__graph_get","parameters":[{"name":"entity_id","in":"path","required":true,"schema":{"type":"string","title":"Entity Id"}},{"name":"depth","in":"query","required":false,"schema":{"type":"integer","maximum":4,"minimum":1,"default":2,"title":"Depth"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"default":100,"title":"Limit"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/TraversalEdge"},"title":"Response Traverse Graph Endpoint V1 Search Entities  Entity Id  Graph Get"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/search/entities/{entity_id}/context":{"get":{"tags":["search","search"],"summary":"Hybrid retrieval — full context for an entity","description":"Single-call retrieval bundle: the entity, its top-20 relationships, its top-10 linked documents, plus related chunks via three retrieval methods (graph-walk, semantic search on an enriched query, ±1 adjacent-chunk windowing). Use this when an agent or UI needs comprehensive context in one round-trip.\n\nCosts credits — a Gemini embedding call is part of the semantic-search step. Refused at 402 when balance is empty. Returns ~45 chunks max (15 graph + 10 semantic + 20 adjacent). ACL-filtered at every node and chunk.","operationId":"get_entity_context_endpoint_v1_search_entities__entity_id__context_get","parameters":[{"name":"entity_id","in":"path","required":true,"schema":{"type":"string","title":"Entity Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Entity Context Endpoint V1 Search Entities  Entity Id  Context Get"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"402":{"description":"Insufficient credits for the requested operation. Top up at [Dashboard → Billing](https://app.mantleai.dev/dashboard/billing).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientCreditsResponse"}}}},"422":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}},"404":{"description":"Resource not found, or not visible to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/search/chunks/{chunk_id}/context":{"get":{"tags":["search","search"],"summary":"Get a chunk plus ±N adjacent chunks from the same document","description":"Returns the chunk identified by `chunk_id` plus up to `window` adjacent chunks before and after it in the same document. Use to expand context around a search hit when the summary alone is insufficient. ACL-filtered. `window` capped at 3 to keep the response bounded.\n\n**Pass `with_prose=true`** to fetch the actual prose for each chunk via the lazy-fetch path (D-001 / D-012 — Mantle does not store chunk text; the connector re-downloads the source bytes and the server slices `text[char_start:char_end]`). Chunks whose `char_range` is NULL (pre-Phase-1.1 extractions) or whose source is unreachable return `prose=null` plus `prose_unavailable=true` — never an error.","operationId":"get_chunk_context_endpoint_v1_search_chunks__chunk_id__context_get","parameters":[{"name":"chunk_id","in":"path","required":true,"schema":{"type":"string","title":"Chunk Id"}},{"name":"window","in":"query","required":false,"schema":{"type":"integer","maximum":3,"minimum":0,"default":1,"title":"Window"}},{"name":"with_prose","in":"query","required":false,"schema":{"type":"boolean","description":"When true, lazy-fetch each chunk's prose from source by slicing the re-downloaded bytes at char_start:char_end. Default false preserves the legacy metadata-only response.","default":false,"title":"With Prose"},"description":"When true, lazy-fetch each chunk's prose from source by slicing the re-downloaded bytes at char_start:char_end. Default false preserves the legacy metadata-only response."},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","additionalProperties":true},"title":"Response Get Chunk Context Endpoint V1 Search Chunks  Chunk Id  Context Get"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"404":{"description":"Resource not found, or not visible to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/api-keys":{"post":{"tags":["api-keys","api-keys"],"summary":"Create an API key","description":"Mints a new API key for the active tenant with the requested scopes. The raw secret is returned **once** in the `key` field; Mantle stores only a SHA-256 hash and cannot recover the value. Surface the key to the user immediately and never persist it.\n\nScopes are validated against the canonical set (`read`, `write`, `admin`); admin grants write grants read.","operationId":"create_api_key_v1_api_keys_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateKeyRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/APIKeyCreatedResponse"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"403":{"description":"Caller lacks the required role/scope. Session users need `admin` role; API keys need the `admin` scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}}}},"get":{"tags":["api-keys","api-keys"],"summary":"List API keys for the active tenant","description":"Returns every API key on the tenant — both active and revoked — ordered newest first. Each row carries `last_used_at`, useful for spotting dormant keys before rotating. Raw secrets are NEVER returned by this endpoint; only by `POST /v1/api-keys` and `POST /v1/api-keys/{id}/rotate`.","operationId":"list_api_keys_v1_api_keys_get","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/APIKeyResponse"},"title":"Response List Api Keys V1 Api Keys Get"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/api-keys/{key_id}":{"delete":{"tags":["api-keys","api-keys"],"summary":"Revoke a single API key","description":"Soft-deletes the key by stamping `revoked_at`. Subsequent requests bearing the key fail authentication. The row is retained for audit. To revoke every active key in one call, use `POST /v1/api-keys/revoke-all`.","operationId":"revoke_api_key_v1_api_keys__key_id__delete","parameters":[{"name":"key_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Key Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Revoke Api Key V1 Api Keys  Key Id  Delete"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"403":{"description":"Caller lacks the required role/scope. Session users need `admin` role; API keys need the `admin` scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}},"404":{"description":"Resource not found, or not visible to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"patch":{"tags":["api-keys","api-keys"],"summary":"Rename or re-scope an API key","description":"Updates `name` and/or `scopes` on an existing API key. Does **not** mint a new secret — the existing key continues to work with the new metadata. Use this to rename keys for clarity or to tighten scopes without breaking the integration. To rotate the underlying secret, see `POST /{id}/rotate`.","operationId":"update_api_key_v1_api_keys__key_id__patch","parameters":[{"name":"key_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Key Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateKeyRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/APIKeyResponse"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"403":{"description":"Caller lacks the required role/scope. Session users need `admin` role; API keys need the `admin` scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}},"404":{"description":"Resource not found, or not visible to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/api-keys/{key_id}/rotate":{"post":{"tags":["api-keys","api-keys"],"summary":"Rotate an API key (mint replacement, revoke original)","description":"Creates a NEW key with the same name and scopes as the source key, then revokes the source key. The new key's raw secret is returned **once** in the `key` field.\n\nRotation is the right move when a key is suspected leaked but the integration must keep running — there is never a window of zero valid keys (mint first, revoke second). Rotating an already-revoked key still mints a fresh key with the same configuration, so this doubles as a 'restore' path. Cross-tenant rotation attempts return 404 (no existence leak).","operationId":"rotate_api_key_v1_api_keys__key_id__rotate_post","parameters":[{"name":"key_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Key Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/APIKeyCreatedResponse"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"403":{"description":"Caller lacks the required role/scope. Session users need `admin` role; API keys need the `admin` scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}},"404":{"description":"Resource not found, or not visible to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/api-keys/revoke-all":{"post":{"tags":["api-keys","api-keys"],"summary":"Emergency: revoke every active API key for the tenant","description":"Kill switch for broad-compromise scenarios — laptop stolen, credentials pushed to a public repo, departing engineer held keys. Revokes every active API key on the active tenant in one call.\n\n**All integrations break immediately.** Returned `revoked_count` excludes keys already revoked. Rate-limited to 3/min to prevent accidental mass-revocations. Tenant-isolated — never affects another tenant's keys, even within the same organization.","operationId":"revoke_all_api_keys_v1_api_keys_revoke_all_post","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkRevokeResponse"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"403":{"description":"Caller lacks the required role/scope. Session users need `admin` role; API keys need the `admin` scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponse"}}}}}}},"/v1/status/public":{"get":{"tags":["status","status"],"summary":"Public service status","description":"Coarse-grained operational status of the Mantle Services. Powers the marketing-site `/status` page and Azure Monitor Availability Tests. **Public — no authentication required.**\n\nReturns one of three overall states: `operational` (all checks passing), `degraded` (Redis or another non-core dep failed but core data paths still work), `down` (both Postgres AND Neo4j are unreachable; HTTP 503).\n\nCached 5 seconds per replica. Rate-limited to 60/minute per IP.","operationId":"public_status_v1_status_public_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicStatusResponse"}}}},"503":{"description":"Core data path unavailable. Body still parses.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicStatusResponse"}}}}}}},"/v1/ontology/object-types":{"get":{"tags":["ontology","ontology"],"summary":"List the tenant's canonical ObjectTypes","description":"Returns every ObjectType registered for the caller's tenant. By default lists only canonical types (`is_canonical=true`). Pass `source_authored_only=true` to narrow to types seeded from Pattern-A sources (the Foundry-light typed-ontology view).","operationId":"list_object_types_endpoint_v1_ontology_object_types_get","parameters":[{"name":"canonical_only","in":"query","required":false,"schema":{"type":"boolean","description":"Exclude auto-discovered types pending promotion.","default":true,"title":"Canonical Only"},"description":"Exclude auto-discovered types pending promotion."},{"name":"source_authored_only","in":"query","required":false,"schema":{"type":"boolean","description":"Narrow to types seeded from a Pattern-A source's declared schema.","default":false,"title":"Source Authored Only"},"description":"Narrow to types seeded from a Pattern-A source's declared schema."},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ObjectTypeResponse"},"title":"Response List Object Types Endpoint V1 Ontology Object Types Get"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"404":{"description":"Resource not found, or not visible to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/ontology/object-types/{name}":{"get":{"tags":["ontology","ontology"],"summary":"Get an ObjectType by name","description":"Returns the full schema (typed properties + typed relations) for one ObjectType. Names are unique per tenant; cross-tenant isolation is enforced via `tenant_id` filter.","operationId":"get_object_type_endpoint_v1_ontology_object_types__name__get","parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","title":"Name"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ObjectTypeResponse"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"404":{"description":"Resource not found, or not visible to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/ontology/link-types":{"get":{"tags":["ontology","ontology"],"summary":"List canonical LinkTypes (typed predicates)","description":"Returns the typed predicate vocabulary for the caller's tenant, paginated. Defaults to top 100 by usage_count desc, then name asc. Pass `query` for substring match on name/description, `min_usage_count` to drop singletons, `from_object_type_id` / `to_object_type_id` to narrow by typed-link endpoints. Phase-1.5 merged variants are excluded by default.","operationId":"list_link_types_endpoint_v1_ontology_link_types_get","parameters":[{"name":"from_object_type_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"From Object Type Id"}},{"name":"to_object_type_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"To Object Type Id"}},{"name":"canonical_only","in":"query","required":false,"schema":{"type":"boolean","description":"Exclude Phase-1.5 merged variants.","default":true,"title":"Canonical Only"},"description":"Exclude Phase-1.5 merged variants."},{"name":"query","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Case-insensitive substring on name OR description.","title":"Query"},"description":"Case-insensitive substring on name OR description."},{"name":"min_usage_count","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Drop rel_types whose usage_count is below this floor.","default":0,"title":"Min Usage Count"},"description":"Drop rel_types whose usage_count is below this floor."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"description":"Max results per page (1-500). Default 100.","default":100,"title":"Limit"},"description":"Max results per page (1-500). Default 100."},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Pagination offset.","default":0,"title":"Offset"},"description":"Pagination offset."},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkTypesListResponse"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"404":{"description":"Resource not found, or not visible to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/ontology/link-types/{name}":{"get":{"tags":["ontology","ontology"],"summary":"Get a LinkType by name","description":"Returns the canonical LinkType for the given name. Resolves through Phase-1.5 merge chains automatically — looking up a merged variant returns its canonical.","operationId":"get_link_type_endpoint_v1_ontology_link_types__name__get","parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","title":"Name"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkTypeResponse"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"404":{"description":"Resource not found, or not visible to this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/resolution/stats":{"get":{"tags":["resolution-admin","resolution-admin"],"summary":"Aggregate resolution-layer metrics for operator dashboard","description":"Per-tenant counts of resolved/negative/pending entity-row links, shadow size, and recent LLM-judge spend. Powers the operator dashboard's resolution panel + the customer-facing 'you're using X' widget. Admin-only.","operationId":"get_resolution_stats_v1_admin_resolution_stats_get","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResolutionStatsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/sources/{source_id}/force-reextract":{"post":{"tags":["admin-sources","admin-sources"],"summary":"Force re-extraction of a source (purge + re-sync)","description":"Operational tooling: captures BEFORE counts, soft-deletes ContextObjects + retracts Neo4j source_refs + drops Documents, deletes entity_row_links derived from this source, resets `sync_state` and `synced_files`, then triggers a fresh sync. The source row + credentials are preserved so the customer doesn't need to re-authenticate. Used to validate extraction-pipeline changes against existing data — call this, wait for sync to complete, then compare entity/fact counts against the returned BEFORE snapshot.","operationId":"force_reextract_source_v1_admin_sources__source_id__force_reextract_post","parameters":[{"name":"source_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Source Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ForceReextractResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/maintenance/tenants":{"get":{"tags":["admin-maintenance","admin-maintenance"],"summary":"List every tenant with operator-relevant metadata","description":"Operator-only inventory of all tenants. Returns identity + headline counts (members, documents, credit balance, last sync) so an operator can triage at a glance which tenant needs attention. Joins org → admin user → counts in one round-trip per metric to avoid N+1 query patterns.","operationId":"list_tenants_v1_admin_maintenance_tenants_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Max tenants returned.","default":200,"title":"Limit"},"description":"Max tenants returned."},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TenantListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/maintenance/tenants/{tenant_id}":{"get":{"tags":["admin-maintenance","admin-maintenance"],"summary":"Detailed operator view of a single tenant","description":"Single-tenant deep dive: same headline summary as the list endpoint, plus the source list, the latest 50 operator actions targeting this tenant, the latest 50 customer-side audit events, and a live Neo4j count of entities + relationships. Use this to triage an issue before deciding whether to reidentify or purge.","operationId":"get_tenant_detail_v1_admin_maintenance_tenants__tenant_id__get","parameters":[{"name":"tenant_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Tenant Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TenantDetailResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/maintenance/operator-actions":{"get":{"tags":["admin-maintenance","admin-maintenance"],"summary":"Paginated operator-action audit log","description":"Filterable audit trail of every operator action. Defaults to the last 90 days, latest first. Use ``action_type``, ``operator_user_id``, ``target_tenant_id``, and date params to narrow when investigating a specific incident.","operationId":"list_operator_actions_v1_admin_maintenance_operator_actions_get","parameters":[{"name":"action_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Exact match on action_type.","title":"Action Type"},"description":"Exact match on action_type."},{"name":"operator_user_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Operator User Id"}},{"name":"target_tenant_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Target Tenant Id"}},{"name":"since","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"Only return actions created at or after this timestamp. Default: 90 days ago.","title":"Since"},"description":"Only return actions created at or after this timestamp. Default: 90 days ago."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":100,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OperatorActionListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/maintenance/reidentify-entities/{tenant_id}":{"post":{"tags":["admin-maintenance","admin-maintenance"],"summary":"Heal legacy entity-id fragmentation in Neo4j","description":"Recomputes every Entity's canonical id using the current ``_make_entity_id`` rules. Duplicate nodes within the new canonical group are merged via ``apoc.refactor.mergeNodes``. Pre-PR-23 this also rewrote a Postgres ``facts`` table; that table was retired so the operation is now Neo4j-only.\n\nIdempotent. Per-tenant. Operator-only. Writes an ``OperatorAction`` audit row pre- and post-action.","operationId":"reidentify_entities_v1_admin_maintenance_reidentify_entities__tenant_id__post","parameters":[{"name":"tenant_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Tenant Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReidentifyResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/maintenance/purge-tenant-graph/{tenant_id}":{"post":{"tags":["admin-maintenance","admin-maintenance"],"summary":"Destructively erase ALL extracted state for a tenant","description":"DESTRUCTIVE. Deletes every Entity, Document, and Community node from Neo4j; every ContextObject + EntityRowLink row from Postgres; resets ``sync_state`` on every source. Source rows + credentials_encrypted survive. Operator-only. Writes an ``OperatorAction`` audit row.","operationId":"purge_tenant_graph_endpoint_v1_admin_maintenance_purge_tenant_graph__tenant_id__post","parameters":[{"name":"tenant_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Tenant Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PurgeTenantGraphResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/maintenance/consolidate-entities/{tenant_id}":{"post":{"tags":["admin-maintenance","admin-maintenance"],"summary":"Report cross-doc entity-merge candidates (read-only)","description":"Runs the Phase 1 cross-doc merge signal pipeline (see ``mantle.extraction.cross_doc_signals``) over every entity pair of the same type in a tenant's graph and reports the candidates that would auto-merge.\n\n**Read-only.** No graph mutation. Use this to preview what the auto-merge would do before flipping the ``CROSS_DOC_EMBEDDING_MERGE`` feature flag on for the tenant. Phase 3b (apply mode) is a separate endpoint; this one strictly reports.\n\nRefuses with 413 when the tenant has more than ``5000`` entities — the O(N²/T) scan is unbounded in time otherwise. Per-source consolidation is a future enhancement for very large tenants.\n\nOperator-only. Writes an ``OperatorAction`` audit row with the candidate count for traceability.","operationId":"consolidate_entities_report_v1_admin_maintenance_consolidate_entities__tenant_id__post","parameters":[{"name":"tenant_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Tenant Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConsolidateReportResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/maintenance/backfill-entity-embeddings/{tenant_id}":{"post":{"tags":["admin-maintenance","admin-maintenance"],"summary":"Backfill entity_embedding rows for an existing tenant","description":"Walks every ``Entity`` in the tenant's Neo4j graph, builds the canonical signature (name + entity_type + top-K relationships), embeds via Gemini, and upserts into the ``entity_embedding`` table.\n\nPhase 1.5a was the storage migration; this endpoint is the one-shot operator backfill that mirrors ``scripts/backfill_entity_embeddings.py``. Idempotent — safe to re-run. Cost: ~$0.0014 + ~6s wall time for a 300-entity tenant; ~$0.50 + ~20 min for a 100k-entity tenant.\n\nOnce entity_embeddings are populated, the next extraction with ``CROSS_DOC_EMBEDDING_MERGE=true`` can auto-merge new entities into historically-fragmented ones (the 2026-05-15 Acme x4 case).","operationId":"backfill_entity_embeddings_endpoint_v1_admin_maintenance_backfill_entity_embeddings__tenant_id__post","parameters":[{"name":"tenant_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Tenant Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackfillEntityEmbeddingsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/maintenance/cleanup-self-doc-entities/{tenant_id}":{"post":{"tags":["admin-maintenance","admin-maintenance"],"summary":"Delete orphan document-type entities whose name matches a source file_name","description":"PR #106 added a filter at extraction time that drops the ``entity_type=\"document\"`` node when the LLM emits one with the same name as the document being extracted (e.g. ``IC Memo — Sunset Boulevard Hotel`` as a self-referential entity). The filter prevents NEW self-doc pollution; this endpoint deletes the HISTORICAL pollution.\n\nTargets: any ``Entity`` with ``entity_type=\"document\"`` whose ``name`` (normalized) equals the normalized ``file_name`` of one of the tenant's current source ``data_sources.sync_state.files``. Drops the entity node plus its incoming edges (``AUTHOR_OF`` and friends).\n\nIdempotent — re-running on a clean tenant deletes zero entities. Per-tenant. Operator-only. Writes an ``OperatorAction`` audit row pre- and post-action.","operationId":"cleanup_self_doc_entities_endpoint_v1_admin_maintenance_cleanup_self_doc_entities__tenant_id__post","parameters":[{"name":"tenant_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Tenant Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SelfDocCleanupResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/maintenance/pairwise-merge-entities/{tenant_id}":{"post":{"tags":["admin-maintenance","admin-maintenance"],"summary":"Pairwise reconcile existing entities — Tier 1/2/3 classification + apply","description":"Walks every pair of existing entities of the same ``entity_type``, computes the cross-doc signal breakdown (``compose_signals``), and classifies each pair into HARD_MERGE / SAME_AS / CANDIDATE / NO_ACTION via the false-positive-aware tier model.\n\n* **HARD_MERGE** (score ≥ 0.65, name_sim ≥ 0.60): collapsed via ``apoc.refactor.mergeNodes``. Loser node deleted; relationships re-pointed; ``MERGED_FROM`` audit edge written first.\n* **SAME_AS**: ``auto_merge`` signals but below the higher bar. Bidirectional ``SAME_AS`` edges written; both nodes remain queryable. Read-time consumers can return the cluster as one logical entity.\n* **CANDIDATE**: signals warrant review but not auto-apply. Returned in the response only (v1).\n\nDefaults to ``dry_run=true`` — returns the decision list without mutating Neo4j. Operator reviews, then re-runs with ``dry_run=false`` to apply.\n\nIdempotent on apply: HARD_MERGE losers are gone after a run; SAME_AS edges use ``MERGE`` so re-runs are no-ops.\n\nPer-tenant. Operator-only. Writes an ``OperatorAction`` audit row.","operationId":"pairwise_merge_entities_endpoint_v1_admin_maintenance_pairwise_merge_entities__tenant_id__post","parameters":[{"name":"tenant_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Tenant Id"}},{"name":"dry_run","in":"query","required":false,"schema":{"type":"boolean","description":"When true (default), return decisions without mutating Neo4j.","default":true,"title":"Dry Run"},"description":"When true (default), return decisions without mutating Neo4j."},{"name":"apply_tiers","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated list of tiers to apply when dry_run=false. Allowed values: HARD_MERGE, SAME_AS. Default (omitted) = apply both. Pre-soft-merge safety hatch — pass apply_tiers=SAME_AS to skip destructive HARD_MERGE collapses entirely (see docs/plans/entity-merge-architecture.md). Ignored when dry_run=true.","title":"Apply Tiers"},"description":"Comma-separated list of tiers to apply when dry_run=false. Allowed values: HARD_MERGE, SAME_AS. Default (omitted) = apply both. Pre-soft-merge safety hatch — pass apply_tiers=SAME_AS to skip destructive HARD_MERGE collapses entirely (see docs/plans/entity-merge-architecture.md). Ignored when dry_run=true."},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PairwiseMergeResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/maintenance/reject-soft-merge/{edge_id}":{"post":{"tags":["admin-maintenance","admin-maintenance"],"summary":"Reject a soft-merge — deletes the MERGED_INTO edge","description":"Deletes a ``MERGED_INTO`` edge by its stable ``id`` (returned by the pairwise-merge-entities response or the active-soft-merges list). The loser node returns to fully independent state. O(1) — single Cypher round-trip.\n\nIdempotent: rejecting an already-deleted edge returns 200 with ``deleted=false``. The reason is still recorded in the ``OperatorAction`` audit row.\n\nPhase 1.5g Layer 1 — the load-bearing reversibility primitive that lets us safely auto-apply soft-merges. False-positive recovery is now a single API call, not a re-extraction.","operationId":"reject_soft_merge_endpoint_v1_admin_maintenance_reject_soft_merge__edge_id__post","parameters":[{"name":"edge_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Edge Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RejectSoftMergeRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RejectSoftMergeResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/maintenance/active-soft-merges/{tenant_id}":{"get":{"tags":["admin-maintenance","admin-maintenance"],"summary":"List live MERGED_INTO edges for a tenant","description":"Read-only listing of every active soft-merge in this tenant — the operator review surface for Phase 1.5g Layer 1. Each item carries the edge_id needed to reject via ``POST /reject-soft-merge/{edge_id}``.\n\nSorted most-recent-decided first. Capped at ``limit`` (default 200) — for larger tenants the queue endpoints from Layer 3 (PR #117) are the right surface.","operationId":"list_active_soft_merges_endpoint_v1_admin_maintenance_active_soft_merges__tenant_id__get","parameters":[{"name":"tenant_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Tenant Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"default":200,"title":"Limit"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActiveSoftMergesResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/maintenance/pending-merges/{tenant_id}":{"get":{"tags":["admin-maintenance","admin-maintenance"],"summary":"List open entity-merge review candidates","description":"Returns the operator review queue for a tenant — every CANDIDATE-tier pair the reconcile pipeline has surfaced and that hasn't been decided yet. Sorted most-recently-proposed first.\n\nCursor-paginated when more rows exist than ``limit``. Filter by ``entity_type`` for type-specific review sessions.","operationId":"list_pending_merges_endpoint_v1_admin_maintenance_pending_merges__tenant_id__get","parameters":[{"name":"tenant_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Tenant Id"}},{"name":"entity_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Entity Type"}},{"name":"min_score","in":"query","required":false,"schema":{"anyOf":[{"type":"number","maximum":1.0,"minimum":0.0},{"type":"null"}],"title":"Min Score"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PendingMergeReviewList"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/maintenance/pending-merges/{review_id}/accept":{"post":{"tags":["admin-maintenance","admin-maintenance"],"summary":"Accept a pending merge — promote to soft-merge","description":"Promote a CANDIDATE to a soft-merge edge.\n\nPicks the winner by source_ref count using ``pick_canonical``.\nWrites a ``MERGED_INTO`` edge via ``_apply_soft_merge`` —\nreversible via the existing ``POST /reject-soft-merge/{edge_id}``\nendpoint.\n\nIdempotent: accepting an already-accepted row returns the\nexisting soft-merge edge id. Rejecting an already-accepted row\nrequires reverting via the soft-merge reject endpoint first.","operationId":"accept_pending_merge_endpoint_v1_admin_maintenance_pending_merges__review_id__accept_post","parameters":[{"name":"review_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Review Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AcceptMergeRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AcceptMergeResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/maintenance/pending-merges/{review_id}/reject":{"post":{"tags":["admin-maintenance","admin-maintenance"],"summary":"Reject a pending merge — pair won't be re-proposed","description":"Marks the candidate as rejected and writes a ``MERGE_REJECTED`` edge in Neo4j so future reconcile runs skip this pair. The pair stays in ``pending_merge_review`` for audit (``decision=rejected``).","operationId":"reject_pending_merge_endpoint_v1_admin_maintenance_pending_merges__review_id__reject_post","parameters":[{"name":"review_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Review Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RejectMergeRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RejectMergeResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/users/{user_id}/principals":{"get":{"tags":["admin-users","admin-users"],"summary":"Operator: resolved ACL principals + linked identities for any user","description":"Returns the principal set the ACL filter would compute for the given user, plus every ``UserExternalIdentity`` row tied to them. Read-only, not audited (operators inspect state freely; only mutations are audited). Useful when a user's graph queries return empty unexpectedly — the operator can see whether the external-identity expansion gave them the right principals.","operationId":"get_user_principals_v1_admin_users__user_id__principals_get","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}},{"name":"tenant_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"description":"Resolve principals as if the user were calling within this tenant context. Defaults to the user's first (by membership creation date) tenant.","title":"Tenant Id"},"description":"Resolve principals as if the user were calling within this tenant context. Defaults to the user's first (by membership creation date) tenant."},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserPrincipalsResponse"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/users/{user_id}/sources/{source_id}/claim-identity":{"post":{"tags":["admin-users","admin-users"],"summary":"Operator: claim external identity on behalf of any user","description":"Same logic as the user-facing ``POST /v1/sources/{source_id}/claim-identity`` but the target user is taken from the path instead of the auth context. Operator must be a platform operator; the target user must be a member of the source's tenant. Idempotent — re-running updates the existing row's ``verified_at``. Writes an ``OperatorAction`` row.","operationId":"admin_claim_identity_v1_admin_users__user_id__sources__source_id__claim_identity_post","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}},{"name":"source_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Source Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminClaimIdentityResponse"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/entities/{entity_id}/source-refs":{"get":{"tags":["admin-users","admin-users"],"summary":"Operator: raw source_refs on a Neo4j Entity (no ACL filter)","description":"Returns the entity's name, type, tenant, and the decoded ``source_refs`` list with each ref's file_id + acl_mode + acl_principals. Read-only, not audited. Use this to verify what ACL data is actually stored on the graph for a given entity — the counterpart to the principals diagnostic. The ACL filter is bypassed (operator scope); the response includes data the calling operator might not otherwise see.","operationId":"get_entity_source_refs_v1_admin_entities__entity_id__source_refs_get","parameters":[{"name":"entity_id","in":"path","required":true,"schema":{"type":"string","title":"Entity Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntitySourceRefsResponse"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/admin/users/{user_id}/sources/{source_id}/identities/backfill":{"post":{"tags":["admin-users","admin-users"],"summary":"Operator: backfill historical OAuth identities for a user+source","description":"Writes ``user_external_identities`` rows for emails the user previously OAuth'd against this source but whose identity was never captured (early sources predate identity capture; OAuth callback inline capture sometimes failed). After migration f203 the row table supports multiple identities per (user, source); this endpoint populates them.\n\nTwo modes:\n\n* **Explicit** (default): operator passes ``external_emails``   in the body — the safe choice when you know exactly which   emails belong to the user.\n* **Discover** (``discover_from_acl=true``): scans the source's   ``context_objects.acl`` and writes a row for every distinct   ``user:<email>`` principal. Convenient but ATTRIBUTES   SHARED-WITH EMAILS to the user as well. Use only when you   intend that scope (e.g. a personal-only Drive where the   owner shares everything).\n\nBoth modes are audited via ``OperatorAction`` (``action_type='admin_backfill_identities'``).","operationId":"admin_backfill_identities_v1_admin_users__user_id__sources__source_id__identities_backfill_post","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"User Id"}},{"name":"source_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Source Id"}},{"name":"authorization","in":"header","required":false,"schema":{"type":"string","title":"Authorization"}},{"name":"X-Tenant-Id","in":"header","required":false,"schema":{"type":"string","title":"X-Tenant-Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityBackfillRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityBackfillResponse"}}}},"401":{"description":"Missing or invalid Bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded; respect Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"APIKeyCreatedResponse":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"key_prefix":{"type":"string","title":"Key Prefix"},"scopes":{"items":{"type":"string"},"type":"array","title":"Scopes"},"created_at":{"type":"string","title":"Created At"},"last_used_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Used At"},"revoked_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Revoked At"},"acts_as_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Acts As Email"},"key":{"type":"string","title":"Key"}},"type":"object","required":["id","name","key_prefix","scopes","created_at","last_used_at","revoked_at","key"],"title":"APIKeyCreatedResponse"},"APIKeyResponse":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"key_prefix":{"type":"string","title":"Key Prefix"},"scopes":{"items":{"type":"string"},"type":"array","title":"Scopes"},"created_at":{"type":"string","title":"Created At"},"last_used_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Used At"},"revoked_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Revoked At"},"acts_as_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Acts As Email"}},"type":"object","required":["id","name","key_prefix","scopes","created_at","last_used_at","revoked_at"],"title":"APIKeyResponse"},"AcceptMergeRequest":{"properties":{"reason":{"type":"string","maxLength":500,"minLength":1,"title":"Reason"}},"type":"object","required":["reason"],"title":"AcceptMergeRequest"},"AcceptMergeResponse":{"properties":{"review_id":{"type":"string","title":"Review Id"},"soft_merge_edge_id":{"type":"string","title":"Soft Merge Edge Id","default":""},"operator_action_id":{"type":"string","title":"Operator Action Id"},"status":{"type":"string","title":"Status"}},"type":"object","required":["review_id","operator_action_id","status"],"title":"AcceptMergeResponse"},"AcceptResponse":{"properties":{"accepted":{"type":"boolean","title":"Accepted"},"tenant_id":{"type":"string","title":"Tenant Id"},"org_id":{"type":"string","title":"Org Id"},"role":{"type":"string","title":"Role"}},"type":"object","required":["accepted","tenant_id","org_id","role"],"title":"AcceptResponse"},"ActiveCommitment":{"properties":{"credits_remaining":{"type":"number","title":"Credits Remaining"},"expires_at":{"type":"string","title":"Expires At"},"description":{"type":"string","title":"Description"}},"type":"object","required":["credits_remaining","expires_at","description"],"title":"ActiveCommitment"},"ActiveSoftMergeItem":{"properties":{"edge_id":{"type":"string","title":"Edge Id"},"loser_id":{"type":"string","title":"Loser Id"},"loser_name":{"type":"string","title":"Loser Name"},"winner_id":{"type":"string","title":"Winner Id"},"winner_name":{"type":"string","title":"Winner Name"},"entity_type":{"type":"string","title":"Entity Type"},"decided_at":{"type":"string","title":"Decided At"},"decided_by":{"type":"string","title":"Decided By"},"decision_id":{"type":"string","title":"Decision Id"},"score":{"type":"number","title":"Score"},"s1_name":{"type":"number","title":"S1 Name"},"s2_embedding":{"type":"number","title":"S2 Embedding"},"s3_neighborhood":{"type":"number","title":"S3 Neighborhood"},"shared_neighbors":{"type":"integer","title":"Shared Neighbors"},"veto_reason":{"type":"string","title":"Veto Reason","default":""},"tier_origin":{"type":"string","title":"Tier Origin","default":"HARD_MERGE"}},"type":"object","required":["edge_id","loser_id","loser_name","winner_id","winner_name","entity_type","decided_at","decided_by","decision_id","score","s1_name","s2_embedding","s3_neighborhood","shared_neighbors"],"title":"ActiveSoftMergeItem","description":"One live MERGED_INTO edge for the operator review surface."},"ActiveSoftMergesResponse":{"properties":{"tenant_id":{"type":"string","title":"Tenant Id"},"items":{"items":{"$ref":"#/components/schemas/ActiveSoftMergeItem"},"type":"array","title":"Items"},"total":{"type":"integer","title":"Total"}},"type":"object","required":["tenant_id","items","total"],"title":"ActiveSoftMergesResponse"},"AdminClaimIdentityResponse":{"properties":{"data_source_id":{"type":"string","title":"Data Source Id"},"user_id":{"type":"string","title":"User Id"},"connector_type":{"type":"string","title":"Connector Type"},"external_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"External Email"},"external_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"External User Id"},"operator_action_id":{"type":"string","title":"Operator Action Id"}},"type":"object","required":["data_source_id","user_id","connector_type","operator_action_id"],"title":"AdminClaimIdentityResponse"},"AuthCallbackRequest":{"properties":{"code":{"type":"string","title":"Code"}},"type":"object","required":["code"],"title":"AuthCallbackRequest"},"AuthResponse":{"properties":{"access_token":{"type":"string","title":"Access Token"},"refresh_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Refresh Token"},"user":{"additionalProperties":true,"type":"object","title":"User"}},"type":"object","required":["access_token","refresh_token","user"],"title":"AuthResponse"},"BackfillEntityEmbeddingsResponse":{"properties":{"entities_scanned":{"type":"integer","title":"Entities Scanned","description":"Total Entities loaded from Neo4j for this tenant."},"embeddings_written":{"type":"integer","title":"Embeddings Written","description":"Number of ``entity_embedding`` rows upserted."},"batches":{"type":"integer","title":"Batches","description":"Number of Gemini embed_content batches issued (batch_size=100)."},"elapsed_seconds":{"type":"number","title":"Elapsed Seconds","description":"Wall time for the whole pass."},"operator_action_id":{"type":"string","title":"Operator Action Id","description":"UUID of the OperatorAction audit row."}},"type":"object","required":["entities_scanned","embeddings_written","batches","elapsed_seconds","operator_action_id"],"title":"BackfillEntityEmbeddingsResponse","description":"Counts from a per-tenant entity-embedding backfill run."},"BalanceResponse":{"properties":{"tenant_id":{"type":"string","title":"Tenant Id"},"balance":{"type":"number","title":"Balance"}},"type":"object","required":["tenant_id","balance"],"title":"BalanceResponse"},"BillingInfoResponse":{"properties":{"has_payment_method":{"type":"boolean","title":"Has Payment Method"},"customer_portal_available":{"type":"boolean","title":"Customer Portal Available"},"active_commitments":{"items":{"$ref":"#/components/schemas/ActiveCommitment"},"type":"array","title":"Active Commitments"}},"type":"object","required":["has_payment_method","customer_portal_available","active_commitments"],"title":"BillingInfoResponse"},"BulkRevokeResponse":{"properties":{"revoked_count":{"type":"integer","title":"Revoked Count"},"tenant_id":{"type":"string","title":"Tenant Id"}},"type":"object","required":["revoked_count","tenant_id"],"title":"BulkRevokeResponse"},"ChangeMemberRoleBody":{"properties":{"role":{"type":"string","title":"Role"}},"type":"object","required":["role"],"title":"ChangeMemberRoleBody","description":"Body for ``PATCH /v1/tenants/{id}/members/{user_id}``."},"CharRange":{"properties":{"start":{"type":"integer","title":"Start"},"end":{"type":"integer","title":"End"}},"type":"object","required":["start","end"],"title":"CharRange","description":"Character-offset range into the original source document.\n\nUsed by ``fetch_document(chunk_id)`` to lazy-fetch the right byte\nrange from the source connector — Mantle never persists chunk text\n(D-001 / D-012). ``start`` and ``end`` are inclusive/exclusive\nrespectively, matching Python slice semantics."},"CheckoutResponse":{"properties":{"url":{"type":"string","title":"Url"}},"type":"object","required":["url"],"title":"CheckoutResponse"},"ChunkResult":{"properties":{"id":{"type":"string","title":"Id"},"chunk_id":{"type":"string","title":"Chunk Id","description":"Stable chunk id. Pass to fetch_document(chunk_id) for prose."},"contextual_header":{"type":"string","title":"Contextual Header","description":"1-2 sentence anchor produced at extraction time describing where this chunk lives in the document. Use for triage between hits.","default":""},"char_range":{"anyOf":[{"$ref":"#/components/schemas/CharRange"},{"type":"null"}],"description":"Character range into the source document; null on pre-Phase-1.1 chunks (re-extract to populate). Required for fetch_document."},"file_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"File Id","description":"Source file identifier; null on pre-Phase-1.1 chunks."},"entity_refs":{"items":{"type":"string"},"type":"array","title":"Entity Refs","description":"Names of entities the LLM extracted from this chunk. Lets agents decide whether to fetch prose without paying the round-trip."},"content_type":{"type":"string","title":"Content Type"},"chunk_index":{"type":"integer","title":"Chunk Index"},"source_ref":{"additionalProperties":true,"type":"object","title":"Source Ref"},"provenance":{"additionalProperties":true,"type":"object","title":"Provenance"},"properties":{"additionalProperties":true,"type":"object","title":"Properties"},"score":{"type":"number","title":"Score"},"semantic_summary":{"type":"string","title":"Semantic Summary","description":"DEPRECATED: Haiku-paraphrased chunk summary. Will be removed after the 90-day grace window OR when API consumption drops to zero (whichever later). Migrate to contextual_header + fetch_document(chunk_id) for prose.","deprecated":true}},"type":"object","required":["id","chunk_id","content_type","chunk_index","source_ref","provenance","properties","score","semantic_summary"],"title":"ChunkResult"},"CleanupPreviewRequest":{"properties":{"config":{"additionalProperties":true,"type":"object","title":"Config"}},"type":"object","required":["config"],"title":"CleanupPreviewRequest"},"CleanupPreviewResponse":{"properties":{"added":{"items":{"type":"string"},"type":"array","title":"Added"},"removed":{"items":{"type":"string"},"type":"array","title":"Removed"},"affected_file_count":{"type":"integer","title":"Affected File Count"}},"type":"object","required":["added","removed","affected_file_count"],"title":"CleanupPreviewResponse"},"CommitmentCheckoutRequest":{"properties":{"tier":{"type":"string","title":"Tier"}},"type":"object","required":["tier"],"title":"CommitmentCheckoutRequest","description":"Body for ``POST /v1/billing/checkout/commitment``."},"CompleteSignupRequest":{"properties":{"org_name":{"type":"string","title":"Org Name"}},"type":"object","required":["org_name"],"title":"CompleteSignupRequest","description":"Body for ``POST /v1/auth/complete-signup``.\n\nMandatory ``org_name`` for self-serve users (no WorkOS organization).\nLength-bounded so a malicious actor can't fill the column with a\nnovel; case is preserved as the user typed it."},"CompleteSignupResponse":{"properties":{"org_id":{"type":"string","title":"Org Id"},"org_name":{"type":"string","title":"Org Name"},"tenant_id":{"type":"string","title":"Tenant Id"},"tenant_name":{"type":"string","title":"Tenant Name"},"role":{"type":"string","title":"Role"}},"type":"object","required":["org_id","org_name","tenant_id","tenant_name","role"],"title":"CompleteSignupResponse"},"ComponentState":{"properties":{"status":{"type":"string","enum":["operational","degraded","down"],"title":"Status"}},"type":"object","required":["status"],"title":"ComponentState","description":"Per-component status — public-safe shape.\n\n``status`` is the operator-friendly label. **Latency is no longer\nsurfaced** in the public response: pre-launch audit flagged\nper-component latencies as a recon-vector for an external\nattacker timing an attack to a failover window. Operators get\nthe granular timing via ``/v1/db-check`` + ``/metrics``."},"ConnectorManifestResponse":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"description":{"type":"string","title":"Description"},"auth_type":{"type":"string","title":"Auth Type"},"config_schema":{"additionalProperties":true,"type":"object","title":"Config Schema"},"icon":{"type":"string","title":"Icon"}},"type":"object","required":["id","name","description","auth_type","config_schema","icon"],"title":"ConnectorManifestResponse"},"ConsolidateReportResponse":{"properties":{"tenant_id":{"type":"string","title":"Tenant Id"},"entities_scanned":{"type":"integer","title":"Entities Scanned"},"candidates":{"items":{"$ref":"#/components/schemas/MergeCandidate"},"type":"array","title":"Candidates"},"auto_merge_count":{"type":"integer","title":"Auto Merge Count"},"candidate_count":{"type":"integer","title":"Candidate Count"},"operator_action_id":{"type":"string","title":"Operator Action Id"}},"type":"object","required":["tenant_id","entities_scanned","candidates","auto_merge_count","candidate_count","operator_action_id"],"title":"ConsolidateReportResponse"},"CreateInvitationBody":{"properties":{"email":{"type":"string","title":"Email"},"role":{"type":"string","title":"Role","default":"member"}},"type":"object","required":["email"],"title":"CreateInvitationBody","description":"Body for ``POST /v1/tenants/{id}/invitations``."},"CreateKeyRequest":{"properties":{"name":{"type":"string","title":"Name"},"scopes":{"items":{"type":"string"},"type":"array","title":"Scopes","default":["read"]}},"type":"object","required":["name"],"title":"CreateKeyRequest"},"CreateSourceRequest":{"properties":{"connector_type":{"type":"string","title":"Connector Type"},"name":{"type":"string","title":"Name"},"config":{"additionalProperties":true,"type":"object","title":"Config","default":{}}},"type":"object","required":["connector_type","name"],"title":"CreateSourceRequest"},"CreateTenantBody":{"properties":{"name":{"type":"string","title":"Name"}},"type":"object","required":["name"],"title":"CreateTenantBody"},"CredentialBody":{"properties":{"credentials":{"additionalProperties":true,"type":"object","title":"Credentials"},"config":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Config"}},"type":"object","required":["credentials"],"title":"CredentialBody"},"DeleteTenantRequest":{"properties":{"confirm":{"type":"string","title":"Confirm"}},"type":"object","required":["confirm"],"title":"DeleteTenantRequest","description":"Body of ``DELETE /v1/tenants/{tenant_id}``.\n\n``confirm`` must match the target tenant's slug. This is a typed\nconfirmation (not a boolean) so it can't be satisfied by an empty\nJSON body or a default ``true`` — the caller has to know which\ntenant they're deleting."},"DeleteTenantResponse":{"properties":{"deleted":{"type":"boolean","title":"Deleted"},"tenant_id":{"type":"string","title":"Tenant Id"},"graph_counts":{"additionalProperties":true,"type":"object","title":"Graph Counts"}},"type":"object","required":["deleted","tenant_id","graph_counts"],"title":"DeleteTenantResponse"},"EntityDegreesRequest":{"properties":{"entity_ids":{"items":{"type":"string"},"type":"array","title":"Entity Ids"}},"type":"object","required":["entity_ids"],"title":"EntityDegreesRequest"},"EntityDegreesResponse":{"properties":{"degrees":{"additionalProperties":{"type":"integer"},"type":"object","title":"Degrees"}},"type":"object","required":["degrees"],"title":"EntityDegreesResponse","description":"Map of entity_id → total relationship degree (ACL-filtered). Used by\nthe graph canvas to render the \"hidden edges\" frontier hint on nodes\nwhose true degree exceeds what's currently rendered."},"EntityPath":{"properties":{"found":{"type":"boolean","title":"Found"},"length":{"type":"integer","title":"Length","default":0},"nodes":{"items":{"$ref":"#/components/schemas/EntityPathNode"},"type":"array","title":"Nodes","default":[]},"edges":{"items":{"$ref":"#/components/schemas/EntityPathEdge"},"type":"array","title":"Edges","default":[]},"suggestion":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Suggestion"},"shared_neighbors":{"items":{"$ref":"#/components/schemas/SharedNeighborHint"},"type":"array","title":"Shared Neighbors","default":[]},"from_neighbor_count":{"type":"integer","title":"From Neighbor Count","default":0},"to_neighbor_count":{"type":"integer","title":"To Neighbor Count","default":0}},"type":"object","required":["found"],"title":"EntityPath","description":"Shortest path between two entities with evidence on every hop.\n\n`length == len(edges)` (number of hops). `found=False` means no path\nexisted within the allowed `max_hops` that satisfied the confidence\nfloor and ACL on every node — nodes and edges will be empty.\n\nWhen ``found=False``, the response carries diagnostic fields the\nagent can use to retry productively instead of giving up:\n``suggestion`` (one-line human-readable next-step hint),\n``shared_neighbors`` (entities both endpoints connect to —\nbridge candidates), and the immediate ``from_neighbor_count`` /\n``to_neighbor_count``. When ``found=True``, all four are unset /\nempty."},"EntityPathEdge":{"properties":{"from_id":{"type":"string","title":"From Id"},"to_id":{"type":"string","title":"To Id"},"rel_type":{"type":"string","title":"Rel Type"},"confidence":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Confidence"},"description":{"type":"string","title":"Description","default":""}},"type":"object","required":["from_id","to_id","rel_type"],"title":"EntityPathEdge","description":"One hop along a path. `from_id` / `to_id` point at consecutive\n`EntityPathNode.entity_id` values — but the stored direction may run\neither way (relationships are undirected for path-finding), so agents\nshould not assume every hop flows forward along the list."},"EntityPathNode":{"properties":{"entity_id":{"type":"string","title":"Entity Id"},"name":{"type":"string","title":"Name"},"entity_type":{"type":"string","title":"Entity Type","default":""},"properties":{"type":"string","title":"Properties","default":""},"sources":{"items":{"$ref":"#/components/schemas/ResolvedSource"},"type":"array","title":"Sources","default":[]}},"type":"object","required":["entity_id","name"],"title":"EntityPathNode","description":"One entity along a `trace_connection` path. `properties` is the raw\nNeo4j JSON string (or empty string); the frontend / MCP tool flattens\nit consistently with `EntityRelationshipEdge.other`."},"EntityRelationshipEdge":{"properties":{"rel_type":{"type":"string","title":"Rel Type"},"description":{"type":"string","title":"Description","default":""},"confidence":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Confidence"},"direction":{"type":"string","title":"Direction","default":"outgoing"},"other":{"additionalProperties":true,"type":"object","title":"Other"}},"type":"object","required":["rel_type","other"],"title":"EntityRelationshipEdge","description":"One relationship involving a subject entity, flattened for UI rendering."},"EntityRelationshipsPage":{"properties":{"edges":{"items":{"$ref":"#/components/schemas/EntityRelationshipEdge"},"type":"array","title":"Edges"},"total":{"type":"integer","title":"Total"}},"type":"object","required":["edges","total"],"title":"EntityRelationshipsPage"},"EntityRelationshipsSummary":{"properties":{"total":{"type":"integer","title":"Total"},"by_type":{"items":{"$ref":"#/components/schemas/RelationshipTypeBucket"},"type":"array","title":"By Type"},"buckets_truncated":{"type":"boolean","title":"Buckets Truncated","default":false}},"type":"object","required":["total","by_type"],"title":"EntityRelationshipsSummary"},"EntityResult":{"properties":{"entity_id":{"type":"string","title":"Entity Id"},"name":{"type":"string","title":"Name"},"entity_type":{"type":"string","title":"Entity Type"},"properties":{"type":"string","title":"Properties"},"source_refs":{"items":{"type":"string"},"type":"array","title":"Source Refs"}},"type":"object","required":["entity_id","name","entity_type","properties","source_refs"],"title":"EntityResult"},"EntitySourceRefEntry":{"properties":{"file_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"File Id"},"acl_mode":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Acl Mode"},"acl_principals":{"items":{"type":"string"},"type":"array","title":"Acl Principals"}},"type":"object","title":"EntitySourceRefEntry"},"EntitySourceRefsResponse":{"properties":{"entity_id":{"type":"string","title":"Entity Id"},"name":{"type":"string","title":"Name"},"entity_type":{"type":"string","title":"Entity Type"},"tenant_id":{"type":"string","title":"Tenant Id"},"source_refs":{"items":{"$ref":"#/components/schemas/EntitySourceRefEntry"},"type":"array","title":"Source Refs"}},"type":"object","required":["entity_id","name","entity_type","tenant_id","source_refs"],"title":"EntitySourceRefsResponse"},"ErrorResponse":{"properties":{"detail":{"type":"string","title":"Detail"}},"type":"object","required":["detail"],"title":"ErrorResponse","description":"Generic error envelope used for 4xx / 5xx responses."},"ExtractionFailure":{"properties":{"event_id":{"type":"string","title":"Event Id"},"file_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"File Id"},"file_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"File Name"},"chunk_index":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Chunk Index"},"error_class":{"type":"string","title":"Error Class"},"error_message":{"type":"string","title":"Error Message"},"attempts":{"type":"integer","title":"Attempts"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["event_id","file_id","file_name","chunk_index","error_class","error_message","attempts","created_at"],"title":"ExtractionFailure","description":"One unresolved entry in the extraction DLQ."},"FilesPurgeRequest":{"properties":{"file_ids":{"items":{"type":"string"},"type":"array","title":"File Ids"},"mode":{"type":"string","enum":["unlink","purge"],"title":"Mode","default":"purge"}},"type":"object","required":["file_ids"],"title":"FilesPurgeRequest"},"FilesPurgeResponse":{"properties":{"purged_file_count":{"type":"integer","title":"Purged File Count"},"mode_effective":{"type":"string","enum":["unlink","purge"],"title":"Mode Effective"}},"type":"object","required":["purged_file_count","mode_effective"],"title":"FilesPurgeResponse"},"ForceReextractResponse":{"properties":{"source_id":{"type":"string","title":"Source Id"},"before":{"$ref":"#/components/schemas/ReextractCounts"},"purged_file_count":{"type":"integer","title":"Purged File Count","description":"Files soft-deleted (Postgres ContextObjects + Neo4j retract)."},"sync_run_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sync Run Id","description":"Celery sync_run_id if a new sync was enqueued; null if the source had no credentials and re-sync was skipped."},"status":{"type":"string","title":"Status","description":"'queued' if sync was triggered; 'skipped_no_credentials' otherwise."}},"type":"object","required":["source_id","before","purged_file_count","status"],"title":"ForceReextractResponse"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"IdentityBackfillRequest":{"properties":{"external_emails":{"items":{"type":"string"},"type":"array","title":"External Emails","description":"Emails to attribute to the user as historical OAuth identities. Each becomes a row in ``user_external_identities`` keyed on (user, source, email). Ignored when ``discover_from_acl=true``."},"discover_from_acl":{"type":"boolean","title":"Discover From Acl","description":"When true, scan ``context_objects.acl`` for the source and treat every distinct ``user:<email>`` principal as a candidate to attribute to the user. Convenient but coarse — it will also attribute emails of OTHER users whose docs the connecting user was shared into. Use ``external_emails`` explicitly when you want a precise set.","default":false}},"type":"object","title":"IdentityBackfillRequest"},"IdentityBackfillResponse":{"properties":{"user_id":{"type":"string","title":"User Id"},"data_source_id":{"type":"string","title":"Data Source Id"},"connector_type":{"type":"string","title":"Connector Type"},"written_emails":{"items":{"type":"string"},"type":"array","title":"Written Emails","description":"Emails newly written or whose verified_at was refreshed."},"discovered_emails":{"items":{"type":"string"},"type":"array","title":"Discovered Emails","description":"When ``discover_from_acl=true``, the full set scanned from ``context_objects.acl`` (a superset of ``written_emails`` if any rows already existed for unrelated emails)."},"operator_action_id":{"type":"string","title":"Operator Action Id"}},"type":"object","required":["user_id","data_source_id","connector_type","written_emails","operator_action_id"],"title":"IdentityBackfillResponse"},"InsufficientCreditsResponse":{"properties":{"error":{"type":"string","title":"Error","default":"insufficient_credits"},"detail":{"type":"string","title":"Detail"},"balance":{"type":"number","title":"Balance"},"required":{"type":"number","title":"Required"}},"type":"object","required":["detail","balance","required"],"title":"InsufficientCreditsResponse","description":"402 Payment Required — tenant balance too low for the requested work.\n\nThe frontend treats 402 specifically (top-up CTA); generic 4xx do\nnot trigger that flow. Keeping ``balance`` and ``required`` on the\nbody lets clients render an informative empty state."},"InvitationPreview":{"properties":{"org_name":{"type":"string","title":"Org Name"},"tenant_name":{"type":"string","title":"Tenant Name"},"role":{"type":"string","title":"Role"},"invited_email":{"type":"string","title":"Invited Email"},"inviter_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Inviter Name"},"inviter_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Inviter Email"},"expires_at":{"type":"string","title":"Expires At"}},"type":"object","required":["org_name","tenant_name","role","invited_email","inviter_name","inviter_email","expires_at"],"title":"InvitationPreview","description":"Public-safe preview shown on the `/invite/[token]` page."},"InvitationResponse":{"properties":{"id":{"type":"string","title":"Id"},"tenant_id":{"type":"string","title":"Tenant Id"},"email":{"type":"string","title":"Email"},"role":{"type":"string","title":"Role"},"invite_url":{"type":"string","title":"Invite Url"},"workos_invitation_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Workos Invitation Id"},"expires_at":{"type":"string","title":"Expires At"},"created_at":{"type":"string","title":"Created At"},"invited_by_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Invited By Email"}},"type":"object","required":["id","tenant_id","email","role","invite_url","workos_invitation_id","expires_at","created_at","invited_by_email"],"title":"InvitationResponse","description":"Public-safe shape for an invitation record."},"LinkTypeResponse":{"properties":{"link_type_id":{"type":"string","format":"uuid","title":"Link Type Id"},"name":{"type":"string","title":"Name"},"description":{"type":"string","title":"Description"},"from_object_type_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"From Object Type Id"},"to_object_type_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"To Object Type Id"},"cardinality":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cardinality"},"is_canonical":{"type":"boolean","title":"Is Canonical","description":"True when this is a canonical LinkType. False for variants merged into another canonical (Phase 1.5)."},"usage_count":{"type":"integer","title":"Usage Count"}},"type":"object","required":["link_type_id","name","description","is_canonical","usage_count"],"title":"LinkTypeResponse","description":"One canonical LinkType (typed predicate) in the tenant's ontology."},"LinkTypesListResponse":{"properties":{"link_types":{"items":{"$ref":"#/components/schemas/LinkTypeResponse"},"type":"array","title":"Link Types"},"total_count":{"type":"integer","title":"Total Count","description":"Total LinkTypes matching the filters BEFORE pagination. Use with ``limit`` + ``offset`` to know when to stop paging."},"has_more":{"type":"boolean","title":"Has More","description":"True when there are more results past the current page."},"offset":{"type":"integer","title":"Offset","description":"Echo of the request's offset."},"limit":{"type":"integer","title":"Limit","description":"Echo of the request's limit."}},"type":"object","required":["link_types","total_count","has_more","offset","limit"],"title":"LinkTypesListResponse","description":"Paginated list of LinkTypes. Replaces the bare ``list[LinkTypeResponse]``\nresponse so callers can page through tenants that have grown the rel_type\nvocabulary into the thousands (1,200+ in a single tenant has been\nobserved in real corpora — see the MCP audit)."},"LinkedExternalIdentity":{"properties":{"data_source_id":{"type":"string","title":"Data Source Id"},"connector_type":{"type":"string","title":"Connector Type"},"external_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"External Email"},"external_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"External User Id"},"verified_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Verified At"}},"type":"object","required":["data_source_id","connector_type"],"title":"LinkedExternalIdentity"},"LogoutRequest":{"properties":{"return_to":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Return To"}},"type":"object","title":"LogoutRequest","description":"Body for ``POST /v1/auth/logout``.\n\n``return_to`` is optional — when omitted we send the user back\nto ``settings.frontend_url + \"/auth/login\"``. Callers can override\nfor deep-linked logout flows (e.g. log out then land on a\nmarketing page)."},"LogoutResponse":{"properties":{"logout_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Logout Url"}},"type":"object","required":["logout_url"],"title":"LogoutResponse"},"MeResponse":{"properties":{"user_id":{"type":"string","title":"User Id"},"email":{"type":"string","title":"Email"},"name":{"type":"string","title":"Name"},"org_id":{"type":"string","title":"Org Id"},"org_name":{"type":"string","title":"Org Name"},"tenant_id":{"type":"string","title":"Tenant Id"},"tenant_name":{"type":"string","title":"Tenant Name"},"role":{"type":"string","title":"Role"},"is_org_admin":{"type":"boolean","title":"Is Org Admin","default":false},"is_platform_operator":{"type":"boolean","title":"Is Platform Operator","default":false},"needs_org_setup":{"type":"boolean","title":"Needs Org Setup","default":false}},"type":"object","required":["user_id","email","name","org_id","org_name","tenant_id","tenant_name","role"],"title":"MeResponse","description":"Current authenticated user + active tenant context.\n\nShape is a stable public contract: additive changes are non-breaking,\nfield renames or removals are breaking.\n\nPending-org users (self-serve signup that hasn't completed\n``POST /v1/auth/complete-signup``) get ``needs_org_setup=True`` and\nempty strings for ``org_id``, ``org_name``, ``tenant_id``,\n``tenant_name``, ``role``. The frontend's dashboard layout\nintercepts on this flag and redirects to ``/onboarding/setup-org``."},"MemberResponse":{"properties":{"user_id":{"type":"string","title":"User Id"},"email":{"type":"string","title":"Email"},"name":{"type":"string","title":"Name"},"role":{"type":"string","title":"Role"},"joined_at":{"type":"string","title":"Joined At"}},"type":"object","required":["user_id","email","name","role","joined_at"],"title":"MemberResponse"},"MembershipMutationResponse":{"properties":{"tenant_id":{"type":"string","title":"Tenant Id"},"user_id":{"type":"string","title":"User Id"},"role":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Role"},"removed":{"type":"boolean","title":"Removed"}},"type":"object","required":["tenant_id","user_id","role","removed"],"title":"MembershipMutationResponse","description":"Shared response for member remove + role-change."},"MergeCandidate":{"properties":{"entity_a":{"$ref":"#/components/schemas/MergeCandidateEntity","description":"Older / better-merged-into target. Typically the entity that already had more provenance."},"entity_b":{"$ref":"#/components/schemas/MergeCandidateEntity","description":"The fragmented surface form that would become an alias."},"signals":{"$ref":"#/components/schemas/MergeCandidateSignals"},"decision":{"type":"string","title":"Decision","description":"``auto_merge`` (passed both score floor + strong-individual guard) or ``candidate`` (above candidate floor but below auto_merge)."}},"type":"object","required":["entity_a","entity_b","signals","decision"],"title":"MergeCandidate"},"MergeCandidateEntity":{"properties":{"entity_id":{"type":"string","title":"Entity Id"},"name":{"type":"string","title":"Name"},"entity_type":{"type":"string","title":"Entity Type"}},"type":"object","required":["entity_id","name","entity_type"],"title":"MergeCandidateEntity"},"MergeCandidateSignals":{"properties":{"s1_name_seqmatch":{"type":"number","title":"S1 Name Seqmatch","description":"0–1 SequenceMatcher ratio on normalized names."},"s2_embedding":{"type":"number","title":"S2 Embedding","description":"0–1 cosine similarity on entity-signature embeddings (Phase 1.5c). Lit up only when both entities have an ``entity_embedding`` row — for un-backfilled tenants or rows created after the last backfill, S2 stays 0.0 and the decision falls back to S1/S3/S4."},"s3_neighborhood":{"type":"number","title":"S3 Neighborhood","description":"0–1 overlap coefficient of shared graph neighbors (``|A ∩ B| / min(|A|, |B|)``). Phase 1.1 changed this from Jaccard to overlap_coef so subset-of-fuller-form patterns (Holmes ⊂ Sherlock Holmes) score 1.0 even when the larger entity has many more neighbors."},"s4_property":{"type":"number","title":"S4 Property","description":"0–1 shared-property-value score."},"score":{"type":"number","title":"Score","description":"Weighted composite — drives the decision bucket."},"shared_neighbor_count":{"type":"integer","title":"Shared Neighbor Count","description":"Absolute count of shared graph neighbors (|A ∩ B|). Operators need this alongside the S3 rate to assess candidates — S3=1.0 with shared_count=2 is the pilot false-positive shape; same S3 with shared_count=15 is convincing. Phase 1.1 gates auto_merge on this clearing MIN_SHARED_NEIGHBORS_FOR_MERGE.","default":0}},"type":"object","required":["s1_name_seqmatch","s2_embedding","s3_neighborhood","s4_property","score"],"title":"MergeCandidateSignals"},"OAuthCallbackRequest":{"properties":{"code":{"type":"string","title":"Code"},"state":{"type":"string","title":"State"}},"type":"object","required":["code","state"],"title":"OAuthCallbackRequest"},"OAuthUrlResponse":{"properties":{"url":{"type":"string","title":"Url"}},"type":"object","required":["url"],"title":"OAuthUrlResponse"},"ObjectTypeResponse":{"properties":{"object_type_id":{"type":"string","format":"uuid","title":"Object Type Id"},"name":{"type":"string","title":"Name","description":"Canonical name (e.g. 'salesforce.Account', 'pg.public.users')."},"display_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Display Name"},"is_canonical":{"type":"boolean","title":"Is Canonical"},"source_authored":{"type":"boolean","title":"Source Authored","description":"True when registered from a Pattern-A connector's declared schema (the Foundry-light unlock). False for auto-discovered types pending customer promotion."},"properties":{"items":{"$ref":"#/components/schemas/PropertySpecResponse"},"type":"array","title":"Properties"},"relations":{"items":{"$ref":"#/components/schemas/RelationSpecResponse"},"type":"array","title":"Relations"},"seeded_from_source_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Seeded From Source Id"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"}},"type":"object","required":["object_type_id","name","is_canonical","source_authored","properties","relations","seeded_from_source_id","created_at","updated_at"],"title":"ObjectTypeResponse","description":"One canonical ObjectType in the tenant's ontology."},"OperatorActionEntry":{"properties":{"id":{"type":"string","title":"Id"},"operator_user_id":{"type":"string","title":"Operator User Id"},"action_type":{"type":"string","title":"Action Type"},"target_tenant_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Target Tenant Id"},"action_metadata":{"additionalProperties":true,"type":"object","title":"Action Metadata"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","operator_user_id","action_type","target_tenant_id","action_metadata","created_at"],"title":"OperatorActionEntry"},"OperatorActionListResponse":{"properties":{"actions":{"items":{"$ref":"#/components/schemas/OperatorActionEntry"},"type":"array","title":"Actions"},"total":{"type":"integer","title":"Total"}},"type":"object","required":["actions","total"],"title":"OperatorActionListResponse"},"PairwiseMergeDecisionSummary":{"properties":{"entity_a_id":{"type":"string","title":"Entity A Id"},"entity_a_name":{"type":"string","title":"Entity A Name"},"entity_b_id":{"type":"string","title":"Entity B Id"},"entity_b_name":{"type":"string","title":"Entity B Name"},"entity_type":{"type":"string","title":"Entity Type"},"tier":{"type":"string","title":"Tier","description":"HARD_MERGE | SAME_AS | CANDIDATE"},"score":{"type":"number","title":"Score"},"s1_name_seqmatch":{"type":"number","title":"S1 Name Seqmatch"},"s2_embedding":{"type":"number","title":"S2 Embedding"},"s3_neighborhood":{"type":"number","title":"S3 Neighborhood"},"s4_property":{"type":"number","title":"S4 Property"},"shared_neighbor_count":{"type":"integer","title":"Shared Neighbor Count"},"winner_id":{"type":"string","title":"Winner Id","description":"Tier 1 only: the entity_id chosen as canonical for this merge group.","default":""},"veto_reason":{"type":"string","title":"Veto Reason","description":"Audit string when a hard veto demoted this pair from a higher tier. Empty when no veto fired. Phase 1.5f rules: 'first_name_disagreement' (person with shared surname, different first names).","default":""}},"type":"object","required":["entity_a_id","entity_a_name","entity_b_id","entity_b_name","entity_type","tier","score","s1_name_seqmatch","s2_embedding","s3_neighborhood","s4_property","shared_neighbor_count"],"title":"PairwiseMergeDecisionSummary","description":"One pair's outcome — exposed in the endpoint response so the\noperator can review WHY each decision was made before re-running\nwith ``dry_run=false``."},"PairwiseMergeResponse":{"properties":{"entities_loaded":{"type":"integer","title":"Entities Loaded","description":"Number of entities loaded from Neo4j."},"pairs_evaluated":{"type":"integer","title":"Pairs Evaluated","description":"Number of within-type entity pairs scored."},"hard_merge_group_count":{"type":"integer","title":"Hard Merge Group Count","description":"Number of disjoint merge groups (each group has ≥2 members and collapses to 1)."},"hard_merge_nodes_absorbed":{"type":"integer","title":"Hard Merge Nodes Absorbed","description":"Total losing-node count across all groups (count drops by this many)."},"same_as_pair_count":{"type":"integer","title":"Same As Pair Count","description":"Number of SAME_AS bidirectional links written (or to be written if dry_run)."},"candidate_pair_count":{"type":"integer","title":"Candidate Pair Count","description":"Number of CANDIDATE pairs in the response. v1 does not persist; Phase 2 adds a review table."},"dry_run":{"type":"boolean","title":"Dry Run"},"decisions":{"items":{"$ref":"#/components/schemas/PairwiseMergeDecisionSummary"},"type":"array","title":"Decisions","description":"All Tier 1-3 decisions for operator review."},"operator_action_id":{"type":"string","title":"Operator Action Id","description":"UUID of the OperatorAction audit row."}},"type":"object","required":["entities_loaded","pairs_evaluated","hard_merge_group_count","hard_merge_nodes_absorbed","same_as_pair_count","candidate_pair_count","dry_run","operator_action_id"],"title":"PairwiseMergeResponse","description":"Per-tenant pairwise-reconciliation result."},"PaygCheckoutRequest":{"properties":{"credits":{"type":"integer","title":"Credits"}},"type":"object","required":["credits"],"title":"PaygCheckoutRequest","description":"Body for ``POST /v1/billing/checkout/payg``.\n\n``credits`` is the literal number of credits the customer is\nbuying — not dollars. The frontend exposes preset tiles ($10,\n$25, $50, $100) and a custom amount; both produce a credit\ninteger at the API boundary. Pricing is 1 credit = $0.01, so\na $10 tile sends ``credits=1000``."},"PendingMergeReviewItem":{"properties":{"review_id":{"type":"string","title":"Review Id"},"entity_a_id":{"type":"string","title":"Entity A Id"},"entity_a_name":{"type":"string","title":"Entity A Name"},"entity_b_id":{"type":"string","title":"Entity B Id"},"entity_b_name":{"type":"string","title":"Entity B Name"},"entity_type":{"type":"string","title":"Entity Type"},"score":{"type":"number","title":"Score"},"name_sim":{"type":"number","title":"Name Sim"},"signals":{"additionalProperties":true,"type":"object","title":"Signals","description":"Full MergeSignals breakdown captured at proposal time."},"reason":{"type":"string","title":"Reason","description":"Why this pair landed in CANDIDATE — judge rationale, veto reason, or empty for the plain candidate band.","default":""},"proposed_at":{"type":"string","title":"Proposed At"},"decision":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Decision"},"decided_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Decided At"}},"type":"object","required":["review_id","entity_a_id","entity_a_name","entity_b_id","entity_b_name","entity_type","score","name_sim","proposed_at"],"title":"PendingMergeReviewItem","description":"One row in the operator review queue. Carries everything the\noperator needs to decide without re-running reconciliation."},"PendingMergeReviewList":{"properties":{"tenant_id":{"type":"string","title":"Tenant Id"},"items":{"items":{"$ref":"#/components/schemas/PendingMergeReviewItem"},"type":"array","title":"Items"},"total":{"type":"integer","title":"Total"},"has_more":{"type":"boolean","title":"Has More"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"}},"type":"object","required":["tenant_id","items","total","has_more"],"title":"PendingMergeReviewList"},"PropertySpecResponse":{"properties":{"name":{"type":"string","title":"Name"},"type":{"type":"string","title":"Type","description":"Mantle property type: string / text / enum / int / float / currency / date / datetime / duration / bool / geo_point / url / email / phone / reference."},"required":{"type":"boolean","title":"Required","default":false},"enum_values":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Enum Values"},"is_free_text":{"type":"boolean","title":"Is Free Text","default":false}},"type":"object","required":["name","type"],"title":"PropertySpecResponse","description":"Typed property declaration on an ObjectType.\n\nMirrors ``connectors.base.PropertySpec`` but as a Pydantic model\nfor OpenAPI schema generation. ``enum_values`` is null for\nnon-enum types; ``is_free_text=true`` flags columns whose\nPattern-D-lite sub-extraction is enabled (Phase 3+)."},"PublicStatusResponse":{"properties":{"overall":{"type":"string","enum":["operational","degraded","down"],"title":"Overall"},"components":{"additionalProperties":{"$ref":"#/components/schemas/ComponentState"},"type":"object","title":"Components"},"region":{"type":"string","title":"Region"},"probed_at":{"type":"string","title":"Probed At"}},"type":"object","required":["overall","components","region","probed_at"],"title":"PublicStatusResponse","description":"Public status response. This shape is a public contract —\nadding fields is fine, renaming or removing them is breaking.\n\nComponent names are intentionally **generic** (``api`` / ``data``\n/ ``cache``) rather than infra-specific (``postgres`` / ``neo4j``\n/ ``redis``) so the response doesn't expose our stack to an\nexternal attacker. The page client renders human-readable labels\nfrom these keys — see ``docs/marketing-copy/status-page``."},"PurgeTenantGraphResponse":{"properties":{"neo4j_entities":{"type":"integer","title":"Neo4J Entities"},"neo4j_documents":{"type":"integer","title":"Neo4J Documents"},"neo4j_communities":{"type":"integer","title":"Neo4J Communities"},"postgres_context_objects":{"type":"integer","title":"Postgres Context Objects"},"postgres_entity_row_links":{"type":"integer","title":"Postgres Entity Row Links"},"sync_state_resets":{"type":"integer","title":"Sync State Resets"},"operator_action_id":{"type":"string","title":"Operator Action Id","description":"UUID of the OperatorAction audit row written for this run."}},"type":"object","required":["neo4j_entities","neo4j_documents","neo4j_communities","postgres_context_objects","postgres_entity_row_links","sync_state_resets","operator_action_id"],"title":"PurgeTenantGraphResponse","description":"Counts returned by the tenant-purge endpoint."},"QueryBody":{"properties":{"sql":{"type":"string","title":"Sql"},"limit":{"type":"integer","title":"Limit","default":50}},"type":"object","required":["sql"],"title":"QueryBody"},"RateLimitResponse":{"properties":{"error":{"type":"string","title":"Error","default":"rate_limit_exceeded"},"detail":{"type":"string","title":"Detail"}},"type":"object","required":["detail"],"title":"RateLimitResponse","description":"429 Too Many Requests — slowapi rejected the call.\n\nClients should respect the ``Retry-After`` header on the response."},"ReextractCounts":{"properties":{"context_objects":{"type":"integer","title":"Context Objects","description":"ContextObject rows in Postgres for this source."},"entity_row_links":{"type":"integer","title":"Entity Row Links","description":"entity_row_links rows scoped to this source."},"file_ids_in_sync_state":{"type":"integer","title":"File Ids In Sync State","description":"Distinct file_ids in data_sources.sync_state.files."}},"type":"object","required":["context_objects","entity_row_links","file_ids_in_sync_state"],"title":"ReextractCounts","description":"Counts derived from a source. Captured before purge so the\ncaller can compare against post-sync counts."},"RefreshRequest":{"properties":{"refresh_token":{"type":"string","title":"Refresh Token"}},"type":"object","required":["refresh_token"],"title":"RefreshRequest"},"ReidentifyResponse":{"properties":{"scanned":{"type":"integer","title":"Scanned","description":"Entity rows scanned for the tenant."},"groups":{"type":"integer","title":"Groups","description":"Distinct canonical ids after re-normalization."},"merged":{"type":"integer","title":"Merged","description":"Duplicate nodes folded into canonicals."},"kept":{"type":"integer","title":"Kept","description":"Canonical nodes retained."},"errors":{"type":"integer","title":"Errors","description":"Groups that failed to merge."},"id_remappings":{"type":"integer","title":"Id Remappings","description":"Distinct (old_id, new_id) pairs generated."},"operator_action_id":{"type":"string","title":"Operator Action Id","description":"UUID of the OperatorAction audit row written for this run."}},"type":"object","required":["scanned","groups","merged","kept","errors","id_remappings","operator_action_id"],"title":"ReidentifyResponse","description":"Counts returned from ``reidentify_tenant_entities`` plus\ncross-store rewrite stats so the operator can verify the full\nend-to-end heal succeeded."},"RejectMergeRequest":{"properties":{"reason":{"type":"string","maxLength":500,"minLength":1,"title":"Reason"}},"type":"object","required":["reason"],"title":"RejectMergeRequest"},"RejectMergeResponse":{"properties":{"review_id":{"type":"string","title":"Review Id"},"operator_action_id":{"type":"string","title":"Operator Action Id"},"status":{"type":"string","title":"Status"}},"type":"object","required":["review_id","operator_action_id","status"],"title":"RejectMergeResponse"},"RejectSoftMergeRequest":{"properties":{"reason":{"type":"string","maxLength":500,"minLength":1,"title":"Reason","description":"Free-text rationale for the reversal. Required for audit."}},"type":"object","required":["reason"],"title":"RejectSoftMergeRequest","description":"Reason carried into the audit log so operators can pull\nforensics later (why was this soft-merge un-done?)."},"RejectSoftMergeResponse":{"properties":{"deleted":{"type":"boolean","title":"Deleted","description":"True when an edge with this id was found and deleted. False when no edge with this id existed (idempotent — re-rejecting an already-rejected edge returns False, not an error)."},"edge_id":{"type":"string","title":"Edge Id"},"loser_id":{"type":"string","title":"Loser Id","default":""},"winner_id":{"type":"string","title":"Winner Id","default":""},"tenant_id":{"type":"string","title":"Tenant Id","default":""},"score":{"type":"number","title":"Score","default":0.0},"decided_at":{"type":"string","title":"Decided At","default":""},"decided_by":{"type":"string","title":"Decided By","default":""},"operator_action_id":{"type":"string","title":"Operator Action Id"}},"type":"object","required":["deleted","edge_id","operator_action_id"],"title":"RejectSoftMergeResponse","description":"Outcome of a reject — surfaces what was deleted so the caller\ncan verify the right edge came off."},"RelationSpecResponse":{"properties":{"name":{"type":"string","title":"Name"},"target_element_type":{"type":"string","title":"Target Element Type"},"cardinality":{"type":"string","title":"Cardinality","description":"Cardinality of the relation: '1:1', '1:N', 'N:1', 'N:M'.","default":"N:1"}},"type":"object","required":["name","target_element_type"],"title":"RelationSpecResponse","description":"Typed relation (FK / reference) declaration."},"RelationshipTypeBucket":{"properties":{"rel_type":{"type":"string","title":"Rel Type"},"count":{"type":"integer","title":"Count"},"examples":{"anyOf":[{"items":{"$ref":"#/components/schemas/EntityRelationshipEdge"},"type":"array"},{"type":"null"}],"title":"Examples"}},"type":"object","required":["rel_type","count"],"title":"RelationshipTypeBucket"},"RelationshipTypeResult":{"properties":{"name":{"type":"string","title":"Name"},"description":{"type":"string","title":"Description"},"usage_count":{"type":"integer","title":"Usage Count"},"score":{"type":"number","title":"Score"}},"type":"object","required":["name","description","usage_count","score"],"title":"RelationshipTypeResult"},"RenameTenantBody":{"properties":{"name":{"type":"string","title":"Name"}},"type":"object","required":["name"],"title":"RenameTenantBody"},"ResolutionStatsResponse":{"properties":{"tenants":{"items":{"$ref":"#/components/schemas/TenantResolutionStats"},"type":"array","title":"Tenants"},"snapshot_at":{"type":"string","title":"Snapshot At","description":"ISO timestamp of when this snapshot was computed."}},"type":"object","required":["tenants","snapshot_at"],"title":"ResolutionStatsResponse"},"ResolvedSource":{"properties":{"file_id":{"type":"string","title":"File Id"},"title":{"type":"string","title":"Title"},"connector_type":{"type":"string","title":"Connector Type"},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url"}},"type":"object","required":["file_id","title","connector_type"],"title":"ResolvedSource","description":"A source document attributed to an entity, resolved from raw file_id\ninto a displayable shape. `connector_type` is used by the frontend to pick\na brand icon; unresolved file_ids fall back to `connector_type=\"unknown\"`\nand use the raw id as the title. `url` is a deep-link into the source\nsystem when we can construct one for that connector (Drive / Gmail today)."},"RetryFailuresRequest":{"properties":{"file_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"File Ids"}},"type":"object","title":"RetryFailuresRequest","description":"Optional whitelist of file_ids to retry. Empty/missing = retry\nevery file that currently has an unresolved failure."},"RetryFailuresResponse":{"properties":{"task_id":{"type":"string","title":"Task Id"},"queued_file_count":{"type":"integer","title":"Queued File Count"},"message":{"type":"string","title":"Message"}},"type":"object","required":["task_id","queued_file_count","message"],"title":"RetryFailuresResponse"},"SelfDocCleanupResponse":{"properties":{"candidates_scanned":{"type":"integer","title":"Candidates Scanned","description":"Document-type entities considered."},"matched_file_names":{"type":"integer","title":"Matched File Names","description":"Candidates whose name matched a current source file_name."},"relationships_deleted":{"type":"integer","title":"Relationships Deleted","description":"Incoming AUTHOR_OF (and other) edges removed."},"entities_deleted":{"type":"integer","title":"Entities Deleted","description":"Self-document entity nodes deleted."},"operator_action_id":{"type":"string","title":"Operator Action Id","description":"UUID of the OperatorAction audit row."}},"type":"object","required":["candidates_scanned","matched_file_names","relationships_deleted","entities_deleted","operator_action_id"],"title":"SelfDocCleanupResponse","description":"Counts from a per-tenant self-document-entity cleanup run."},"SemanticSearchRequest":{"properties":{"query":{"type":"string","maxLength":4000,"title":"Query"},"limit":{"type":"integer","maximum":100.0,"minimum":1.0,"title":"Limit","default":10},"source_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Id"},"source_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Source Ids"},"min_score":{"type":"number","title":"Min Score","default":0.0}},"type":"object","required":["query"],"title":"SemanticSearchRequest"},"SharedNeighborHint":{"properties":{"entity_id":{"type":"string","title":"Entity Id"},"name":{"type":"string","title":"Name"},"entity_type":{"type":"string","title":"Entity Type","default":""}},"type":"object","required":["entity_id","name"],"title":"SharedNeighborHint","description":"One entity both endpoints connect to (1 hop from each).\n\nSurfaced in the ``no path found`` diagnostic so an agent can see\nbridge candidates and retry productively. The presence of any\nshared neighbor means there IS a 2-hop path between the endpoints\nthat the caller's ``min_confidence`` / ``max_hops`` rejected."},"SourceEditResponse":{"properties":{"id":{"type":"string","title":"Id"},"connector_type":{"type":"string","title":"Connector Type"},"name":{"type":"string","title":"Name"},"status":{"type":"string","title":"Status"},"config":{"additionalProperties":true,"type":"object","title":"Config"},"added_folders":{"items":{"type":"string"},"type":"array","title":"Added Folders"},"removed_folders":{"items":{"type":"string"},"type":"array","title":"Removed Folders"},"cleanup_mode_effective":{"type":"string","enum":["unlink","purge","unlink_fallback"],"title":"Cleanup Mode Effective"},"purged_file_count":{"type":"integer","title":"Purged File Count"}},"type":"object","required":["id","connector_type","name","status","config","added_folders","removed_folders","cleanup_mode_effective","purged_file_count"],"title":"SourceEditResponse","description":"Extended PATCH response showing what actually happened."},"SourceFailuresResponse":{"properties":{"source_id":{"type":"string","title":"Source Id"},"total":{"type":"integer","title":"Total"},"failures":{"items":{"$ref":"#/components/schemas/ExtractionFailure"},"type":"array","title":"Failures"}},"type":"object","required":["source_id","total","failures"],"title":"SourceFailuresResponse"},"SourceFileEntry":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"modified_at":{"type":"string","title":"Modified At","default":""},"size_bytes":{"type":"integer","title":"Size Bytes","default":0}},"type":"object","required":["id","name"],"title":"SourceFileEntry"},"SourceFilesResponse":{"properties":{"files":{"items":{"$ref":"#/components/schemas/SourceFileEntry"},"type":"array","title":"Files"},"total":{"type":"integer","title":"Total"}},"type":"object","required":["files","total"],"title":"SourceFilesResponse"},"SourceHealthResetResponse":{"properties":{"source_id":{"type":"string","title":"Source Id"},"cleared":{"additionalProperties":{"anyOf":[{"type":"string"},{"type":"integer"},{"type":"null"}]},"type":"object","title":"Cleared"},"message":{"type":"string","title":"Message"}},"type":"object","required":["source_id","cleared","message"],"title":"SourceHealthResetResponse"},"SourceHealthResponse":{"properties":{"source_id":{"type":"string","title":"Source Id"},"status":{"type":"string","title":"Status"},"auth_status":{"type":"string","title":"Auth Status"},"consecutive_failures":{"type":"integer","title":"Consecutive Failures"},"last_error_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Error At"},"last_error_class":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Error Class"},"last_error_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Error Message"},"next_retry_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Retry At"},"last_success_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Success At"},"extraction_failure_count":{"type":"integer","title":"Extraction Failure Count"},"circuit_breaker":{"type":"string","title":"Circuit Breaker"}},"type":"object","required":["source_id","status","auth_status","consecutive_failures","last_error_at","last_error_class","last_error_message","next_retry_at","last_success_at","extraction_failure_count","circuit_breaker"],"title":"SourceHealthResponse","description":"Durable connector-health state. See\n``docs/ops/connector-health-runbook.md`` for the full taxonomy\nand operator triage flow."},"SourceResponse":{"properties":{"id":{"type":"string","title":"Id"},"connector_type":{"type":"string","title":"Connector Type"},"name":{"type":"string","title":"Name"},"status":{"type":"string","title":"Status"},"config":{"additionalProperties":true,"type":"object","title":"Config"},"last_sync_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Sync At"},"document_count":{"type":"integer","title":"Document Count"},"error_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Message"},"created_at":{"type":"string","title":"Created At"},"sync_run_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sync Run Id"},"auth_status":{"type":"string","title":"Auth Status","default":"ok"},"consecutive_failures":{"type":"integer","title":"Consecutive Failures","default":0},"next_retry_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Retry At"},"last_error_class":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Error Class"},"webhooks_enabled":{"type":"boolean","title":"Webhooks Enabled","default":true}},"type":"object","required":["id","connector_type","name","status","config","last_sync_at","document_count","error_message","created_at"],"title":"SourceResponse"},"SyncEventResponse":{"properties":{"id":{"type":"string","title":"Id"},"event_type":{"type":"string","title":"Event Type"},"message":{"type":"string","title":"Message"},"metadata":{"additionalProperties":true,"type":"object","title":"Metadata"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["id","event_type","message","metadata","created_at"],"title":"SyncEventResponse"},"TenantAuditEntry":{"properties":{"id":{"type":"string","title":"Id"},"event_type":{"type":"string","title":"Event Type"},"actor_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Actor User Id"},"target_user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Target User Id"},"event_metadata":{"additionalProperties":true,"type":"object","title":"Event Metadata"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","event_type","actor_user_id","target_user_id","event_metadata","created_at"],"title":"TenantAuditEntry"},"TenantDetailResponse":{"properties":{"tenant":{"$ref":"#/components/schemas/TenantSummary"},"sources":{"items":{"$ref":"#/components/schemas/TenantSourceSummary"},"type":"array","title":"Sources"},"recent_operator_actions":{"items":{"$ref":"#/components/schemas/OperatorActionEntry"},"type":"array","title":"Recent Operator Actions","description":"Latest 50 OperatorAction rows targeting this tenant."},"recent_audit_events":{"items":{"$ref":"#/components/schemas/TenantAuditEntry"},"type":"array","title":"Recent Audit Events","description":"Latest 50 TenantAuditLog rows for this tenant (customer-side activity)."},"neo4j_entity_count":{"type":"integer","title":"Neo4J Entity Count","description":"Live Entity-node count from Neo4j."},"neo4j_relationship_count":{"type":"integer","title":"Neo4J Relationship Count","description":"Live RELATES_TO edge count from Neo4j."}},"type":"object","required":["tenant","sources","recent_operator_actions","recent_audit_events","neo4j_entity_count","neo4j_relationship_count"],"title":"TenantDetailResponse"},"TenantListResponse":{"properties":{"tenants":{"items":{"$ref":"#/components/schemas/TenantSummary"},"type":"array","title":"Tenants"},"total":{"type":"integer","title":"Total","description":"Total tenants visible to this operator."}},"type":"object","required":["tenants","total"],"title":"TenantListResponse"},"TenantResolutionStats":{"properties":{"tenant_id":{"type":"string","title":"Tenant Id"},"resolved_links":{"type":"integer","title":"Resolved Links","description":"Positive entity_row_links (row_pk_value IS NOT NULL)."},"negative_links":{"type":"integer","title":"Negative Links","description":"Negative cache entries (row_pk_value IS NULL, strategy='none')."},"pending_ambiguous":{"type":"integer","title":"Pending Ambiguous","description":"Negative entries that still have candidates in evidence — Layer 4 hasn't resolved them or judged 'unclear'. These are the rows the FE drawer should surface for manual override."},"shadow_rows":{"type":"integer","title":"Shadow Rows","description":"resolution_index row count (the shadow size)."},"llm_judge_calls_24h":{"type":"integer","title":"Llm Judge Calls 24H","description":"Layer 4 LLM-judge calls in the last 24h (cost signal)."},"by_strategy":{"additionalProperties":{"type":"integer"},"type":"object","title":"By Strategy","description":"Resolved links grouped by match_strategy: {'manual': N, 'exact': N, 'normalized': N, 'trigram': N, 'llm_judge': N}."}},"type":"object","required":["tenant_id","resolved_links","negative_links","pending_ambiguous","shadow_rows","llm_judge_calls_24h"],"title":"TenantResolutionStats","description":"Per-tenant resolution metrics. Stable contract for the\ndashboard + future billing rollups."},"TenantResponse":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"slug":{"type":"string","title":"Slug"},"is_active":{"type":"boolean","title":"Is Active"}},"type":"object","required":["id","name","slug","is_active"],"title":"TenantResponse"},"TenantSourceSummary":{"properties":{"source_id":{"type":"string","title":"Source Id"},"name":{"type":"string","title":"Name"},"connector_type":{"type":"string","title":"Connector Type"},"status":{"type":"string","title":"Status"},"document_count":{"type":"integer","title":"Document Count"},"last_sync_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Sync At"},"error_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Message"}},"type":"object","required":["source_id","name","connector_type","status","document_count","last_sync_at","error_message"],"title":"TenantSourceSummary"},"TenantSummary":{"properties":{"tenant_id":{"type":"string","title":"Tenant Id"},"tenant_name":{"type":"string","title":"Tenant Name"},"tenant_slug":{"type":"string","title":"Tenant Slug"},"org_id":{"type":"string","title":"Org Id"},"org_name":{"type":"string","title":"Org Name"},"org_admin_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Org Admin Email","description":"Email of an org-admin user (User.is_org_admin=true). May be null for self-serve orgs without a designated admin."},"member_count":{"type":"integer","title":"Member Count","description":"Distinct UserTenantMembership rows."},"document_count":{"type":"integer","title":"Document Count","description":"DataSource.document_count summed across tenant sources."},"credit_balance":{"type":"number","title":"Credit Balance","description":"Current available credits for this tenant."},"last_sync_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Sync At","description":"Latest successful sync across all sources. Null if no source has ever synced."},"created_at":{"type":"string","format":"date-time","title":"Created At"},"source_count":{"type":"integer","title":"Source Count","description":"Total DataSource rows for this tenant."}},"type":"object","required":["tenant_id","tenant_name","tenant_slug","org_id","org_name","member_count","document_count","credit_balance","created_at","source_count"],"title":"TenantSummary","description":"One row in the operator tenant-list. Identity + headline counts."},"TraversalEdge":{"properties":{"from_id":{"type":"string","title":"From Id"},"from_name":{"type":"string","title":"From Name"},"from_type":{"type":"string","title":"From Type","default":""},"rel_type":{"type":"string","title":"Rel Type"},"confidence":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Confidence"},"rel_evidence":{"type":"string","title":"Rel Evidence","default":""},"to_id":{"type":"string","title":"To Id"},"to_name":{"type":"string","title":"To Name"},"to_type":{"type":"string","title":"To Type","default":""}},"type":"object","required":["from_id","from_name","rel_type","confidence","to_id","to_name"],"title":"TraversalEdge","description":"Slim graph edge for the topology-rendering endpoint.\n\nIdentity-only payload — name, type, ids, and the relationship.\nProperties and source-resolution metadata are NOT inlined here:\nthe agent / FE retrieves them on demand via ``GET /entities/{id}``\nor ``mcp__mantle__get_entity_context`` for whichever node they\nactually need to expand. This keeps the response payload bounded\neven for hub entities (e.g., a heavily-discussed property) where\na depth=2/limit=500 traversal would otherwise return multi-MB\npayloads — the FE was hitting \"Failed to fetch\" from browser\ntransport limits, with the underlying data fine but un-deliverable."},"UpdateKeyRequest":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"scopes":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Scopes"}},"type":"object","title":"UpdateKeyRequest"},"UpdateSourceRequest":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"config":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Config"},"cleanup_mode":{"type":"string","enum":["unlink","purge"],"title":"Cleanup Mode","default":"unlink"},"webhooks_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Webhooks Enabled"}},"type":"object","title":"UpdateSourceRequest"},"UsageEventResponse":{"properties":{"id":{"type":"string","title":"Id"},"event_type":{"type":"string","title":"Event Type"},"resource_type":{"type":"string","title":"Resource Type"},"provider":{"type":"string","title":"Provider"},"model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model"},"input_tokens":{"type":"integer","title":"Input Tokens"},"output_tokens":{"type":"integer","title":"Output Tokens"},"credits_consumed":{"type":"number","title":"Credits Consumed"},"duration_ms":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Duration Ms"},"status":{"type":"string","title":"Status"},"created_at":{"type":"string","title":"Created At"}},"type":"object","required":["id","event_type","resource_type","provider","model","input_tokens","output_tokens","credits_consumed","duration_ms","status","created_at"],"title":"UsageEventResponse"},"UserPrincipalsResponse":{"properties":{"user_id":{"type":"string","title":"User Id"},"user_email":{"type":"string","title":"User Email"},"tenant_id":{"type":"string","title":"Tenant Id"},"principals":{"items":{"type":"string"},"type":"array","title":"Principals","description":"Sorted, fully-resolved principal set the ACL filter compares against on every read."},"linked_external_identities":{"items":{"$ref":"#/components/schemas/LinkedExternalIdentity"},"type":"array","title":"Linked External Identities"}},"type":"object","required":["user_id","user_email","tenant_id","principals","linked_external_identities"],"title":"UserPrincipalsResponse"},"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"},"ValidationErrorResponse":{"properties":{"error":{"type":"string","title":"Error","default":"validation_error"},"detail":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Detail"}},"type":"object","required":["detail"],"title":"ValidationErrorResponse","description":"Pydantic validation error — emitted by FastAPI for malformed bodies."},"_ManualLinkRequest":{"properties":{"source_id":{"type":"string","title":"Source Id","description":"DataSource UUID containing the row to link."},"table_name":{"type":"string","title":"Table Name","description":"Fully-qualified table name (e.g. ``public.companies``)."},"row_pk_value":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Row Pk Value","description":"Customer-DB primary key value to link to. Pass ``null`` to explicitly REJECT — the user is asserting the entity has no match in this table; we'll cache that decision."},"action":{"type":"string","title":"Action","description":"'confirm' — link entity to row_pk_value. 'reject' — cache 'no match' for this (entity, source-table). row_pk_value is required for 'confirm', forbidden for 'reject'.","default":"confirm"}},"type":"object","required":["source_id","table_name"],"title":"_ManualLinkRequest","description":"Body for ``POST /entities/{id}/links``."}}}}