Skip to content

API Reference

CIPRIAN STEFAN PLESCA edited this page Sep 16, 2026 · 1 revision

API Reference

Author: Ciprian Ștefan Pleșca

This page documents every route exposed by create_app() in src/openlongevity/api.py. The API is a FastAPI application, and its defining structural feature is that every response is explicitly labeled by mode"persisted" for database-backed publication data, "fixture-only" for synthetic demonstration data — so a client can never mistake one for the other.

Route map

flowchart LR
    subgraph Health["Health & meta"]
        H1["GET /api/v1/health"]
        H2["GET /api/v1/version"]
        H3["GET /api/v1/health/database"]
    end

    subgraph Ingestion["Ingestion — requires X-Ingestion-Key"]
        I1["POST /api/v1/ingestion/pubmed"]
    end

    subgraph Publications["Persisted publications — mode: persisted"]
        P1["GET /api/v1/publications"]
        P2["GET /api/v1/search"]
        P3["GET /api/v1/publications/:identifier"]
        P4["GET /api/v1/publications/:identifier/history"]
    end

    subgraph Fixtures["Synthetic demonstrations — mode: fixture-only"]
        F1["GET /api/v1/evidence"]
        F2["GET /api/v1/evidence/:identifier"]
        F3["GET /api/v1/research-gaps"]
        F4["GET /api/v1/graph"]
    end

    subgraph Fallback["Catch-all"]
        C1["GET /api/v1/:resource -> 404 RESOURCE_UNAVAILABLE"]
    end
Loading

Endpoint details

GET /api/v1/health

Returns status, version, database ("ok" / "unavailable" / "not_configured"), a static provider_status of "not_probed" (the health check does not make an outbound call to any literature provider), and the standard disclaimer.

GET /api/v1/version

Returns the package __version__ only. No database access.

GET /api/v1/health/database

A narrower probe returning only {"status": ...}, delegating to Database.health(), which checks the alembic_version table rather than mere connectivity (see Persistence-and-Data-Governance).

POST /api/v1/ingestion/pubmed

The only write-triggering, network-triggering route.

Aspect Behavior
Auth X-Ingestion-Key header, compared with secrets.compare_digest against a server-configured key (OPENLONGEVITY_INGESTION_KEY or an explicit constructor argument)
Body { "query": str (1-200 chars), "limit": int (1-25) }, extra="forbid" — unknown fields are rejected
No key configured 503 INGESTION_DISABLED
Key missing/invalid 401 UNAUTHORIZED
No database configured 503 DATABASE_NOT_CONFIGURED (via require_repository())
Invalid query 422 INVALID_QUERY (from a ValueError in SearchQuery)
Upstream failure 502 PROVIDER_UNAVAILABLE (from a ProviderError)
Success PublicationPage with mode: "persisted", each item run through PublicationRepository.save()
sequenceDiagram
    autonumber
    participant C as Client
    participant A as /api/v1/ingestion/pubmed
    C->>A: POST {query, limit}, header X-Ingestion-Key
    A->>A: key configured? compare_digest?
    A->>A: repository configured?
    A->>A: source.search(SearchQuery(query, limit))
    A->>A: for each Publication: repository.save(...)
    A-->>C: PublicationPage(items, total, page=1, page_size=limit, mode="persisted")
Loading

GET /api/v1/publications and GET /api/v1/search

Both map to the same handler. Query parameters: query (default "", max 200 chars), page (default 1, 1-10000), page_size (default 20, 1-100). Delegates to PublicationRepository.list(), which performs a bounded, autoescaped title search (see Persistence-and-Data-Governance). Returns 503 DATABASE_NOT_CONFIGURED if no database is wired.

GET /api/v1/publications/{identifier}

Rejects identifiers over 160 characters with 422 INVALID_IDENTIFIER before touching the database. Returns 404 NOT_FOUND if the repository has no matching row, otherwise the full persisted publication with synthetic: false fixed by the response model's Literal[False] type.

GET /api/v1/publications/{identifier}/history

Calls the single-publication handler first (to get its 404 behavior for free) and then returns the full ordered list of RevisionResponse entries from PublicationRepository.history().

GET /api/v1/evidence and GET /api/v1/evidence/{identifier}

Backed entirely by a hardcoded, process-local fixture list (currently a single SYN-001 in-vitro synthetic record). Filters by case-insensitive substring match against topic. Every item in the response has synthetic: True forced onto it and includes its computed level from EvidenceEngine.grade(). The summary is computed by EvidenceEngine.summarize().

Implementation note preserved from source: the single-identifier route calls the collection handler directly as a plain Python function with topic="" rather than relying on FastAPI's Query default, because calling a route function outside of an actual HTTP request leaves default Query(...) objects unresolved. The source code comment documenting this fix is preserved verbatim in api.py as a caution against a specific, non-obvious AttributeError.

GET /api/v1/research-gaps

Requires a non-empty topic (1-120 chars). Runs ResearchGapDetector(engine).detect(topic, fixtures) against the same synthetic fixture list used by the evidence routes — this route can only ever surface gaps within that tiny fixture set, which is a deliberate demonstration boundary, not a claim about gaps in the live literature.

GET /api/v1/graph

Returns a fixed illustrative graph — two nodes and one edge — regardless of any query parameters. There is no query parameter on this route at all; it exists to demonstrate the response shape of a future graph-backed endpoint.

GET /api/v1/{resource} (catch-all)

Any unmatched path under /api/v1/ returns 404 RESOURCE_UNAVAILABLE with the message "This resource is not implemented in this preview" — an explicit, honest 404 rather than a generic framework-level 404, so a client always receives the same structured error envelope.

Error envelope

Every error response, regardless of source, is normalized to the same shape via three exception handlers:

flowchart TD
    Exc[Exception raised in a route] --> T1{"StarletteHTTPException?"}
    T1 -- yes --> E1["error(code, message) with original status code"]
    T1 -- no --> T2{"RequestValidationError?"}
    T2 -- yes --> E2["422 error(code=INVALID_REQUEST, message)"]
    T2 -- no --> T3{"SQLAlchemyError?"}
    T3 -- yes --> E3["503 error(code=DATABASE_UNAVAILABLE, message)"]
Loading

This means a client integrating against the API needs to handle exactly one JSON error shape, {"error": {"code": ..., "message": ...}}, across the entire route surface.

CORS

CORS is enabled only if OPENLONGEVITY_WEB_ORIGINS is set (a comma-separated origin list), and even then only GET methods and the Accept header are allowed, with allow_credentials=False — a minimal, read-oriented cross-origin posture appropriate for a public research API with no session state.

Next

See Analysis-Toolkit for the standalone statistical utilities that are not currently wired into any API route, or Scientific-Methodology-and-Limitations for how to correctly describe what a call to this API does and does not demonstrate.