Skip to main content
Everything under API Reference documents one FastAPI application: services/api/product_app.py. That module — not services/api/main.py — is the process the deployed container actually runs (Dockerfile starts uvicorn services.api.product_app:app). If you came here looking for the older research/clustering surface, it exists but is a different app entirely; see Research API.

Base URLs

http://127.0.0.1:18000This is the port the product Compose stack publishes for the API container (infra/docker-compose.product.yml, overridable with CAUSELOOP_API_PORT). The browser never talks to it directly — the Next.js frontend’s route handler at src/app/api/backend/[...path]/route.ts proxies every call from http://127.0.0.1:13000. See Local product stack to bring the stack up.
Whichever base URL you’re pointed at, every path in this reference is relative to it with no version prefix — e.g. POST /login, not POST /v1/login.

The allowlist philosophy

product_app.py does not simply include_router() the auth/onboarding/insights/members/sources/remediation/metrics routers and expose whatever routes they happen to define. Each router is passed through _selected_router() alongside an explicit frozenset of (METHOD, path) tuples — AUTH_OPERATIONS, ONBOARDING_OPERATIONS, INSIGHT_OPERATIONS, MEMBER_OPERATIONS, SOURCE_OPERATIONS, REMEDIATION_OPERATIONS, OBSERVABILITY_OPERATIONS — and only routes whose method+path appear in that set are copied onto the app that actually serves traffic. Everything else defined in the same router module is simply never registered on product_app. This is enforced at import time, not by convention: _selected_router() raises RuntimeError at startup if an allowlisted operation is missing from the router, or if a route mixes an allowlisted method with a non-allowlisted one on the same path. A route that exists in auth_api.py, onboarding_api.py, sources_api.py, or remediation_api.py but isn’t in one of those frozensets is not a 403 or a hidden route — it is a plain 404, because FastAPI’s routing table never learned about it in the first place. Concretely, the following all exist as real, working code in their router modules but are not part of the deployed product surface:
  • GET /sources/connectors, POST /sources/{source_id}/test-connection, POST /sources/{source_id}/sync, GET /sources/{source_id}/history, GET /sources/jobs/{job_id}/events (services/api/sources_api.py)
  • GET /admin/sessions, DELETE /admin/sessions/{session_id}, POST /admin/password-reset/request, POST /admin/password-reset/confirm, POST /password-reset/request, POST /password-reset/confirm, GET /dev/mailbox (services/api/auth_api.py)
  • POST /remediation/reviews, POST /remediation/caps/{cap_id}/steps/{step}, DELETE /remediation/caps/{cap_id} (services/api/remediation_api.py)
  • POST /admin/tenants/{tenant_id}/materialize-now, POST /admin/tenants/{tenant_id}/run-migrations, POST /admin/tenants/{tenant_id}/import-checkpoint, POST /admin/tenants/{tenant_id}/rename (services/api/onboarding_api.py — each marked TEMPORARY in an adjacent comment)
Counting the three /health* routes registered directly on product_app alongside the seven allowlisted route groups gives exactly 44 operations — the entire published surface. If you’re extending the product, adding a route to a router is necessary but not sufficient: it also has to be added to the relevant *_OPERATIONS frozenset in services/api/product_app.py, or it will 404 in every environment including local Compose.
Don’t infer the deployed surface from a router file alone. services/api/sources_api.py, for example, defines eight routes; only three of them (GET /sources, POST /sources, POST /sources/{source_id}/upload) are reachable on the product API. Always cross-check against product_app.py’s *_OPERATIONS sets, or just call the endpoint against a running instance and expect 404 if you guessed wrong.

OpenAPI: source of truth

Two OpenAPI documents matter, and they should agree:
  1. Live schema — GET /openapi.json on a running product API (http://127.0.0.1:18000/openapi.json locally, https://api.causeloop.ai/openapi.json in production). FastAPI generates this from the routes actually registered on product_app, so it can never drift ahead of the allowlist — a route that isn’t allowlisted cannot appear here either. Interactive docs are at /docs (Swagger UI) and /redoc on the same base URL; neither is disabled in this app.
  2. Committed snapshot — docs-site/api-reference/openapi.json. This is what Mintlify reads to auto-generate the per-endpoint pages under the Endpoints group in the sidebar (configured in docs-site/docs.json). It is a point-in-time copy, not a live proxy — if you change a route’s request/response shape, regenerate it by starting the product API locally and overwriting this file with the contents of its /openapi.json, then re-check the auto-generated pages render as expected.
The two can only disagree if the snapshot is stale. When in doubt, trust the live /openapi.json over the committed file, and the file over anything written in prose (including this reference).

Endpoint groups

Authentication

Staff session (/admin/login, /admin/me, /admin/logout), tenant session (/login, /me, /logout), /workspaces, /accept-invite, impersonation, cookies, and CSRF.

Onboarding (staff)

Tenant lifecycle under /admin/tenants/*: create, provision, ingest, training config, data policy, source seeding, train, checkpoints, activation, invitations, go-live. services/api/onboarding_api.py.

Insights (tenant)

GET /insights/snapshot — one coherent (dataset_version, model_version) pair’s worth of materialized insight collections. services/api/insights_api.py.

Members (tenant)

GET /members — read-only tenant user directory with role labels. services/api/members_api.py.

Sources (tenant)

GET /sources, POST /sources, POST /sources/{source_id}/upload — connector registration and the file-upload ingest path. services/api/sources_api.py.

Remediation (tenant)

CAP (corrective action plan) lifecycle, review decisions, tenant audit log, branded Excel export. services/api/remediation_api.py.
GET /metrics (Prometheus text format, employee-authenticated) rounds out the allowlist as the observability operation; see Observability for how it’s scraped.

Content types

Nearly every request and response body on this API is application/json. The exceptions:
  • File uploads (POST /admin/tenants/{tenant_id}/ingest/upload, POST /sources/{source_id}/upload) accept multipart/form-data with a single file field.
  • GET /remediation/export.xlsx returns application/vnd.openxmlformats-officedocument.spreadsheetml.sheet bytes as a streamed attachment, not JSON.
  • GET /metrics returns Prometheus text exposition format, not JSON (see above).
Mutating JSON requests are matched by FastAPI/Pydantic models defined next to each router (see the auto-generated endpoint pages for exact schemas); malformed bodies fail with 422 before any handler code runs. See Errors and conventions for the full error contract.

The /health family

Three routes are registered directly on product_app (not through the allowlist mechanism, since they exist purely for orchestration and carry no tenant or auth semantics): /health and /health/live are also two of the handful of paths the frontend proxy treats as public — reachable without a session cookie, so an external uptime check only needs the frontend’s URL. See Authentication for the full public-path list.